From 5e12688ad6ed4265e9eab6866e2d89e8e46bdaad Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Fri, 24 May 2024 16:35:45 +0100 Subject: [PATCH 01/17] Convert docs to asciidoc --- cmd/tools/benthos_docs_gen/main.go | 30 +- config/test/protobuf/people.yaml | 2 +- docs/antora.yml | 56 + .../components/pages/buffers/memory.adoc | 178 + .../components/pages/buffers/none.adoc | 29 + .../components/pages/buffers/sqlite.adoc | 99 + .../pages/buffers/system_window.adoc | 208 + .../components/pages/caches/aws_dynamodb.adoc | 283 ++ .../components/pages/caches/aws_s3.adoc | 252 ++ .../components/pages/caches/couchbase.adoc | 157 + .../modules/components/pages/caches/file.adoc | 38 + .../pages/caches/gcp_cloud_storage.adoc | 47 + docs/modules/components/pages/caches/lru.adoc | 148 + .../components/pages/caches/memcached.adoc | 142 + .../components/pages/caches/memory.adoc | 117 + .../components/pages/caches/mongodb.adoc | 106 + .../components/pages/caches/multilevel.adoc | 65 + .../components/pages/caches/nats_kv.adoc | 364 ++ .../modules/components/pages/caches/noop.adoc | 27 + .../components/pages/caches/redis.adoc | 360 ++ .../components/pages/caches/ristretto.adoc | 142 + docs/modules/components/pages/caches/sql.adoc | 321 ++ .../components/pages/caches/ttlru.adoc | 130 + docs/modules/components/pages/http/about.adoc | 269 ++ .../components/pages/inputs/amqp_0_9.adoc | 417 ++ .../components/pages/inputs/amqp_1.adoc | 395 ++ .../components/pages/inputs/aws_kinesis.adoc | 438 ++ .../components/pages/inputs/aws_s3.adoc | 356 ++ .../components/pages/inputs/aws_sqs.adoc | 229 + .../pages/inputs/azure_blob_storage.adoc | 208 + .../pages/inputs/azure_cosmosdb.adoc | 303 ++ .../pages/inputs/azure_queue_storage.adoc | 147 + .../pages/inputs/azure_table_storage.adoc | 164 + .../components/pages/inputs/batched.adoc | 178 + .../components/pages/inputs/beanstalkd.adoc | 46 + .../components/pages/inputs/broker.adoc | 224 + .../components/pages/inputs/cassandra.adoc | 404 ++ .../pages/inputs/cockroachdb_changefeed.adoc | 292 ++ docs/modules/components/pages/inputs/csv.adoc | 209 + .../components/pages/inputs/discord.adoc | 104 + .../components/pages/inputs/dynamic.adoc | 70 + .../modules/components/pages/inputs/file.adoc | 130 + .../pages/inputs/gcp_bigquery_select.adoc | 177 + .../pages/inputs/gcp_cloud_storage.adoc | 118 + .../components/pages/inputs/gcp_pubsub.adoc | 167 + .../components/pages/inputs/generate.adoc | 156 + .../modules/components/pages/inputs/hdfs.adoc | 75 + .../components/pages/inputs/http_client.adoc | 875 ++++ .../components/pages/inputs/http_server.adoc | 418 ++ .../components/pages/inputs/inproc.adoc | 30 + .../components/pages/inputs/kafka.adoc | 705 +++ .../components/pages/inputs/kafka_franz.adoc | 692 +++ .../components/pages/inputs/mongodb.adoc | 238 + .../modules/components/pages/inputs/mqtt.adoc | 434 ++ .../components/pages/inputs/nanomsg.adoc | 122 + .../modules/components/pages/inputs/nats.adoc | 446 ++ .../pages/inputs/nats_jetstream.adoc | 504 +++ .../components/pages/inputs/nats_kv.adoc | 443 ++ .../components/pages/inputs/nats_stream.adoc | 474 ++ docs/modules/components/pages/inputs/nsq.adoc | 305 ++ .../components/pages/inputs/parquet.adoc | 100 + .../components/pages/inputs/pulsar.adoc | 258 ++ .../components/pages/inputs/read_until.adoc | 134 + .../components/pages/inputs/redis_list.adoc | 339 ++ .../components/pages/inputs/redis_pubsub.adoc | 321 ++ .../components/pages/inputs/redis_scan.adoc | 330 ++ .../pages/inputs/redis_streams.adoc | 388 ++ .../components/pages/inputs/resource.adoc | 67 + .../components/pages/inputs/sequence.adoc | 262 ++ .../modules/components/pages/inputs/sftp.adoc | 257 ++ .../components/pages/inputs/socket.adoc | 82 + .../pages/inputs/socket_server.adoc | 131 + .../components/pages/inputs/sql_raw.adoc | 317 ++ .../components/pages/inputs/sql_select.adoc | 363 ++ .../components/pages/inputs/stdin.adoc | 51 + .../components/pages/inputs/subprocess.adoc | 124 + .../pages/inputs/twitter_search.adoc | 153 + .../components/pages/inputs/websocket.adoc | 478 ++ .../modules/components/pages/inputs/zmq4.adoc | 114 + .../components/pages/logger/about.adoc | 157 + .../pages/metrics/aws_cloudwatch.adoc | 199 + .../components/pages/metrics/influxdb.adoc | 385 ++ .../components/pages/metrics/json_api.adoc | 28 + .../components/pages/metrics/logger.adoc | 51 + .../components/pages/metrics/none.adoc | 26 + .../components/pages/metrics/prometheus.adoc | 219 + .../components/pages/metrics/statsd.adoc | 63 + .../components/pages/outputs/amqp_0_9.adoc | 514 +++ .../components/pages/outputs/amqp_1.adoc | 382 ++ .../pages/outputs/aws_dynamodb.adoc | 444 ++ .../components/pages/outputs/aws_kinesis.adoc | 383 ++ .../pages/outputs/aws_kinesis_firehose.adoc | 353 ++ .../components/pages/outputs/aws_s3.adoc | 548 +++ .../components/pages/outputs/aws_sns.adoc | 244 ++ .../components/pages/outputs/aws_sqs.adoc | 410 ++ .../pages/outputs/azure_blob_storage.adoc | 199 + .../pages/outputs/azure_cosmosdb.adoc | 532 +++ .../pages/outputs/azure_queue_storage.adoc | 263 ++ .../pages/outputs/azure_table_storage.adoc | 371 ++ .../components/pages/outputs/beanstalkd.adoc | 56 + .../components/pages/outputs/broker.adoc | 255 ++ .../components/pages/outputs/cache.adoc | 139 + .../components/pages/outputs/cassandra.adoc | 583 +++ .../components/pages/outputs/discord.adoc | 52 + .../components/pages/outputs/drop.adoc | 27 + .../components/pages/outputs/drop_on.adoc | 132 + .../components/pages/outputs/dynamic.adoc | 72 + .../pages/outputs/elasticsearch.adoc | 699 +++ .../components/pages/outputs/fallback.adoc | 75 + .../components/pages/outputs/file.adoc | 87 + .../pages/outputs/gcp_bigquery.adoc | 414 ++ .../pages/outputs/gcp_cloud_storage.adoc | 338 ++ .../components/pages/outputs/gcp_pubsub.adoc | 367 ++ .../components/pages/outputs/hdfs.adoc | 231 + .../components/pages/outputs/http_client.adoc | 971 ++++ .../components/pages/outputs/http_server.adoc | 190 + .../components/pages/outputs/inproc.adoc | 30 + .../components/pages/outputs/kafka.adoc | 827 ++++ .../components/pages/outputs/kafka_franz.adoc | 756 ++++ .../components/pages/outputs/mongodb.adoc | 379 ++ .../components/pages/outputs/mqtt.adoc | 460 ++ .../components/pages/outputs/nanomsg.adoc | 89 + .../components/pages/outputs/nats.adoc | 472 ++ .../pages/outputs/nats_jetstream.adoc | 477 ++ .../components/pages/outputs/nats_kv.adoc | 402 ++ .../components/pages/outputs/nats_stream.adoc | 410 ++ .../modules/components/pages/outputs/nsq.adoc | 267 ++ .../components/pages/outputs/opensearch.adoc | 625 +++ .../components/pages/outputs/pulsar.adoc | 233 + .../components/pages/outputs/pusher.adoc | 257 ++ .../components/pages/outputs/redis_hash.adoc | 378 ++ .../components/pages/outputs/redis_list.adoc | 452 ++ .../pages/outputs/redis_pubsub.adoc | 424 ++ .../pages/outputs/redis_streams.adoc | 469 ++ .../components/pages/outputs/reject.adoc | 60 + .../pages/outputs/reject_errored.adoc | 86 + .../components/pages/outputs/resource.adoc | 65 + .../components/pages/outputs/retry.adoc | 116 + .../components/pages/outputs/sftp.adoc | 161 + .../pages/outputs/snowflake_put.adoc | 791 ++++ .../components/pages/outputs/socket.adoc | 95 + .../components/pages/outputs/splunk_hec.adoc | 192 + .../modules/components/pages/outputs/sql.adoc | 264 ++ .../components/pages/outputs/sql_insert.adoc | 462 ++ .../components/pages/outputs/sql_raw.adoc | 440 ++ .../components/pages/outputs/stdout.adoc | 64 + .../components/pages/outputs/subprocess.adoc | 68 + .../components/pages/outputs/switch.adoc | 206 + .../pages/outputs/sync_response.adoc | 51 + .../components/pages/outputs/websocket.adoc | 404 ++ .../components/pages/outputs/zmq4.adoc | 105 + .../components/pages/processors/archive.adoc | 109 + .../components/pages/processors/avro.adoc | 101 + .../components/pages/processors/awk.adoc | 407 ++ .../processors/aws_dynamodb_partiql.adoc | 216 + .../pages/processors/aws_lambda.adoc | 270 ++ .../pages/processors/azure_cosmosdb.adoc | 420 ++ .../components/pages/processors/bloblang.adoc | 127 + .../pages/processors/bounds_check.adoc | 91 + .../components/pages/processors/branch.adoc | 217 + .../components/pages/processors/cache.adoc | 235 + .../components/pages/processors/cached.adoc | 162 + .../components/pages/processors/catch.adoc | 45 + .../components/pages/processors/command.adoc | 117 + .../components/pages/processors/compress.adoc | 58 + .../pages/processors/couchbase.adoc | 205 + .../pages/processors/decompress.adoc | 47 + .../components/pages/processors/dedupe.adoc | 105 + .../components/pages/processors/for_each.adoc | 30 + .../pages/processors/gcp_bigquery_select.adoc | 158 + .../components/pages/processors/grok.adoc | 156 + .../components/pages/processors/group_by.adoc | 98 + .../pages/processors/group_by_value.adoc | 68 + .../components/pages/processors/http.adoc | 822 ++++ .../pages/processors/insert_part.adoc | 55 + .../pages/processors/javascript.adoc | 228 + .../components/pages/processors/jmespath.adoc | 84 + .../components/pages/processors/jq.adoc | 139 + .../pages/processors/json_schema.adoc | 98 + .../components/pages/processors/log.adoc | 102 + .../components/pages/processors/mapping.adoc | 141 + .../components/pages/processors/metric.adoc | 187 + .../components/pages/processors/mongodb.adoc | 268 ++ .../components/pages/processors/msgpack.adoc | 49 + .../components/pages/processors/mutation.adoc | 145 + .../components/pages/processors/nats_kv.adoc | 482 ++ .../pages/processors/nats_request_reply.adoc | 487 +++ .../components/pages/processors/noop.adoc | 25 + .../components/pages/processors/parallel.adoc | 51 + .../components/pages/processors/parquet.adoc | 155 + .../pages/processors/parquet_decode.adoc | 61 + .../pages/processors/parquet_encode.adoc | 195 + .../pages/processors/parse_log.adoc | 140 + .../pages/processors/processors.adoc | 53 + .../components/pages/processors/protobuf.adoc | 198 + .../pages/processors/rate_limit.adoc | 37 + .../components/pages/processors/redis.adoc | 403 ++ .../pages/processors/redis_script.adoc | 389 ++ .../components/pages/processors/resource.adoc | 53 + .../components/pages/processors/retry.adoc | 188 + .../processors/schema_registry_decode.adoc | 429 ++ .../processors/schema_registry_encode.adoc | 482 ++ .../pages/processors/select_parts.adoc | 46 + .../pages/processors/sentry_capture.adoc | 157 + .../components/pages/processors/sleep.adoc | 38 + .../components/pages/processors/split.adoc | 52 + .../components/pages/processors/sql.adoc | 161 + .../pages/processors/sql_insert.adoc | 339 ++ .../components/pages/processors/sql_raw.adoc | 345 ++ .../pages/processors/sql_select.adoc | 356 ++ .../pages/processors/subprocess.adoc | 145 + .../components/pages/processors/switch.adoc | 104 + .../pages/processors/sync_response.adoc | 30 + .../components/pages/processors/try.adoc | 66 + .../pages/processors/unarchive.adoc | 68 + .../components/pages/processors/wasm.adoc | 60 + .../components/pages/processors/while.adoc | 107 + .../components/pages/processors/workflow.adoc | 390 ++ .../components/pages/processors/xml.adoc | 113 + .../components/pages/rate_limits/local.adoc | 47 + .../components/pages/rate_limits/redis.adoc | 312 ++ .../components/pages/scanners/avro.adoc | 72 + .../components/pages/scanners/chunker.adoc | 35 + .../components/pages/scanners/csv.adoc | 73 + .../components/pages/scanners/decompress.adoc | 46 + .../pages/scanners/json_documents.adoc | 26 + .../components/pages/scanners/lines.adoc | 45 + .../components/pages/scanners/re_match.adoc | 51 + .../components/pages/scanners/skip_bom.adoc | 37 + .../components/pages/scanners/switch.adoc | 89 + .../components/pages/scanners/tar.adoc | 32 + .../components/pages/scanners/to_the_end.adoc | 30 + .../pages/tracers/gcp_cloudtrace.adoc | 97 + .../components/pages/tracers/jaeger.adoc | 132 + .../components/pages/tracers/none.adoc | 25 + .../tracers/open_telemetry_collector.adoc | 164 + .../configuration/pages/templating.adoc | 326 ++ .../configuration/pages/unit_testing.adoc | 619 +++ .../guides/pages/bloblang/functions.adoc | 728 +++ .../guides/pages/bloblang/methods.adoc | 3893 +++++++++++++++++ internal/api/{docs.md => docs.adoc} | 65 +- internal/api/docs.go | 9 +- internal/batch/policy/docs.go | 6 +- internal/bloblang/query/functions.go | 14 +- internal/bloblang/query/methods.go | 8 +- internal/bloblang/query/methods_strings.go | 12 +- internal/bloblang/query/methods_structured.go | 16 +- internal/component/errors.go | 2 +- internal/config/test/case.go | 2 +- internal/config/test/{docs.md => docs.adoc} | 100 +- internal/config/test/docs.go | 2 +- internal/docs/benchmark_test.go | 4 +- internal/docs/bloblang_markdown.go | 91 +- internal/docs/component.go | 6 +- internal/docs/component_markdown.go | 86 +- internal/docs/field.go | 2 +- internal/docs/field_template.go | 40 +- internal/docs/metrics_mapping.go | 2 +- internal/filepath/glob.go | 2 +- internal/httpclient/config.go | 2 +- internal/impl/amqp09/input.go | 4 +- internal/impl/amqp09/output.go | 2 +- internal/impl/amqp1/config.go | 2 +- internal/impl/amqp1/input.go | 4 +- ..._description.md => input_description.adoc} | 12 +- internal/impl/amqp1/output.go | 4 +- internal/impl/avro/processor.go | 10 +- internal/impl/avro/scanner.go | 8 +- internal/impl/awk/processor.go | 88 +- internal/impl/aws/config/config.go | 4 +- internal/impl/aws/input_kinesis.go | 10 +- internal/impl/aws/input_s3.go | 22 +- internal/impl/aws/input_sqs.go | 10 +- internal/impl/aws/metrics_cloudwatch.go | 4 +- internal/impl/aws/output_dynamodb.go | 14 +- internal/impl/aws/output_kinesis.go | 6 +- internal/impl/aws/output_kinesis_firehose.go | 8 +- internal/impl/aws/output_s3.go | 18 +- internal/impl/aws/output_sns.go | 4 +- internal/impl/aws/output_sqs.go | 6 +- .../impl/aws/processor_dynamodb_partiql.go | 2 +- internal/impl/aws/processor_lambda.go | 18 +- internal/impl/azure/cosmosdb/docs.go | 26 +- internal/impl/azure/input_blob_storage.go | 16 +- internal/impl/azure/input_cosmosdb.go | 8 +- internal/impl/azure/input_table_storage.go | 4 +- internal/impl/azure/output_blob_storage.go | 6 +- internal/impl/azure/output_cosmosdb.go | 4 +- internal/impl/azure/output_queue_storage.go | 2 +- internal/impl/azure/output_table_storage.go | 2 +- internal/impl/azure/processor_cosmosdb.go | 4 +- internal/impl/cassandra/output.go | 2 +- internal/impl/changelog/bloblang.go | 4 +- internal/impl/cockroachdb/input_changefeed.go | 6 +- .../processor_schema_registry_decode.go | 17 +- .../processor_schema_registry_encode.go | 26 +- internal/impl/couchbase/client/docs.go | 2 +- internal/impl/dgraph/cache_ristretto.go | 2 +- internal/impl/discord/output.go | 4 +- internal/impl/elasticsearch/output.go | 4 +- internal/impl/gcp/input_bigquery_select.go | 4 +- internal/impl/gcp/input_cloud_storage.go | 8 +- internal/impl/gcp/input_pubsub.go | 10 +- internal/impl/gcp/output_bigquery.go | 12 +- internal/impl/gcp/output_cloud_storage.go | 14 +- internal/impl/gcp/output_pubsub.go | 10 +- .../impl/gcp/processor_bigquery_select.go | 2 +- internal/impl/gcp/tracer_cloudtrace.go | 2 +- internal/impl/hdfs/input.go | 6 +- internal/impl/hdfs/output.go | 2 +- internal/impl/io/bloblang.go | 8 +- internal/impl/io/input_csv.go | 12 +- internal/impl/io/input_dynamic.go | 12 +- internal/impl/io/input_file.go | 4 +- internal/impl/io/input_http_client.go | 8 +- internal/impl/io/input_http_server.go | 30 +- internal/impl/io/input_socket_server.go | 4 +- internal/impl/io/output_dynamic.go | 12 +- internal/impl/io/output_file.go | 2 +- internal/impl/io/output_http_client.go | 16 +- internal/impl/io/output_http_server.go | 4 +- internal/impl/io/processor_command.go | 12 +- internal/impl/io/processor_http.go | 18 +- internal/impl/io/processor_subprocess.go | 17 +- internal/impl/jaeger/tracer_jaeger.go | 2 +- internal/impl/javascript/processor.go | 14 +- internal/impl/kafka/input_kafka_franz.go | 8 +- internal/impl/kafka/input_sarama_kafka.go | 20 +- internal/impl/kafka/output_kafka_franz.go | 2 +- internal/impl/kafka/output_sarama_kafka.go | 14 +- internal/impl/kafka/sasl.go | 4 +- internal/impl/lang/bloblang.go | 8 +- internal/impl/maxmind/bloblang_geoip.go | 2 +- internal/impl/mongodb/common.go | 6 +- internal/impl/mongodb/input.go | 2 +- internal/impl/mqtt/input.go | 6 +- internal/impl/mqtt/output.go | 2 +- internal/impl/msgpack/bloblang.go | 4 +- internal/impl/msgpack/processor.go | 2 +- internal/impl/nats/auth.go | 28 +- internal/impl/nats/docs.go | 2 +- internal/impl/nats/input.go | 6 +- internal/impl/nats/input_jetstream.go | 6 +- internal/impl/nats/input_kv.go | 2 +- internal/impl/nats/input_stream.go | 14 +- internal/impl/nats/output.go | 2 +- internal/impl/nats/output_kv.go | 2 +- internal/impl/nats/output_stream.go | 8 +- internal/impl/nats/processor_kv.go | 14 +- internal/impl/nats/processor_request_reply.go | 4 +- internal/impl/nsq/input.go | 4 +- internal/impl/nsq/output.go | 2 +- internal/impl/opensearch/output.go | 4 +- internal/impl/otlp/tracer_otlp.go | 2 +- internal/impl/parquet/bloblang.go | 2 +- internal/impl/parquet/input_parquet.go | 4 +- internal/impl/parquet/processor.go | 12 +- internal/impl/parquet/processor_decode.go | 6 +- internal/impl/parquet/processor_encode.go | 4 +- .../impl/prometheus/metrics_prometheus.go | 6 +- internal/impl/protobuf/processor_protobuf.go | 12 +- internal/impl/pulsar/input.go | 6 +- internal/impl/pure/bloblang_general.go | 2 +- internal/impl/pure/bloblang_numbers.go | 6 +- internal/impl/pure/bloblang_objects.go | 2 +- internal/impl/pure/bloblang_time.go | 30 +- internal/impl/pure/buffer_memory.go | 6 +- internal/impl/pure/buffer_system_window.go | 16 +- internal/impl/pure/cache_lru.go | 2 +- internal/impl/pure/cache_ttlru.go | 2 +- internal/impl/pure/input_broker.go | 8 +- internal/impl/pure/input_generate.go | 4 +- internal/impl/pure/input_inproc.go | 2 +- internal/impl/pure/input_read_until.go | 8 +- internal/impl/pure/input_resource.go | 6 +- internal/impl/pure/input_sequence.go | 4 +- internal/impl/pure/output_broker.go | 20 +- internal/impl/pure/output_cache.go | 6 +- internal/impl/pure/output_fallback.go | 4 +- internal/impl/pure/output_inproc.go | 2 +- internal/impl/pure/output_reject.go | 2 +- internal/impl/pure/output_reject_errored.go | 4 +- internal/impl/pure/output_resource.go | 4 +- internal/impl/pure/output_retry.go | 4 +- internal/impl/pure/output_switch.go | 6 +- internal/impl/pure/output_sync_response.go | 4 +- internal/impl/pure/processor_archive.go | 6 +- internal/impl/pure/processor_bloblang.go | 14 +- internal/impl/pure/processor_branch.go | 16 +- internal/impl/pure/processor_cache.go | 32 +- internal/impl/pure/processor_cached.go | 6 +- internal/impl/pure/processor_catch.go | 6 +- internal/impl/pure/processor_dedupe.go | 14 +- internal/impl/pure/processor_for_each.go | 2 +- internal/impl/pure/processor_grok.go | 12 +- internal/impl/pure/processor_group_by.go | 10 +- .../impl/pure/processor_group_by_value.go | 6 +- internal/impl/pure/processor_insert_part.go | 2 +- internal/impl/pure/processor_jmespath.go | 10 +- internal/impl/pure/processor_jq.go | 20 +- internal/impl/pure/processor_jsonschema.go | 6 +- internal/impl/pure/processor_log.go | 8 +- internal/impl/pure/processor_mapping.go | 18 +- internal/impl/pure/processor_metric.go | 16 +- internal/impl/pure/processor_mutation.go | 18 +- internal/impl/pure/processor_parallel.go | 4 +- internal/impl/pure/processor_parse_log.go | 18 +- internal/impl/pure/processor_processors.go | 2 +- internal/impl/pure/processor_rate_limit.go | 4 +- internal/impl/pure/processor_resource.go | 2 +- internal/impl/pure/processor_retry.go | 18 +- internal/impl/pure/processor_select_parts.go | 2 +- internal/impl/pure/processor_sleep.go | 2 +- internal/impl/pure/processor_split.go | 2 +- internal/impl/pure/processor_switch.go | 12 +- internal/impl/pure/processor_sync_response.go | 4 +- internal/impl/pure/processor_try.go | 10 +- internal/impl/pure/processor_unarchive.go | 8 +- internal/impl/pure/processor_while.go | 6 +- internal/impl/pure/processor_workflow.go | 54 +- internal/impl/pure/scanner_csv.go | 2 +- internal/impl/pure/scanner_tar.go | 2 +- internal/impl/pure/scanner_to_the_end.go | 5 +- internal/impl/redis/output_hash.go | 2 +- internal/impl/redis/output_list.go | 2 +- internal/impl/redis/output_pubsub.go | 2 +- internal/impl/redis/processor.go | 8 +- internal/impl/redis/script_processor.go | 8 +- internal/impl/sentry/processor_capture.go | 4 +- internal/impl/sftp/input.go | 6 +- internal/impl/sftp/output.go | 2 +- .../impl/snowflake/output_snowflake_put.go | 83 +- internal/impl/sql/buffer_sqlite.go | 6 +- internal/impl/sql/cache_sql.go | 8 +- internal/impl/sql/conn_fields.go | 49 +- internal/impl/sql/input_sql_raw.go | 4 +- internal/impl/sql/input_sql_select.go | 4 +- internal/impl/sql/output_sql_deprecated.go | 6 +- internal/impl/sql/output_sql_insert.go | 2 +- internal/impl/sql/output_sql_raw.go | 4 +- internal/impl/sql/processor_sql_deprecated.go | 10 +- internal/impl/sql/processor_sql_insert.go | 4 +- internal/impl/sql/processor_sql_raw.go | 8 +- internal/impl/sql/processor_sql_select.go | 6 +- internal/impl/statsd/metrics_statsd.go | 2 +- internal/impl/wasm/processor_wazero.go | 8 +- internal/impl/xml/processor.go | 6 +- internal/impl/zeromq/input_zmq4.go | 2 +- internal/impl/zeromq/output_zmq4.go | 2 +- internal/log/docs.adoc | 42 + internal/log/docs.go | 2 +- internal/log/docs.md | 51 - internal/template/config.go | 4 +- internal/template/docs.adoc | 109 + internal/template/docs.go | 2 +- internal/template/docs.md | 112 - public/service/codec/scanner.go | 2 +- public/service/config_extract_tracing.go | 2 +- public/service/config_inject_tracing.go | 2 +- public/service/message.go | 4 +- public/service/output.go | 4 +- public/service/processor.go | 2 +- 462 files changed, 60284 insertions(+), 1254 deletions(-) create mode 100644 docs/antora.yml create mode 100644 docs/modules/components/pages/buffers/memory.adoc create mode 100644 docs/modules/components/pages/buffers/none.adoc create mode 100644 docs/modules/components/pages/buffers/sqlite.adoc create mode 100644 docs/modules/components/pages/buffers/system_window.adoc create mode 100644 docs/modules/components/pages/caches/aws_dynamodb.adoc create mode 100644 docs/modules/components/pages/caches/aws_s3.adoc create mode 100644 docs/modules/components/pages/caches/couchbase.adoc create mode 100644 docs/modules/components/pages/caches/file.adoc create mode 100644 docs/modules/components/pages/caches/gcp_cloud_storage.adoc create mode 100644 docs/modules/components/pages/caches/lru.adoc create mode 100644 docs/modules/components/pages/caches/memcached.adoc create mode 100644 docs/modules/components/pages/caches/memory.adoc create mode 100644 docs/modules/components/pages/caches/mongodb.adoc create mode 100644 docs/modules/components/pages/caches/multilevel.adoc create mode 100644 docs/modules/components/pages/caches/nats_kv.adoc create mode 100644 docs/modules/components/pages/caches/noop.adoc create mode 100644 docs/modules/components/pages/caches/redis.adoc create mode 100644 docs/modules/components/pages/caches/ristretto.adoc create mode 100644 docs/modules/components/pages/caches/sql.adoc create mode 100644 docs/modules/components/pages/caches/ttlru.adoc create mode 100644 docs/modules/components/pages/http/about.adoc create mode 100644 docs/modules/components/pages/inputs/amqp_0_9.adoc create mode 100644 docs/modules/components/pages/inputs/amqp_1.adoc create mode 100644 docs/modules/components/pages/inputs/aws_kinesis.adoc create mode 100644 docs/modules/components/pages/inputs/aws_s3.adoc create mode 100644 docs/modules/components/pages/inputs/aws_sqs.adoc create mode 100644 docs/modules/components/pages/inputs/azure_blob_storage.adoc create mode 100644 docs/modules/components/pages/inputs/azure_cosmosdb.adoc create mode 100644 docs/modules/components/pages/inputs/azure_queue_storage.adoc create mode 100644 docs/modules/components/pages/inputs/azure_table_storage.adoc create mode 100644 docs/modules/components/pages/inputs/batched.adoc create mode 100644 docs/modules/components/pages/inputs/beanstalkd.adoc create mode 100644 docs/modules/components/pages/inputs/broker.adoc create mode 100644 docs/modules/components/pages/inputs/cassandra.adoc create mode 100644 docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc create mode 100644 docs/modules/components/pages/inputs/csv.adoc create mode 100644 docs/modules/components/pages/inputs/discord.adoc create mode 100644 docs/modules/components/pages/inputs/dynamic.adoc create mode 100644 docs/modules/components/pages/inputs/file.adoc create mode 100644 docs/modules/components/pages/inputs/gcp_bigquery_select.adoc create mode 100644 docs/modules/components/pages/inputs/gcp_cloud_storage.adoc create mode 100644 docs/modules/components/pages/inputs/gcp_pubsub.adoc create mode 100644 docs/modules/components/pages/inputs/generate.adoc create mode 100644 docs/modules/components/pages/inputs/hdfs.adoc create mode 100644 docs/modules/components/pages/inputs/http_client.adoc create mode 100644 docs/modules/components/pages/inputs/http_server.adoc create mode 100644 docs/modules/components/pages/inputs/inproc.adoc create mode 100644 docs/modules/components/pages/inputs/kafka.adoc create mode 100644 docs/modules/components/pages/inputs/kafka_franz.adoc create mode 100644 docs/modules/components/pages/inputs/mongodb.adoc create mode 100644 docs/modules/components/pages/inputs/mqtt.adoc create mode 100644 docs/modules/components/pages/inputs/nanomsg.adoc create mode 100644 docs/modules/components/pages/inputs/nats.adoc create mode 100644 docs/modules/components/pages/inputs/nats_jetstream.adoc create mode 100644 docs/modules/components/pages/inputs/nats_kv.adoc create mode 100644 docs/modules/components/pages/inputs/nats_stream.adoc create mode 100644 docs/modules/components/pages/inputs/nsq.adoc create mode 100644 docs/modules/components/pages/inputs/parquet.adoc create mode 100644 docs/modules/components/pages/inputs/pulsar.adoc create mode 100644 docs/modules/components/pages/inputs/read_until.adoc create mode 100644 docs/modules/components/pages/inputs/redis_list.adoc create mode 100644 docs/modules/components/pages/inputs/redis_pubsub.adoc create mode 100644 docs/modules/components/pages/inputs/redis_scan.adoc create mode 100644 docs/modules/components/pages/inputs/redis_streams.adoc create mode 100644 docs/modules/components/pages/inputs/resource.adoc create mode 100644 docs/modules/components/pages/inputs/sequence.adoc create mode 100644 docs/modules/components/pages/inputs/sftp.adoc create mode 100644 docs/modules/components/pages/inputs/socket.adoc create mode 100644 docs/modules/components/pages/inputs/socket_server.adoc create mode 100644 docs/modules/components/pages/inputs/sql_raw.adoc create mode 100644 docs/modules/components/pages/inputs/sql_select.adoc create mode 100644 docs/modules/components/pages/inputs/stdin.adoc create mode 100644 docs/modules/components/pages/inputs/subprocess.adoc create mode 100644 docs/modules/components/pages/inputs/twitter_search.adoc create mode 100644 docs/modules/components/pages/inputs/websocket.adoc create mode 100644 docs/modules/components/pages/inputs/zmq4.adoc create mode 100644 docs/modules/components/pages/logger/about.adoc create mode 100644 docs/modules/components/pages/metrics/aws_cloudwatch.adoc create mode 100644 docs/modules/components/pages/metrics/influxdb.adoc create mode 100644 docs/modules/components/pages/metrics/json_api.adoc create mode 100644 docs/modules/components/pages/metrics/logger.adoc create mode 100644 docs/modules/components/pages/metrics/none.adoc create mode 100644 docs/modules/components/pages/metrics/prometheus.adoc create mode 100644 docs/modules/components/pages/metrics/statsd.adoc create mode 100644 docs/modules/components/pages/outputs/amqp_0_9.adoc create mode 100644 docs/modules/components/pages/outputs/amqp_1.adoc create mode 100644 docs/modules/components/pages/outputs/aws_dynamodb.adoc create mode 100644 docs/modules/components/pages/outputs/aws_kinesis.adoc create mode 100644 docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc create mode 100644 docs/modules/components/pages/outputs/aws_s3.adoc create mode 100644 docs/modules/components/pages/outputs/aws_sns.adoc create mode 100644 docs/modules/components/pages/outputs/aws_sqs.adoc create mode 100644 docs/modules/components/pages/outputs/azure_blob_storage.adoc create mode 100644 docs/modules/components/pages/outputs/azure_cosmosdb.adoc create mode 100644 docs/modules/components/pages/outputs/azure_queue_storage.adoc create mode 100644 docs/modules/components/pages/outputs/azure_table_storage.adoc create mode 100644 docs/modules/components/pages/outputs/beanstalkd.adoc create mode 100644 docs/modules/components/pages/outputs/broker.adoc create mode 100644 docs/modules/components/pages/outputs/cache.adoc create mode 100644 docs/modules/components/pages/outputs/cassandra.adoc create mode 100644 docs/modules/components/pages/outputs/discord.adoc create mode 100644 docs/modules/components/pages/outputs/drop.adoc create mode 100644 docs/modules/components/pages/outputs/drop_on.adoc create mode 100644 docs/modules/components/pages/outputs/dynamic.adoc create mode 100644 docs/modules/components/pages/outputs/elasticsearch.adoc create mode 100644 docs/modules/components/pages/outputs/fallback.adoc create mode 100644 docs/modules/components/pages/outputs/file.adoc create mode 100644 docs/modules/components/pages/outputs/gcp_bigquery.adoc create mode 100644 docs/modules/components/pages/outputs/gcp_cloud_storage.adoc create mode 100644 docs/modules/components/pages/outputs/gcp_pubsub.adoc create mode 100644 docs/modules/components/pages/outputs/hdfs.adoc create mode 100644 docs/modules/components/pages/outputs/http_client.adoc create mode 100644 docs/modules/components/pages/outputs/http_server.adoc create mode 100644 docs/modules/components/pages/outputs/inproc.adoc create mode 100644 docs/modules/components/pages/outputs/kafka.adoc create mode 100644 docs/modules/components/pages/outputs/kafka_franz.adoc create mode 100644 docs/modules/components/pages/outputs/mongodb.adoc create mode 100644 docs/modules/components/pages/outputs/mqtt.adoc create mode 100644 docs/modules/components/pages/outputs/nanomsg.adoc create mode 100644 docs/modules/components/pages/outputs/nats.adoc create mode 100644 docs/modules/components/pages/outputs/nats_jetstream.adoc create mode 100644 docs/modules/components/pages/outputs/nats_kv.adoc create mode 100644 docs/modules/components/pages/outputs/nats_stream.adoc create mode 100644 docs/modules/components/pages/outputs/nsq.adoc create mode 100644 docs/modules/components/pages/outputs/opensearch.adoc create mode 100644 docs/modules/components/pages/outputs/pulsar.adoc create mode 100644 docs/modules/components/pages/outputs/pusher.adoc create mode 100644 docs/modules/components/pages/outputs/redis_hash.adoc create mode 100644 docs/modules/components/pages/outputs/redis_list.adoc create mode 100644 docs/modules/components/pages/outputs/redis_pubsub.adoc create mode 100644 docs/modules/components/pages/outputs/redis_streams.adoc create mode 100644 docs/modules/components/pages/outputs/reject.adoc create mode 100644 docs/modules/components/pages/outputs/reject_errored.adoc create mode 100644 docs/modules/components/pages/outputs/resource.adoc create mode 100644 docs/modules/components/pages/outputs/retry.adoc create mode 100644 docs/modules/components/pages/outputs/sftp.adoc create mode 100644 docs/modules/components/pages/outputs/snowflake_put.adoc create mode 100644 docs/modules/components/pages/outputs/socket.adoc create mode 100644 docs/modules/components/pages/outputs/splunk_hec.adoc create mode 100644 docs/modules/components/pages/outputs/sql.adoc create mode 100644 docs/modules/components/pages/outputs/sql_insert.adoc create mode 100644 docs/modules/components/pages/outputs/sql_raw.adoc create mode 100644 docs/modules/components/pages/outputs/stdout.adoc create mode 100644 docs/modules/components/pages/outputs/subprocess.adoc create mode 100644 docs/modules/components/pages/outputs/switch.adoc create mode 100644 docs/modules/components/pages/outputs/sync_response.adoc create mode 100644 docs/modules/components/pages/outputs/websocket.adoc create mode 100644 docs/modules/components/pages/outputs/zmq4.adoc create mode 100644 docs/modules/components/pages/processors/archive.adoc create mode 100644 docs/modules/components/pages/processors/avro.adoc create mode 100644 docs/modules/components/pages/processors/awk.adoc create mode 100644 docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc create mode 100644 docs/modules/components/pages/processors/aws_lambda.adoc create mode 100644 docs/modules/components/pages/processors/azure_cosmosdb.adoc create mode 100644 docs/modules/components/pages/processors/bloblang.adoc create mode 100644 docs/modules/components/pages/processors/bounds_check.adoc create mode 100644 docs/modules/components/pages/processors/branch.adoc create mode 100644 docs/modules/components/pages/processors/cache.adoc create mode 100644 docs/modules/components/pages/processors/cached.adoc create mode 100644 docs/modules/components/pages/processors/catch.adoc create mode 100644 docs/modules/components/pages/processors/command.adoc create mode 100644 docs/modules/components/pages/processors/compress.adoc create mode 100644 docs/modules/components/pages/processors/couchbase.adoc create mode 100644 docs/modules/components/pages/processors/decompress.adoc create mode 100644 docs/modules/components/pages/processors/dedupe.adoc create mode 100644 docs/modules/components/pages/processors/for_each.adoc create mode 100644 docs/modules/components/pages/processors/gcp_bigquery_select.adoc create mode 100644 docs/modules/components/pages/processors/grok.adoc create mode 100644 docs/modules/components/pages/processors/group_by.adoc create mode 100644 docs/modules/components/pages/processors/group_by_value.adoc create mode 100644 docs/modules/components/pages/processors/http.adoc create mode 100644 docs/modules/components/pages/processors/insert_part.adoc create mode 100644 docs/modules/components/pages/processors/javascript.adoc create mode 100644 docs/modules/components/pages/processors/jmespath.adoc create mode 100644 docs/modules/components/pages/processors/jq.adoc create mode 100644 docs/modules/components/pages/processors/json_schema.adoc create mode 100644 docs/modules/components/pages/processors/log.adoc create mode 100644 docs/modules/components/pages/processors/mapping.adoc create mode 100644 docs/modules/components/pages/processors/metric.adoc create mode 100644 docs/modules/components/pages/processors/mongodb.adoc create mode 100644 docs/modules/components/pages/processors/msgpack.adoc create mode 100644 docs/modules/components/pages/processors/mutation.adoc create mode 100644 docs/modules/components/pages/processors/nats_kv.adoc create mode 100644 docs/modules/components/pages/processors/nats_request_reply.adoc create mode 100644 docs/modules/components/pages/processors/noop.adoc create mode 100644 docs/modules/components/pages/processors/parallel.adoc create mode 100644 docs/modules/components/pages/processors/parquet.adoc create mode 100644 docs/modules/components/pages/processors/parquet_decode.adoc create mode 100644 docs/modules/components/pages/processors/parquet_encode.adoc create mode 100644 docs/modules/components/pages/processors/parse_log.adoc create mode 100644 docs/modules/components/pages/processors/processors.adoc create mode 100644 docs/modules/components/pages/processors/protobuf.adoc create mode 100644 docs/modules/components/pages/processors/rate_limit.adoc create mode 100644 docs/modules/components/pages/processors/redis.adoc create mode 100644 docs/modules/components/pages/processors/redis_script.adoc create mode 100644 docs/modules/components/pages/processors/resource.adoc create mode 100644 docs/modules/components/pages/processors/retry.adoc create mode 100644 docs/modules/components/pages/processors/schema_registry_decode.adoc create mode 100644 docs/modules/components/pages/processors/schema_registry_encode.adoc create mode 100644 docs/modules/components/pages/processors/select_parts.adoc create mode 100644 docs/modules/components/pages/processors/sentry_capture.adoc create mode 100644 docs/modules/components/pages/processors/sleep.adoc create mode 100644 docs/modules/components/pages/processors/split.adoc create mode 100644 docs/modules/components/pages/processors/sql.adoc create mode 100644 docs/modules/components/pages/processors/sql_insert.adoc create mode 100644 docs/modules/components/pages/processors/sql_raw.adoc create mode 100644 docs/modules/components/pages/processors/sql_select.adoc create mode 100644 docs/modules/components/pages/processors/subprocess.adoc create mode 100644 docs/modules/components/pages/processors/switch.adoc create mode 100644 docs/modules/components/pages/processors/sync_response.adoc create mode 100644 docs/modules/components/pages/processors/try.adoc create mode 100644 docs/modules/components/pages/processors/unarchive.adoc create mode 100644 docs/modules/components/pages/processors/wasm.adoc create mode 100644 docs/modules/components/pages/processors/while.adoc create mode 100644 docs/modules/components/pages/processors/workflow.adoc create mode 100644 docs/modules/components/pages/processors/xml.adoc create mode 100644 docs/modules/components/pages/rate_limits/local.adoc create mode 100644 docs/modules/components/pages/rate_limits/redis.adoc create mode 100644 docs/modules/components/pages/scanners/avro.adoc create mode 100644 docs/modules/components/pages/scanners/chunker.adoc create mode 100644 docs/modules/components/pages/scanners/csv.adoc create mode 100644 docs/modules/components/pages/scanners/decompress.adoc create mode 100644 docs/modules/components/pages/scanners/json_documents.adoc create mode 100644 docs/modules/components/pages/scanners/lines.adoc create mode 100644 docs/modules/components/pages/scanners/re_match.adoc create mode 100644 docs/modules/components/pages/scanners/skip_bom.adoc create mode 100644 docs/modules/components/pages/scanners/switch.adoc create mode 100644 docs/modules/components/pages/scanners/tar.adoc create mode 100644 docs/modules/components/pages/scanners/to_the_end.adoc create mode 100644 docs/modules/components/pages/tracers/gcp_cloudtrace.adoc create mode 100644 docs/modules/components/pages/tracers/jaeger.adoc create mode 100644 docs/modules/components/pages/tracers/none.adoc create mode 100644 docs/modules/components/pages/tracers/open_telemetry_collector.adoc create mode 100644 docs/modules/configuration/pages/templating.adoc create mode 100644 docs/modules/configuration/pages/unit_testing.adoc create mode 100644 docs/modules/guides/pages/bloblang/functions.adoc create mode 100644 docs/modules/guides/pages/bloblang/methods.adoc rename internal/api/{docs.md => docs.adoc} (61%) rename internal/config/test/{docs.md => docs.adoc} (69%) rename internal/impl/amqp1/{input_description.md => input_description.adoc} (71%) create mode 100644 internal/log/docs.adoc delete mode 100644 internal/log/docs.md create mode 100644 internal/template/docs.adoc delete mode 100644 internal/template/docs.md diff --git a/cmd/tools/benthos_docs_gen/main.go b/cmd/tools/benthos_docs_gen/main.go index 5eec8fff73..384afedc23 100644 --- a/cmd/tools/benthos_docs_gen/main.go +++ b/cmd/tools/benthos_docs_gen/main.go @@ -31,7 +31,7 @@ func create(t, path string, resBytes []byte) { } func main() { - docsDir := "./website/docs/components" + docsDir := "./docs/modules/components/pages" flag.StringVar(&docsDir, "dir", docsDir, "The directory to write docs to") flag.Parse() @@ -63,61 +63,61 @@ func main() { func viewForDir(docsDir string) func(name string, config *service.ConfigView) { return func(name string, config *service.ConfigView) { - mdSpec, err := config.RenderDocs() + adocSpec, err := config.RenderDocs() if err != nil { panic(fmt.Sprintf("Failed to generate docs for '%v': %v", name, err)) } - create(name, path.Join(docsDir, name+".md"), mdSpec) + create(name, path.Join(docsDir, name+".adoc"), adocSpec) } } func doBloblang(dir string) { - mdSpec, err := docs.BloblangFunctionsMarkdown() + adocSpec, err := docs.BloblangFunctionsMarkdown() if err != nil { panic(fmt.Sprintf("Failed to generate docs for bloblang functions: %v", err)) } - create("bloblang functions", filepath.Join(dir, "..", "guides", "bloblang", "functions.md"), mdSpec) + create("bloblang functions", filepath.Join(dir, "../..", "guides", "pages", "bloblang", "functions.adoc"), adocSpec) - if mdSpec, err = docs.BloblangMethodsMarkdown(); err != nil { + if adocSpec, err = docs.BloblangMethodsMarkdown(); err != nil { panic(fmt.Sprintf("Failed to generate docs for bloblang methods: %v", err)) } - create("bloblang methods", filepath.Join(dir, "..", "guides", "bloblang", "methods.md"), mdSpec) + create("bloblang methods", filepath.Join(dir, "../..", "guides", "pages", "bloblang", "methods.adoc"), adocSpec) } func doTestDocs(dir string) { - mdSpec, err := test.DocsMarkdown() + adocSpec, err := test.DocsMarkdown() if err != nil { panic(fmt.Sprintf("Failed to generate docs for unit tests: %v", err)) } - create("test docs", filepath.Join(dir, "..", "configuration", "unit_testing.md"), mdSpec) + create("test docs", filepath.Join(dir, "../..", "configuration", "pages", "unit_testing.adoc"), adocSpec) } func doHTTP(dir string) { - mdSpec, err := api.DocsMarkdown() + adocSpec, err := api.DocsMarkdown() if err != nil { panic(fmt.Sprintf("Failed to generate docs for http: %v", err)) } - create("http docs", filepath.Join(dir, "http", "about.md"), mdSpec) + create("http docs", filepath.Join(dir, "http", "about.adoc"), adocSpec) } func doLogger(dir string) { - mdSpec, err := log.DocsMarkdown() + adocSpec, err := log.DocsMarkdown() if err != nil { panic(fmt.Sprintf("Failed to generate docs for logger: %v", err)) } - create("logger docs", filepath.Join(dir, "logger", "about.md"), mdSpec) + create("logger docs", filepath.Join(dir, "logger", "about.adoc"), adocSpec) } func doTemplates(dir string) { - mdSpec, err := template.DocsMarkdown() + adocSpec, err := template.DocsMarkdown() if err != nil { panic(fmt.Sprintf("Failed to generate docs for templates: %v", err)) } - create("template docs", filepath.Join(dir, "..", "configuration", "templating.md"), mdSpec) + create("template docs", filepath.Join(dir, "../..", "configuration", "pages", "templating.adoc"), adocSpec) } diff --git a/config/test/protobuf/people.yaml b/config/test/protobuf/people.yaml index 7b3a7f0806..a702612a02 100644 --- a/config/test/protobuf/people.yaml +++ b/config/test/protobuf/people.yaml @@ -34,7 +34,7 @@ tests: - content: '{"firstName":"john","lastName":"oates","age":10}' - content: '{"firstName":"daryl","lastName":"hall"}' - content: '{"firstName":"caleb","lastName":"quaye","email":"caleb@myspace.com"}' - - content: '{"firstName":"bad","lastName":"data","contains":"unrecognized fields"}' + - content: '{"firstName":"bad","lastName":"data","contains":"unrecognised fields"}' output_batches: - - json_equals: '{"firstName":"john","lastName":"oates","fullName":"john oates","age":20}' - json_equals: '{"firstName":"daryl","lastName":"hall","fullName":"daryl hall","age":10}' diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 0000000000..7b9be6de5b --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,56 @@ +name: redpanda-connect +title: Redpanda Connect +version: ~ +asciidoc: + attributes: + categories: + input: + - name: "Services" + description: "Inputs that consume from storage or message streaming services." + - name: "Network" + description: "Inputs that consume directly from low level network protocols." + - name: "AWS" + description: "Inputs that consume from Amazon Web Services products." + - name: "GCP" + description: "Inputs that consume from Google Cloud Platform services." + - name: "Azure" + description: "Inputs that consume from Microsoft Azure services." + - name: "Social" + description: "Inputs that consume from social applications and services." + - name: "Local" + description: "Inputs that consume from the local machine/filesystem." + - name: "Utility" + description: "Inputs that provide utility by generating data or combining/wrapping other inputs." + buffer: + - name: "Windowing" + description: "Buffers that provide message windowing capabilities." + - name: "Utility" + description: "Buffers that are intended for niche but general use." + processor: + - name: "Mapping" + description: "Processors that specialize in restructuring messages." + - name: "Integration" + description: "Processors that interact with external services." + - name: "Parsing" + description: "Processors that specialize in translating messages from one format to another." + - name: "Composition" + description: "Higher level processors that compose other processors and modify their behavior." + - name: "Utility" + description: "Processors that provide general utility or do not fit in another category." + output: + - name: "Services" + description: "Outputs that write to storage or message streaming services." + - name: "Network" + description: "Outputs that write directly to low level network protocols." + - name: "AWS" + description: "Outputs that write to Amazon Web Services products." + - name: "GCP" + description: "Outputs that write to Google Cloud Platform services." + - name: "Azure" + description: "Outputs that write to Microsoft Azure services." + - name: "Social" + description: "Outputs that write to social applications and services." + - name: "Local" + description: "Outputs that write to the local machine/filesystem." + - name: "Utility" + description: "Outputs that provide utility by combining/wrapping other outputs." diff --git a/docs/modules/components/pages/buffers/memory.adoc b/docs/modules/components/pages/buffers/memory.adoc new file mode 100644 index 0000000000..9e02c71b1a --- /dev/null +++ b/docs/modules/components/pages/buffers/memory.adoc @@ -0,0 +1,178 @@ += memory +:type: buffer +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores consumed messages in memory and acknowledges them at the input level. During shutdown Benthos will make a best attempt at flushing all remaining messages before exiting cleanly. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +buffer: + memory: + limit: 524288000 + batch_policy: + enabled: false + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +buffer: + memory: + limit: 524288000 + batch_policy: + enabled: false + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +This buffer is appropriate when consuming messages from inputs that do not gracefully handle back pressure and where delivery guarantees aren't critical. + +This buffer has a configurable limit, where consumption will be stopped with back pressure upstream if the total size of messages in the buffer reaches this amount. Since this calculation is only an estimate, and the real size of messages in RAM is always higher, it is recommended to set the limit significantly below the amount of RAM available. + +== Delivery guarantees + +This buffer intentionally weakens the delivery guarantees of the pipeline and therefore should never be used in places where data loss is unacceptable. + +== Batching + +It is possible to batch up messages sent from this buffer using a xref:configuration:batching.adoc#batch-policy[batch policy]. + +== Fields + +=== `limit` + +The maximum buffer size (in bytes) to allow before applying backpressure upstream. + + +*Type*: `int` + +*Default*: `524288000` + +=== `batch_policy` + +Optionally configure a policy to flush buffered messages in batches. + + +*Type*: `object` + + +=== `batch_policy.enabled` + +Whether to batch messages as they are flushed. + + +*Type*: `bool` + +*Default*: `false` + +=== `batch_policy.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batch_policy.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batch_policy.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batch_policy.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batch_policy.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/buffers/none.adoc b/docs/modules/components/pages/buffers/none.adoc new file mode 100644 index 0000000000..5293f63761 --- /dev/null +++ b/docs/modules/components/pages/buffers/none.adoc @@ -0,0 +1,29 @@ += none +:type: buffer +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Do not buffer messages. This is the default and most resilient configuration. + +```yml +# Config fields, showing default values +buffer: + none: {} +``` + +Selecting no buffer means the output layer is directly coupled with the input layer. This is the safest and lowest latency option since acknowledgements from at-least-once protocols can be propagated all the way from the output protocol to the input protocol. + +If the output layer is hit with back pressure it will propagate all the way to the input layer, and further up the data stream. If you need to relieve your pipeline of this back pressure consider using a more robust buffering solution such as Kafka before resorting to alternatives. + + diff --git a/docs/modules/components/pages/buffers/sqlite.adoc b/docs/modules/components/pages/buffers/sqlite.adoc new file mode 100644 index 0000000000..01409abac0 --- /dev/null +++ b/docs/modules/components/pages/buffers/sqlite.adoc @@ -0,0 +1,99 @@ += sqlite +:type: buffer +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores messages in an SQLite database and acknowledges them at the input level. + +```yml +# Config fields, showing default values +buffer: + sqlite: + path: "" # No default (required) + pre_processors: [] # No default (optional) + post_processors: [] # No default (optional) +``` + +Stored messages are then consumed as a stream from the database and deleted only once they are successfully sent at the output level. If the service is restarted Benthos will make a best attempt to finish delivering messages that are already read from the database, and when it starts again it will consume from the oldest message that has not yet been delivered. + +== Delivery guarantees + +Messages are not acknowledged at the input level until they have been added to the SQLite database, and they are not removed from the SQLite database until they have been successfully delivered. This means at-least-once delivery guarantees are preserved in cases where the service is shut down unexpectedly. However, since this process relies on interaction with the disk (wherever the SQLite DB is stored) these delivery guarantees are not resilient to disk corruption or loss. + +== Batching + +Messages that are logically batched at the point where they are added to the buffer will continue to be associated with that batch when they are consumed. This buffer is also more efficient when storing messages within batches, and therefore it is recommended to use batching at the input level in high-throughput use cases even if they are not required for processing. + + +== Fields + +=== `path` + +The path of the database file, which will be created if it does not already exist. + + +*Type*: `string` + + +=== `pre_processors` + +An optional list of processors to apply to messages before they are stored within the buffer. These processors are useful for compressing, archiving or otherwise reducing the data in size before it's stored on disk. + + +*Type*: `array` + + +=== `post_processors` + +An optional list of processors to apply to messages after they are consumed from the buffer. These processors are useful for undoing any compression, archiving, etc that may have been done by your `pre_processors`. + + +*Type*: `array` + + +== Examples + +[tabs] +====== +Batching for optimization:: ++ +-- + +Batching at the input level greatly increases the throughput of this buffer. If logical batches aren't needed for processing add a xref:components:processors/split.adoc[`split` processor] to the `post_processors`. + +```yaml +input: + batched: + child: + sql_select: + driver: postgres + dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable + table: footable + columns: [ '*' ] + policy: + count: 100 + period: 500ms + +buffer: + sqlite: + path: ./foo.db + post_processors: + - split: {} +``` + +-- +====== + + diff --git a/docs/modules/components/pages/buffers/system_window.adoc b/docs/modules/components/pages/buffers/system_window.adoc new file mode 100644 index 0000000000..e1766001e0 --- /dev/null +++ b/docs/modules/components/pages/buffers/system_window.adoc @@ -0,0 +1,208 @@ += system_window +:type: buffer +:status: beta +:categories: ["Windowing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Chops a stream of messages into tumbling or sliding windows of fixed temporal size, following the system clock. + +Introduced in version 3.53.0. + +```yml +# Config fields, showing default values +buffer: + system_window: + timestamp_mapping: root = now() + size: 30s # No default (required) + slide: "" + offset: "" + allowed_lateness: "" +``` + +A window is a grouping of messages that fit within a discrete measure of time following the system clock. Messages are allocated to a window either by the processing time (the time at which they're ingested) or by the event time, and this is controlled via the <>. + +In tumbling mode (default) the beginning of a window immediately follows the end of a prior window. When the buffer is initialized the first window to be created and populated is aligned against the zeroth minute of the zeroth hour of the day by default, and may therefore be open for a shorter period than the specified size. + +A window is flushed only once the system clock surpasses its scheduled end. If an <> is specified then the window will not be flushed until the scheduled end plus that length of time. + +When a message is added to a window it has a metadata field `window_end_timestamp` added to it containing the timestamp of the end of the window as an RFC3339 string. + +== Sliding windows + +Sliding windows begin from an offset of the prior windows' beginning rather than its end, and therefore messages may belong to multiple windows. In order to produce sliding windows specify a <>. + +== Back pressure + +If back pressure is applied to this buffer either due to output services being unavailable or resources being saturated, windows older than the current and last according to the system clock will be dropped in order to prevent unbounded resource usage. This means you should ensure that under the worst case scenario you have enough system memory to store two windows' worth of data at a given time (plus extra for redundancy and other services). + +If messages could potentially arrive with event timestamps in the future (according to the system clock) then you should also factor in these extra messages in memory usage estimates. + +== Delivery guarantees + +This buffer honours the transaction model within Benthos in order to ensure that messages are not acknowledged until they are either intentionally dropped or successfully delivered to outputs. However, since messages belonging to an expired window are intentionally dropped there are circumstances where not all messages entering the system will be delivered. + +When this buffer is configured with a slide duration it is possible for messages to belong to multiple windows, and therefore be delivered multiple times. In this case the first time the message is delivered it will be acked (or nacked) and subsequent deliveries of the same message will be a "best attempt". + +During graceful termination if the current window is partially populated with messages they will be nacked such that they are re-consumed the next time the service starts. + + +== Examples + +[tabs] +====== +Counting Passengers at Traffic:: ++ +-- + +Given a stream of messages relating to cars passing through various traffic lights of the form: + +```json +{ + "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", + "created_at": "2021-08-07T09:49:35Z", + "registration_plate": "AB1C DEF", + "passengers": 3 +} +``` + +We can use a window buffer in order to create periodic messages summarizing the traffic for a period of time of this form: + +```json +{ + "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", + "created_at": "2021-08-07T10:00:00Z", + "total_cars": 15, + "passengers": 43 +} +``` + +With the following config: + +```yaml +buffer: + system_window: + timestamp_mapping: root = this.created_at + size: 1h + +pipeline: + processors: + # Group messages of the window into batches of common traffic light IDs + - group_by_value: + value: '${! json("traffic_light") }' + + # Reduce each batch to a single message by deleting indexes > 0, and + # aggregate the car and passenger counts. + - mapping: | + root = if batch_index() == 0 { + { + "traffic_light": this.traffic_light, + "created_at": meta("window_end_timestamp"), + "total_cars": json("registration_plate").from_all().unique().length(), + "passengers": json("passengers").from_all().sum(), + } + } else { deleted() } +``` + +-- +====== + +== Fields + +=== `timestamp_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] applied to each message during ingestion that provides the timestamp to use for allocating it a window. By default the function `now()` is used in order to generate a fresh timestamp at the time of ingestion (the processing time), whereas this mapping can instead extract a timestamp from the message itself (the event time). + +The timestamp value assigned to `root` must either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in ISO 8601 format. If the mapping fails or provides an invalid result the message will be dropped (with logging to describe the problem). + + +*Type*: `string` + +*Default*: `"root = now()"` + +```yml +# Examples + +timestamp_mapping: root = this.created_at + +timestamp_mapping: root = meta("kafka_timestamp_unix").number() +``` + +=== `size` + +A duration string describing the size of each window. By default windows are aligned to the zeroth minute and zeroth hour on the UTC clock, meaning windows of 1 hour duration will match the turn of each hour in the day, this can be adjusted with the `offset` field. + + +*Type*: `string` + + +```yml +# Examples + +size: 30s + +size: 10m +``` + +=== `slide` + +An optional duration string describing by how much time the beginning of each window should be offset from the beginning of the previous, and therefore creates sliding windows instead of tumbling. When specified this duration must be smaller than the `size` of the window. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +slide: 30s + +slide: 10m +``` + +=== `offset` + +An optional duration string to offset the beginning of each window by, otherwise they are aligned to the zeroth minute and zeroth hour on the UTC clock. The offset cannot be a larger or equal measure to the window size or the slide. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +offset: -6h + +offset: 30m +``` + +=== `allowed_lateness` + +An optional duration string describing the length of time to wait after a window has ended before flushing it, allowing late arrivals to be included. Since this windowing buffer uses the system clock an allowed lateness can improve the matching of messages when using event time. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +allowed_lateness: 10s + +allowed_lateness: 1m +``` + + diff --git a/docs/modules/components/pages/caches/aws_dynamodb.adoc b/docs/modules/components/pages/caches/aws_dynamodb.adoc new file mode 100644 index 0000000000..4b1abb2adf --- /dev/null +++ b/docs/modules/components/pages/caches/aws_dynamodb.adoc @@ -0,0 +1,283 @@ += aws_dynamodb +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores key/value pairs as a single document in a DynamoDB table. The key is stored as a string value and used as the table hash key. The value is stored as +a binary value using the `data_key` field name. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +aws_dynamodb: + table: "" # No default (required) + hash_key: "" # No default (required) + data_key: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +aws_dynamodb: + table: "" # No default (required) + hash_key: "" # No default (required) + data_key: "" # No default (required) + consistent_read: false + default_ttl: "" # No default (optional) + ttl_key: "" # No default (optional) + retries: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +A prefix can be specified to allow multiple cache types to share a single DynamoDB table. An optional TTL duration (`ttl`) and field +(`ttl_key`) can be specified if the backing table has TTL enabled. + +Strong read consistency can be enabled using the `consistent_read` configuration field. + +== Fields + +=== `table` + +The table to store items in. + + +*Type*: `string` + + +=== `hash_key` + +The key of the table column to store item keys within. + + +*Type*: `string` + + +=== `data_key` + +The key of the table column to store item values within. + + +*Type*: `string` + + +=== `consistent_read` + +Whether to use strongly consistent reads on Get commands. + + +*Type*: `bool` + +*Default*: `false` + +=== `default_ttl` + +An optional default TTL to set for items, calculated from the moment the item is cached. A `ttl_key` must be specified in order to set item TTLs. + + +*Type*: `string` + + +=== `ttl_key` + +The column key to place the TTL value within. + + +*Type*: `string` + + +=== `retries` + +Determine time intervals and cut offs for retry attempts. + + +*Type*: `object` + + +=== `retries.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `retries.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"5s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `retries.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. + + +*Type*: `string` + +*Default*: `"30s"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/caches/aws_s3.adoc b/docs/modules/components/pages/caches/aws_s3.adoc new file mode 100644 index 0000000000..1b7428cc6f --- /dev/null +++ b/docs/modules/components/pages/caches/aws_s3.adoc @@ -0,0 +1,252 @@ += aws_s3 +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores each item in an S3 bucket as a file, where an item ID is the path of the item within the bucket. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +aws_s3: + bucket: "" # No default (required) + content_type: application/octet-stream +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +aws_s3: + bucket: "" # No default (required) + content_type: application/octet-stream + force_path_style_urls: false + retries: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +It is not possible to atomically upload S3 objects exclusively when the target does not already exist, therefore this cache is not suitable for deduplication. + +== Fields + +=== `bucket` + +The S3 bucket to store items in. + + +*Type*: `string` + + +=== `content_type` + +The content type to set for each item. + + +*Type*: `string` + +*Default*: `"application/octet-stream"` + +=== `force_path_style_urls` + +Forces the client API to use path style URLs, which helps when connecting to custom endpoints. + + +*Type*: `bool` + +*Default*: `false` + +=== `retries` + +Determine time intervals and cut offs for retry attempts. + + +*Type*: `object` + + +=== `retries.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `retries.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"5s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `retries.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. + + +*Type*: `string` + +*Default*: `"30s"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/caches/couchbase.adoc b/docs/modules/components/pages/caches/couchbase.adoc new file mode 100644 index 0000000000..feaefb9431 --- /dev/null +++ b/docs/modules/components/pages/caches/couchbase.adoc @@ -0,0 +1,157 @@ += couchbase +:type: cache +:status: experimental + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Use a Couchbase instance as a cache. + +Introduced in version 4.12.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +couchbase: + url: couchbase://localhost:11210 # No default (required) + username: "" # No default (optional) + password: "" # No default (optional) + bucket: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +couchbase: + url: couchbase://localhost:11210 # No default (required) + username: "" # No default (optional) + password: "" # No default (optional) + bucket: "" # No default (required) + collection: _default + transcoder: legacy + timeout: 15s + default_ttl: "" # No default (optional) +``` + +-- +====== + +== Fields + +=== `url` + +Couchbase connection string. + + +*Type*: `string` + + +```yml +# Examples + +url: couchbase://localhost:11210 +``` + +=== `username` + +Username to connect to the cluster. + + +*Type*: `string` + + +=== `password` + +Password to connect to the cluster. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `bucket` + +Couchbase bucket. + + +*Type*: `string` + + +=== `collection` + +Bucket collection. + + +*Type*: `string` + +*Default*: `"_default"` + +=== `transcoder` + +Couchbase transcoder to use. + + +*Type*: `string` + +*Default*: `"legacy"` + +|=== +| Option | Summary + +| `json` +| JSONTranscoder implements the default transcoding behavior and applies JSON transcoding to all values. This will apply the following behavior to the value: binary ([]byte) -> error. default -> JSON value, JSON Flags. +| `legacy` +| LegacyTranscoder implements the behavior for a backward-compatible transcoder. This transcoder implements behavior matching that of gocb v1.This will apply the following behavior to the value: binary ([]byte) -> binary bytes, Binary expectedFlags. string -> string bytes, String expectedFlags. default -> JSON value, JSON expectedFlags. +| `raw` +| RawBinaryTranscoder implements passthrough behavior of raw binary data. This transcoder does not apply any serialization. This will apply the following behavior to the value: binary ([]byte) -> binary bytes, binary expectedFlags. default -> error. +| `rawjson` +| RawJSONTranscoder implements passthrough behavior of JSON data. This transcoder does not apply any serialization. It will forward data across the network without incurring unnecessary parsing costs. This will apply the following behavior to the value: binary ([]byte) -> JSON bytes, JSON expectedFlags. string -> JSON bytes, JSON expectedFlags. default -> error. +| `rawstring` +| RawStringTranscoder implements passthrough behavior of raw string data. This transcoder does not apply any serialization. This will apply the following behavior to the value: string -> string bytes, string expectedFlags. default -> error. + +|=== + +=== `timeout` + +Operation timeout. + + +*Type*: `string` + +*Default*: `"15s"` + +=== `default_ttl` + +An optional default TTL to set for items, calculated from the moment the item is cached. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/caches/file.adoc b/docs/modules/components/pages/caches/file.adoc new file mode 100644 index 0000000000..71d1574f38 --- /dev/null +++ b/docs/modules/components/pages/caches/file.adoc @@ -0,0 +1,38 @@ += file +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores each item in a directory as a file, where an item ID is the path relative to the configured directory. + +```yml +# Config fields, showing default values +label: "" +file: + directory: "" # No default (required) +``` + +This type currently offers no form of item expiry or garbage collection, and is intended to be used for development and debugging purposes only. + +== Fields + +=== `directory` + +The directory within which to store items. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/caches/gcp_cloud_storage.adoc b/docs/modules/components/pages/caches/gcp_cloud_storage.adoc new file mode 100644 index 0000000000..6fa4da76fc --- /dev/null +++ b/docs/modules/components/pages/caches/gcp_cloud_storage.adoc @@ -0,0 +1,47 @@ += gcp_cloud_storage +:type: cache +:status: beta + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Use a Google Cloud Storage bucket as a cache. + +```yml +# Config fields, showing default values +label: "" +gcp_cloud_storage: + bucket: "" # No default (required) + content_type: "" # No default (optional) +``` + +It is not possible to atomically upload cloud storage objects exclusively when the target does not already exist, therefore this cache is not suitable for deduplication. + +== Fields + +=== `bucket` + +The Google Cloud Storage bucket to store items in. + + +*Type*: `string` + + +=== `content_type` + +Optional field to explicitly set the Content-Type. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/caches/lru.adoc b/docs/modules/components/pages/caches/lru.adoc new file mode 100644 index 0000000000..20b325e373 --- /dev/null +++ b/docs/modules/components/pages/caches/lru.adoc @@ -0,0 +1,148 @@ += lru +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores key/value pairs in a lru in-memory cache. This cache is therefore reset every time the service restarts. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +lru: + cap: 1000 + init_values: {} +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +lru: + cap: 1000 + init_values: {} + algorithm: standard + two_queues_recent_ratio: 0.25 + two_queues_ghost_ratio: 0.5 + optimistic: false +``` + +-- +====== + +This provides the lru package which implements a fixed-size thread safe LRU cache. + +It uses the package https://github.com/hashicorp/golang-lru/v2[`lru`] + +The field init_values can be used to pre-populate the memory cache with any number of key/value pairs: + +```yaml +cache_resources: + - label: foocache + lru: + cap: 1024 + init_values: + foo: bar +``` + +These values can be overridden during execution. + +== Fields + +=== `cap` + +The cache maximum capacity (number of entries) + + +*Type*: `int` + +*Default*: `1000` + +=== `init_values` + +A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +init_values: + Nickelback: "1995" + Spice Girls: "1994" + The Human League: "1977" +``` + +=== `algorithm` + +the lru cache implementation + + +*Type*: `string` + +*Default*: `"standard"` + +|=== +| Option | Summary + +| `arc` +| is an adaptive replacement cache. It tracks recent evictions as well as recent usage in both the frequent and recent caches. Its computational overhead is comparable to two_queues, but the memory overhead is linear with the size of the cache. ARC has been patented by IBM. +| `standard` +| is a simple LRU cache. It is based on the LRU implementation in groupcache +| `two_queues` +| tracks frequently used and recently used entries separately. This avoids a burst of accesses from taking out frequently used entries, at the cost of about 2x computational overhead and some extra bookkeeping. + +|=== + +=== `two_queues_recent_ratio` + +is the ratio of the two_queues cache dedicated to recently added entries that have only been accessed once. + + +*Type*: `float` + +*Default*: `0.25` + +=== `two_queues_ghost_ratio` + +is the default ratio of ghost entries kept to track entries recently evicted on two_queues cache. + + +*Type*: `float` + +*Default*: `0.5` + +=== `optimistic` + +If true, we do not lock on read/write events. The lru package is thread-safe, however the ADD operation is not atomic. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/caches/memcached.adoc b/docs/modules/components/pages/caches/memcached.adoc new file mode 100644 index 0000000000..fb87a4722a --- /dev/null +++ b/docs/modules/components/pages/caches/memcached.adoc @@ -0,0 +1,142 @@ += memcached +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to a cluster of memcached services, a prefix can be specified to allow multiple cache types to share a memcached cluster under different namespaces. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +memcached: + addresses: [] # No default (required) + prefix: "" # No default (optional) + default_ttl: 300s +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +memcached: + addresses: [] # No default (required) + prefix: "" # No default (optional) + default_ttl: 300s + retries: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s +``` + +-- +====== + +== Fields + +=== `addresses` + +A list of addresses of memcached servers to use. + + +*Type*: `array` + + +=== `prefix` + +An optional string to prefix item keys with in order to prevent collisions with similar services. + + +*Type*: `string` + + +=== `default_ttl` + +A default TTL to set for items, calculated from the moment the item is cached. + + +*Type*: `string` + +*Default*: `"300s"` + +=== `retries` + +Determine time intervals and cut offs for retry attempts. + + +*Type*: `object` + + +=== `retries.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `retries.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"5s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `retries.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. + + +*Type*: `string` + +*Default*: `"30s"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + + diff --git a/docs/modules/components/pages/caches/memory.adoc b/docs/modules/components/pages/caches/memory.adoc new file mode 100644 index 0000000000..376ade2a65 --- /dev/null +++ b/docs/modules/components/pages/caches/memory.adoc @@ -0,0 +1,117 @@ += memory +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores key/value pairs in a map held in memory. This cache is therefore reset every time the service restarts. Each item in the cache has a TTL set from the moment it was last edited, after which it will be removed during the next compaction. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +memory: + default_ttl: 5m + compaction_interval: 60s + init_values: {} +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +memory: + default_ttl: 5m + compaction_interval: 60s + init_values: {} + shards: 1 +``` + +-- +====== + +The compaction interval determines how often the cache is cleared of expired items, and this process is only triggered on writes to the cache. Access to the cache is blocked during this process. + +Item expiry can be disabled entirely by setting the `compaction_interval` to an empty string. + +The field `init_values` can be used to prepopulate the memory cache with any number of key/value pairs which are exempt from TTLs: + +```yaml +cache_resources: + - label: foocache + memory: + default_ttl: 60s + init_values: + foo: bar +``` + +These values can be overridden during execution, at which point the configured TTL is respected as usual. + +== Fields + +=== `default_ttl` + +The default TTL of each item. After this period an item will be eligible for removal during the next compaction. + + +*Type*: `string` + +*Default*: `"5m"` + +=== `compaction_interval` + +The period of time to wait before each compaction, at which point expired items are removed. This field can be set to an empty string in order to disable compactions/expiry entirely. + + +*Type*: `string` + +*Default*: `"60s"` + +=== `init_values` + +A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +init_values: + Nickelback: "1995" + Spice Girls: "1994" + The Human League: "1977" +``` + +=== `shards` + +A number of logical shards to spread keys across, increasing the shards can have a performance benefit when processing a large number of keys. + + +*Type*: `int` + +*Default*: `1` + + diff --git a/docs/modules/components/pages/caches/mongodb.adoc b/docs/modules/components/pages/caches/mongodb.adoc new file mode 100644 index 0000000000..586da9ca67 --- /dev/null +++ b/docs/modules/components/pages/caches/mongodb.adoc @@ -0,0 +1,106 @@ += mongodb +:type: cache +:status: experimental + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Use a MongoDB instance as a cache. + +Introduced in version 3.43.0. + +```yml +# Config fields, showing default values +label: "" +mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + key_field: "" # No default (required) + value_field: "" # No default (required) +``` + +== Fields + +=== `url` + +The URL of the target MongoDB server. + + +*Type*: `string` + + +```yml +# Examples + +url: mongodb://localhost:27017 +``` + +=== `database` + +The name of the target MongoDB database. + + +*Type*: `string` + + +=== `username` + +The username to connect to the database. + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +The password to connect to the database. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `collection` + +The name of the target collection. + + +*Type*: `string` + + +=== `key_field` + +The field in the document that is used as the key. + + +*Type*: `string` + + +=== `value_field` + +The field in the document that is used as the value. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/caches/multilevel.adoc b/docs/modules/components/pages/caches/multilevel.adoc new file mode 100644 index 0000000000..88addf868c --- /dev/null +++ b/docs/modules/components/pages/caches/multilevel.adoc @@ -0,0 +1,65 @@ += multilevel +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Combines multiple caches as levels, performing read-through and write-through operations across them. + +```yml +# Config fields, showing default values +label: "" +multilevel: [] # No default (required) +``` + +== Examples + +[tabs] +====== +Hot and cold cache:: ++ +-- + +The multilevel cache is useful for reducing traffic against a remote cache by routing it through a local cache. In the following example requests will only go through to the memcached server if the local memory cache is missing the key. + +```yaml +pipeline: + processors: + - branch: + processors: + - cache: + resource: leveled + operator: get + key: ${! json("key") } + - catch: + - mapping: 'root = {"err":error()}' + result_map: 'root.result = this' + +cache_resources: + - label: leveled + multilevel: [ hot, cold ] + + - label: hot + memory: + default_ttl: 60s + + - label: cold + memcached: + addresses: [ TODO:11211 ] + default_ttl: 60s +``` + +-- +====== + + diff --git a/docs/modules/components/pages/caches/nats_kv.adoc b/docs/modules/components/pages/caches/nats_kv.adoc new file mode 100644 index 0000000000..ee9e4a7812 --- /dev/null +++ b/docs/modules/components/pages/caches/nats_kv.adoc @@ -0,0 +1,364 @@ += nats_kv +:type: cache +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Cache key/values in a NATS key-value bucket. + +Introduced in version 4.27.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) +``` + +-- +====== + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `bucket` + +The name of the KV bucket. + + +*Type*: `string` + + +```yml +# Examples + +bucket: my_kv_bucket +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/caches/noop.adoc b/docs/modules/components/pages/caches/noop.adoc new file mode 100644 index 0000000000..1fb794e0a3 --- /dev/null +++ b/docs/modules/components/pages/caches/noop.adoc @@ -0,0 +1,27 @@ += noop +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Noop is a cache that stores nothing, all gets returns not found. Why? Sometimes doing nothing is the braver option. + +Introduced in version 4.27.0. + +```yml +# Config fields, showing default values +label: "" +noop: {} +``` + + diff --git a/docs/modules/components/pages/caches/redis.adoc b/docs/modules/components/pages/caches/redis.adoc new file mode 100644 index 0000000000..5920f63e59 --- /dev/null +++ b/docs/modules/components/pages/caches/redis.adoc @@ -0,0 +1,360 @@ += redis +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Use a Redis instance as a cache. The expiration can be set to zero or an empty string in order to set no expiration. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +redis: + url: redis://:6397 # No default (required) + prefix: "" # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +redis: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + prefix: "" # No default (optional) + default_ttl: "" # No default (optional) + retries: + initial_interval: 500ms + max_interval: 1s + max_elapsed_time: 5s +``` + +-- +====== + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `prefix` + +An optional string to prefix item keys with in order to prevent collisions with similar services. + + +*Type*: `string` + + +=== `default_ttl` + +An optional default TTL to set for items, calculated from the moment the item is cached. + + +*Type*: `string` + + +=== `retries` + +Determine time intervals and cut offs for retry attempts. + + +*Type*: `object` + + +=== `retries.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"500ms"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `retries.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `retries.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. + + +*Type*: `string` + +*Default*: `"5s"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + + diff --git a/docs/modules/components/pages/caches/ristretto.adoc b/docs/modules/components/pages/caches/ristretto.adoc new file mode 100644 index 0000000000..4a81b380b0 --- /dev/null +++ b/docs/modules/components/pages/caches/ristretto.adoc @@ -0,0 +1,142 @@ += ristretto +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores key/value pairs in a map held in the memory-bound https://github.com/dgraph-io/ristretto[Ristretto cache]. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +ristretto: + default_ttl: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +ristretto: + default_ttl: "" + get_retries: + enabled: false + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s +``` + +-- +====== + +This cache is more efficient and appropriate for high-volume use cases than the standard memory cache. However, the add command is non-atomic, and therefore this cache is not suitable for deduplication. + +== Fields + +=== `default_ttl` + +A default TTL to set for items, calculated from the moment the item is cached. Set to an empty string or zero duration to disable TTLs. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +default_ttl: 5m + +default_ttl: 60s +``` + +=== `get_retries` + +Determines how and whether get attempts should be retried if the key is not found. Ristretto is a concurrent cache that does not immediately reflect writes, and so it can sometimes be useful to enable retries at the cost of speed in cases where the key is expected to exist. + + +*Type*: `object` + + +=== `get_retries.enabled` + +Whether retries should be enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `get_retries.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `get_retries.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"5s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `get_retries.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. + + +*Type*: `string` + +*Default*: `"30s"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + + diff --git a/docs/modules/components/pages/caches/sql.adoc b/docs/modules/components/pages/caches/sql.adoc new file mode 100644 index 0000000000..12a7555d03 --- /dev/null +++ b/docs/modules/components/pages/caches/sql.adoc @@ -0,0 +1,321 @@ += sql +:type: cache +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Uses an SQL database table as a destination for storing cache key/value items. + +Introduced in version 4.26.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +sql: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + key_column: foo # No default (required) + value_column: bar # No default (required) + set_suffix: ON DUPLICATE KEY UPDATE bar=VALUES(bar) # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +sql: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + key_column: foo # No default (required) + value_column: bar # No default (required) + set_suffix: ON DUPLICATE KEY UPDATE bar=VALUES(bar) # No default (optional) + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) +``` + +-- +====== + +Each cache key/value pair will exist as a row within the specified table. Currently only the key and value columns are set, and therefore any other columns present within the target table must allow NULL values if this cache is going to be used for set and add operations. + +Cache operations are translated into SQL statements as follows: + +== Get + +All `get` operations are performed with a traditional `select` statement. + +== Delete + +All `delete` operations are performed with a traditional `delete` statement. + +== Set + +The `set` operation is performed with a traditional `insert` statement. + +This will behave as an `add` operation by default, and so ideally needs to be adapted in order to provide updates instead of failing on collision s. Since different SQL engines implement upserts differently it is necessary to specify a `set_suffix` that modifies an `insert` statement in order to perform updates on conflict. + +== Add + +The `add` operation is performed with a traditional `insert` statement. + + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `table` + +The table to insert/read/delete cache items. + + +*Type*: `string` + + +```yml +# Examples + +table: foo +``` + +=== `key_column` + +The name of a column to be used for storing cache item keys. This column should support strings of arbitrary size. + + +*Type*: `string` + + +```yml +# Examples + +key_column: foo +``` + +=== `value_column` + +The name of a column to be used for storing cache item values. This column should support strings of arbitrary size. + + +*Type*: `string` + + +```yml +# Examples + +value_column: bar +``` + +=== `set_suffix` + +An optional suffix to append to each insert query for a cache `set` operation. This should modify an insert statement into an upsert appropriate for the given SQL engine. + + +*Type*: `string` + + +```yml +# Examples + +set_suffix: ON DUPLICATE KEY UPDATE bar=VALUES(bar) + +set_suffix: ON CONFLICT (foo) DO UPDATE SET bar=excluded.bar + +set_suffix: ON CONFLICT (foo) DO NOTHING +``` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/caches/ttlru.adoc b/docs/modules/components/pages/caches/ttlru.adoc new file mode 100644 index 0000000000..2f9ef4df39 --- /dev/null +++ b/docs/modules/components/pages/caches/ttlru.adoc @@ -0,0 +1,130 @@ += ttlru +:type: cache +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores key/value pairs in a ttlru in-memory cache. This cache is therefore reset every time the service restarts. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +ttlru: + cap: 1024 + default_ttl: 5m0s + init_values: {} +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +ttlru: + cap: 1024 + default_ttl: 5m0s + ttl: "" # No default (optional) + init_values: {} + optimistic: false +``` + +-- +====== + +The cache ttlru provides a simple, goroutine safe, cache with a fixed number of entries. Each entry has a per-cache defined TTL. + +This TTL is reset on both modification and access of the value. As a result, if the cache is full, and no items have expired, when adding a new item, the item with the soonest expiration will be evicted. + +It uses the package https://github.com/hashicorp/golang-lru/v2/expirable[`expirable`] + +The field init_values can be used to pre-populate the memory cache with any number of key/value pairs: + +```yaml +cache_resources: + - label: foocache + ttlru: + default_ttl: '5m' + cap: 1024 + init_values: + foo: bar +``` + +These values can be overridden during execution. + +== Fields + +=== `cap` + +The cache maximum capacity (number of entries) + + +*Type*: `int` + +*Default*: `1024` + +=== `default_ttl` + +The cache ttl of each element + + +*Type*: `string` + +*Default*: `"5m0s"` +Requires version 4.21.0 or newer + +=== `ttl` + +Deprecated. Please use `default_ttl` field + + +*Type*: `string` + + +=== `init_values` + +A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +init_values: + Nickelback: "1995" + Spice Girls: "1994" + The Human League: "1977" +``` + +=== `optimistic` + +If true, we do not lock on read/write events. The ttlru package is thread-safe, however the ADD operation is not atomic. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/http/about.adoc b/docs/modules/components/pages/http/about.adoc new file mode 100644 index 0000000000..bca8ca9157 --- /dev/null +++ b/docs/modules/components/pages/http/about.adoc @@ -0,0 +1,269 @@ += HTTP + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/api/docs.adoc +//// + +When {page-component-title} runs it kicks off an HTTP server that provides a few generally useful endpoints and is also where configured components such as the xref:components:inputs/http_server.adoc[`http_server` input] xref:components:outputs/http_server.adoc[and output] can register their own endpoints if they don't require their own host/port. + +The configuration for this server lives under the `http` namespace, with the following default values: + + + +[tabs] +====== +Common:: ++ +-- + +```yaml +# Common config fields, showing default values + +http: + address: 0.0.0.0:4195 + enabled: true + root_path: /benthos + debug_endpoints: false +``` + +-- +Advanced:: ++ +-- + +```yaml +# All config fields, showing default values + +http: + address: 0.0.0.0:4195 + enabled: true + root_path: /benthos + debug_endpoints: false + cert_file: "" + key_file: "" + cors: + enabled: false + allowed_origins: [] + basic_auth: + enabled: false + username: "" + password_hash: "" + algorithm: "sha256" + salt: "" +``` +-- +====== +The field `enabled` can be set to `false` in order to disable the server. + +The field `root_path` specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a `root_path` set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`. + +== Enabling HTTPS + +By default {page-component-title} will serve traffic over HTTP. In order to enforce TLS and serve traffic exclusively over HTTPS you must provide a `cert_file` and `key_file` path in your config, which point to a file containing a certificate and a matching private key for the server respectively. + +If the certificate is signed by a certificate authority, the `cert_file` should be the concatenation of the server's certificate, any intermediates, and the CA's certificate. + +== Enabling basic authentication + +By default {page-component-title} does not do any sort of authentication for the service-wide HTTP server. However, it's possible to configure basic authentication with the <> field. Passwords configured must be hashed according to the specified algorithm and base64 encoded, for some hashing algorithms you can do this using {page-component-title} itself: + +```sh +echo mynewpassword | benthos blobl 'root = content().hash("sha256").encode("base64")' +``` + +== Endpoints + +The following endpoints will be generally available when the HTTP server is enabled: + +- `/version` provides version info. +- `/ping` can be used as a liveness probe as it always returns a 200. +- `/ready` can be used as a readiness probe as it serves a 200 only when both the input and output are connected, otherwise a 503 is returned. +- `/metrics`, `/stats` both provide metrics when the metrics type is either xref:components:metrics/json_api.adoc[`json_api`] or xref:components:metrics/prometheus.adoc[`prometheus`]. +- `/endpoints` provides a JSON object containing a list of available endpoints, including those registered by configured components. + +== CORS + +In order to serve Cross-Origin Resource Sharing headers, which instruct browsers to allow CORS requests, set the subfield `cors.enabled` to `true`. + +=== allowed_origins + +A list of allowed origins to connect from. The literal value `*` can be specified as a wildcard. Note `cors.enabled` must be set to `true` for this list to take effect. + +== Debug endpoints + +The field `debug_endpoints` when set to `true` prompts {page-component-title} to register a few extra endpoints that can be useful for debugging performance or behavioral problems: + +- `/debug/config/json` returns the loaded config as JSON. +- `/debug/config/yaml` returns the loaded config as YAML. +- `/debug/pprof/block` responds with a pprof-formatted block profile. +- `/debug/pprof/heap` responds with a pprof-formatted heap profile. +- `/debug/pprof/mutex` responds with a pprof-formatted mutex profile. +- `/debug/pprof/profile` responds with a pprof-formatted cpu profile. +- `/debug/pprof/goroutine` responds with a pprof-formatted goroutine profile. +- `/debug/pprof/symbol` looks up the program counters listed in the request, responding with a table mapping program counters to function names. +- `/debug/pprof/trace` responds with the execution trace in binary form. Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified. +- `/debug/stack` returns a snapshot of the current service stack trace. + +== Fields + +The schema of the `http` section is as follows: + +=== `enabled` + +Whether to enable to HTTP server. + + +*Type*: `bool` + +*Default*: `true` + +=== `address` + +The address to bind to. + + +*Type*: `string` + +*Default*: `"0.0.0.0:4195"` + +=== `root_path` + +Specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a root_path set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`. + + +*Type*: `string` + +*Default*: `"/benthos"` + +=== `debug_endpoints` + +Whether to register a few extra endpoints that can be useful for debugging performance or behavioral problems. + + +*Type*: `bool` + +*Default*: `false` + +=== `cert_file` + +An optional certificate file for enabling TLS. + + +*Type*: `string` + +*Default*: `""` + +=== `key_file` + +An optional key file for enabling TLS. + + +*Type*: `string` + +*Default*: `""` + +=== `cors` + +Adds Cross-Origin Resource Sharing headers. + + +*Type*: `object` + +Requires version 3.63.0 or newer + +=== `cors.enabled` + +Whether to allow CORS requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `cors.allowed_origins` + +An explicit list of origins that are allowed for CORS requests. + + +*Type*: list of `string` + +*Default*: `[]` + +=== `basic_auth` + +Allows you to enforce and customise basic authentication for requests to the HTTP server. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Enable basic authentication + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.realm` + +Custom realm name + + +*Type*: `string` + +*Default*: `"restricted"` + +=== `basic_auth.username` + +Username required to authenticate. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password_hash` + +Hashed password required to authenticate. (base64 encoded) + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.algorithm` + +Encryption algorithm used to generate `password_hash`. + + +*Type*: `string` + +*Default*: `"sha256"` + +```yml +# Examples + +algorithm: md5 + +algorithm: sha256 + +algorithm: bcrypt + +algorithm: scrypt +``` + +=== `basic_auth.salt` + +Salt for scrypt algorithm. (base64 encoded) + + +*Type*: `string` + +*Default*: `""` + diff --git a/docs/modules/components/pages/inputs/amqp_0_9.adoc b/docs/modules/components/pages/inputs/amqp_0_9.adoc new file mode 100644 index 0000000000..ae9f624a5d --- /dev/null +++ b/docs/modules/components/pages/inputs/amqp_0_9.adoc @@ -0,0 +1,417 @@ += amqp_0_9 +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to an AMQP (0.91) queue. AMQP is a messaging protocol used by various message brokers, including RabbitMQ. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + amqp_0_9: + urls: [] # No default (required) + queue: "" # No default (required) + consumer_tag: "" + prefetch_count: 10 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + amqp_0_9: + urls: [] # No default (required) + queue: "" # No default (required) + queue_declare: + enabled: false + durable: true + auto_delete: false + bindings_declare: [] # No default (optional) + consumer_tag: "" + auto_ack: false + nack_reject_patterns: [] + prefetch_count: 10 + prefetch_size: 0 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] +``` + +-- +====== + +TLS is automatic when connecting to an `amqps` URL, but custom settings can be enabled in the `tls` section. + +== Metadata + +This input adds the following metadata fields to each message: + +``` text +- amqp_content_type +- amqp_content_encoding +- amqp_delivery_mode +- amqp_priority +- amqp_correlation_id +- amqp_reply_to +- amqp_expiration +- amqp_message_id +- amqp_timestamp +- amqp_type +- amqp_user_id +- amqp_app_id +- amqp_consumer_tag +- amqp_delivery_tag +- amqp_redelivered +- amqp_exchange +- amqp_routing_key +- All existing message headers, including nested headers prefixed with the key of their respective parent. +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolations]. + +== Fields + +=== `urls` + +A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + +Requires version 3.58.0 or newer + +```yml +# Examples + +urls: + - amqp://guest:guest@127.0.0.1:5672/ + +urls: + - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ + +urls: + - amqp://127.0.0.1:5672/ + - amqp://127.0.0.2:5672/ +``` + +=== `queue` + +An AMQP queue to consume from. + + +*Type*: `string` + + +=== `queue_declare` + +Allows you to passively declare the target queue. If the queue already exists then the declaration passively verifies that they match the target fields. + + +*Type*: `object` + + +=== `queue_declare.enabled` + +Whether to enable queue declaration. + + +*Type*: `bool` + +*Default*: `false` + +=== `queue_declare.durable` + +Whether the declared queue is durable. + + +*Type*: `bool` + +*Default*: `true` + +=== `queue_declare.auto_delete` + +Whether the declared queue will auto-delete. + + +*Type*: `bool` + +*Default*: `false` + +=== `bindings_declare` + +Allows you to passively declare bindings for the target queue. + + +*Type*: `array` + + +```yml +# Examples + +bindings_declare: + - exchange: foo + key: bar +``` + +=== `bindings_declare[].exchange` + +The exchange of the declared binding. + + +*Type*: `string` + +*Default*: `""` + +=== `bindings_declare[].key` + +The key of the declared binding. + + +*Type*: `string` + +*Default*: `""` + +=== `consumer_tag` + +A consumer tag. + + +*Type*: `string` + +*Default*: `""` + +=== `auto_ack` + +Acknowledge messages automatically as they are consumed rather than waiting for acknowledgments from downstream. This can improve throughput and prevent the pipeline from blocking but at the cost of eliminating delivery guarantees. + + +*Type*: `bool` + +*Default*: `false` + +=== `nack_reject_patterns` + +A list of regular expression patterns whereby if a message that has failed to be delivered by Benthos has an error that matches it will be dropped (or delivered to a dead-letter queue if one exists). By default failed messages are nacked with requeue enabled. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.64.0 or newer + +```yml +# Examples + +nack_reject_patterns: + - ^reject me please:.+$ +``` + +=== `prefetch_count` + +The maximum number of pending messages to have consumed at a time. + + +*Type*: `int` + +*Default*: `10` + +=== `prefetch_size` + +The maximum amount of pending messages measured in bytes to have consumed at a time. + + +*Type*: `int` + +*Default*: `0` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + + diff --git a/docs/modules/components/pages/inputs/amqp_1.adoc b/docs/modules/components/pages/inputs/amqp_1.adoc new file mode 100644 index 0000000000..dd50a091b5 --- /dev/null +++ b/docs/modules/components/pages/inputs/amqp_1.adoc @@ -0,0 +1,395 @@ += amqp_1 +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads messages from an AMQP (1.0) server. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + amqp_1: + urls: [] # No default (optional) + source_address: /foo # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + amqp_1: + urls: [] # No default (optional) + source_address: /foo # No default (required) + azure_renew_lock: false + read_header: false + credit: 64 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + sasl: + mechanism: none + user: "" + password: "" +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- amqp_content_type +- amqp_content_encoding +- amqp_creation_time +- All string typed message annotations +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +By setting `read_header` to `true`, additional message header properties will be added to each message: + +```text +- amqp_durable +- amqp_priority +- amqp_ttl +- amqp_first_acquirer +- amqp_delivery_count +``` + +== Performance + +This input benefits from receiving multiple messages in flight in parallel for improved performance. +You can tune the max number of in flight messages with the field `credit`. + + +== Fields + +=== `urls` + +A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + +Requires version 4.23.0 or newer + +```yml +# Examples + +urls: + - amqp://guest:guest@127.0.0.1:5672/ + +urls: + - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ + +urls: + - amqp://127.0.0.1:5672/ + - amqp://127.0.0.2:5672/ +``` + +=== `source_address` + +The source address to consume from. + + +*Type*: `string` + + +```yml +# Examples + +source_address: /foo + +source_address: queue:/bar + +source_address: topic:/baz +``` + +=== `azure_renew_lock` + +Experimental: Azure service bus specific option to renew lock if processing takes more then configured lock time + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `read_header` + +Read additional message header fields into `amqp_*` metadata properties. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.25.0 or newer + +=== `credit` + +Specifies the maximum number of unacknowledged messages the sender can transmit. Once this limit is reached, no more messages will arrive until messages are acknowledged and settled. + + +*Type*: `int` + +*Default*: `64` +Requires version 4.26.0 or newer + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `sasl` + +Enables SASL authentication. + + +*Type*: `object` + + +=== `sasl.mechanism` + +The SASL authentication mechanism to use. + + +*Type*: `string` + +*Default*: `"none"` + +|=== +| Option | Summary + +| `anonymous` +| Anonymous SASL authentication. +| `none` +| No SASL based authentication. +| `plain` +| Plain text SASL authentication. + +|=== + +=== `sasl.user` + +A SASL plain text username. It is recommended that you use environment variables to populate this field. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +user: ${USER} +``` + +=== `sasl.password` + +A SASL plain text password. It is recommended that you use environment variables to populate this field. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: ${PASSWORD} +``` + + diff --git a/docs/modules/components/pages/inputs/aws_kinesis.adoc b/docs/modules/components/pages/inputs/aws_kinesis.adoc new file mode 100644 index 0000000000..26ab42a668 --- /dev/null +++ b/docs/modules/components/pages/inputs/aws_kinesis.adoc @@ -0,0 +1,438 @@ += aws_kinesis +:type: input +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Receive messages from one or more Kinesis streams. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + aws_kinesis: + streams: [] # No default (required) + dynamodb: + table: "" + create: false + checkpoint_limit: 1024 + auto_replay_nacks: true + commit_period: 5s + start_from_oldest: true + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + aws_kinesis: + streams: [] # No default (required) + dynamodb: + table: "" + create: false + billing_mode: PAY_PER_REQUEST + read_capacity_units: 0 + write_capacity_units: 0 + checkpoint_limit: 1024 + auto_replay_nacks: true + commit_period: 5s + rebalance_period: 30s + lease_period: 30s + start_from_oldest: true + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Consumes messages from one or more Kinesis streams either by automatically balancing shards across other instances of this input, or by consuming shards listed explicitly. The latest message sequence consumed by this input is stored within a <>, which allows it to resume at the correct sequence of the shard during restarts. This table is also used for coordination across distributed inputs when shard balancing. + +Benthos will not store a consumed sequence unless it is acknowledged at the output level, which ensures at-least-once delivery guarantees. + +== Ordering + +By default messages of a shard can be processed in parallel, up to a limit determined by the field `checkpoint_limit`. However, if strict ordered processing is required then this value must be set to 1 in order to process shard messages in lock-step. When doing so it is recommended that you perform batching at this component for performance as it will not be possible to batch lock-stepped messages at the output level. + +== Table schema + +It's possible to configure Benthos to create the DynamoDB table required for coordination if it does not already exist. However, if you wish to create this yourself (recommended) then create a table with a string HASH key `StreamID` and a string RANGE key `ShardID`. + +== Batching + +Use the `batching` fields to configure an optional xref:configuration:batching.adoc#batch-policy[batching policy]. Each stream shard will be batched separately in order to ensure that acknowledgements aren't contaminated. + + +== Fields + +=== `streams` + +One or more Kinesis data streams to consume from. Streams can either be specified by their name or full ARN. Shards of a stream are automatically balanced across consumers by coordinating through the provided DynamoDB table. Multiple comma separated streams can be listed in a single element. Shards are automatically distributed across consumers of a stream by coordinating through the provided DynamoDB table. Alternatively, it's possible to specify an explicit shard to consume from with a colon after the stream name, e.g. `foo:0` would consume the shard `0` of the stream `foo`. + + +*Type*: `array` + + +```yml +# Examples + +streams: + - foo + - arn:aws:kinesis:*:111122223333:stream/my-stream +``` + +=== `dynamodb` + +Determines the table used for storing and accessing the latest consumed sequence for shards, and for coordinating balanced consumers of streams. + + +*Type*: `object` + + +=== `dynamodb.table` + +The name of the table to access. + + +*Type*: `string` + +*Default*: `""` + +=== `dynamodb.create` + +Whether, if the table does not exist, it should be created. + + +*Type*: `bool` + +*Default*: `false` + +=== `dynamodb.billing_mode` + +When creating the table determines the billing mode. + + +*Type*: `string` + +*Default*: `"PAY_PER_REQUEST"` + +Options: +`PROVISIONED` +, `PAY_PER_REQUEST` +. + +=== `dynamodb.read_capacity_units` + +Set the provisioned read capacity when creating the table with a `billing_mode` of `PROVISIONED`. + + +*Type*: `int` + +*Default*: `0` + +=== `dynamodb.write_capacity_units` + +Set the provisioned write capacity when creating the table with a `billing_mode` of `PROVISIONED`. + + +*Type*: `int` + +*Default*: `0` + +=== `checkpoint_limit` + +The maximum gap between the in flight sequence versus the latest acknowledged sequence at a given time. Increasing this limit enables parallel processing and batching at the output level to work on individual shards. Any given sequence will not be committed unless all messages under that offset are delivered in order to preserve at least once delivery guarantees. + + +*Type*: `int` + +*Default*: `1024` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `commit_period` + +The period of time between each update to the checkpoint table. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `rebalance_period` + +The period of time between each attempt to rebalance shards across clients. + + +*Type*: `string` + +*Default*: `"30s"` + +=== `lease_period` + +The period of time after which a client that has failed to update a shard checkpoint is assumed to be inactive. + + +*Type*: `string` + +*Default*: `"30s"` + +=== `start_from_oldest` + +Whether to consume from the oldest message when a sequence does not yet exist for the stream. + + +*Type*: `bool` + +*Default*: `true` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/inputs/aws_s3.adoc b/docs/modules/components/pages/inputs/aws_s3.adoc new file mode 100644 index 0000000000..02dfd2e679 --- /dev/null +++ b/docs/modules/components/pages/inputs/aws_s3.adoc @@ -0,0 +1,356 @@ += aws_s3 +:type: input +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Downloads objects within an Amazon S3 bucket, optionally filtered by a prefix, either by walking the items in the bucket or by streaming upload notifications in realtime. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + aws_s3: + bucket: "" + prefix: "" + scanner: + to_the_end: {} + sqs: + url: "" + key_path: Records.*.s3.object.key + bucket_path: Records.*.s3.bucket.name + envelope_path: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + aws_s3: + bucket: "" + prefix: "" + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + force_path_style_urls: false + delete_objects: false + scanner: + to_the_end: {} + sqs: + url: "" + endpoint: "" + key_path: Records.*.s3.object.key + bucket_path: Records.*.s3.bucket.name + envelope_path: "" + delay_period: "" + max_messages: 10 + wait_time_seconds: 0 +``` + +-- +====== + +== Stream objects on upload with SQS + +A common pattern for consuming S3 objects is to emit upload notification events from the bucket either directly to an SQS queue, or to an SNS topic that is consumed by an SQS queue, and then have your consumer listen for events which prompt it to download the newly uploaded objects. More information about this pattern and how to set it up can be found at: https://docs.aws.amazon.com/AmazonS3/latest/dev/ways-to-add-notification-config-to-bucket.html. + +Benthos is able to follow this pattern when you configure an `sqs.url`, where it consumes events from SQS and only downloads object keys received within those events. In order for this to work Benthos needs to know where within the event the key and bucket names can be found, specified as xref:configuration:field_paths.adoc[dot paths] with the fields `sqs.key_path` and `sqs.bucket_path`. The default values for these fields should already be correct when following the guide above. + +If your notification events are being routed to SQS via an SNS topic then the events will be enveloped by SNS, in which case you also need to specify the field `sqs.envelope_path`, which in the case of SNS to SQS will usually be `Message`. + +When using SQS please make sure you have sensible values for `sqs.max_messages` and also the visibility timeout of the queue itself. When Benthos consumes an S3 object the SQS message that triggered it is not deleted until the S3 object has been sent onwards. This ensures at-least-once crash resiliency, but also means that if the S3 object takes longer to process than the visibility timeout of your queue then the same objects might be processed multiple times. + +== Download large files + +When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a <> can be specified that determines how to break the input into smaller individual messages. + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Metadata + +This input adds the following metadata fields to each message: + +``` +- s3_key +- s3_bucket +- s3_last_modified_unix +- s3_last_modified (RFC3339) +- s3_content_type +- s3_content_encoding +- s3_version_id +- All user defined metadata +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. Note that user defined metadata is case insensitive within AWS, and it is likely that the keys will be received in a capitalized form, if you wish to make them consistent you can map all metadata keys to lower or uppercase using a Bloblang mapping such as `meta = meta().map_each_key(key -> key.lowercase())`. + +== Fields + +=== `bucket` + +The bucket to consume from. If the field `sqs.url` is specified this field is optional. + + +*Type*: `string` + +*Default*: `""` + +=== `prefix` + +An optional path prefix, if set only objects with the prefix are consumed when walking a bucket. + + +*Type*: `string` + +*Default*: `""` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `force_path_style_urls` + +Forces the client API to use path style URLs for downloading keys, which is often required when connecting to custom endpoints. + + +*Type*: `bool` + +*Default*: `false` + +=== `delete_objects` + +Whether to delete downloaded objects from the bucket once they are processed. + + +*Type*: `bool` + +*Default*: `false` + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"to_the_end":{}}` +Requires version 4.25.0 or newer + +=== `sqs` + +Consume SQS messages in order to trigger key downloads. + + +*Type*: `object` + + +=== `sqs.url` + +An optional SQS URL to connect to. When specified this queue will control which objects are downloaded. + + +*Type*: `string` + +*Default*: `""` + +=== `sqs.endpoint` + +A custom endpoint to use when connecting to SQS. + + +*Type*: `string` + +*Default*: `""` + +=== `sqs.key_path` + +A xref:configuration:field_paths.adoc[dot path] whereby object keys are found in SQS messages. + + +*Type*: `string` + +*Default*: `"Records.*.s3.object.key"` + +=== `sqs.bucket_path` + +A xref:configuration:field_paths.adoc[dot path] whereby the bucket name can be found in SQS messages. + + +*Type*: `string` + +*Default*: `"Records.*.s3.bucket.name"` + +=== `sqs.envelope_path` + +A xref:configuration:field_paths.adoc[dot path] of a field to extract an enveloped JSON payload for further extracting the key and bucket from SQS messages. This is specifically useful when subscribing an SQS queue to an SNS topic that receives bucket events. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +envelope_path: Message +``` + +=== `sqs.delay_period` + +An optional period of time to wait from when a notification was originally sent to when the target key download is attempted. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +delay_period: 10s + +delay_period: 5m +``` + +=== `sqs.max_messages` + +The maximum number of SQS messages to consume from each request. + + +*Type*: `int` + +*Default*: `10` + +=== `sqs.wait_time_seconds` + +Whether to set the wait time. Enabling this activates long-polling. Valid values: 0 to 20. + + +*Type*: `int` + +*Default*: `0` + + diff --git a/docs/modules/components/pages/inputs/aws_sqs.adoc b/docs/modules/components/pages/inputs/aws_sqs.adoc new file mode 100644 index 0000000000..ddf193969b --- /dev/null +++ b/docs/modules/components/pages/inputs/aws_sqs.adoc @@ -0,0 +1,229 @@ += aws_sqs +:type: input +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consume messages from an AWS SQS URL. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + aws_sqs: + url: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + aws_sqs: + url: "" # No default (required) + delete_message: true + reset_visibility: true + max_number_of_messages: 10 + wait_time_seconds: 0 + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS +services. It's also possible to set them explicitly at the component level, +allowing you to transfer data across accounts. You can find out more in +xref:guides:cloud/aws.adoc[]. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- sqs_message_id +- sqs_receipt_handle +- sqs_approximate_receive_count +- All message attributes +``` + +You can access these metadata fields using +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `url` + +The SQS URL to consume from. + + +*Type*: `string` + + +=== `delete_message` + +Whether to delete the consumed message once it is acked. Disabling allows you to handle the deletion using a different mechanism. + + +*Type*: `bool` + +*Default*: `true` + +=== `reset_visibility` + +Whether to set the visibility timeout of the consumed message to zero once it is nacked. Disabling honors the preset visibility timeout specified for the queue. + + +*Type*: `bool` + +*Default*: `true` +Requires version 3.58.0 or newer + +=== `max_number_of_messages` + +The maximum number of messages to return on one poll. Valid values: 1 to 10. + + +*Type*: `int` + +*Default*: `10` + +=== `wait_time_seconds` + +Whether to set the wait time. Enabling this activates long-polling. Valid values: 0 to 20. + + +*Type*: `int` + +*Default*: `0` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/inputs/azure_blob_storage.adoc b/docs/modules/components/pages/inputs/azure_blob_storage.adoc new file mode 100644 index 0000000000..e9bafabd28 --- /dev/null +++ b/docs/modules/components/pages/inputs/azure_blob_storage.adoc @@ -0,0 +1,208 @@ += azure_blob_storage +:type: input +:status: beta +:categories: ["Services","Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Downloads objects within an Azure Blob Storage container, optionally filtered by a prefix. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + azure_blob_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + container: "" # No default (required) + prefix: "" + scanner: + to_the_end: {} + targets_input: null # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + azure_blob_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + container: "" # No default (required) + prefix: "" + scanner: + to_the_end: {} + delete_objects: false + targets_input: null # No default (optional) +``` + +-- +====== + +Supports multiple authentication methods but only one of the following is required: +- `storage_connection_string` +- `storage_account` and `storage_access_key` +- `storage_account` and `storage_sas_token` +- `storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] + +If multiple are set then the `storage_connection_string` is given priority. + +If the `storage_connection_string` does not contain the `AccountName` parameter, please specify it in the +`storage_account` field. + +== Download large files + +When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a <> can be specified that determines how to break the input into smaller individual messages. + +== Stream new files + +By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage[an Event Grid source] and then use this as a <>, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. + +== Metadata + +This input adds the following metadata fields to each message: + +``` +- blob_storage_key +- blob_storage_container +- blob_storage_last_modified +- blob_storage_last_modified_unix +- blob_storage_content_type +- blob_storage_content_encoding +- All user defined metadata +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `storage_account` + +The storage account to access. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_access_key` + +The storage account access key. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_connection_string` + +A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_sas_token` + +The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. + + +*Type*: `string` + +*Default*: `""` + +=== `container` + +The name of the container from which to download blobs. + + +*Type*: `string` + + +=== `prefix` + +An optional path prefix, if set only objects with the prefix are consumed. + + +*Type*: `string` + +*Default*: `""` + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"to_the_end":{}}` +Requires version 4.25.0 or newer + +=== `delete_objects` + +Whether to delete downloaded objects from the blob once they are processed. + + +*Type*: `bool` + +*Default*: `false` + +=== `targets_input` + +EXPERIMENTAL: An optional source of download targets, configured as a xref:components:inputs/about.adoc[regular Benthos input]. Each message yielded by this input should be a single structured object containing a field `name`, which represents the blob to be downloaded. + + +*Type*: `input` + +Requires version 4.27.0 or newer + +```yml +# Examples + +targets_input: + mqtt: + topics: + - some-topic + urls: + - example.westeurope-1.ts.eventgrid.azure.net:8883 + processors: + - unarchive: + format: json_array + - mapping: |- + if this.eventType == "Microsoft.Storage.BlobCreated" { + root.name = this.data.url.parse_url().path.trim_prefix("/foocontainer/") + } else { + root = deleted() + } +``` + + diff --git a/docs/modules/components/pages/inputs/azure_cosmosdb.adoc b/docs/modules/components/pages/inputs/azure_cosmosdb.adoc new file mode 100644 index 0000000000..cf9b8c112d --- /dev/null +++ b/docs/modules/components/pages/inputs/azure_cosmosdb.adoc @@ -0,0 +1,303 @@ += azure_cosmosdb +:type: input +:status: experimental +:categories: ["Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a SQL query against https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB] and creates a batch of messages from each page of items. + +Introduced in version v4.25.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + azure_cosmosdb: + endpoint: https://localhost:8081 # No default (optional) + account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) + connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) + database: testdb # No default (required) + container: testcontainer # No default (required) + partition_keys_map: root = "blobfish" # No default (required) + query: SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp # No default (required) + args_mapping: |- # No default (optional) + root = [ + { "Name": "@name", "Value": "benthos" }, + ] + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + azure_cosmosdb: + endpoint: https://localhost:8081 # No default (optional) + account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) + connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) + database: testdb # No default (required) + container: testcontainer # No default (required) + partition_keys_map: root = "blobfish" # No default (required) + query: SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp # No default (required) + args_mapping: |- # No default (optional) + root = [ + { "Name": "@name", "Value": "benthos" }, + ] + batch_count: -1 + auto_replay_nacks: true +``` + +-- +====== + +== Cross-partition queries + +Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey values must be known in advance and specified in the config. https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989[See details]. + + +== Credentials + +You can use one of the following authentication mechanisms: + +- Set the `endpoint` field and the `account_key` field +- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set the `connection_string` field + + +== Metadata + +This component adds the following metadata fields to each message: +``` +- activity_id +- request_charge +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + + +== Examples + +[tabs] +====== +Query container:: ++ +-- + +Execute a parametrized SQL query to select documents from a container. + +```yaml +input: + azure_cosmosdb: + endpoint: http://localhost:8080 + account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + database: blobbase + container: blobfish + partition_keys_map: root = "AbyssalPlain" + query: SELECT * FROM blobfish AS b WHERE b.species = @species + args_mapping: | + root = [ + { "Name": "@species", "Value": "smooth-head" }, + ] +``` + +-- +====== + +== Fields + +=== `endpoint` + +CosmosDB endpoint. + + +*Type*: `string` + + +```yml +# Examples + +endpoint: https://localhost:8081 +``` + +=== `account_key` + +Account key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +```yml +# Examples + +account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== +``` + +=== `connection_string` + +Connection string. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +```yml +# Examples + +connection_string: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; +``` + +=== `database` + +Database. + + +*Type*: `string` + + +```yml +# Examples + +database: testdb +``` + +=== `container` + +Container. + + +*Type*: `string` + + +```yml +# Examples + +container: testcontainer +``` + +=== `partition_keys_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided. + + +*Type*: `string` + + +```yml +# Examples + +partition_keys_map: root = "blobfish" + +partition_keys_map: root = 41 + +partition_keys_map: root = true + +partition_keys_map: root = null + +partition_keys_map: root = now().ts_format("2006-01-02") +``` + +=== `query` + +The query to execute + + +*Type*: `string` + + +```yml +# Examples + +query: SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp +``` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that, for each message, creates a list of arguments to use with the query. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: |- + root = [ + { "Name": "@name", "Value": "benthos" }, + ] +``` + +=== `batch_count` + +The maximum number of messages that should be accumulated into each batch. Use '-1' specify dynamic page size. + + +*Type*: `int` + +*Default*: `-1` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + +== CosmosDB emulator + +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: + +```bash +> docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator +``` + +Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. + +Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy] like so: + +```bash +> mitmproxy -k --mode "reverse:https://localhost:8081" +``` + +Then you can access the CosmosDB UI via `http://localhost:8080/_explorer/index.html` and use `http://localhost:8080` as the CosmosDB endpoint. + + diff --git a/docs/modules/components/pages/inputs/azure_queue_storage.adoc b/docs/modules/components/pages/inputs/azure_queue_storage.adoc new file mode 100644 index 0000000000..6b68870f68 --- /dev/null +++ b/docs/modules/components/pages/inputs/azure_queue_storage.adoc @@ -0,0 +1,147 @@ += azure_queue_storage +:type: input +:status: beta +:categories: ["Services","Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Dequeue objects from an Azure Storage Queue. + +Introduced in version 3.42.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + azure_queue_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + queue_name: foo_queue # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + azure_queue_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + queue_name: foo_queue # No default (required) + dequeue_visibility_timeout: 30s + max_in_flight: 10 + track_properties: false +``` + +-- +====== + +This input adds the following metadata fields to each message: + +``` +- queue_storage_insertion_time +- queue_storage_queue_name +- queue_storage_message_lag (if 'track_properties' set to true) +- All user defined queue metadata +``` + +Only one authentication method is required, `storage_connection_string` or `storage_account` and `storage_access_key`. If both are set then the `storage_connection_string` is given priority. + +== Fields + +=== `storage_account` + +The storage account to access. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_access_key` + +The storage account access key. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_connection_string` + +A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. + + +*Type*: `string` + +*Default*: `""` + +=== `queue_name` + +The name of the source storage queue. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +queue_name: foo_queue + +queue_name: ${! env("MESSAGE_TYPE").lowercase() } +``` + +=== `dequeue_visibility_timeout` + +The timeout duration until a dequeued message gets visible again, 30s by default + + +*Type*: `string` + +*Default*: `"30s"` +Requires version 3.45.0 or newer + +=== `max_in_flight` + +The maximum number of unprocessed messages to fetch at a given time. + + +*Type*: `int` + +*Default*: `10` + +=== `track_properties` + +If set to `true` the queue is polled on each read request for information such as the queue message lag. These properties are added to consumed messages as metadata, but will also have a negative performance impact. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/inputs/azure_table_storage.adoc b/docs/modules/components/pages/inputs/azure_table_storage.adoc new file mode 100644 index 0000000000..e913aeb9ca --- /dev/null +++ b/docs/modules/components/pages/inputs/azure_table_storage.adoc @@ -0,0 +1,164 @@ += azure_table_storage +:type: input +:status: beta +:categories: ["Services","Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Queries an Azure Storage Account Table, optionally with multiple filters. + +Introduced in version 4.10.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + azure_table_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + table_name: Foo # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + azure_table_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + table_name: Foo # No default (required) + filter: "" + select: "" + page_size: 1000 +``` + +-- +====== + +Queries an Azure Storage Account Table, optionally with multiple filters. +== Metadata +This input adds the following metadata fields to each message: +``` +- table_storage_name +- row_num +``` +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `storage_account` + +The storage account to access. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_access_key` + +The storage account access key. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_connection_string` + +A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_sas_token` + +The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. + + +*Type*: `string` + +*Default*: `""` + +=== `table_name` + +The table to read messages from. + + +*Type*: `string` + + +```yml +# Examples + +table_name: Foo +``` + +=== `filter` + +OData filter expression. Is not set all rows are returned. Valid operators are `eq, ne, gt, lt, ge and le` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +filter: PartitionKey eq 'foo' and RowKey gt '1000' +``` + +=== `select` + +Select expression using OData notation. Limits the columns on each record to just those requested. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +select: PartitionKey,RowKey,Foo,Bar,Timestamp +``` + +=== `page_size` + +Maximum number of records to return on each page. + + +*Type*: `int` + +*Default*: `1000` + + diff --git a/docs/modules/components/pages/inputs/batched.adoc b/docs/modules/components/pages/inputs/batched.adoc new file mode 100644 index 0000000000..a508dca5e6 --- /dev/null +++ b/docs/modules/components/pages/inputs/batched.adoc @@ -0,0 +1,178 @@ += batched +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes data from a child input and applies a batching policy to the stream. + +Introduced in version 4.11.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + batched: + child: null # No default (required) + policy: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + batched: + child: null # No default (required) + policy: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Batching at the input level is sometimes useful for processing across micro-batches, and can also sometimes be a useful performance trick. However, most inputs are fine without it so unless you have a specific plan for batching this component is not worth using. + +== Fields + +=== `child` + +The child input. + + +*Type*: `input` + + +=== `policy` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +policy: + byte_size: 5000 + count: 0 + period: 1s + +policy: + count: 10 + period: 1s + +policy: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `policy.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `policy.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `policy.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `policy.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `policy.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/inputs/beanstalkd.adoc b/docs/modules/components/pages/inputs/beanstalkd.adoc new file mode 100644 index 0000000000..7dfe1f2e5a --- /dev/null +++ b/docs/modules/components/pages/inputs/beanstalkd.adoc @@ -0,0 +1,46 @@ += beanstalkd +:type: input +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads messages from a Beanstalkd queue. + +Introduced in version 4.7.0. + +```yml +# Config fields, showing default values +input: + label: "" + beanstalkd: + address: 127.0.0.1:11300 # No default (required) +``` + +== Fields + +=== `address` + +An address to connect to. + + +*Type*: `string` + + +```yml +# Examples + +address: 127.0.0.1:11300 +``` + + diff --git a/docs/modules/components/pages/inputs/broker.adoc b/docs/modules/components/pages/inputs/broker.adoc new file mode 100644 index 0000000000..6f76713f94 --- /dev/null +++ b/docs/modules/components/pages/inputs/broker.adoc @@ -0,0 +1,224 @@ += broker +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Allows you to combine multiple inputs into a single stream of data, where each input will be read in parallel. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + broker: + inputs: [] # No default (required) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + broker: + copies: 1 + inputs: [] # No default (required) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +A broker type is configured with its own list of input configurations and a field to specify how many copies of the list of inputs should be created. + +Adding more input types allows you to combine streams from multiple sources into one. For example, reading from both RabbitMQ and Kafka: + +```yaml +input: + broker: + copies: 1 + inputs: + - amqp_0_9: + urls: + - amqp://guest:guest@localhost:5672/ + consumer_tag: benthos-consumer + queue: benthos-queue + + # Optional list of input specific processing steps + processors: + - mapping: | + root.message = this + root.meta.link_count = this.links.length() + root.user.age = this.user.age.number() + + - kafka: + addresses: + - localhost:9092 + client_id: benthos_kafka_input + consumer_group: benthos_consumer_group + topics: [ benthos_stream:0 ] +``` + +If the number of copies is greater than zero the list will be copied that number of times. For example, if your inputs were of type foo and bar, with 'copies' set to '2', you would end up with two 'foo' inputs and two 'bar' inputs. + +== Batching + +It's possible to configure a xref:configuration:batching.adoc#batch-policy[batch policy] with a broker using the `batching` fields. When doing this the feeds from all child inputs are combined. Some inputs do not support broker based batching and specify this in their documentation. + +== Processors + +It is possible to configure xref:components:processors/about.adoc[processors] at the broker level, where they will be applied to _all_ child inputs, as well as on the individual child inputs. If you have processors at both the broker level _and_ on child inputs then the broker processors will be applied _after_ the child nodes processors. + +== Fields + +=== `copies` + +Whatever is specified within `inputs` will be created this many times. + + +*Type*: `int` + +*Default*: `1` + +=== `inputs` + +A list of inputs to create. + + +*Type*: `array` + + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/inputs/cassandra.adoc b/docs/modules/components/pages/inputs/cassandra.adoc new file mode 100644 index 0000000000..b3f229f2ee --- /dev/null +++ b/docs/modules/components/pages/inputs/cassandra.adoc @@ -0,0 +1,404 @@ += cassandra +:type: input +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a find query and creates a message for each row received. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + cassandra: + addresses: [] # No default (required) + timeout: 600ms + query: "" # No default (required) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + cassandra: + addresses: [] # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + password_authenticator: + enabled: false + username: "" + password: "" + disable_initial_host_lookup: false + max_retries: 3 + backoff: + initial_interval: 1s + max_interval: 5s + timeout: 600ms + query: "" # No default (required) + auto_replay_nacks: true +``` + +-- +====== + +== Examples + +[tabs] +====== +Minimal Select (Cassandra/Scylla):: ++ +-- + + +Let's presume that we have 3 Cassandra nodes, like in this tutorial by Sebastian Sigl from freeCodeCamp: + +https://www.freecodecamp.org/news/the-apache-cassandra-beginner-tutorial/ + +Then if we want to select everything from the table users_by_country, we should use the configuration below. +If we specify the stdin output, the result will look like: + +```json +{"age":23,"country":"UK","first_name":"Bob","last_name":"Sandler","user_email":"bob@email.com"} +``` + +This configuration also works for Scylla. + + +```yaml +input: + cassandra: + addresses: + - 172.17.0.2 + query: + 'SELECT * FROM learn_cassandra.users_by_country' +``` + +-- +====== + +== Fields + +=== `addresses` + +A list of Cassandra nodes to connect to. Multiple comma separated addresses can be specified on a single line. + + +*Type*: `array` + + +```yml +# Examples + +addresses: + - localhost:9042 + +addresses: + - foo:9042 + - bar:9042 + +addresses: + - foo:9042,bar:9042 +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `password_authenticator` + +Optional configuration of Cassandra authentication parameters. + + +*Type*: `object` + + +=== `password_authenticator.enabled` + +Whether to use password authentication + + +*Type*: `bool` + +*Default*: `false` + +=== `password_authenticator.username` + +The username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `password_authenticator.password` + +The password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `disable_initial_host_lookup` + +If enabled the driver will not attempt to get host info from the system.peers table. This can speed up queries but will mean that data_centre, rack and token information will not be available. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_retries` + +The maximum number of retries before giving up on a request. + + +*Type*: `int` + +*Default*: `3` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `timeout` + +The client connection timeout. + + +*Type*: `string` + +*Default*: `"600ms"` + +=== `query` + +A query to execute. + + +*Type*: `string` + + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc b/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc new file mode 100644 index 0000000000..73d1a3ce60 --- /dev/null +++ b/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc @@ -0,0 +1,292 @@ += cockroachdb_changefeed +:type: input +:status: experimental +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Listens to a https://www.cockroachlabs.com/docs/stable/changefeed-examples[CockroachDB Core Changefeed] and creates a message for each row received. Each message is a json object looking like: +```json +{ + "primary_key": "[\"1a7ff641-3e3b-47ee-94fe-a0cadb56cd8f\", 2]", // stringifed JSON array + "row": "{\"after\": {\"k\": \"1a7ff641-3e3b-47ee-94fe-a0cadb56cd8f\", \"v\": 2}, \"updated\": \"1637953249519902405.0000000000\"}", // stringified JSON object + "table": "strm_2" +} +``` + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + cockroachdb_changefeed: + dsn: postgres://user:password@example.com:26257/defaultdb?sslmode=require # No default (required) + tables: [] # No default (required) + cursor_cache: "" # No default (optional) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + cockroachdb_changefeed: + dsn: postgres://user:password@example.com:26257/defaultdb?sslmode=require # No default (required) + tls: + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + tables: [] # No default (required) + cursor_cache: "" # No default (optional) + options: [] # No default (optional) + auto_replay_nacks: true +``` + +-- +====== + +This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered. + +Note: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core[the official CockroachDB documentation]. + +== Fields + +=== `dsn` + +A Data Source Name to identify the target database. + + +*Type*: `string` + + +```yml +# Examples + +dsn: postgres://user:password@example.com:26257/defaultdb?sslmode=require +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `tables` + +CSV of tables to be included in the changefeed + + +*Type*: `array` + + +```yml +# Examples + +tables: + - table1 + - table2 +``` + +=== `cursor_cache` + +A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache resource] to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table. + + +*Type*: `string` + + +=== `options` + +A list of options to be included in the changefeed (WITH X, Y...). +**NOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case.** + + +*Type*: `array` + + +```yml +# Examples + +options: + - virtual_columns="omitted" +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/csv.adoc b/docs/modules/components/pages/inputs/csv.adoc new file mode 100644 index 0000000000..c1339a8dff --- /dev/null +++ b/docs/modules/components/pages/inputs/csv.adoc @@ -0,0 +1,209 @@ += csv +:type: input +:status: stable +:categories: ["Local"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads one or more CSV files as structured records following the format described in RFC 4180. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + csv: + paths: [] # No default (required) + parse_header_row: true + delimiter: ',' + lazy_quotes: false + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + csv: + paths: [] # No default (required) + parse_header_row: true + delimiter: ',' + lazy_quotes: false + delete_on_finish: false + batch_count: 1 + auto_replay_nacks: true +``` + +-- +====== + +This input offers more control over CSV parsing than the xref:components:inputs/file.adoc[`file` input]. + +When parsing with a header row each line of the file will be consumed as a structured object, where the key names are determined from the header now. For example, the following CSV file: + +```csv +foo,bar,baz +first foo,first bar,first baz +second foo,second bar,second baz +``` + +Would produce the following messages: + +```json +{"foo":"first foo","bar":"first bar","baz":"first baz"} +{"foo":"second foo","bar":"second bar","baz":"second baz"} +``` + +If, however, the field `parse_header_row` is set to `false` then arrays are produced instead, like follows: + +```json +["first foo","first bar","first baz"] +["second foo","second bar","second baz"] +``` + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- header +- path +- mod_time_unix +- mod_time (RFC3339) +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +Note: The `header` field is only set when `parse_header_row` is `true`. + +=== Output CSV column order + +When xref:guides:bloblang/advanced.adoc#creating-csv[creating CSV] from Benthos messages, the columns must be sorted lexicographically to make the output deterministic. Alternatively, when using the `csv` input, one can leverage the `header` metadata field to retrieve the column order: + +```yaml +input: + csv: + paths: + - ./foo.csv + - ./bar.csv + parse_header_row: true + + processors: + - mapping: | + map escape_csv { + root = if this.re_match("[\"\n,]+") { + "\"" + this.replace_all("\"", "\"\"") + "\"" + } else { + this + } + } + + let header = if count(@path) == 1 { + @header.map_each(c -> c.apply("escape_csv")).join(",") + "\n" + } else { "" } + + root = $header + @header.map_each(c -> this.get(c).string().apply("escape_csv")).join(",") + +output: + file: + path: ./output/${! @path.filepath_split().index(-1) } +``` + + +== Fields + +=== `paths` + +A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star). + + +*Type*: `array` + + +```yml +# Examples + +paths: + - /tmp/foo.csv + - /tmp/bar/*.csv + - /tmp/data/**/*.csv +``` + +=== `parse_header_row` + +Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, each message will consist of an array of values from the corresponding CSV row. + + +*Type*: `bool` + +*Default*: `true` + +=== `delimiter` + +The delimiter to use for splitting values in each record. It must be a single character. + + +*Type*: `string` + +*Default*: `","` + +=== `lazy_quotes` + +If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.1.0 or newer + +=== `delete_on_finish` + +Whether to delete input files from the disk once they are fully consumed. + + +*Type*: `bool` + +*Default*: `false` + +=== `batch_count` + +Optionally process records in batches. This can help to speed up the consumption of exceptionally large CSV files. When the end of the file is reached the remaining records are processed as a (potentially smaller) batch. + + +*Type*: `int` + +*Default*: `1` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +This input is particularly useful when consuming CSV from files too large to parse entirely within memory. However, in cases where CSV is consumed from other input types it's also possible to parse them using the xref:guides:bloblang/methods.adoc#parse_csv[Bloblang `parse_csv` method]. + diff --git a/docs/modules/components/pages/inputs/discord.adoc b/docs/modules/components/pages/inputs/discord.adoc new file mode 100644 index 0000000000..a5fda7df21 --- /dev/null +++ b/docs/modules/components/pages/inputs/discord.adoc @@ -0,0 +1,104 @@ += discord +:type: input +:status: experimental +:categories: ["Services","Social"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes messages posted in a Discord channel. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + discord: + channel_id: "" # No default (required) + bot_token: "" # No default (required) + cache: "" # No default (required) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + discord: + channel_id: "" # No default (required) + bot_token: "" # No default (required) + cache: "" # No default (required) + cache_key: last_message_id + auto_replay_nacks: true +``` + +-- +====== + +This input works by authenticating as a bot using token based authentication. The ID of the newest message consumed and acked is stored in a cache in order to perform a backfill of unread messages each time the input is initialised. Ideally this cache should be persisted across restarts. + +== Fields + +=== `channel_id` + +A discord channel ID to consume messages from. + + +*Type*: `string` + + +=== `bot_token` + +A bot token used for authentication. + + +*Type*: `string` + + +=== `cache` + +A cache resource to use for performing unread message backfills, the ID of the last message received will be stored in this cache and used for subsequent requests. + + +*Type*: `string` + + +=== `cache_key` + +The key identifier used when storing the ID of the last message received. + + +*Type*: `string` + +*Default*: `"last_message_id"` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/dynamic.adoc b/docs/modules/components/pages/inputs/dynamic.adoc new file mode 100644 index 0000000000..2b49b118f0 --- /dev/null +++ b/docs/modules/components/pages/inputs/dynamic.adoc @@ -0,0 +1,70 @@ += dynamic +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A special broker type where the inputs are identified by unique labels and can be created, changed and removed during runtime via a REST HTTP interface. + +```yml +# Config fields, showing default values +input: + label: "" + dynamic: + inputs: {} + prefix: "" +``` + +== Fields + +=== `inputs` + +A map of inputs to statically create. + + +*Type*: `object` + +*Default*: `{}` + +=== `prefix` + +A path prefix for HTTP endpoints that are registered. + + +*Type*: `string` + +*Default*: `""` + +== Endpoints + +=== GET `/inputs` + +Returns a JSON object detailing all dynamic inputs, providing information such as their current uptime and configuration. + +=== GET `/inputs/\{id}` + +Returns the configuration of an input. + +=== POST `/inputs/\{id}` + +Creates or updates an input with a configuration provided in the request body (in YAML or JSON format). + +=== DELETE `/inputs/\{id}` + +Stops and removes an input. + +=== GET `/inputs/\{id}/uptime` + +Returns the uptime of an input as a duration string (of the form "72h3m0.5s"), or "stopped" in the case where the input has gracefully terminated. + diff --git a/docs/modules/components/pages/inputs/file.adoc b/docs/modules/components/pages/inputs/file.adoc new file mode 100644 index 0000000000..e672a5d223 --- /dev/null +++ b/docs/modules/components/pages/inputs/file.adoc @@ -0,0 +1,130 @@ += file +:type: input +:status: stable +:categories: ["Local"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes data from files on disk, emitting messages according to a chosen codec. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + file: + paths: [] # No default (required) + scanner: + lines: {} + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + file: + paths: [] # No default (required) + scanner: + lines: {} + delete_on_finish: false + auto_replay_nacks: true +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- path +- mod_time_unix +- mod_time (RFC3339) +``` + +You can access these metadata fields using +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `paths` + +A list of paths to consume sequentially. Glob patterns are supported, including super globs (double star). + + +*Type*: `array` + + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"lines":{}}` +Requires version 4.25.0 or newer + +=== `delete_on_finish` + +Whether to delete input files from the disk once they are fully consumed. + + +*Type*: `bool` + +*Default*: `false` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +== Examples + +[tabs] +====== +Read a Bunch of CSVs:: ++ +-- + +If we wished to consume a directory of CSV files as structured documents we can use a glob pattern and the `csv` scanner: + +```yaml +input: + file: + paths: [ ./data/*.csv ] + scanner: + csv: {} +``` + +-- +====== + + diff --git a/docs/modules/components/pages/inputs/gcp_bigquery_select.adoc b/docs/modules/components/pages/inputs/gcp_bigquery_select.adoc new file mode 100644 index 0000000000..c125557ac9 --- /dev/null +++ b/docs/modules/components/pages/inputs/gcp_bigquery_select.adoc @@ -0,0 +1,177 @@ += gcp_bigquery_select +:type: input +:status: beta +:categories: ["Services","GCP"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a `SELECT` query against BigQuery and creates a message for each row received. + +Introduced in version 3.63.0. + +```yml +# Config fields, showing default values +input: + label: "" + gcp_bigquery_select: + project: "" # No default (required) + table: bigquery-public-data.samples.shakespeare # No default (required) + columns: [] # No default (required) + where: type = ? and created_at > ? # No default (optional) + auto_replay_nacks: true + job_labels: {} + priority: "" + args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) + prefix: "" # No default (optional) + suffix: "" # No default (optional) +``` + +Once the rows from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute). + +== Examples + +[tabs] +====== +Word counts:: ++ +-- + + +Here we query the public corpus of Shakespeare's works to generate a stream of the top 10 words that are 3 or more characters long: + +```yaml +input: + gcp_bigquery_select: + project: sample-project + table: bigquery-public-data.samples.shakespeare + columns: + - word + - sum(word_count) as total_count + where: length(word) >= ? + suffix: | + GROUP BY word + ORDER BY total_count DESC + LIMIT 10 + args_mapping: | + root = [ 3 ] +``` + +-- +====== + +== Fields + +=== `project` + +GCP project where the query job will execute. + + +*Type*: `string` + + +=== `table` + +Fully-qualified BigQuery table name to query. + + +*Type*: `string` + + +```yml +# Examples + +table: bigquery-public-data.samples.shakespeare +``` + +=== `columns` + +A list of columns to query. + + +*Type*: `array` + + +=== `where` + +An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks (`?`). + + +*Type*: `string` + + +```yml +# Examples + +where: type = ? and created_at > ? + +where: user_id = ? +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `job_labels` + +A list of labels to add to the query job. + + +*Type*: `object` + +*Default*: `{}` + +=== `priority` + +The priority with which to schedule the query. + + +*Type*: `string` + +*Default*: `""` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ "article", now().ts_format("2006-01-02") ] +``` + +=== `prefix` + +An optional prefix to prepend to the select query (before SELECT). + + +*Type*: `string` + + +=== `suffix` + +An optional suffix to append to the select query. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/inputs/gcp_cloud_storage.adoc b/docs/modules/components/pages/inputs/gcp_cloud_storage.adoc new file mode 100644 index 0000000000..8dbe651280 --- /dev/null +++ b/docs/modules/components/pages/inputs/gcp_cloud_storage.adoc @@ -0,0 +1,118 @@ += gcp_cloud_storage +:type: input +:status: beta +:categories: ["Services","GCP"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Downloads objects within a Google Cloud Storage bucket, optionally filtered by a prefix. + +Introduced in version 3.43.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + gcp_cloud_storage: + bucket: "" # No default (required) + prefix: "" + scanner: + to_the_end: {} +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + gcp_cloud_storage: + bucket: "" # No default (required) + prefix: "" + scanner: + to_the_end: {} + delete_objects: false +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +``` +- gcs_key +- gcs_bucket +- gcs_last_modified +- gcs_last_modified_unix +- gcs_content_type +- gcs_content_encoding +- All user defined metadata +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +=== Credentials + +By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more in xref:guides:cloud/gcp.adoc[]. + +== Fields + +=== `bucket` + +The name of the bucket from which to download objects. + + +*Type*: `string` + + +=== `prefix` + +An optional path prefix, if set only objects with the prefix are consumed. + + +*Type*: `string` + +*Default*: `""` + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"to_the_end":{}}` +Requires version 4.25.0 or newer + +=== `delete_objects` + +Whether to delete downloaded objects from the bucket once they are processed. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/inputs/gcp_pubsub.adoc b/docs/modules/components/pages/inputs/gcp_pubsub.adoc new file mode 100644 index 0000000000..e61dfd59ba --- /dev/null +++ b/docs/modules/components/pages/inputs/gcp_pubsub.adoc @@ -0,0 +1,167 @@ += gcp_pubsub +:type: input +:status: stable +:categories: ["Services","GCP"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes messages from a GCP Cloud Pub/Sub subscription. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + gcp_pubsub: + project: "" # No default (required) + subscription: "" # No default (required) + endpoint: "" + sync: false + max_outstanding_messages: 1000 + max_outstanding_bytes: 1e+09 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + gcp_pubsub: + project: "" # No default (required) + subscription: "" # No default (required) + endpoint: "" + sync: false + max_outstanding_messages: 1000 + max_outstanding_bytes: 1e+09 + create_subscription: + enabled: false + topic: "" +``` + +-- +====== + +For information on how to set up credentials see https://cloud.google.com/docs/authentication/production[this guide]. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- gcp_pubsub_publish_time_unix - The time at which the message was published to the topic. +- gcp_pubsub_delivery_attempt - When dead lettering is enabled, this is set to the number of times PubSub has attempted to deliver a message. +- All message attributes +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + + +== Fields + +=== `project` + +The project ID of the target subscription. + + +*Type*: `string` + + +=== `subscription` + +The target subscription ID. + + +*Type*: `string` + + +=== `endpoint` + +An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +endpoint: us-central1-pubsub.googleapis.com:443 + +endpoint: us-west3-pubsub.googleapis.com:443 +``` + +=== `sync` + +Enable synchronous pull mode. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_outstanding_messages` + +The maximum number of outstanding pending messages to be consumed at a given time. + + +*Type*: `int` + +*Default*: `1000` + +=== `max_outstanding_bytes` + +The maximum number of outstanding pending messages to be consumed measured in bytes. + + +*Type*: `int` + +*Default*: `1000000000` + +=== `create_subscription` + +Allows you to configure the input subscription and creates if it doesn't exist. + + +*Type*: `object` + + +=== `create_subscription.enabled` + +Whether to configure subscription or not. + + +*Type*: `bool` + +*Default*: `false` + +=== `create_subscription.topic` + +Defines the topic that the subscription should be vinculated to. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/inputs/generate.adoc b/docs/modules/components/pages/inputs/generate.adoc new file mode 100644 index 0000000000..a699c662e5 --- /dev/null +++ b/docs/modules/components/pages/inputs/generate.adoc @@ -0,0 +1,156 @@ += generate +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Generates messages at a given interval using a xref:guides:bloblang/about.adoc[Bloblang] mapping executed without a context. This allows you to generate messages for testing your pipeline configs. + +Introduced in version 3.40.0. + +```yml +# Config fields, showing default values +input: + label: "" + generate: + mapping: root = "hello world" # No default (required) + interval: 1s + count: 0 + batch_size: 1 + auto_replay_nacks: true +``` + +== Examples + +[tabs] +====== +Cron Scheduled Processing:: ++ +-- + +A common use case for the generate input is to trigger processors on a schedule so that the processors themselves can behave similarly to an input. The following configuration reads rows from a PostgreSQL table every 5 minutes. + +```yaml +input: + generate: + interval: '@every 5m' + mapping: 'root = {}' + processors: + - sql_select: + driver: postgres + dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable + table: foo + columns: [ "*" ] +``` + +-- +Generate 100 Rows:: ++ +-- + +The generate input can be used as a convenient way to generate test data. The following example generates 100 rows of structured data by setting an explicit count. The interval field is set to empty, which means data is generated as fast as the downstream components can consume it. + +```yaml +input: + generate: + count: 100 + interval: "" + mapping: | + root = if random_int() % 2 == 0 { + { + "type": "foo", + "foo": "is yummy" + } + } else { + { + "type": "bar", + "bar": "is gross" + } + } +``` + +-- +====== + +== Fields + +=== `mapping` + +A xref:guides:bloblang/about.adoc[Bloblang] mapping to use for generating messages. + + +*Type*: `string` + + +```yml +# Examples + +mapping: root = "hello world" + +mapping: root = {"test":"message","id":uuid_v4()} +``` + +=== `interval` + +The time interval at which messages should be generated, expressed either as a duration string or as a cron expression. If set to an empty string messages will be generated as fast as downstream services can process them. Cron expressions can specify a timezone by prefixing the expression with `TZ=`, where the location name corresponds to a file within the IANA Time Zone database. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +interval: 5s + +interval: 1m + +interval: 1h + +interval: '@every 1s' + +interval: 0,30 */2 * * * * + +interval: TZ=Europe/London 30 3-6,20-23 * * * +``` + +=== `count` + +An optional number of messages to generate, if set above 0 the specified number of messages is generated and then the input will shut down. + + +*Type*: `int` + +*Default*: `0` + +=== `batch_size` + +The number of generated messages that should be accumulated into each batch flushed at the specified interval. + + +*Type*: `int` + +*Default*: `1` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/hdfs.adoc b/docs/modules/components/pages/inputs/hdfs.adoc new file mode 100644 index 0000000000..52ba577462 --- /dev/null +++ b/docs/modules/components/pages/inputs/hdfs.adoc @@ -0,0 +1,75 @@ += hdfs +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads files from a HDFS directory, where each discrete file will be consumed as a single message payload. + +```yml +# Config fields, showing default values +input: + label: "" + hdfs: + hosts: [] # No default (required) + user: "" + directory: "" # No default (required) +``` + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- hdfs_name +- hdfs_path +``` + +You can access these metadata fields using +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `hosts` + +A list of target host addresses to connect to. + + +*Type*: `array` + + +```yml +# Examples + +hosts: localhost:9000 +``` + +=== `user` + +A user ID to connect as. + + +*Type*: `string` + +*Default*: `""` + +=== `directory` + +The directory to consume from. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/inputs/http_client.adoc b/docs/modules/components/pages/inputs/http_client.adoc new file mode 100644 index 0000000000..053e35687c --- /dev/null +++ b/docs/modules/components/pages/inputs/http_client.adoc @@ -0,0 +1,875 @@ += http_client +:type: input +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to a server and continuously performs requests for a single message. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + http_client: + url: "" # No default (required) + verb: GET + headers: {} + rate_limit: "" # No default (optional) + timeout: 5s + payload: "" # No default (optional) + stream: + enabled: false + reconnect: true + scanner: + lines: {} + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + http_client: + url: "" # No default (required) + verb: GET + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + dump_request_log_level: "" + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + oauth2: + enabled: false + client_key: "" + client_secret: "" + token_url: "" + scopes: [] + endpoint_params: {} + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + extract_headers: + include_prefixes: [] + include_patterns: [] + rate_limit: "" # No default (optional) + timeout: 5s + retry_period: 1s + max_retry_backoff: 300s + retries: 3 + backoff_on: + - 429 + drop_on: [] + successful_on: [] + proxy_url: "" # No default (optional) + payload: "" # No default (optional) + drop_empty_bodies: true + stream: + enabled: false + reconnect: true + scanner: + lines: {} + auto_replay_nacks: true +``` + +-- +====== + +The URL and header values of this type can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. + +== Streaming + +If you enable streaming then Benthos will consume the body of the response as a continuous stream of data, breaking messages out following a chosen scanner. This allows you to consume APIs that provide long lived streamed data feeds (such as Twitter). + +== Pagination + +This input supports interpolation functions in the `url` and `headers` fields where data from the previous successfully consumed message (if there was one) can be referenced. This can be used in order to support basic levels of pagination. However, in cases where pagination depends on logic it is recommended that you use an xref:components:processors/http.adoc[`http` processor] instead, often combined with a xref:components:inputs/generate.adoc[`generate` input] in order to schedule the processor. + +== Examples + +[tabs] +====== +Basic Pagination:: ++ +-- + +Interpolation functions within the `url` and `headers` fields can be used to reference the previously consumed message, which allows simple pagination. + +```yaml +input: + http_client: + url: >- + https://api.example.com/search?query=allmyfoos&start_time=${! ( + (timestamp_unix()-300).ts_format("2006-01-02T15:04:05Z","UTC").escape_url_query() + ) }${! ("&next_token="+this.meta.next_token.not_null()) | "" } + verb: GET + rate_limit: foo_searches + oauth2: + enabled: true + token_url: https://api.example.com/oauth2/token + client_key: "${EXAMPLE_KEY}" + client_secret: "${EXAMPLE_SECRET}" + +rate_limit_resources: + - label: foo_searches + local: + count: 1 + interval: 30s +``` + +-- +====== + +== Fields + +=== `url` + +The URL to connect to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `verb` + +A verb to connect with + + +*Type*: `string` + +*Default*: `"GET"` + +```yml +# Examples + +verb: POST + +verb: GET + +verb: DELETE +``` + +=== `headers` + +A map of headers to add to the request. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +headers: + Content-Type: application/octet-stream + traceparent: ${! tracing_span().traceparent } +``` + +=== `metadata` + +Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `dump_request_log_level` + +EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged. + + +*Type*: `string` + +*Default*: `""` +Requires version 4.12.0 or newer + +Options: +`TRACE` +, `DEBUG` +, `INFO` +, `WARN` +, `ERROR` +, `FATAL` +, `` +. + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2` + +Allows you to specify open authentication via OAuth version 2 using the client credentials token flow. + + +*Type*: `object` + + +=== `oauth2.enabled` + +Whether to use OAuth version 2 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth2.client_key` + +A value used to identify the client to the token provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.client_secret` + +A secret used to establish ownership of the client key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.token_url` + +The URL of the token provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.scopes` + +A list of optional requested permissions. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.45.0 or newer + +=== `oauth2.endpoint_params` + +A list of optional endpoint parameters, values should be arrays of strings. + + +*Type*: `object` + +*Default*: `{}` +Requires version 4.21.0 or newer + +```yml +# Examples + +endpoint_params: + bar: + - woof + foo: + - meow + - quack +``` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `extract_headers` + +Specify which response headers should be added to resulting messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. + + +*Type*: `object` + + +=== `extract_headers.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `extract_headers.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `rate_limit` + +An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by. + + +*Type*: `string` + + +=== `timeout` + +A static timeout to apply to requests. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `retry_period` + +The base period to wait between failed requests. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `max_retry_backoff` + +The maximum period to wait between failed requests. + + +*Type*: `string` + +*Default*: `"300s"` + +=== `retries` + +The maximum number of retry attempts to make. + + +*Type*: `int` + +*Default*: `3` + +=== `backoff_on` + +A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually. + + +*Type*: `array` + +*Default*: `[429]` + +=== `drop_on` + +A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped. + + +*Type*: `array` + +*Default*: `[]` + +=== `successful_on` + +A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field. + + +*Type*: `array` + +*Default*: `[]` + +=== `proxy_url` + +An optional HTTP proxy URL. + + +*Type*: `string` + + +=== `payload` + +An optional payload to deliver for each request. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `drop_empty_bodies` + +Whether empty payloads received from the target server should be dropped. + + +*Type*: `bool` + +*Default*: `true` + +=== `stream` + +Allows you to set streaming mode, where requests are kept open and messages are processed line-by-line. + + +*Type*: `object` + + +=== `stream.enabled` + +Enables streaming mode. + + +*Type*: `bool` + +*Default*: `false` + +=== `stream.reconnect` + +Sets whether to re-establish the connection once it is lost. + + +*Type*: `bool` + +*Default*: `true` + +=== `stream.scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"lines":{}}` +Requires version 4.25.0 or newer + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/http_server.adoc b/docs/modules/components/pages/inputs/http_server.adoc new file mode 100644 index 0000000000..0a3262b65f --- /dev/null +++ b/docs/modules/components/pages/inputs/http_server.adoc @@ -0,0 +1,418 @@ += http_server +:type: input +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Receive messages POSTed over HTTP(S). HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + http_server: + address: "" + path: /post + ws_path: /post/ws + allowed_verbs: + - POST + timeout: 5s + rate_limit: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + http_server: + address: "" + path: /post + ws_path: /post/ws + ws_welcome_message: "" + ws_rate_limit_message: "" + allowed_verbs: + - POST + timeout: 5s + rate_limit: "" + cert_file: "" + key_file: "" + cors: + enabled: false + allowed_origins: [] + sync_response: + status: "200" + headers: + Content-Type: application/octet-stream + metadata_headers: + include_prefixes: [] + include_patterns: [] +``` + +-- +====== + +If the `address` config field is left blank the xref:components:http/about.adoc[service-wide HTTP server] will be used. + +The field `rate_limit` allows you to specify an optional xref:components:rate_limits/about.adoc[`rate_limit` resource], which will be applied to each HTTP request made and each websocket payload received. + +When the rate limit is breached HTTP requests will have a 429 response returned with a Retry-After header. Websocket payloads will be dropped and an optional response payload will be sent as per `ws_rate_limit_message`. + +== Responses + +It's possible to return a response for each message received using xref:guides:sync_responses.adoc[synchronous responses]. When doing so you can customize headers with the `sync_response` field `headers`, which can also use xref:configuration:interpolation.adoc#bloblang-queries[function interpolation] in the value based on the response message contents. + +== Endpoints + +The following fields specify endpoints that are registered for sending messages, and support path parameters of the form `/\{foo}`, which are added to ingested messages as metadata. A path ending in `/` will match against all extensions of that path: + +=== `path` (defaults to `/post`) + +This endpoint expects POST requests where the entire request body is consumed as a single message. + +If the request contains a multipart `content-type` header as per https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[rfc1341] then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. + +=== `ws_path` (defaults to `/post/ws`) + +Creates a websocket connection, where payloads received on the socket are passed through the pipeline as a batch of one message. + + +[CAUTION] +.Endpoint caveats +==== +Components within a Benthos config will register their respective endpoints in a non-deterministic order. This means that establishing precedence of endpoints that are registered via multiple `http_server` inputs or outputs (either within brokers or from cohabiting streams) is not possible in a predictable way. + +This ambiguity makes it difficult to ensure that paths which are both a subset of a path registered by a separate component, and end in a slash (`/`) and will therefore match against all extensions of that path, do not prevent the more specific path from matching against requests. + +It is therefore recommended that you ensure paths of separate components do not collide unless they are explicitly non-competing. + +For example, if you were to deploy two separate `http_server` inputs, one with a path `/foo/` and the other with a path `/foo/bar`, it would not be possible to ensure that the path `/foo/` does not swallow requests made to `/foo/bar`. +==== + +You may specify an optional `ws_welcome_message`, which is a static payload to be sent to all clients once a websocket connection is first established. + +It's also possible to specify a `ws_rate_limit_message`, which is a static payload to be sent to clients that have triggered the servers rate limit. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- http_server_user_agent +- http_server_request_path +- http_server_verb +- http_server_remote_ip +- All headers (only first values are taken) +- All query parameters +- All path parameters +- All cookies +``` + +If HTTPS is enabled, the following fields are added as well: +```text +- http_server_tls_version +- http_server_tls_subject +- http_server_tls_cipher_suite +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Examples + +[tabs] +====== +Path Switching:: ++ +-- + +This example shows an `http_server` input that captures all requests and processes them by switching on that path: + +```yaml +input: + http_server: + path: / + allowed_verbs: [ GET, POST ] + sync_response: + headers: + Content-Type: application/json + + processors: + - switch: + - check: '@http_server_request_path == "/foo"' + processors: + - mapping: | + root.title = "You Got Fooed!" + root.result = content().string().uppercase() + + - check: '@http_server_request_path == "/bar"' + processors: + - mapping: 'root.title = "Bar Is Slow"' + - sleep: # Simulate a slow endpoint + duration: 1s +``` + +-- +Mock OAuth 2.0 Server:: ++ +-- + +This example shows an `http_server` input that mocks an OAuth 2.0 Client Credentials flow server at the endpoint `/oauth2_test`: + +```yaml +input: + http_server: + path: /oauth2_test + allowed_verbs: [ GET, POST ] + sync_response: + headers: + Content-Type: application/json + + processors: + - log: + message: "Received request" + level: INFO + fields_mapping: | + root = @ + root.body = content().string() + + - mapping: | + root.access_token = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3" + root.token_type = "Bearer" + root.expires_in = 3600 + + - sync_response: {} + - mapping: 'root = deleted()' +``` + +-- +====== + +== Fields + +=== `address` + +An alternative address to host from. If left empty the service wide address is used. + + +*Type*: `string` + +*Default*: `""` + +=== `path` + +The endpoint path to listen for POST requests. + + +*Type*: `string` + +*Default*: `"/post"` + +=== `ws_path` + +The endpoint path to create websocket connections from. + + +*Type*: `string` + +*Default*: `"/post/ws"` + +=== `ws_welcome_message` + +An optional message to deliver to fresh websocket connections. + + +*Type*: `string` + +*Default*: `""` + +=== `ws_rate_limit_message` + +An optional message to delivery to websocket connections that are rate limited. + + +*Type*: `string` + +*Default*: `""` + +=== `allowed_verbs` + +An array of verbs that are allowed for the `path` endpoint. + + +*Type*: `array` + +*Default*: `["POST"]` +Requires version 3.33.0 or newer + +=== `timeout` + +Timeout for requests. If a consumed messages takes longer than this to be delivered the connection is closed, but the message may still be delivered. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `rate_limit` + +An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by. + + +*Type*: `string` + +*Default*: `""` + +=== `cert_file` + +Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. + + +*Type*: `string` + +*Default*: `""` + +=== `key_file` + +Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. + + +*Type*: `string` + +*Default*: `""` + +=== `cors` + +Adds Cross-Origin Resource Sharing headers. Only valid with a custom `address`. + + +*Type*: `object` + +Requires version 3.63.0 or newer + +=== `cors.enabled` + +Whether to allow CORS requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `cors.allowed_origins` + +An explicit list of origins that are allowed for CORS requests. + + +*Type*: `array` + +*Default*: `[]` + +=== `sync_response` + +Customize messages returned via xref:guides:sync_responses.adoc[synchronous responses]. + + +*Type*: `object` + + +=== `sync_response.status` + +Specify the status code to return with synchronous responses. This is a string value, which allows you to customize it based on resulting payloads and their metadata. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"200"` + +```yml +# Examples + +status: ${! json("status") } + +status: ${! meta("status") } +``` + +=== `sync_response.headers` + +Specify headers to return with synchronous responses. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{"Content-Type":"application/octet-stream"}` + +=== `sync_response.metadata_headers` + +Specify criteria for which metadata values are added to the response as headers. + + +*Type*: `object` + + +=== `sync_response.metadata_headers.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `sync_response.metadata_headers.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + + diff --git a/docs/modules/components/pages/inputs/inproc.adoc b/docs/modules/components/pages/inputs/inproc.adoc new file mode 100644 index 0000000000..e89599d927 --- /dev/null +++ b/docs/modules/components/pages/inputs/inproc.adoc @@ -0,0 +1,30 @@ += inproc +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + + +```yml +# Config fields, showing default values +input: + label: "" + inproc: "" +``` + +Directly connect to an output within a Benthos process by referencing it by a chosen ID. This allows you to hook up isolated streams whilst running Benthos in xref:guides:streams_mode/about.adoc[streams mode], it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. + +It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs. + + diff --git a/docs/modules/components/pages/inputs/kafka.adoc b/docs/modules/components/pages/inputs/kafka.adoc new file mode 100644 index 0000000000..61b2a36e06 --- /dev/null +++ b/docs/modules/components/pages/inputs/kafka.adoc @@ -0,0 +1,705 @@ += kafka +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to Kafka brokers and consumes one or more topics. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + kafka: + addresses: [] # No default (required) + topics: [] # No default (required) + target_version: 2.1.0 # No default (optional) + consumer_group: "" + checkpoint_limit: 1024 + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + kafka: + addresses: [] # No default (required) + topics: [] # No default (required) + target_version: 2.1.0 # No default (optional) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + sasl: + mechanism: none + user: "" + password: "" + access_token: "" + token_cache: "" + token_key: "" + consumer_group: "" + client_id: benthos + rack_id: "" + start_from_oldest: true + checkpoint_limit: 1024 + auto_replay_nacks: true + commit_period: 1s + max_processing_period: 100ms + extract_tracing_map: root = @ # No default (optional) + group: + session_timeout: 10s + heartbeat_interval: 3s + rebalance_timeout: 60s + fetch_buffer_cap: 256 + multi_header: false + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Offsets are managed within Kafka under the specified consumer group, and partitions for each topic are automatically balanced across members of the consumer group. + +The Kafka input allows parallel processing of messages from different topic partitions, and messages of the same topic partition are processed with a maximum parallelism determined by the field <>. + +In order to enforce ordered processing of partition messages set the > to `1` and this will force partitions to be processed in lock-step, where a message will only be processed once the prior message is delivered. + +Batching messages before processing can be enabled using the <> field, and this batching is performed per-partition such that messages of a batch will always originate from the same partition. This batching mechanism is capable of creating batches of greater size than the <>, in which case the next batch will only be created upon delivery of the current one. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- kafka_key +- kafka_topic +- kafka_partition +- kafka_offset +- kafka_lag +- kafka_timestamp_unix +- kafka_tombstone_message +- All existing message headers (version 0.11+) +``` + +The field `kafka_lag` is the calculated difference between the high water mark offset of the partition at the time of ingestion and the current message offset. + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Ordering + +By default messages of a topic partition can be processed in parallel, up to a limit determined by the field `checkpoint_limit`. However, if strict ordered processing is required then this value must be set to 1 in order to process shard messages in lock-step. When doing so it is recommended that you perform batching at this component for performance as it will not be possible to batch lock-stepped messages at the output level. + +== Troubleshooting + +If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer xref:components:inputs/kafka_franz.adoc[`kafka_franz` input]. + +- I'm seeing logs that report `Failed to connect to kafka: kafka: client has run out of available brokers to talk to (Is your cluster reachable?)`, but the brokers are definitely reachable. + +Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have <> if applicable. + +== Fields + +=== `addresses` + +A list of broker addresses to connect to. If an item of the list contains commas it will be expanded into multiple addresses. + + +*Type*: `array` + + +```yml +# Examples + +addresses: + - localhost:9092 + +addresses: + - localhost:9041,localhost:9042 + +addresses: + - localhost:9041 + - localhost:9042 +``` + +=== `topics` + +A list of topics to consume from. Multiple comma separated topics can be listed in a single element. Partitions are automatically distributed across consumers of a topic. Alternatively, it's possible to specify explicit partitions to consume from with a colon after the topic name, e.g. `foo:0` would consume the partition 0 of the topic foo. This syntax supports ranges, e.g. `foo:0-10` would consume partitions 0 through to 10 inclusive. + + +*Type*: `array` + +Requires version 3.33.0 or newer + +```yml +# Examples + +topics: + - foo + - bar + +topics: + - foo,bar + +topics: + - foo:0 + - bar:1 + - bar:3 + +topics: + - foo:0,bar:1,bar:3 + +topics: + - foo:0-5 +``` + +=== `target_version` + +The version of the Kafka protocol to use. This limits the capabilities used by the client and should ideally match the version of your brokers. Defaults to the oldest supported stable version. + + +*Type*: `string` + + +```yml +# Examples + +target_version: 2.1.0 + +target_version: 3.1.0 +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `sasl` + +Enables SASL authentication. + + +*Type*: `object` + + +=== `sasl.mechanism` + +The SASL authentication mechanism, if left empty SASL authentication is not used. + + +*Type*: `string` + +*Default*: `"none"` + +|=== +| Option | Summary + +| `OAUTHBEARER` +| OAuth Bearer based authentication. +| `PLAIN` +| Plain text authentication. NOTE: When using plain text auth it is extremely likely that you'll also need to <>. +| `SCRAM-SHA-256` +| Authentication using the SCRAM-SHA-256 mechanism. +| `SCRAM-SHA-512` +| Authentication using the SCRAM-SHA-512 mechanism. +| `none` +| Default, no SASL authentication. + +|=== + +=== `sasl.user` + +A PLAIN username. It is recommended that you use environment variables to populate this field. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +user: ${USER} +``` + +=== `sasl.password` + +A PLAIN password. It is recommended that you use environment variables to populate this field. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: ${PASSWORD} +``` + +=== `sasl.access_token` + +A static OAUTHBEARER access token + + +*Type*: `string` + +*Default*: `""` + +=== `sasl.token_cache` + +Instead of using a static `access_token` allows you to query a xref:components:caches/about.adoc[`cache`] resource to fetch OAUTHBEARER tokens from + + +*Type*: `string` + +*Default*: `""` + +=== `sasl.token_key` + +Required when using a `token_cache`, the key to query the cache with for tokens. + + +*Type*: `string` + +*Default*: `""` + +=== `consumer_group` + +An identifier for the consumer group of the connection. This field can be explicitly made empty in order to disable stored offsets for the consumed topic partitions. + + +*Type*: `string` + +*Default*: `""` + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `"benthos"` + +=== `rack_id` + +A rack identifier for this client. + + +*Type*: `string` + +*Default*: `""` + +=== `start_from_oldest` + +Determines whether to consume from the oldest available offset, otherwise messages are consumed from the latest offset. The setting is applied when creating a new consumer group or the saved offset no longer exists. + + +*Type*: `bool` + +*Default*: `true` + +=== `checkpoint_limit` + +The maximum number of messages of the same topic and partition that can be processed at a given time. Increasing this limit enables parallel processing and batching at the output level to work on individual partitions. Any given offset will not be committed unless all messages under that offset are delivered in order to preserve at least once delivery guarantees. + + +*Type*: `int` + +*Default*: `1024` +Requires version 3.33.0 or newer + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `commit_period` + +The period of time between each commit of the current partition offsets. Offsets are always committed during shutdown. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `max_processing_period` + +A maximum estimate for the time taken to process a message, this is used for tuning consumer group synchronization. + + +*Type*: `string` + +*Default*: `"100ms"` + +=== `extract_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 3.45.0 or newer + +```yml +# Examples + +extract_tracing_map: root = @ + +extract_tracing_map: root = this.meta.span +``` + +=== `group` + +Tuning parameters for consumer group synchronization. + + +*Type*: `object` + + +=== `group.session_timeout` + +A period after which a consumer of the group is kicked after no heartbeats. + + +*Type*: `string` + +*Default*: `"10s"` + +=== `group.heartbeat_interval` + +A period in which heartbeats should be sent out. + + +*Type*: `string` + +*Default*: `"3s"` + +=== `group.rebalance_timeout` + +A period after which rebalancing is abandoned if unresolved. + + +*Type*: `string` + +*Default*: `"60s"` + +=== `fetch_buffer_cap` + +The maximum number of unprocessed messages to fetch at a given time. + + +*Type*: `int` + +*Default*: `256` + +=== `multi_header` + +Decode headers into lists to allow handling of multiple values with the same key + + +*Type*: `bool` + +*Default*: `false` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/inputs/kafka_franz.adoc b/docs/modules/components/pages/inputs/kafka_franz.adoc new file mode 100644 index 0000000000..2f6f9721d9 --- /dev/null +++ b/docs/modules/components/pages/inputs/kafka_franz.adoc @@ -0,0 +1,692 @@ += kafka_franz +:type: input +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A Kafka input using the https://github.com/twmb/franz-go[Franz Kafka client library]. + +Introduced in version 3.61.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + kafka_franz: + seed_brokers: [] # No default (required) + topics: [] # No default (required) + regexp_topics: false + consumer_group: "" # No default (optional) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + kafka_franz: + seed_brokers: [] # No default (required) + topics: [] # No default (required) + regexp_topics: false + consumer_group: "" # No default (optional) + client_id: benthos + rack_id: "" + checkpoint_limit: 1024 + auto_replay_nacks: true + commit_period: 5s + start_from_oldest: true + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + sasl: [] # No default (optional) + multi_header: false + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +When a consumer group is specified this input consumes one or more topics where partitions will automatically balance across any other connected clients with the same consumer group. When a consumer group is not specified topics can either be consumed in their entirety or with explicit partitions. + +This input often out-performs the traditional `kafka` input as well as providing more useful logs and error messages. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- kafka_key +- kafka_topic +- kafka_partition +- kafka_offset +- kafka_timestamp_unix +- kafka_tombstone_message +- All record headers +``` + + +== Fields + +=== `seed_brokers` + +A list of broker addresses to connect to in order to establish connections. If an item of the list contains commas it will be expanded into multiple addresses. + + +*Type*: `array` + + +```yml +# Examples + +seed_brokers: + - localhost:9092 + +seed_brokers: + - foo:9092 + - bar:9092 + +seed_brokers: + - foo:9092,bar:9092 +``` + +=== `topics` + +A list of topics to consume from. Multiple comma separated topics can be listed in a single element. When a `consumer_group` is specified partitions are automatically distributed across consumers of a topic, otherwise all partitions are consumed. + +Alternatively, it's possible to specify explicit partitions to consume from with a colon after the topic name, e.g. `foo:0` would consume the partition 0 of the topic foo. This syntax supports ranges, e.g. `foo:0-10` would consume partitions 0 through to 10 inclusive. + +Finally, it's also possible to specify an explicit offset to consume from by adding another colon after the partition, e.g. `foo:0:10` would consume the partition 0 of the topic foo starting from the offset 10. If the offset is not present (or remains unspecified) then the field `start_from_oldest` determines which offset to start from. + + +*Type*: `array` + + +```yml +# Examples + +topics: + - foo + - bar + +topics: + - things.* + +topics: + - foo,bar + +topics: + - foo:0 + - bar:1 + - bar:3 + +topics: + - foo:0,bar:1,bar:3 + +topics: + - foo:0-5 +``` + +=== `regexp_topics` + +Whether listed topics should be interpreted as regular expression patterns for matching multiple topics. When topics are specified with explicit partitions this field must remain set to `false`. + + +*Type*: `bool` + +*Default*: `false` + +=== `consumer_group` + +An optional consumer group to consume as. When specified the partitions of specified topics are automatically distributed across consumers sharing a consumer group, and partition offsets are automatically committed and resumed under this name. Consumer groups are not supported when specifying explicit partitions to consume from in the `topics` field. + + +*Type*: `string` + + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `"benthos"` + +=== `rack_id` + +A rack identifier for this client. + + +*Type*: `string` + +*Default*: `""` + +=== `checkpoint_limit` + +Determines how many messages of the same partition can be processed in parallel before applying back pressure. When a message of a given offset is delivered to the output the offset is only allowed to be committed when all messages of prior offsets have also been delivered, this ensures at-least-once delivery guarantees. However, this mechanism also increases the likelihood of duplicates in the event of crashes or server faults, reducing the checkpoint limit will mitigate this. + + +*Type*: `int` + +*Default*: `1024` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `commit_period` + +The period of time between each commit of the current partition offsets. Offsets are always committed during shutdown. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `start_from_oldest` + +Determines whether to consume from the oldest available offset, otherwise messages are consumed from the latest offset. The setting is applied when creating a new consumer group or the saved offset no longer exists. + + +*Type*: `bool` + +*Default*: `true` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `sasl` + +Specify one or more methods of SASL authentication. SASL is tried in order; if the broker supports the first mechanism, all connections will use that mechanism. If the first mechanism fails, the client will pick the first supported mechanism. If the broker does not support any client mechanisms, connections will fail. + + +*Type*: `array` + + +```yml +# Examples + +sasl: + - mechanism: SCRAM-SHA-512 + password: bar + username: foo +``` + +=== `sasl[].mechanism` + +The SASL mechanism to use. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `AWS_MSK_IAM` +| AWS IAM based authentication as specified by the 'aws-msk-iam-auth' java library. +| `OAUTHBEARER` +| OAuth Bearer based authentication. +| `PLAIN` +| Plain text authentication. +| `SCRAM-SHA-256` +| SCRAM based authentication as specified in RFC5802. +| `SCRAM-SHA-512` +| SCRAM based authentication as specified in RFC5802. +| `none` +| Disable sasl authentication + +|=== + +=== `sasl[].username` + +A username to provide for PLAIN or SCRAM-* authentication. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].password` + +A password to provide for PLAIN or SCRAM-* authentication. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].token` + +The token to use for a single session's OAUTHBEARER authentication. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].extensions` + +Key/value pairs to add to OAUTHBEARER authentication requests. + + +*Type*: `object` + + +=== `sasl[].aws` + +Contains AWS specific fields for when the `mechanism` is set to `AWS_MSK_IAM`. + + +*Type*: `object` + + +=== `sasl[].aws.region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `sasl[].aws.credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `sasl[].aws.credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `multi_header` + +Decode headers into lists to allow handling of multiple values with the same key + + +*Type*: `bool` + +*Default*: `false` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy] that applies to individual topic partitions in order to batch messages together before flushing them for processing. Batching can be beneficial for performance as well as useful for windowed processing, and doing so this way preserves the ordering of topic partitions. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/inputs/mongodb.adoc b/docs/modules/components/pages/inputs/mongodb.adoc new file mode 100644 index 0000000000..e1a1b30bd8 --- /dev/null +++ b/docs/modules/components/pages/inputs/mongodb.adoc @@ -0,0 +1,238 @@ += mongodb +:type: input +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a query and creates a message for each document received. + +Introduced in version 3.64.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + query: |2 # No default (required) + root.from = {"$lte": timestamp_unix()} + root.to = {"$gte": timestamp_unix()} + auto_replay_nacks: true + batch_size: 1000 # No default (optional) + sort: {} # No default (optional) + limit: 0 # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + operation: find + json_marshal_mode: canonical + query: |2 # No default (required) + root.from = {"$lte": timestamp_unix()} + root.to = {"$gte": timestamp_unix()} + auto_replay_nacks: true + batch_size: 1000 # No default (optional) + sort: {} # No default (optional) + limit: 0 # No default (optional) +``` + +-- +====== + +Once the documents from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute). + +== Fields + +=== `url` + +The URL of the target MongoDB server. + + +*Type*: `string` + + +```yml +# Examples + +url: mongodb://localhost:27017 +``` + +=== `database` + +The name of the target MongoDB database. + + +*Type*: `string` + + +=== `username` + +The username to connect to the database. + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +The password to connect to the database. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `collection` + +The collection to select from. + + +*Type*: `string` + + +=== `operation` + +The mongodb operation to perform. + + +*Type*: `string` + +*Default*: `"find"` +Requires version 4.2.0 or newer + +Options: +`find` +, `aggregate` +. + +=== `json_marshal_mode` + +The json_marshal_mode setting is optional and controls the format of the output message. + + +*Type*: `string` + +*Default*: `"canonical"` +Requires version 4.7.0 or newer + +|=== +| Option | Summary + +| `canonical` +| A string format that emphasizes type preservation at the expense of readability and interoperability. That is, conversion from canonical to BSON will generally preserve type information except in certain specific cases. +| `relaxed` +| A string format that emphasizes readability and interoperability at the expense of type preservation.That is, conversion from relaxed format to BSON can lose type information. + +|=== + +=== `query` + +Bloblang expression describing MongoDB query. + + +*Type*: `string` + + +```yml +# Examples + +query: |2 + root.from = {"$lte": timestamp_unix()} + root.to = {"$gte": timestamp_unix()} +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `batch_size` + +A explicit number of documents to batch up before flushing them for processing. Must be greater than `0`. Operations: `find`, `aggregate` + + +*Type*: `int` + +Requires version 4.26.0 or newer + +```yml +# Examples + +batch_size: 1000 +``` + +=== `sort` + +An object specifying fields to sort by, and the respective sort order (`1` ascending, `-1` descending). Note: The driver currently appears to support only one sorting key. Operations: `find` + + +*Type*: `object` + +Requires version 4.26.0 or newer + +```yml +# Examples + +sort: + name: 1 + +sort: + age: -1 +``` + +=== `limit` + +An explicit maximum number of documents to return. Operations: `find` + + +*Type*: `int` + +Requires version 4.26.0 or newer + + diff --git a/docs/modules/components/pages/inputs/mqtt.adoc b/docs/modules/components/pages/inputs/mqtt.adoc new file mode 100644 index 0000000000..a81c657027 --- /dev/null +++ b/docs/modules/components/pages/inputs/mqtt.adoc @@ -0,0 +1,434 @@ += mqtt +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Subscribe to topics on MQTT brokers. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + mqtt: + urls: [] # No default (required) + client_id: "" + connect_timeout: 30s + topics: [] # No default (required) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + mqtt: + urls: [] # No default (required) + client_id: "" + dynamic_client_id_suffix: "" # No default (optional) + connect_timeout: 30s + will: + enabled: false + qos: 0 + retained: false + topic: "" + payload: "" + user: "" + password: "" + keepalive: 30 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + topics: [] # No default (required) + qos: 1 + clean_session: true + auto_replay_nacks: true +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- mqtt_duplicate +- mqtt_qos +- mqtt_retained +- mqtt_topic +- mqtt_message_id +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - tcp://localhost:1883 +``` + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `""` + +=== `dynamic_client_id_suffix` + +Append a dynamically generated suffix to the specified `client_id` on each run of the pipeline. This can be useful when clustering Benthos producers. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `nanoid` +| append a nanoid of length 21 characters + +|=== + +=== `connect_timeout` + +The maximum amount of time to wait in order to establish a connection before the attempt is abandoned. + + +*Type*: `string` + +*Default*: `"30s"` +Requires version 3.58.0 or newer + +```yml +# Examples + +connect_timeout: 1s + +connect_timeout: 500ms +``` + +=== `will` + +Set last will message in case of Benthos failure + + +*Type*: `object` + + +=== `will.enabled` + +Whether to enable last will messages. + + +*Type*: `bool` + +*Default*: `false` + +=== `will.qos` + +Set QoS for last will message. Valid values are: 0, 1, 2. + + +*Type*: `int` + +*Default*: `0` + +=== `will.retained` + +Set retained for last will message. + + +*Type*: `bool` + +*Default*: `false` + +=== `will.topic` + +Set topic for last will message. + + +*Type*: `string` + +*Default*: `""` + +=== `will.payload` + +Set payload for last will message. + + +*Type*: `string` + +*Default*: `""` + +=== `user` + +A username to connect with. + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +A password to connect with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `keepalive` + +Max seconds of inactivity before a keepalive message is sent. + + +*Type*: `int` + +*Default*: `30` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `topics` + +A list of topics to consume from. + + +*Type*: `array` + + +=== `qos` + +The level of delivery guarantee to enforce. Has options 0, 1, 2. + + +*Type*: `int` + +*Default*: `1` + +=== `clean_session` + +Set whether the connection is non-persistent. + + +*Type*: `bool` + +*Default*: `true` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/nanomsg.adoc b/docs/modules/components/pages/inputs/nanomsg.adoc new file mode 100644 index 0000000000..9146de0c4b --- /dev/null +++ b/docs/modules/components/pages/inputs/nanomsg.adoc @@ -0,0 +1,122 @@ += nanomsg +:type: input +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes messages via Nanomsg sockets (scalability protocols). + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + nanomsg: + urls: [] # No default (required) + bind: true + socket_type: PULL + auto_replay_nacks: true + sub_filters: [] +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + nanomsg: + urls: [] # No default (required) + bind: true + socket_type: PULL + auto_replay_nacks: true + sub_filters: [] + poll_timeout: 5s +``` + +-- +====== + +Currently only PULL and SUB sockets are supported. + +== Fields + +=== `urls` + +A list of URLs to connect to (or as). If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +=== `bind` + +Whether the URLs provided should be connected to, or bound as. + + +*Type*: `bool` + +*Default*: `true` + +=== `socket_type` + +The socket type to use. + + +*Type*: `string` + +*Default*: `"PULL"` + +Options: +`PULL` +, `SUB` +. + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `sub_filters` + +A list of subscription topic filters to use when consuming from a SUB socket. Specifying a single sub_filter of `''` will subscribe to everything. + + +*Type*: `array` + +*Default*: `[]` + +=== `poll_timeout` + +The period to wait until a poll is abandoned and reattempted. + + +*Type*: `string` + +*Default*: `"5s"` + + diff --git a/docs/modules/components/pages/inputs/nats.adoc b/docs/modules/components/pages/inputs/nats.adoc new file mode 100644 index 0000000000..6a933f81af --- /dev/null +++ b/docs/modules/components/pages/inputs/nats.adoc @@ -0,0 +1,446 @@ += nats +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Subscribe to a NATS subject. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + nats: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + queue: "" # No default (optional) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + nats: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + queue: "" # No default (optional) + auto_replay_nacks: true + nak_delay: 1m # No default (optional) + prefetch_count: 524288 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) + extract_tracing_map: root = @ # No default (optional) +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- nats_subject +- nats_reply_subject +- All message headers (when supported by the connection) +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `subject` + +A subject to consume from. Supports wildcards for consuming multiple subjects. Either a subject or stream must be specified. + + +*Type*: `string` + + +```yml +# Examples + +subject: foo.bar.baz + +subject: foo.*.baz + +subject: foo.bar.* + +subject: foo.> +``` + +=== `queue` + +An optional queue group to consume as. + + +*Type*: `string` + + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `nak_delay` + +An optional delay duration on redelivering a message when negatively acknowledged. + + +*Type*: `string` + + +```yml +# Examples + +nak_delay: 1m +``` + +=== `prefetch_count` + +The maximum number of messages to pull at a time. + + +*Type*: `int` + +*Default*: `524288` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `extract_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 4.23.0 or newer + +```yml +# Examples + +extract_tracing_map: root = @ + +extract_tracing_map: root = this.meta.span +``` + + diff --git a/docs/modules/components/pages/inputs/nats_jetstream.adoc b/docs/modules/components/pages/inputs/nats_jetstream.adoc new file mode 100644 index 0000000000..e830aa6d8e --- /dev/null +++ b/docs/modules/components/pages/inputs/nats_jetstream.adoc @@ -0,0 +1,504 @@ += nats_jetstream +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads messages from NATS JetStream subjects. + +Introduced in version 3.46.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + nats_jetstream: + urls: [] # No default (required) + queue: "" # No default (optional) + subject: foo.bar.baz # No default (optional) + durable: "" # No default (optional) + stream: "" # No default (optional) + bind: false # No default (optional) + deliver: all +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + nats_jetstream: + urls: [] # No default (required) + queue: "" # No default (optional) + subject: foo.bar.baz # No default (optional) + durable: "" # No default (optional) + stream: "" # No default (optional) + bind: false # No default (optional) + deliver: all + ack_wait: 30s + max_ack_pending: 1024 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) + extract_tracing_map: root = @ # No default (optional) +``` + +-- +====== + +== Consume mirrored streams + +In the case where a stream being consumed is mirrored from a different JetStream domain the stream cannot be resolved from the subject name alone, and so the stream name as well as the subject (if applicable) must both be specified. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- nats_subject +- nats_sequence_stream +- nats_sequence_consumer +- nats_num_delivered +- nats_num_pending +- nats_domain +- nats_timestamp_unix_nano +``` + +You can access these metadata fields using +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `queue` + +An optional queue group to consume as. + + +*Type*: `string` + + +=== `subject` + +A subject to consume from. Supports wildcards for consuming multiple subjects. Either a subject or stream must be specified. + + +*Type*: `string` + + +```yml +# Examples + +subject: foo.bar.baz + +subject: foo.*.baz + +subject: foo.bar.* + +subject: foo.> +``` + +=== `durable` + +Preserve the state of your consumer under a durable name. + + +*Type*: `string` + + +=== `stream` + +A stream to consume from. Either a subject or stream must be specified. + + +*Type*: `string` + + +=== `bind` + +Indicates that the subscription should use an existing consumer. + + +*Type*: `bool` + + +=== `deliver` + +Determines which messages to deliver when consuming without a durable subscriber. + + +*Type*: `string` + +*Default*: `"all"` + +|=== +| Option | Summary + +| `all` +| Deliver all available messages. +| `last` +| Deliver starting with the last published messages. +| `last_per_subject` +| Deliver starting with the last published message per subject. +| `new` +| Deliver starting from now, not taking into account any previous messages. + +|=== + +=== `ack_wait` + +The maximum amount of time NATS server should wait for an ack from consumer. + + +*Type*: `string` + +*Default*: `"30s"` + +```yml +# Examples + +ack_wait: 100ms + +ack_wait: 5m +``` + +=== `max_ack_pending` + +The maximum number of outstanding acks to be allowed before consuming is halted. + + +*Type*: `int` + +*Default*: `1024` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `extract_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 4.23.0 or newer + +```yml +# Examples + +extract_tracing_map: root = @ + +extract_tracing_map: root = this.meta.span +``` + + diff --git a/docs/modules/components/pages/inputs/nats_kv.adoc b/docs/modules/components/pages/inputs/nats_kv.adoc new file mode 100644 index 0000000000..5912d7350a --- /dev/null +++ b/docs/modules/components/pages/inputs/nats_kv.adoc @@ -0,0 +1,443 @@ += nats_kv +:type: input +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Watches for updates in a NATS key-value bucket. + +Introduced in version 4.12.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + key: '>' + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + key: '>' + auto_replay_nacks: true + ignore_deletes: false + include_history: false + meta_only: false + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +``` text +- nats_kv_key +- nats_kv_bucket +- nats_kv_revision +- nats_kv_delta +- nats_kv_operation +- nats_kv_created +``` + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `bucket` + +The name of the KV bucket. + + +*Type*: `string` + + +```yml +# Examples + +bucket: my_kv_bucket +``` + +=== `key` + +Key to watch for updates, can include wildcards. + + +*Type*: `string` + +*Default*: `"\u003e"` + +```yml +# Examples + +key: foo.bar.baz + +key: foo.*.baz + +key: foo.bar.* + +key: foo.> +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `ignore_deletes` + +Do not send delete markers as messages. + + +*Type*: `bool` + +*Default*: `false` + +=== `include_history` + +Include all the history per key, not just the last one. + + +*Type*: `bool` + +*Default*: `false` + +=== `meta_only` + +Retrieve only the metadata of the entry + + +*Type*: `bool` + +*Default*: `false` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/inputs/nats_stream.adoc b/docs/modules/components/pages/inputs/nats_stream.adoc new file mode 100644 index 0000000000..9f08cee74b --- /dev/null +++ b/docs/modules/components/pages/inputs/nats_stream.adoc @@ -0,0 +1,474 @@ += nats_stream +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Subscribe to a NATS Stream subject. Joining a queue is optional and allows multiple clients of a subject to consume using queue semantics. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + nats_stream: + urls: [] # No default (required) + cluster_id: "" # No default (required) + client_id: "" + queue: "" + subject: "" + durable_name: "" + unsubscribe_on_close: false +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + nats_stream: + urls: [] # No default (required) + cluster_id: "" # No default (required) + client_id: "" + queue: "" + subject: "" + durable_name: "" + unsubscribe_on_close: false + start_from_oldest: true + max_inflight: 1024 + ack_wait: 30s + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) + extract_tracing_map: root = @ # No default (optional) +``` + +-- +====== + +[CAUTION] +.Deprecation notice +==== +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +==== + +Tracking and persisting offsets through a durable name is also optional and works with or without a queue. If a durable name is not provided then subjects are consumed from the most recently published message. + +When a consumer closes its connection it unsubscribes, when all consumers of a durable queue do this the offsets are deleted. In order to avoid this you can stop the consumers from unsubscribing by setting the field `unsubscribe_on_close` to `false`. + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- nats_stream_subject +- nats_stream_sequence +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `cluster_id` + +The ID of the cluster to consume from. + + +*Type*: `string` + + +=== `client_id` + +A client ID to connect as. + + +*Type*: `string` + +*Default*: `""` + +=== `queue` + +The queue to consume from. + + +*Type*: `string` + +*Default*: `""` + +=== `subject` + +A subject to consume from. + + +*Type*: `string` + +*Default*: `""` + +=== `durable_name` + +Preserve the state of your consumer under a durable name. + + +*Type*: `string` + +*Default*: `""` + +=== `unsubscribe_on_close` + +Whether the subscription should be destroyed when this client disconnects. + + +*Type*: `bool` + +*Default*: `false` + +=== `start_from_oldest` + +If a position is not found for a queue, determines whether to consume from the oldest available message, otherwise messages are consumed from the latest. + + +*Type*: `bool` + +*Default*: `true` + +=== `max_inflight` + +The maximum number of unprocessed messages to fetch at a given time. + + +*Type*: `int` + +*Default*: `1024` + +=== `ack_wait` + +An optional duration to specify at which a message that is yet to be acked will be automatically retried. + + +*Type*: `string` + +*Default*: `"30s"` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `extract_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 4.23.0 or newer + +```yml +# Examples + +extract_tracing_map: root = @ + +extract_tracing_map: root = this.meta.span +``` + + diff --git a/docs/modules/components/pages/inputs/nsq.adoc b/docs/modules/components/pages/inputs/nsq.adoc new file mode 100644 index 0000000000..61f371bdd6 --- /dev/null +++ b/docs/modules/components/pages/inputs/nsq.adoc @@ -0,0 +1,305 @@ += nsq +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Subscribe to an NSQ instance topic and channel. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + nsq: + nsqd_tcp_addresses: [] # No default (required) + lookupd_http_addresses: [] # No default (required) + topic: "" # No default (required) + channel: "" # No default (required) + user_agent: "" # No default (optional) + max_in_flight: 100 + max_attempts: 5 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + nsq: + nsqd_tcp_addresses: [] # No default (required) + lookupd_http_addresses: [] # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + topic: "" # No default (required) + channel: "" # No default (required) + user_agent: "" # No default (optional) + max_in_flight: 100 + max_attempts: 5 +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +``` text +- nsq_attempts +- nsq_id +- nsq_nsqd_address +- nsq_timestamp +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + + +== Fields + +=== `nsqd_tcp_addresses` + +A list of nsqd addresses to connect to. + + +*Type*: `array` + + +=== `lookupd_http_addresses` + +A list of nsqlookupd addresses to connect to. + + +*Type*: `array` + + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `topic` + +The topic to consume from. + + +*Type*: `string` + + +=== `channel` + +The channel to consume from. + + +*Type*: `string` + + +=== `user_agent` + +A user agent to assume when connecting. + + +*Type*: `string` + + +=== `max_in_flight` + +The maximum number of pending messages to consume at any given time. + + +*Type*: `int` + +*Default*: `100` + +=== `max_attempts` + +The maximum number of attempts to successfully consume a messages. + + +*Type*: `int` + +*Default*: `5` + + diff --git a/docs/modules/components/pages/inputs/parquet.adoc b/docs/modules/components/pages/inputs/parquet.adoc new file mode 100644 index 0000000000..b890e519df --- /dev/null +++ b/docs/modules/components/pages/inputs/parquet.adoc @@ -0,0 +1,100 @@ += parquet +:type: input +:status: experimental +:categories: ["Local"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads and decodes https://parquet.apache.org/docs/[Parquet files] into a stream of structured messages. + +Introduced in version 4.8.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + parquet: + paths: [] # No default (required) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + parquet: + paths: [] # No default (required) + batch_count: 1 + auto_replay_nacks: true +``` + +-- +====== + +This input uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. + +By default any BYTE_ARRAY or FIXED_LEN_BYTE_ARRAY value will be extracted as a byte slice (`[]byte`) unless the logical type is UTF8, in which case they are extracted as a string (`string`). + +When a value extracted as a byte slice exists within a document which is later JSON serialized by default it will be base 64 encoded into strings, which is the default for arbitrary data fields. It is possible to convert these binary values to strings (or other data types) using Bloblang transformations such as `root.foo = this.foo.string()` or `root.foo = this.foo.encode("hex")`, etc. + +== Fields + +=== `paths` + +A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star). + + +*Type*: `array` + + +```yml +# Examples + +paths: /tmp/foo.parquet + +paths: /tmp/bar/*.parquet + +paths: /tmp/data/**/*.parquet +``` + +=== `batch_count` + +Optionally process records in batches. This can help to speed up the consumption of exceptionally large files. When the end of the file is reached the remaining records are processed as a (potentially smaller) batch. + + +*Type*: `int` + +*Default*: `1` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/pulsar.adoc b/docs/modules/components/pages/inputs/pulsar.adoc new file mode 100644 index 0000000000..983fae4f9c --- /dev/null +++ b/docs/modules/components/pages/inputs/pulsar.adoc @@ -0,0 +1,258 @@ += pulsar +:type: input +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads messages from an Apache Pulsar server. + +Introduced in version 3.43.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + pulsar: + url: pulsar://localhost:6650 # No default (required) + topics: [] # No default (optional) + topics_pattern: "" # No default (optional) + subscription_name: "" # No default (required) + subscription_type: shared + tls: + root_cas_file: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + pulsar: + url: pulsar://localhost:6650 # No default (required) + topics: [] # No default (optional) + topics_pattern: "" # No default (optional) + subscription_name: "" # No default (required) + subscription_type: shared + tls: + root_cas_file: "" + auth: + oauth2: + enabled: false + audience: "" + issuer_url: "" + private_key_file: "" + token: + enabled: false + token: "" +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- pulsar_message_id +- pulsar_key +- pulsar_ordering_key +- pulsar_event_time_unix +- pulsar_publish_time_unix +- pulsar_topic +- pulsar_producer_name +- pulsar_redelivery_count +- All properties of the message +``` + +You can access these metadata fields using +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + + +== Fields + +=== `url` + +A URL to connect to. + + +*Type*: `string` + + +```yml +# Examples + +url: pulsar://localhost:6650 + +url: pulsar://pulsar.us-west.example.com:6650 + +url: pulsar+ssl://pulsar.us-west.example.com:6651 +``` + +=== `topics` + +A list of topics to subscribe to. This or topics_pattern must be set. + + +*Type*: `array` + + +=== `topics_pattern` + +A regular expression matching the topics to subscribe to. This or topics must be set. + + +*Type*: `string` + + +=== `subscription_name` + +Specify the subscription name for this consumer. + + +*Type*: `string` + + +=== `subscription_type` + +Specify the subscription type for this consumer. + +> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement[Pulsar documentation] and https://github.com/apache/pulsar/issues/12208[this Github issue] for more details. + + +*Type*: `string` + +*Default*: `"shared"` + +Options: +`shared` +, `key_shared` +, `failover` +, `exclusive` +. + +=== `tls` + +Specify the path to a custom CA certificate to trust broker TLS service. + + +*Type*: `object` + + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `auth` + +Optional configuration of Pulsar authentication methods. + + +*Type*: `object` + +Requires version 3.60.0 or newer + +=== `auth.oauth2` + +Parameters for Pulsar OAuth2 authentication. + + +*Type*: `object` + + +=== `auth.oauth2.enabled` + +Whether OAuth2 is enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `auth.oauth2.audience` + +OAuth2 audience. + + +*Type*: `string` + +*Default*: `""` + +=== `auth.oauth2.issuer_url` + +OAuth2 issuer URL. + + +*Type*: `string` + +*Default*: `""` + +=== `auth.oauth2.private_key_file` + +The path to a file containing a private key. + + +*Type*: `string` + +*Default*: `""` + +=== `auth.token` + +Parameters for Pulsar Token authentication. + + +*Type*: `object` + + +=== `auth.token.enabled` + +Whether Token Auth is enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `auth.token.token` + +Actual base64 encoded token. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/inputs/read_until.adoc b/docs/modules/components/pages/inputs/read_until.adoc new file mode 100644 index 0000000000..7370ef689a --- /dev/null +++ b/docs/modules/components/pages/inputs/read_until.adoc @@ -0,0 +1,134 @@ += read_until +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads messages from a child input until a consumed message passes a xref:guides:bloblang/about.adoc[Bloblang query], at which point the input closes. It is also possible to configure a timeout after which the input is closed if no new messages arrive in that period. + +```yml +# Config fields, showing default values +input: + label: "" + read_until: + input: null # No default (required) + check: this.type == "foo" # No default (optional) + idle_timeout: 5s # No default (optional) + restart_input: false +``` + +Messages are read continuously while the query check returns false, when the query returns true the message that triggered the check is sent out and the input is closed. Use this to define inputs where the stream should end once a certain message appears. + +If the idle timeout is configured, the input will be closed if no new messages arrive after that period of time. Use this field if you want to empty out and close an input that doesn't have a logical end. + +Sometimes inputs close themselves. For example, when the `file` input type reaches the end of a file it will shut down. By default this type will also shut down. If you wish for the input type to be restarted every time it shuts down until the query check is met then set `restart_input` to `true`. + +== Metadata + +A metadata key `benthos_read_until` containing the value `final` is added to the first part of the message that triggers the input to stop. + +== Fields + +=== `input` + +The child input to consume from. + + +*Type*: `input` + + +=== `check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether the input should now be closed. + + +*Type*: `string` + + +```yml +# Examples + +check: this.type == "foo" + +check: count("messages") >= 100 +``` + +=== `idle_timeout` + +The maximum amount of time without receiving new messages after which the input is closed. + + +*Type*: `string` + + +```yml +# Examples + +idle_timeout: 5s +``` + +=== `restart_input` + +Whether the input should be reopened if it closes itself before the condition has resolved to true. + + +*Type*: `bool` + +*Default*: `false` + +== Examples + +[tabs] +====== +Consume N Messages:: ++ +-- + +A common reason to use this input is to consume only N messages from an input and then stop. This can easily be done with the xref:guides:bloblang/functions.adoc#count[`count` function]: + +```yaml +# Only read 100 messages, and then exit. +input: + read_until: + check: count("messages") >= 100 + input: + kafka: + addresses: [ TODO ] + topics: [ foo, bar ] + consumer_group: foogroup +``` + +-- +Read from a kafka and close when empty:: ++ +-- + +A common reason to use this input is a job that consumes all messages and exits once its empty: + +```yaml +# Consumes all messages and exit when the last message was consumed 5s ago. +input: + read_until: + idle_timeout: 5s + input: + kafka: + addresses: [ TODO ] + topics: [ foo, bar ] + consumer_group: foogroup +``` + +-- +====== + + diff --git a/docs/modules/components/pages/inputs/redis_list.adoc b/docs/modules/components/pages/inputs/redis_list.adoc new file mode 100644 index 0000000000..95a98319fa --- /dev/null +++ b/docs/modules/components/pages/inputs/redis_list.adoc @@ -0,0 +1,339 @@ += redis_list +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Pops messages from the beginning of a Redis list using the BLPop command. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + redis_list: + url: redis://:6397 # No default (required) + key: "" # No default (required) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + redis_list: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + key: "" # No default (required) + auto_replay_nacks: true + max_in_flight: 0 + timeout: 5s + command: blpop +``` + +-- +====== + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `key` + +The key of a list to read from. + + +*Type*: `string` + + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `max_in_flight` + +Optionally sets a limit on the number of messages that can be flowing through a Benthos stream pending acknowledgment from the input at any given time. Once a message has been either acknowledged or rejected (nacked) it is no longer considered pending. If the input produces logical batches then each batch is considered a single count against the maximum. **WARNING**: Batching policies at the output level will stall if this field limits the number of messages below the batching threshold. Zero (default) or lower implies no limit. + + +*Type*: `int` + +*Default*: `0` +Requires version 4.9.0 or newer + +=== `timeout` + +The length of time to poll for new messages before reattempting. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `command` + +The command used to pop elements from the Redis list + + +*Type*: `string` + +*Default*: `"blpop"` +Requires version 4.22.0 or newer + +Options: +`blpop` +, `brpop` +. + + diff --git a/docs/modules/components/pages/inputs/redis_pubsub.adoc b/docs/modules/components/pages/inputs/redis_pubsub.adoc new file mode 100644 index 0000000000..586093fca5 --- /dev/null +++ b/docs/modules/components/pages/inputs/redis_pubsub.adoc @@ -0,0 +1,321 @@ += redis_pubsub +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consume from a Redis publish/subscribe channel using either the SUBSCRIBE or PSUBSCRIBE commands. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + redis_pubsub: + url: redis://:6397 # No default (required) + channels: [] # No default (required) + use_patterns: false + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + redis_pubsub: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + channels: [] # No default (required) + use_patterns: false + auto_replay_nacks: true +``` + +-- +====== + +In order to subscribe to channels using the `PSUBSCRIBE` command set the field `use_patterns` to `true`, then you can include glob-style patterns in your channel names. For example: + +- `h?llo` subscribes to hello, hallo and hxllo +- `h*llo` subscribes to hllo and heeeello +- `h[ae]llo` subscribes to hello and hallo, but not hillo + +Use `\` to escape special characters if you want to match them verbatim. + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `channels` + +A list of channels to consume from. + + +*Type*: `array` + + +=== `use_patterns` + +Whether to use the PSUBSCRIBE command, allowing for glob-style patterns within target channel names. + + +*Type*: `bool` + +*Default*: `false` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/redis_scan.adoc b/docs/modules/components/pages/inputs/redis_scan.adoc new file mode 100644 index 0000000000..7190bbcd3d --- /dev/null +++ b/docs/modules/components/pages/inputs/redis_scan.adoc @@ -0,0 +1,330 @@ += redis_scan +:type: input +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Scans the set of keys in the current selected database and gets their values, using the Scan and Get commands. + +Introduced in version 4.27.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + redis_scan: + url: redis://:6397 # No default (required) + auto_replay_nacks: true + match: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + redis_scan: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auto_replay_nacks: true + match: "" +``` + +-- +====== + +Optionally, iterates only elements matching a blob-style pattern. For example: +- `*foo*` iterates only keys which contain `foo` in it. +- `foo*` iterates only keys starting with `foo`. + +This input generates a message for each key value pair in the following format: + +```json +{"key":"foo","value":"bar"} +``` + + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `match` + +Iterates only elements matching the optional glob-style pattern. By default, it matches all elements. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +match: '*' + +match: 1* + +match: foo* + +match: foo + +match: '*4*' +``` + + diff --git a/docs/modules/components/pages/inputs/redis_streams.adoc b/docs/modules/components/pages/inputs/redis_streams.adoc new file mode 100644 index 0000000000..581f56edfc --- /dev/null +++ b/docs/modules/components/pages/inputs/redis_streams.adoc @@ -0,0 +1,388 @@ += redis_streams +:type: input +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Pulls messages from Redis (v5.0+) streams with the XREADGROUP command. The `client_id` should be unique for each consumer of a group. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + redis_streams: + url: redis://:6397 # No default (required) + body_key: body + streams: [] # No default (required) + auto_replay_nacks: true + limit: 10 + client_id: "" + consumer_group: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + redis_streams: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + body_key: body + streams: [] # No default (required) + auto_replay_nacks: true + limit: 10 + client_id: "" + consumer_group: "" + create_streams: true + start_from_oldest: true + commit_period: 1s + timeout: 1s +``` + +-- +====== + +Redis stream entries are key/value pairs, as such it is necessary to specify the key that contains the body of the message. All other keys/value pairs are saved as metadata fields. + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `body_key` + +The field key to extract the raw message from. All other keys will be stored in the message as metadata. + + +*Type*: `string` + +*Default*: `"body"` + +=== `streams` + +A list of streams to consume from. + + +*Type*: `array` + + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `limit` + +The maximum number of messages to consume from a single request. + + +*Type*: `int` + +*Default*: `10` + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `""` + +=== `consumer_group` + +An identifier for the consumer group of the stream. + + +*Type*: `string` + +*Default*: `""` + +=== `create_streams` + +Create subscribed streams if they do not exist (MKSTREAM option). + + +*Type*: `bool` + +*Default*: `true` + +=== `start_from_oldest` + +If an offset is not found for a stream, determines whether to consume from the oldest available offset, otherwise messages are consumed from the latest offset. + + +*Type*: `bool` + +*Default*: `true` + +=== `commit_period` + +The period of time between each commit of the current offset. Offsets are always committed during shutdown. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `timeout` + +The length of time to poll for new messages before reattempting. + + +*Type*: `string` + +*Default*: `"1s"` + + diff --git a/docs/modules/components/pages/inputs/resource.adoc b/docs/modules/components/pages/inputs/resource.adoc new file mode 100644 index 0000000000..513a66b691 --- /dev/null +++ b/docs/modules/components/pages/inputs/resource.adoc @@ -0,0 +1,67 @@ += resource +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Resource is an input type that channels messages from a resource input, identified by its name. + +```yml +# Config fields, showing default values +input: + resource: "" +``` + +Resources allow you to tidy up deeply nested configs. For example, the config: + +```yaml +input: + broker: + inputs: + - kafka: + addresses: [ TODO ] + topics: [ foo ] + consumer_group: foogroup + - gcp_pubsub: + project: bar + subscription: baz +``` + +Could also be expressed as: + +```yaml +input: + broker: + inputs: + - resource: foo + - resource: bar + +input_resources: + - label: foo + kafka: + addresses: [ TODO ] + topics: [ foo ] + consumer_group: foogroup + + - label: bar + gcp_pubsub: + project: bar + subscription: baz +``` + +Resources also allow you to reference a single input in multiple places, such as multiple streams mode configs, or multiple entries in a broker input. However, when a resource is referenced more than once the messages it produces are distributed across those references, so each message will only be directed to a single reference, not all of them. + +You can find out more about resources in xref:configuration:resources.adoc[]. + + diff --git a/docs/modules/components/pages/inputs/sequence.adoc b/docs/modules/components/pages/inputs/sequence.adoc new file mode 100644 index 0000000000..6193b7fcc1 --- /dev/null +++ b/docs/modules/components/pages/inputs/sequence.adoc @@ -0,0 +1,262 @@ += sequence +:type: input +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Reads messages from a sequence of child inputs, starting with the first and once that input gracefully terminates starts consuming from the next, and so on. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + sequence: + inputs: [] # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + sequence: + sharded_join: + type: none + id_path: "" + iterations: 1 + merge_strategy: array + inputs: [] # No default (required) +``` + +-- +====== + +This input is useful for consuming from inputs that have an explicit end but must not be consumed in parallel. + +== Examples + +[tabs] +====== +End of Stream Message:: ++ +-- + +A common use case for sequence might be to generate a message at the end of our main input. With the following config once the records within `./dataset.csv` are exhausted our final payload `{"status":"finished"}` will be routed through the pipeline. + +```yaml +input: + sequence: + inputs: + - file: + paths: [ ./dataset.csv ] + scanner: + csv: {} + - generate: + count: 1 + mapping: 'root = {"status":"finished"}' +``` + +-- +Joining Data (Simple):: ++ +-- + +Benthos can be used to join unordered data from fragmented datasets in memory by specifying a common identifier field and a number of sharded iterations. For example, given two CSV files, the first called "main.csv", which contains rows of user data: + +```csv +uuid,name,age +AAA,Melanie,34 +BBB,Emma,28 +CCC,Geri,45 +``` + +And the second called "hobbies.csv" that, for each user, contains zero or more rows of hobbies: + +```csv +uuid,hobby +CCC,pokemon go +AAA,rowing +AAA,golf +``` + +We can parse and join this data into a single dataset: + +```json +{"uuid":"AAA","name":"Melanie","age":34,"hobbies":["rowing","golf"]} +{"uuid":"BBB","name":"Emma","age":28} +{"uuid":"CCC","name":"Geri","age":45,"hobbies":["pokemon go"]} +``` + +With the following config: + +```yaml +input: + sequence: + sharded_join: + type: full-outer + id_path: uuid + merge_strategy: array + inputs: + - file: + paths: + - ./hobbies.csv + - ./main.csv + scanner: + csv: {} +``` + +-- +Joining Data (Advanced):: ++ +-- + +In this example we are able to join unordered and fragmented data from a combination of CSV files and newline-delimited JSON documents by specifying multiple sequence inputs with their own processors for extracting the structured data. + +The first file "main.csv" contains straight forward CSV data: + +```csv +uuid,name,age +AAA,Melanie,34 +BBB,Emma,28 +CCC,Geri,45 +``` + +And the second file called "hobbies.ndjson" contains JSON documents, one per line, that associate an identifier with an array of hobbies. However, these data objects are in a nested format: + +```json +{"document":{"uuid":"CCC","hobbies":[{"type":"pokemon go"}]}} +{"document":{"uuid":"AAA","hobbies":[{"type":"rowing"},{"type":"golf"}]}} +``` + +And so we will want to map these into a flattened structure before the join, and then we will end up with a single dataset that looks like this: + +```json +{"uuid":"AAA","name":"Melanie","age":34,"hobbies":["rowing","golf"]} +{"uuid":"BBB","name":"Emma","age":28} +{"uuid":"CCC","name":"Geri","age":45,"hobbies":["pokemon go"]} +``` + +With the following config: + +```yaml +input: + sequence: + sharded_join: + type: full-outer + id_path: uuid + iterations: 10 + merge_strategy: array + inputs: + - file: + paths: [ ./main.csv ] + scanner: + csv: {} + - file: + paths: [ ./hobbies.ndjson ] + scanner: + lines: {} + processors: + - mapping: | + root.uuid = this.document.uuid + root.hobbies = this.document.hobbies.map_each(this.type) +``` + +-- +====== + +== Fields + +=== `sharded_join` + +EXPERIMENTAL: Provides a way to perform outer joins of arbitrarily structured and unordered data resulting from the input sequence, even when the overall size of the data surpasses the memory available on the machine. + +When configured the sequence of inputs will be consumed one or more times according to the number of iterations, and when more than one iteration is specified each iteration will process an entirely different set of messages by sharding them by the ID field. Increasing the number of iterations reduces the memory consumption at the cost of needing to fully parse the data each time. + +Each message must be structured (JSON or otherwise processed into a structured form) and the fields will be aggregated with those of other messages sharing the ID. At the end of each iteration the joined messages are flushed downstream before the next iteration begins, hence keeping memory usage limited. + + +*Type*: `object` + +Requires version 3.40.0 or newer + +=== `sharded_join.type` + +The type of join to perform. A `full-outer` ensures that all identifiers seen in any of the input sequences are sent, and is performed by consuming all input sequences before flushing the joined results. An `outer` join consumes all input sequences but only writes data joined from the last input in the sequence, similar to a left or right outer join. With an `outer` join if an identifier appears multiple times within the final sequence input it will be flushed each time it appears. `full-outter` and `outter` have been deprecated in favour of `full-outer` and `outer`. + + +*Type*: `string` + +*Default*: `"none"` + +Options: +`none` +, `full-outer` +, `outer` +, `full-outter` +, `outter` +. + +=== `sharded_join.id_path` + +A xref:configuration:field_paths.adoc[dot path] that points to a common field within messages of each fragmented data set and can be used to join them. Messages that are not structured or are missing this field will be dropped. This field must be set in order to enable joins. + + +*Type*: `string` + +*Default*: `""` + +=== `sharded_join.iterations` + +The total number of iterations (shards), increasing this number will increase the overall time taken to process the data, but reduces the memory used in the process. The real memory usage required is significantly higher than the real size of the data and therefore the number of iterations should be at least an order of magnitude higher than the available memory divided by the overall size of the dataset. + + +*Type*: `int` + +*Default*: `1` + +=== `sharded_join.merge_strategy` + +The chosen strategy to use when a data join would otherwise result in a collision of field values. The strategy `array` means non-array colliding values are placed into an array and colliding arrays are merged. The strategy `replace` replaces old values with new values. The strategy `keep` keeps the old value. + + +*Type*: `string` + +*Default*: `"array"` + +Options: +`array` +, `replace` +, `keep` +. + +=== `inputs` + +An array of inputs to read from sequentially. + + +*Type*: `array` + + + diff --git a/docs/modules/components/pages/inputs/sftp.adoc b/docs/modules/components/pages/inputs/sftp.adoc new file mode 100644 index 0000000000..eb5d13152f --- /dev/null +++ b/docs/modules/components/pages/inputs/sftp.adoc @@ -0,0 +1,257 @@ += sftp +:type: input +:status: beta +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes files from an SFTP server. + +Introduced in version 3.39.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + sftp: + address: "" # No default (required) + credentials: + username: "" + password: "" + private_key_file: "" + private_key_pass: "" + paths: [] # No default (required) + auto_replay_nacks: true + scanner: + to_the_end: {} + watcher: + enabled: false + minimum_age: 1s + poll_interval: 1s + cache: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + sftp: + address: "" # No default (required) + credentials: + username: "" + password: "" + private_key_file: "" + private_key_pass: "" + paths: [] # No default (required) + auto_replay_nacks: true + scanner: + to_the_end: {} + delete_on_finish: false + watcher: + enabled: false + minimum_age: 1s + poll_interval: 1s + cache: "" +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +``` +- sftp_path +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Fields + +=== `address` + +The address of the server to connect to. + + +*Type*: `string` + + +=== `credentials` + +The credentials to use to log into the target server. + + +*Type*: `object` + + +=== `credentials.username` + +The username to connect to the SFTP server. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.password` + +The password for the username to connect to the SFTP server. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.private_key_file` + +The private key for the username to connect to the SFTP server. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.private_key_pass` + +Optional passphrase for private key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `paths` + +A list of paths to consume sequentially. Glob patterns are supported. + + +*Type*: `array` + + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"to_the_end":{}}` +Requires version 4.25.0 or newer + +=== `delete_on_finish` + +Whether to delete files from the server once they are processed. + + +*Type*: `bool` + +*Default*: `false` + +=== `watcher` + +An experimental mode whereby the input will periodically scan the target paths for new files and consume them, when all files are consumed the input will continue polling for new files. + + +*Type*: `object` + +Requires version 3.42.0 or newer + +=== `watcher.enabled` + +Whether file watching is enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `watcher.minimum_age` + +The minimum period of time since a file was last updated before attempting to consume it. Increasing this period decreases the likelihood that a file will be consumed whilst it is still being written to. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +minimum_age: 10s + +minimum_age: 1m + +minimum_age: 10m +``` + +=== `watcher.poll_interval` + +The interval between each attempt to scan the target paths for new files. + + +*Type*: `string` + +*Default*: `"1s"` + +```yml +# Examples + +poll_interval: 100ms + +poll_interval: 1s +``` + +=== `watcher.cache` + +A xref:components:caches/about.adoc[cache resource] for storing the paths of files already consumed. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/inputs/socket.adoc b/docs/modules/components/pages/inputs/socket.adoc new file mode 100644 index 0000000000..273424d187 --- /dev/null +++ b/docs/modules/components/pages/inputs/socket.adoc @@ -0,0 +1,82 @@ += socket +:type: input +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to a tcp or unix socket and consumes a continuous stream of messages. + +```yml +# Config fields, showing default values +input: + label: "" + socket: + network: "" # No default (required) + address: /tmp/benthos.sock # No default (required) + auto_replay_nacks: true + scanner: + lines: {} +``` + +== Fields + +=== `network` + +A network type to assume (unix|tcp). + + +*Type*: `string` + + +Options: +`unix` +, `tcp` +. + +=== `address` + +The address to connect to. + + +*Type*: `string` + + +```yml +# Examples + +address: /tmp/benthos.sock + +address: 127.0.0.1:6000 +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"lines":{}}` +Requires version 4.25.0 or newer + + diff --git a/docs/modules/components/pages/inputs/socket_server.adoc b/docs/modules/components/pages/inputs/socket_server.adoc new file mode 100644 index 0000000000..55308c5ece --- /dev/null +++ b/docs/modules/components/pages/inputs/socket_server.adoc @@ -0,0 +1,131 @@ += socket_server +:type: input +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Creates a server that receives a stream of messages over a TCP, UDP or Unix socket. + +```yml +# Config fields, showing default values +input: + label: "" + socket_server: + network: "" # No default (required) + address: /tmp/benthos.sock # No default (required) + address_cache: "" # No default (optional) + tls: + cert_file: "" # No default (optional) + key_file: "" # No default (optional) + self_signed: false + auto_replay_nacks: true + scanner: + lines: {} +``` + +== Fields + +=== `network` + +A network type to accept. + + +*Type*: `string` + + +Options: +`unix` +, `tcp` +, `udp` +, `tls` +. + +=== `address` + +The address to listen from. + + +*Type*: `string` + + +```yml +# Examples + +address: /tmp/benthos.sock + +address: 0.0.0.0:6000 +``` + +=== `address_cache` + +An optional xref:components:caches/about.adoc[`cache`] within which this input should write it's bound address once known. The key of the cache item containing the address will be the label of the component suffixed with `_address` (e.g. `foo_address`), or `socket_server_address` when a label has not been provided. This is useful in situations where the address is dynamically allocated by the server (`127.0.0.1:0`) and you want to store the allocated address somewhere for reference by other systems and components. + + +*Type*: `string` + +Requires version 4.25.0 or newer + +=== `tls` + +TLS specific configuration, valid when the `network` is set to `tls`. + + +*Type*: `object` + + +=== `tls.cert_file` + +PEM encoded certificate for use with TLS. + + +*Type*: `string` + + +=== `tls.key_file` + +PEM encoded private key for use with TLS. + + +*Type*: `string` + + +=== `tls.self_signed` + +Whether to generate self signed certificates. + + +*Type*: `bool` + +*Default*: `false` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"lines":{}}` +Requires version 4.25.0 or newer + + diff --git a/docs/modules/components/pages/inputs/sql_raw.adoc b/docs/modules/components/pages/inputs/sql_raw.adoc new file mode 100644 index 0000000000..b3c40785a9 --- /dev/null +++ b/docs/modules/components/pages/inputs/sql_raw.adoc @@ -0,0 +1,317 @@ += sql_raw +:type: input +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a select query and creates a message for each row received. + +Introduced in version 4.10.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + sql_raw: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + query: SELECT * FROM footable WHERE user_id = $1; # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + sql_raw: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + query: SELECT * FROM footable WHERE user_id = $1; # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + auto_replay_nacks: true + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) +``` + +-- +====== + +Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute). + +== Examples + +[tabs] +====== +Consumes an SQL table using a query as an input.:: ++ +-- + + +Here we preform an aggregate over a list of names in a table that are less than 3600 seconds old. + +```yaml +input: + sql_raw: + driver: postgres + dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable + query: "SELECT name, count(*) FROM person WHERE last_updated < $1 GROUP BY name;" + args_mapping: | + root = [ + now().ts_unix() - 3600 + ] +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `query` + +The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: + +| Driver | Placeholder Style | +|---|---| +| `clickhouse` | Dollar sign | +| `mysql` | Question mark | +| `postgres` | Dollar sign | +| `mssql` | Question mark | +| `sqlite` | Question mark | +| `oracle` | Colon | +| `snowflake` | Question mark | +| `trino` | Question mark | +| `gocosmos` | Colon | + + +*Type*: `string` + + +```yml +# Examples + +query: SELECT * FROM footable WHERE user_id = $1; +``` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of columns specified. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/inputs/sql_select.adoc b/docs/modules/components/pages/inputs/sql_select.adoc new file mode 100644 index 0000000000..d9dc1e7738 --- /dev/null +++ b/docs/modules/components/pages/inputs/sql_select.adoc @@ -0,0 +1,363 @@ += sql_select +:type: input +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a select query and creates a message for each row received. + +Introduced in version 3.59.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + sql_select: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + where: type = ? and created_at > ? # No default (optional) + args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + sql_select: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + where: type = ? and created_at > ? # No default (optional) + args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) + prefix: "" # No default (optional) + suffix: "" # No default (optional) + auto_replay_nacks: true + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) +``` + +-- +====== + +Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute). + +== Examples + +[tabs] +====== +Consume a Table (PostgreSQL):: ++ +-- + + +Here we define a pipeline that will consume all rows from a table created within the last hour by comparing the unix timestamp stored in the row column "created_at": + +```yaml +input: + sql_select: + driver: postgres + dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable + table: footable + columns: [ '*' ] + where: created_at >= ? + args_mapping: | + root = [ + now().ts_unix() - 3600 + ] +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `table` + +The table to select from. + + +*Type*: `string` + + +```yml +# Examples + +table: foo +``` + +=== `columns` + +A list of columns to select. + + +*Type*: `array` + + +```yml +# Examples + +columns: + - '*' + +columns: + - foo + - bar + - baz +``` + +=== `where` + +An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks, and will automatically be converted to dollar syntax when the postgres or clickhouse drivers are used. + + +*Type*: `string` + + +```yml +# Examples + +where: type = ? and created_at > ? + +where: user_id = ? +``` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ "article", now().ts_format("2006-01-02") ] +``` + +=== `prefix` + +An optional prefix to prepend to the select query (before SELECT). + + +*Type*: `string` + + +=== `suffix` + +An optional suffix to append to the select query. + + +*Type*: `string` + + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/inputs/stdin.adoc b/docs/modules/components/pages/inputs/stdin.adoc new file mode 100644 index 0000000000..f581e7e2dd --- /dev/null +++ b/docs/modules/components/pages/inputs/stdin.adoc @@ -0,0 +1,51 @@ += stdin +:type: input +:status: stable +:categories: ["Local"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes data piped to stdin, chopping it into individual messages according to the specified scanner. + +```yml +# Config fields, showing default values +input: + label: "" + stdin: + scanner: + lines: {} + auto_replay_nacks: true +``` + +== Fields + +=== `scanner` + +The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. + + +*Type*: `scanner` + +*Default*: `{"lines":{}}` +Requires version 4.25.0 or newer + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + + diff --git a/docs/modules/components/pages/inputs/subprocess.adoc b/docs/modules/components/pages/inputs/subprocess.adoc new file mode 100644 index 0000000000..d2eac084f0 --- /dev/null +++ b/docs/modules/components/pages/inputs/subprocess.adoc @@ -0,0 +1,124 @@ += subprocess +:type: input +:status: beta +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a command, runs it as a subprocess, and consumes messages from it over stdout. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + subprocess: + name: cat # No default (required) + args: [] + codec: lines + restart_on_exit: false +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + subprocess: + name: cat # No default (required) + args: [] + codec: lines + restart_on_exit: false + max_buffer: 65536 +``` + +-- +====== + +Messages are consumed according to a specified codec. The command is executed once and if it terminates the input also closes down gracefully. Alternatively, the field `restart_on_close` can be set to `true` in order to have Benthos re-execute the command each time it stops. + +The field `max_buffer` defines the maximum message size able to be read from the subprocess. This value should be set significantly above the real expected maximum message size. + +The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. + +== Fields + +=== `name` + +The command to execute as a subprocess. + + +*Type*: `string` + + +```yml +# Examples + +name: cat + +name: sed + +name: awk +``` + +=== `args` + +A list of arguments to provide the command. + + +*Type*: `array` + +*Default*: `[]` + +=== `codec` + +The way in which messages should be consumed from the subprocess. + + +*Type*: `string` + +*Default*: `"lines"` + +Options: +`lines` +. + +=== `restart_on_exit` + +Whether the command should be re-executed each time the subprocess ends. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_buffer` + +The maximum expected size of an individual message. + + +*Type*: `int` + +*Default*: `65536` + + diff --git a/docs/modules/components/pages/inputs/twitter_search.adoc b/docs/modules/components/pages/inputs/twitter_search.adoc new file mode 100644 index 0000000000..9c885ba304 --- /dev/null +++ b/docs/modules/components/pages/inputs/twitter_search.adoc @@ -0,0 +1,153 @@ += twitter_search +:type: input +:status: experimental +:categories: ["Services","Social"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes tweets matching a given search using the Twitter recent search V2 API. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + twitter_search: + query: "" # No default (required) + tweet_fields: [] + poll_period: 1m + backfill_period: 5m + cache: "" # No default (required) + api_key: "" # No default (required) + api_secret: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + twitter_search: + query: "" # No default (required) + tweet_fields: [] + poll_period: 1m + backfill_period: 5m + cache: "" # No default (required) + cache_key: last_tweet_id + rate_limit: "" + api_key: "" # No default (required) + api_secret: "" # No default (required) +``` + +-- +====== + +Continuously polls the [Twitter recent search V2 API](https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent) for tweets that match a given search query. + +Each tweet received is emitted as a JSON object message, with a field `id` and `text` by default. Extra fields [can be obtained from the search API](https://developer.twitter.com/en/docs/twitter-api/fields) when listed with the `tweet_fields` field. + +In order to paginate requests that are made the ID of the latest received tweet is stored in a [cache resource](/docs/components/caches/about), which is then used by subsequent requests to ensure only tweets after it are consumed. It is recommended that the cache you use is persistent so that Benthos can resume searches at the correct place on a restart. + +Authentication is done using OAuth 2.0 credentials which can be generated within the [Twitter developer portal](https://developer.twitter.com). + + +== Fields + +=== `query` + +A search expression to use. + + +*Type*: `string` + + +=== `tweet_fields` + +An optional list of additional fields to obtain for each tweet, by default only the fields `id` and `text` are returned. For more info refer to the [twitter API docs.](https://developer.twitter.com/en/docs/twitter-api/fields) + + +*Type*: `array` + +*Default*: `[]` + +=== `poll_period` + +The length of time (as a duration string) to wait between each search request. This field can be set empty, in which case requests are made at the limit set by the rate limit. This field also supports cron expressions. + + +*Type*: `string` + +*Default*: `"1m"` + +=== `backfill_period` + +A duration string indicating the maximum age of tweets to acquire when starting a search. + + +*Type*: `string` + +*Default*: `"5m"` + +=== `cache` + +A cache resource to use for request pagination. + + +*Type*: `string` + + +=== `cache_key` + +The key identifier used when storing the ID of the last tweet received. + + +*Type*: `string` + +*Default*: `"last_tweet_id"` + +=== `rate_limit` + +An optional rate limit resource to restrict API requests with. + + +*Type*: `string` + +*Default*: `""` + +=== `api_key` + +An API key for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). + + +*Type*: `string` + + +=== `api_secret` + +An API secret for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/inputs/websocket.adoc b/docs/modules/components/pages/inputs/websocket.adoc new file mode 100644 index 0000000000..1021e0f975 --- /dev/null +++ b/docs/modules/components/pages/inputs/websocket.adoc @@ -0,0 +1,478 @@ += websocket +:type: input +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to a websocket server and continuously receives messages. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +input: + label: "" + websocket: + url: ws://localhost:4195/get/ws # No default (required) + auto_replay_nacks: true +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +input: + label: "" + websocket: + url: ws://localhost:4195/get/ws # No default (required) + open_message: "" # No default (optional) + open_message_type: binary + auto_replay_nacks: true + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + connection: + max_retries: -1 # No default (optional) + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} +``` + +-- +====== + +It is possible to configure an `open_message`, which when set to a non-empty string will be sent to the websocket server each time a connection is first established. + +== Fields + +=== `url` + +The URL to connect to. + + +*Type*: `string` + + +```yml +# Examples + +url: ws://localhost:4195/get/ws +``` + +=== `open_message` + +An optional message to send to the server upon connection. + + +*Type*: `string` + + +=== `open_message_type` + +An optional flag to indicate the data type of open_message. + + +*Type*: `string` + +*Default*: `"binary"` + +|=== +| Option | Summary + +| `binary` +| Binary data open_message. +| `text` +| Text data open_message. The text message payload is interpreted as UTF-8 encoded text data. + +|=== + +=== `auto_replay_nacks` + +Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. + + +*Type*: `bool` + +*Default*: `true` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `connection` + +Customise how websocket connection attempts are made. + + +*Type*: `object` + + +=== `connection.max_retries` + +An optional limit to the number of consecutive retry attempts that will be made before abandoning the connection altogether and gracefully terminating the input. When all inputs terminate in this way the service (or stream) will shut down. If set to zero connections will never be reattempted upon a failure. If set below zero this field is ignored (effectively unset). + + +*Type*: `int` + + +```yml +# Examples + +max_retries: -1 + +max_retries: 10 +``` + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + + diff --git a/docs/modules/components/pages/inputs/zmq4.adoc b/docs/modules/components/pages/inputs/zmq4.adoc new file mode 100644 index 0000000000..7d4c542eca --- /dev/null +++ b/docs/modules/components/pages/inputs/zmq4.adoc @@ -0,0 +1,114 @@ +//// +THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + lib/input/zmq4.go +//// += zmq4 +:categories: ["Network"] +:status: stable +:type: input + +Consumes messages from a ZeroMQ socket. + +[tabs] +===== +.common:: ++ +-- +[,yml] +---- +# Common config fields, showing default values +input: + label: "" + zmq4: + urls: [] + bind: false + socket_type: "" + sub_filters: [] +---- + +-- +.advanced:: ++ +-- +[,yml] +---- +# All config fields, showing default values +input: + label: "" + zmq4: + urls: [] + bind: false + socket_type: "" + sub_filters: [] + high_water_mark: 0 + poll_timeout: 5s +---- + +-- +===== + +By default {page-component-title} does not build with components that require linking to external libraries. If you wish to build {page-component-title} locally with this component then set the build tag `x_benthos_extra`: + +[,shell] +---- +# With go +go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest + +# Using make +make TAGS=x_benthos_extra +---- + +There is a specific docker tag postfix `-cgo` for C builds containing this component. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + +Type: `array` + +[,yml] +---- +# Examples + +urls: + - tcp://localhost:5555 +---- + +=== `bind` + +Whether to bind to the specified URLs (otherwise they are connected to). + +Type: `bool` + +Default: `false` + +=== `socket_type` + +The socket type to connect as. + +Type: `string` + +Options: `PULL`, `SUB`. + +=== `sub_filters` + +A list of subscription topic filters to use when consuming from a SUB socket. Specifying a single sub_filter of `''` will subscribe to everything. + +Type: `array` + +Default: `[]` + +=== `high_water_mark` + +The message high water mark to use. + +Type: `int` + +Default: `0` + +=== `poll_timeout` + +The poll timeout to use. + +Type: `string` + +Default: `"5s"` diff --git a/docs/modules/components/pages/logger/about.adoc b/docs/modules/components/pages/logger/about.adoc new file mode 100644 index 0000000000..2645340a66 --- /dev/null +++ b/docs/modules/components/pages/logger/about.adoc @@ -0,0 +1,157 @@ += Logger + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/log/docs.adoc +//// + +{page-component-title} logging prints to stdout (or stderr if your output is stdout) and is formatted as https://brandur.org/logfmt[logfmt] by default. Use these configuration options to change both the logging formats as well as the destination of logs. + +[tabs] +====== +Logfmt to Stdout:: ++ +-- +```yaml +logger: + level: INFO + format: logfmt + add_timestamp: false + static_fields: + '@service': benthos +``` +-- +JSON to File:: ++ +-- +```yaml +logger: + level: WARN + format: json + file: + path: ./logs/benthos.ndjson + rotate: true +``` +-- +====== + +== Fields + +=== `level` + +Set the minimum severity level for emitting logs. + + +*Type*: `string` + +*Default*: `"INFO"` + +Options: +`OFF` +, `FATAL` +, `ERROR` +, `WARN` +, `INFO` +, `DEBUG` +, `TRACE` +, `ALL` +, `NONE` +. + +=== `format` + +Set the format of emitted logs. + + +*Type*: `string` + +*Default*: `"logfmt"` + +Options: +`json` +, `logfmt` +. + +=== `add_timestamp` + +Whether to include timestamps in logs. + + +*Type*: `bool` + +*Default*: `false` + +=== `level_name` + +The name of the level field added to logs when the `format` is `json`. + + +*Type*: `string` + +*Default*: `"level"` + +=== `timestamp_name` + +The name of the timestamp field added to logs when `add_timestamp` is set to `true` and the `format` is `json`. + + +*Type*: `string` + +*Default*: `"time"` + +=== `message_name` + +The name of the message field added to logs when the `format` is `json`. + + +*Type*: `string` + +*Default*: `"msg"` + +=== `static_fields` + +A map of key/value pairs to add to each structured log. + + +*Type*: map of `string` + +*Default*: `{"@service":"benthos"}` + +=== `file` + +Experimental: Specify fields for optionally writing logs to a file. + + +*Type*: `object` + + +=== `file.path` + +The file path to write logs to, if the file does not exist it will be created. Leave this field empty or unset to disable file based logging. + + +*Type*: `string` + +*Default*: `""` + +=== `file.rotate` + +Whether to rotate log files automatically. + + +*Type*: `bool` + +*Default*: `false` + +=== `file.rotate_max_age_days` + +The maximum number of days to retain old log files based on the timestamp encoded in their filename, after which they are deleted. Setting to zero disables this mechanism. + + +*Type*: `int` + +*Default*: `0` + diff --git a/docs/modules/components/pages/metrics/aws_cloudwatch.adoc b/docs/modules/components/pages/metrics/aws_cloudwatch.adoc new file mode 100644 index 0000000000..63682805c5 --- /dev/null +++ b/docs/modules/components/pages/metrics/aws_cloudwatch.adoc @@ -0,0 +1,199 @@ += aws_cloudwatch +:type: metrics +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Send metrics to AWS CloudWatch using the PutMetricData endpoint. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +metrics: + aws_cloudwatch: + namespace: Benthos + mapping: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +metrics: + aws_cloudwatch: + namespace: Benthos + flush_period: 100ms + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + mapping: "" +``` + +-- +====== + +== Timing metrics + +The smallest timing unit that CloudWatch supports is microseconds, therefore timing metrics are automatically downgraded to microseconds (by dividing delta values by 1000). This conversion will also apply to custom timing metrics produced with a `metric` processor. + +== Billing + +AWS bills per metric series exported, it is therefore STRONGLY recommended that you reduce the metrics that are exposed with a `mapping` like this: + +```yaml +metrics: + mapping: | + if ![ + "input_received", + "input_latency", + "output_sent", + ].contains(this) { deleted() } + aws_cloudwatch: + namespace: Foo +``` + +== Fields + +=== `namespace` + +The namespace used to distinguish metrics from other services. + + +*Type*: `string` + +*Default*: `"Benthos"` + +=== `flush_period` + +The period of time between PutMetricData requests. + + +*Type*: `string` + +*Default*: `"100ms"` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/metrics/influxdb.adoc b/docs/modules/components/pages/metrics/influxdb.adoc new file mode 100644 index 0000000000..1c9f0f0c16 --- /dev/null +++ b/docs/modules/components/pages/metrics/influxdb.adoc @@ -0,0 +1,385 @@ += influxdb +:type: metrics +:status: beta + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Send metrics to InfluxDB 1.x using the `/write` endpoint. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +metrics: + influxdb: + url: "" # No default (required) + db: "" # No default (required) + mapping: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +metrics: + influxdb: + url: "" # No default (required) + db: "" # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + username: "" + password: "" + include: + runtime: "" + debug_gc: "" + interval: 1m + ping_interval: 20s + precision: s + timeout: 5s + tags: {} + retention_policy: "" # No default (optional) + write_consistency: "" # No default (optional) + mapping: "" +``` + +-- +====== + +See https://docs.influxdata.com/influxdb/v1.8/tools/api/#write-http-endpoint for further details on the write API. + +== Fields + +=== `url` + +A URL of the format `[https|http|udp]://host:port` to the InfluxDB host. + + +*Type*: `string` + + +=== `db` + +The name of the database to use. + + +*Type*: `string` + + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `username` + +A username (when applicable). + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +A password (when applicable). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `include` + +Optional additional metrics to collect, enabling these metrics may have some performance implications as it acquires a global semaphore and does `stoptheworld()`. + + +*Type*: `object` + + +=== `include.runtime` + +A duration string indicating how often to poll and collect runtime metrics. Leave empty to disable this metric + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +runtime: 1m +``` + +=== `include.debug_gc` + +A duration string indicating how often to poll and collect GC metrics. Leave empty to disable this metric. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +debug_gc: 1m +``` + +=== `interval` + +A duration string indicating how often metrics should be flushed. + + +*Type*: `string` + +*Default*: `"1m"` + +=== `ping_interval` + +A duration string indicating how often to ping the host. + + +*Type*: `string` + +*Default*: `"20s"` + +=== `precision` + +[ns|us|ms|s] timestamp precision passed to write api. + + +*Type*: `string` + +*Default*: `"s"` + +=== `timeout` + +How long to wait for response for both ping and writing metrics. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `tags` + +Global tags added to each metric. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +tags: + hostname: localhost + zone: danger +``` + +=== `retention_policy` + +Sets the retention policy for each write. + + +*Type*: `string` + + +=== `write_consistency` + +[any|one|quorum|all] sets write consistency when available. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/metrics/json_api.adoc b/docs/modules/components/pages/metrics/json_api.adoc new file mode 100644 index 0000000000..0e09836b72 --- /dev/null +++ b/docs/modules/components/pages/metrics/json_api.adoc @@ -0,0 +1,28 @@ += json_api +:type: metrics +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Serves metrics as JSON object with the service wide HTTP service at the endpoints `/stats` and `/metrics`. + +```yml +# Config fields, showing default values +metrics: + json_api: {} + mapping: "" +``` + +This metrics type is useful for debugging as it provides a human readable format that you can parse with tools such as `jq` + + diff --git a/docs/modules/components/pages/metrics/logger.adoc b/docs/modules/components/pages/metrics/logger.adoc new file mode 100644 index 0000000000..3f0ca6cda1 --- /dev/null +++ b/docs/modules/components/pages/metrics/logger.adoc @@ -0,0 +1,51 @@ += logger +:type: metrics +:status: beta + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Prints aggregated metrics through the logger. + +```yml +# Config fields, showing default values +metrics: + logger: + push_interval: "" # No default (optional) + flush_metrics: false + mapping: "" +``` + +Prints each metric produced by Benthos as a log event (level `info` by default) during shutdown, and optionally on an interval. + +This metrics type is useful for debugging pipelines when you only have access to the logger output and not the service-wide server. Otherwise it's recommended that you use either the `prometheus` or `json_api`types. + +== Fields + +=== `push_interval` + +An optional period of time to continuously print all metrics. + + +*Type*: `string` + + +=== `flush_metrics` + +Whether counters and timing metrics should be reset to 0 each time metrics are printed. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/metrics/none.adoc b/docs/modules/components/pages/metrics/none.adoc new file mode 100644 index 0000000000..96ad811c62 --- /dev/null +++ b/docs/modules/components/pages/metrics/none.adoc @@ -0,0 +1,26 @@ += none +:type: metrics +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Disable metrics entirely. + +```yml +# Config fields, showing default values +metrics: + none: {} + mapping: "" +``` + + diff --git a/docs/modules/components/pages/metrics/prometheus.adoc b/docs/modules/components/pages/metrics/prometheus.adoc new file mode 100644 index 0000000000..cb41b91404 --- /dev/null +++ b/docs/modules/components/pages/metrics/prometheus.adoc @@ -0,0 +1,219 @@ += prometheus +:type: metrics +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Host endpoints (`/metrics` and `/stats`) for Prometheus scraping. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +metrics: + prometheus: {} + mapping: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +metrics: + prometheus: + use_histogram_timing: false + histogram_buckets: [] + summary_quantiles_objectives: + - quantile: 0.5 + error: 0.05 + - quantile: 0.9 + error: 0.01 + - quantile: 0.99 + error: 0.001 + add_process_metrics: false + add_go_metrics: false + push_url: "" # No default (optional) + push_interval: "" # No default (optional) + push_job_name: benthos_push + push_basic_auth: + username: "" + password: "" + file_output_path: "" + mapping: "" +``` + +-- +====== + +== Fields + +=== `use_histogram_timing` + +Whether to export timing metrics as a histogram, if `false` a summary is used instead. When exporting histogram timings the delta values are converted from nanoseconds into seconds in order to better fit within bucket definitions. For more information on histograms and summaries refer to: https://prometheus.io/docs/practices/histograms/. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.63.0 or newer + +=== `histogram_buckets` + +Timing metrics histogram buckets (in seconds). If left empty defaults to DefBuckets (https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#pkg-variables). Applicable when `use_histogram_timing` is set to `true`. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.63.0 or newer + +=== `summary_quantiles_objectives` + +A list of timing metrics summary buckets (as quantiles). Applicable when `use_histogram_timing` is set to `false`. + + +*Type*: `array` + +*Default*: `[{"error":0.05,"quantile":0.5},{"error":0.01,"quantile":0.9},{"error":0.001,"quantile":0.99}]` +Requires version 4.23.0 or newer + +```yml +# Examples + +summary_quantiles_objectives: + - error: 0.05 + quantile: 0.5 + - error: 0.01 + quantile: 0.9 + - error: 0.001 + quantile: 0.99 +``` + +=== `summary_quantiles_objectives[].quantile` + +Quantile value. + + +*Type*: `float` + +*Default*: `0` + +=== `summary_quantiles_objectives[].error` + +Permissible margin of error for quantile calculations. Precise calculations in a streaming context (without prior knowledge of the full dataset) can be resource-intensive. To balance accuracy with computational efficiency, an error margin is introduced. For instance, if the 90th quantile (`0.9`) is determined to be `100ms` with a 1% error margin (`0.01`), the true value will fall within the `[99ms, 101ms]` range.) + + +*Type*: `float` + +*Default*: `0` + +=== `add_process_metrics` + +Whether to export process metrics such as CPU and memory usage in addition to Benthos metrics. + + +*Type*: `bool` + +*Default*: `false` + +=== `add_go_metrics` + +Whether to export Go runtime metrics such as GC pauses in addition to Benthos metrics. + + +*Type*: `bool` + +*Default*: `false` + +=== `push_url` + +An optional <> to push metrics to. + + +*Type*: `string` + + +=== `push_interval` + +The period of time between each push when sending metrics to a Push Gateway. + + +*Type*: `string` + + +=== `push_job_name` + +An identifier for push jobs. + + +*Type*: `string` + +*Default*: `"benthos_push"` + +=== `push_basic_auth` + +The Basic Authentication credentials. + + +*Type*: `object` + + +=== `push_basic_auth.username` + +The Basic Authentication username. + + +*Type*: `string` + +*Default*: `""` + +=== `push_basic_auth.password` + +The Basic Authentication password. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `file_output_path` + +An optional file path to write all prometheus metrics on service shutdown. + + +*Type*: `string` + +*Default*: `""` + +== Push gateway + +The field `push_url` is optional and when set will trigger a push of metrics to a https://prometheus.io/docs/instrumenting/pushing/[Prometheus Push Gateway] once Benthos shuts down. It is also possible to specify a `push_interval` which results in periodic pushes. + +The Push Gateway is useful for when Benthos instances are short lived. Do not include the "/metrics/jobs/..." path in the push URL. + +If the Push Gateway requires HTTP Basic Authentication it can be configured with `push_basic_auth`. + diff --git a/docs/modules/components/pages/metrics/statsd.adoc b/docs/modules/components/pages/metrics/statsd.adoc new file mode 100644 index 0000000000..d7634172bb --- /dev/null +++ b/docs/modules/components/pages/metrics/statsd.adoc @@ -0,0 +1,63 @@ += statsd +:type: metrics +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. Supported tagging formats are 'none', 'datadog' and 'influxdb'. + +```yml +# Config fields, showing default values +metrics: + statsd: + address: "" # No default (required) + flush_period: 100ms + tag_format: none + mapping: "" +``` + +== Fields + +=== `address` + +The address to send metrics to. + + +*Type*: `string` + + +=== `flush_period` + +The time interval between metrics flushes. + + +*Type*: `string` + +*Default*: `"100ms"` + +=== `tag_format` + +Metrics tagging is supported in a variety of formats. + + +*Type*: `string` + +*Default*: `"none"` + +Options: +`none` +, `datadog` +, `influxdb` +. + + diff --git a/docs/modules/components/pages/outputs/amqp_0_9.adoc b/docs/modules/components/pages/outputs/amqp_0_9.adoc new file mode 100644 index 0000000000..df08fb135e --- /dev/null +++ b/docs/modules/components/pages/outputs/amqp_0_9.adoc @@ -0,0 +1,514 @@ += amqp_0_9 +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an AMQP (0.91) exchange. AMQP is a messaging protocol used by various message brokers, including RabbitMQ.Connects to an AMQP (0.91) queue. AMQP is a messaging protocol used by various message brokers, including RabbitMQ. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + amqp_0_9: + urls: [] # No default (required) + exchange: "" # No default (required) + key: "" + type: "" + metadata: + exclude_prefixes: [] + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + amqp_0_9: + urls: [] # No default (required) + exchange: "" # No default (required) + exchange_declare: + enabled: false + type: direct + durable: true + key: "" + type: "" + content_type: application/octet-stream + content_encoding: "" + correlation_id: "" + reply_to: "" + expiration: "" + message_id: "" + user_id: "" + app_id: "" + metadata: + exclude_prefixes: [] + priority: "" + max_in_flight: 64 + persistent: false + mandatory: false + immediate: false + timeout: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] +``` + +-- +====== + +The metadata from each message are delivered as headers. + +It's possible for this output type to create the target exchange by setting `exchange_declare.enabled` to `true`, if the exchange already exists then the declaration passively verifies that the settings match. + +TLS is automatic when connecting to an `amqps` URL, but custom settings can be enabled in the `tls` section. + +The fields 'key', 'exchange' and 'type' can be dynamically set using xref:configuration:interpolation.adoc#bloblang-queries[function interpolations]. + +== Fields + +=== `urls` + +A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + +Requires version 3.58.0 or newer + +```yml +# Examples + +urls: + - amqp://guest:guest@127.0.0.1:5672/ + +urls: + - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ + +urls: + - amqp://127.0.0.1:5672/ + - amqp://127.0.0.2:5672/ +``` + +=== `exchange` + +An AMQP exchange to publish to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `exchange_declare` + +Optionally declare the target exchange (passive). + + +*Type*: `object` + + +=== `exchange_declare.enabled` + +Whether to declare the exchange. + + +*Type*: `bool` + +*Default*: `false` + +=== `exchange_declare.type` + +The type of the exchange. + + +*Type*: `string` + +*Default*: `"direct"` + +Options: +`direct` +, `fanout` +, `topic` +, `x-custom` +. + +=== `exchange_declare.durable` + +Whether the exchange should be durable. + + +*Type*: `bool` + +*Default*: `true` + +=== `key` + +The binding key to set for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `type` + +The type property to set for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `content_type` + +The content type attribute to set for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"application/octet-stream"` + +=== `content_encoding` + +The content encoding attribute to set for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `correlation_id` + +Set the correlation ID of each message with a dynamic interpolated expression. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `reply_to` + +Carries response queue name - set with a dynamic interpolated expression. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `expiration` + +Set the per-message TTL +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `message_id` + +Set the message ID of each message with a dynamic interpolated expression. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `user_id` + +Set the user ID to the name of the publisher. If this property is set by a publisher, its value must be equal to the name of the user used to open the connection. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `app_id` + +Set the application ID of each message with a dynamic interpolated expression. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `metadata` + +Specify criteria for which metadata values are attached to messages as headers. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `priority` + +Set the priority of each message with a dynamic interpolated expression. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +priority: "0" + +priority: ${! meta("amqp_priority") } + +priority: ${! json("doc.priority") } +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `persistent` + +Whether message delivery should be persistent (transient by default). + + +*Type*: `bool` + +*Default*: `false` + +=== `mandatory` + +Whether to set the mandatory flag on published messages. When set if a published message is routed to zero queues it is returned. + + +*Type*: `bool` + +*Default*: `false` + +=== `immediate` + +Whether to set the immediate flag on published messages. When set if there are no ready consumers of a queue then the message is dropped instead of waiting. + + +*Type*: `bool` + +*Default*: `false` + +=== `timeout` + +The maximum period to wait before abandoning it and reattempting. If not set, wait indefinitely. + + +*Type*: `string` + +*Default*: `""` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + + diff --git a/docs/modules/components/pages/outputs/amqp_1.adoc b/docs/modules/components/pages/outputs/amqp_1.adoc new file mode 100644 index 0000000000..65c8fa77a9 --- /dev/null +++ b/docs/modules/components/pages/outputs/amqp_1.adoc @@ -0,0 +1,382 @@ += amqp_1 +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an AMQP (1.0) server. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + amqp_1: + urls: [] # No default (optional) + target_address: /foo # No default (required) + max_in_flight: 64 + metadata: + exclude_prefixes: [] +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + amqp_1: + urls: [] # No default (optional) + target_address: /foo # No default (required) + max_in_flight: 64 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + application_properties_map: "" # No default (optional) + sasl: + mechanism: none + user: "" + password: "" + metadata: + exclude_prefixes: [] +``` + +-- +====== + +== Metadata + +Message metadata is added to each AMQP message as string annotations. In order to control which metadata keys are added use the `metadata` config field. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `urls` + +A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + +Requires version 4.23.0 or newer + +```yml +# Examples + +urls: + - amqp://guest:guest@127.0.0.1:5672/ + +urls: + - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ + +urls: + - amqp://127.0.0.1:5672/ + - amqp://127.0.0.2:5672/ +``` + +=== `target_address` + +The target address to write to. + + +*Type*: `string` + + +```yml +# Examples + +target_address: /foo + +target_address: queue:/bar + +target_address: topic:/baz +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `application_properties_map` + +An optional Bloblang mapping that can be defined in order to set the `application-properties` on output messages. + + +*Type*: `string` + + +=== `sasl` + +Enables SASL authentication. + + +*Type*: `object` + + +=== `sasl.mechanism` + +The SASL authentication mechanism to use. + + +*Type*: `string` + +*Default*: `"none"` + +|=== +| Option | Summary + +| `anonymous` +| Anonymous SASL authentication. +| `none` +| No SASL based authentication. +| `plain` +| Plain text SASL authentication. + +|=== + +=== `sasl.user` + +A SASL plain text username. It is recommended that you use environment variables to populate this field. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +user: ${USER} +``` + +=== `sasl.password` + +A SASL plain text password. It is recommended that you use environment variables to populate this field. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: ${PASSWORD} +``` + +=== `metadata` + +Specify criteria for which metadata values are attached to messages as headers. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + + diff --git a/docs/modules/components/pages/outputs/aws_dynamodb.adoc b/docs/modules/components/pages/outputs/aws_dynamodb.adoc new file mode 100644 index 0000000000..48d6366690 --- /dev/null +++ b/docs/modules/components/pages/outputs/aws_dynamodb.adoc @@ -0,0 +1,444 @@ += aws_dynamodb +:type: output +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Inserts items into a DynamoDB table. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + aws_dynamodb: + table: "" # No default (required) + string_columns: {} + json_map_columns: {} + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + aws_dynamodb: + table: "" # No default (required) + string_columns: {} + json_map_columns: {} + ttl: "" + ttl_key: "" + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + max_retries: 3 + backoff: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s +``` + +-- +====== + +The field `string_columns` is a map of column names to string values, where the values are xref:configuration:interpolation.adoc#bloblang-queries[function interpolated] per message of a batch. This allows you to populate string columns of an item by extracting fields within the document payload or metadata like follows: + +```yml +string_columns: + id: ${!json("id")} + title: ${!json("body.title")} + topic: ${!meta("kafka_topic")} + full_content: ${!content()} +``` + +The field `json_map_columns` is a map of column names to json paths, where the xref:configuration:field_paths.adoc[dot path] is extracted from each document and converted into a map value. Both an empty path and the path `.` are interpreted as the root of the document. This allows you to populate map columns of an item like follows: + +```yml +json_map_columns: + user: path.to.user + whole_document: . +``` + +A column name can be empty: + +```yml +json_map_columns: + "": . +``` + +In which case the top level document fields will be written at the root of the item, potentially overwriting previously defined column values. If a path is not found within a document the column will not be populated. + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + + +== Fields + +=== `table` + +The table to store messages in. + + +*Type*: `string` + + +=== `string_columns` + +A map of column keys to string values to store. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +string_columns: + full_content: ${!content()} + id: ${!json("id")} + title: ${!json("body.title")} + topic: ${!meta("kafka_topic")} +``` + +=== `json_map_columns` + +A map of column keys to xref:configuration:field_paths.adoc[field paths] pointing to value data within messages. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +json_map_columns: + user: path.to.user + whole_document: . + +json_map_columns: + "": . +``` + +=== `ttl` + +An optional TTL to set for items, calculated from the moment the message is sent. + + +*Type*: `string` + +*Default*: `""` + +=== `ttl_key` + +The column key to place the TTL value within. + + +*Type*: `string` + +*Default*: `""` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `3` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `backoff.max_elapsed_time` + +The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. + + +*Type*: `string` + +*Default*: `"30s"` + + diff --git a/docs/modules/components/pages/outputs/aws_kinesis.adoc b/docs/modules/components/pages/outputs/aws_kinesis.adoc new file mode 100644 index 0000000000..e1b5b8e068 --- /dev/null +++ b/docs/modules/components/pages/outputs/aws_kinesis.adoc @@ -0,0 +1,383 @@ += aws_kinesis +:type: output +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to a Kinesis stream. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + aws_kinesis: + stream: foo # No default (required) + partition_key: "" # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + aws_kinesis: + stream: foo # No default (required) + partition_key: "" # No default (required) + hash_key: "" # No default (optional) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + max_retries: 0 + backoff: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s +``` + +-- +====== + +Both the `partition_key`(required) and `hash_key` (optional) fields can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages the interpolations are performed per message part. + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `stream` + +The stream to publish messages to. Streams can either be specified by their name or full ARN. + + +*Type*: `string` + + +```yml +# Examples + +stream: foo + +stream: arn:aws:kinesis:*:111122223333:stream/my-stream +``` + +=== `partition_key` + +A required key for partitioning messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `hash_key` + +A optional hash key for partitioning messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `0` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `backoff.max_elapsed_time` + +The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. + + +*Type*: `string` + +*Default*: `"30s"` + + diff --git a/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc b/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc new file mode 100644 index 0000000000..65e718a3a5 --- /dev/null +++ b/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc @@ -0,0 +1,353 @@ += aws_kinesis_firehose +:type: output +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to a Kinesis Firehose delivery stream. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + aws_kinesis_firehose: + stream: "" # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + aws_kinesis_firehose: + stream: "" # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + max_retries: 0 + backoff: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s +``` + +-- +====== + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + + +== Fields + +=== `stream` + +The stream to publish messages to. + + +*Type*: `string` + + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `0` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `backoff.max_elapsed_time` + +The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. + + +*Type*: `string` + +*Default*: `"30s"` + + diff --git a/docs/modules/components/pages/outputs/aws_s3.adoc b/docs/modules/components/pages/outputs/aws_s3.adoc new file mode 100644 index 0000000000..f5b6d1b0e8 --- /dev/null +++ b/docs/modules/components/pages/outputs/aws_s3.adoc @@ -0,0 +1,548 @@ += aws_s3 +:type: output +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends message parts as objects to an Amazon S3 bucket. Each object is uploaded with the path specified with the `path` field. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + aws_s3: + bucket: "" # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + tags: {} + content_type: application/octet-stream + metadata: + exclude_prefixes: [] + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + aws_s3: + bucket: "" # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + tags: {} + content_type: application/octet-stream + content_encoding: "" + cache_control: "" + content_disposition: "" + content_language: "" + website_redirect_location: "" + metadata: + exclude_prefixes: [] + storage_class: STANDARD + kms_key_id: "" + server_side_encryption: "" + force_path_style_urls: false + max_in_flight: 64 + timeout: 5s + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +In order to have a different path for each object you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries], which are calculated per message of a batch. + +== Metadata + +Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the xref:configuration:metadata.adoc[metadata docs]. + +== Tags + +The tags field allows you to specify key/value pairs to attach to objects as tags, where the values support xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]: + +```yaml +output: + aws_s3: + bucket: TODO + path: ${!count("files")}-${!timestamp_unix_nano()}.tar.gz + tags: + Key1: Value1 + Timestamp: ${!meta("Timestamp")} +``` + +=== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Batching + +It's common to want to upload messages to S3 as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an xref:components:processors/archive.adoc[`archive`] and/or xref:components:processors/compress.adoc[`compress`] processor. + +For example, if we wished to upload messages as a .tar.gz archive of documents we could achieve that with the following config: + +```yaml +output: + aws_s3: + bucket: TODO + path: ${!count("files")}-${!timestamp_unix_nano()}.tar.gz + batching: + count: 100 + period: 10s + processors: + - archive: + format: tar + - compress: + algorithm: gzip +``` + +Alternatively, if we wished to upload JSON documents as a single large document containing an array of objects we can do that with: + +```yaml +output: + aws_s3: + bucket: TODO + path: ${!count("files")}-${!timestamp_unix_nano()}.json + batching: + count: 100 + processors: + - archive: + format: json_array +``` + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `bucket` + +The bucket to upload messages to. + + +*Type*: `string` + + +=== `path` + +The path of each message to upload. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` + +```yml +# Examples + +path: ${!count("files")}-${!timestamp_unix_nano()}.txt + +path: ${!meta("kafka_key")}.json + +path: ${!json("doc.namespace")}/${!json("doc.id")}.json +``` + +=== `tags` + +Key/value pairs to store with the object as tags. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +tags: + Key1: Value1 + Timestamp: ${!meta("Timestamp")} +``` + +=== `content_type` + +The content type to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"application/octet-stream"` + +=== `content_encoding` + +An optional content encoding to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `cache_control` + +The cache control to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `content_disposition` + +The content disposition to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `content_language` + +The content language to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `website_redirect_location` + +The website redirect location to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `metadata` + +Specify criteria for which metadata values are attached to objects as headers. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `storage_class` + +The storage class to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"STANDARD"` + +Options: +`STANDARD` +, `REDUCED_REDUNDANCY` +, `GLACIER` +, `STANDARD_IA` +, `ONEZONE_IA` +, `INTELLIGENT_TIERING` +, `DEEP_ARCHIVE` +. + +=== `kms_key_id` + +An optional server side encryption key. + + +*Type*: `string` + +*Default*: `""` + +=== `server_side_encryption` + +An optional server side encryption algorithm. + + +*Type*: `string` + +*Default*: `""` +Requires version 3.63.0 or newer + +=== `force_path_style_urls` + +Forces the client API to use path style URLs, which helps when connecting to custom endpoints. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `timeout` + +The maximum period to wait on an upload before abandoning it and reattempting. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/outputs/aws_sns.adoc b/docs/modules/components/pages/outputs/aws_sns.adoc new file mode 100644 index 0000000000..6e92068453 --- /dev/null +++ b/docs/modules/components/pages/outputs/aws_sns.adoc @@ -0,0 +1,244 @@ += aws_sns +:type: output +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an AWS SNS topic. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + aws_sns: + topic_arn: "" # No default (required) + message_group_id: "" # No default (optional) + message_deduplication_id: "" # No default (optional) + max_in_flight: 64 + metadata: + exclude_prefixes: [] +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + aws_sns: + topic_arn: "" # No default (required) + message_group_id: "" # No default (optional) + message_deduplication_id: "" # No default (optional) + max_in_flight: 64 + metadata: + exclude_prefixes: [] + timeout: 5s + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `topic_arn` + +The topic to publish to. + + +*Type*: `string` + + +=== `message_group_id` + +An optional group ID to set for messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 3.60.0 or newer + +=== `message_deduplication_id` + +An optional deduplication ID to set for messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 3.60.0 or newer + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `metadata` + +Specify criteria for which metadata values are sent as headers. + + +*Type*: `object` + +Requires version 3.60.0 or newer + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `timeout` + +The maximum period to wait on an upload before abandoning it and reattempting. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/outputs/aws_sqs.adoc b/docs/modules/components/pages/outputs/aws_sqs.adoc new file mode 100644 index 0000000000..b88b8d5235 --- /dev/null +++ b/docs/modules/components/pages/outputs/aws_sqs.adoc @@ -0,0 +1,410 @@ += aws_sqs +:type: output +:status: stable +:categories: ["Services","AWS"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an SQS queue. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + aws_sqs: + url: "" # No default (required) + message_group_id: "" # No default (optional) + message_deduplication_id: "" # No default (optional) + delay_seconds: "" # No default (optional) + max_in_flight: 64 + metadata: + exclude_prefixes: [] + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + aws_sqs: + url: "" # No default (required) + message_group_id: "" # No default (optional) + message_deduplication_id: "" # No default (optional) + delay_seconds: "" # No default (optional) + max_in_flight: 64 + metadata: + exclude_prefixes: [] + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + max_retries: 0 + backoff: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s +``` + +-- +====== + +Metadata values are sent along with the payload as attributes with the data type String. If the number of metadata values in a message exceeds the message attribute limit (10) then the top ten keys ordered alphabetically will be selected. + +The fields `message_group_id`, `message_deduplication_id` and `delay_seconds` can be set dynamically using xref:configuration:interpolation.adoc#bloblang-queries[function interpolations], which are resolved individually for each message of a batch. + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `url` + +The URL of the target SQS queue. + + +*Type*: `string` + + +=== `message_group_id` + +An optional group ID to set for messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `message_deduplication_id` + +An optional deduplication ID to set for messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `delay_seconds` + +An optional delay time in seconds for message. Value between 0 and 900 +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `64` + +=== `metadata` + +Specify criteria for which metadata values are sent as headers. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `0` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `backoff.max_elapsed_time` + +The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. + + +*Type*: `string` + +*Default*: `"30s"` + + diff --git a/docs/modules/components/pages/outputs/azure_blob_storage.adoc b/docs/modules/components/pages/outputs/azure_blob_storage.adoc new file mode 100644 index 0000000000..045343382b --- /dev/null +++ b/docs/modules/components/pages/outputs/azure_blob_storage.adoc @@ -0,0 +1,199 @@ += azure_blob_storage +:type: output +:status: beta +:categories: ["Services","Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends message parts as objects to an Azure Blob Storage Account container. Each object is uploaded with the filename specified with the `container` field. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + azure_blob_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + container: messages-${!timestamp("2006")} # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + azure_blob_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + container: messages-${!timestamp("2006")} # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + blob_type: BLOCK + public_access_level: PRIVATE + max_in_flight: 64 +``` + +-- +====== + +In order to have a different path for each object you should use function +interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here], which are +calculated per message of a batch. + +Supports multiple authentication methods but only one of the following is required: +- `storage_connection_string` +- `storage_account` and `storage_access_key` +- `storage_account` and `storage_sas_token` +- `storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] + +If multiple are set then the `storage_connection_string` is given priority. + +If the `storage_connection_string` does not contain the `AccountName` parameter, please specify it in the +`storage_account` field. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `storage_account` + +The storage account to access. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_access_key` + +The storage account access key. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_connection_string` + +A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_sas_token` + +The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. + + +*Type*: `string` + +*Default*: `""` + +=== `container` + +The container for uploading the messages to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +container: messages-${!timestamp("2006")} +``` + +=== `path` + +The path of each message to upload. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` + +```yml +# Examples + +path: ${!count("files")}-${!timestamp_unix_nano()}.json + +path: ${!meta("kafka_key")}.json + +path: ${!json("doc.namespace")}/${!json("doc.id")}.json +``` + +=== `blob_type` + +Block and Append blobs are comprized of blocks, and each blob can support up to 50,000 blocks. The default value is `+"`BLOCK`"+`.` +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"BLOCK"` + +Options: +`BLOCK` +, `APPEND` +. + +=== `public_access_level` + +The container's public access level. The default value is `PRIVATE`. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"PRIVATE"` + +Options: +`PRIVATE` +, `BLOB` +, `CONTAINER` +. + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/azure_cosmosdb.adoc b/docs/modules/components/pages/outputs/azure_cosmosdb.adoc new file mode 100644 index 0000000000..45f47c1c79 --- /dev/null +++ b/docs/modules/components/pages/outputs/azure_cosmosdb.adoc @@ -0,0 +1,532 @@ += azure_cosmosdb +:type: output +:status: experimental +:categories: ["Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]. + +Introduced in version v4.25.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + azure_cosmosdb: + endpoint: https://localhost:8081 # No default (optional) + account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) + connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) + database: testdb # No default (required) + container: testcontainer # No default (required) + partition_keys_map: root = "blobfish" # No default (required) + operation: Create + item_id: ${! json("id") } # No default (optional) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + azure_cosmosdb: + endpoint: https://localhost:8081 # No default (optional) + account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) + connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) + database: testdb # No default (required) + container: testcontainer # No default (required) + partition_keys_map: root = "blobfish" # No default (required) + operation: Create + patch_operations: [] # No default (optional) + patch_condition: from c where not is_defined(c.blobfish) # No default (optional) + auto_id: true + item_id: ${! json("id") } # No default (optional) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + max_in_flight: 64 +``` + +-- +====== + +When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. + +The `partition_keys` field must resolve to the same value(s) across the entire message batch. + + +== Credentials + +You can use one of the following authentication mechanisms: + +- Set the `endpoint` field and the `account_key` field +- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set the `connection_string` field + + +== Batching + +CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here]). + + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Examples + +[tabs] +====== +Create documents:: ++ +-- + +Create new documents in the `blobfish` container with partition key `/habitat`. + +```yaml +output: + azure_cosmosdb: + endpoint: http://localhost:8080 + account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + database: blobbase + container: blobfish + partition_keys_map: root = json("habitat") + operation: Create +``` + +-- +Patch documents:: ++ +-- + +Execute the Patch operation on documents from the `blobfish` container. + +```yaml +output: + azure_cosmosdb: + endpoint: http://localhost:8080 + account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + database: testdb + container: blobfish + partition_keys_map: root = json("habitat") + item_id: ${! json("id") } + operation: Patch + patch_operations: + # Add a new /diet field + - operation: Add + path: /diet + value_map: root = json("diet") + # Remove the first location from the /locations array field + - operation: Remove + path: /locations/0 + # Add new location at the end of the /locations array field + - operation: Add + path: /locations/- + value_map: root = "Challenger Deep" +``` + +-- +====== + +== Fields + +=== `endpoint` + +CosmosDB endpoint. + + +*Type*: `string` + + +```yml +# Examples + +endpoint: https://localhost:8081 +``` + +=== `account_key` + +Account key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +```yml +# Examples + +account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== +``` + +=== `connection_string` + +Connection string. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +```yml +# Examples + +connection_string: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; +``` + +=== `database` + +Database. + + +*Type*: `string` + + +```yml +# Examples + +database: testdb +``` + +=== `container` + +Container. + + +*Type*: `string` + + +```yml +# Examples + +container: testcontainer +``` + +=== `partition_keys_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided. + + +*Type*: `string` + + +```yml +# Examples + +partition_keys_map: root = "blobfish" + +partition_keys_map: root = 41 + +partition_keys_map: root = true + +partition_keys_map: root = null + +partition_keys_map: root = json("blobfish").depth +``` + +=== `operation` + +Operation. + + +*Type*: `string` + +*Default*: `"Create"` + +|=== +| Option | Summary + +| `Create` +| Create operation. +| `Delete` +| Delete operation. +| `Patch` +| Patch operation. +| `Replace` +| Replace operation. +| `Upsert` +| Upsert operation. + +|=== + +=== `patch_operations` + +Patch operations to be performed when `operation: Patch` . + + +*Type*: `array` + + +=== `patch_operations[].operation` + +Operation. + + +*Type*: `string` + +*Default*: `"Add"` + +|=== +| Option | Summary + +| `Add` +| Add patch operation. +| `Increment` +| Increment patch operation. +| `Remove` +| Remove patch operation. +| `Replace` +| Replace patch operation. +| `Set` +| Set patch operation. + +|=== + +=== `patch_operations[].path` + +Path. + + +*Type*: `string` + + +```yml +# Examples + +path: /foo/bar/baz +``` + +=== `patch_operations[].value_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a value of any type that is supported by CosmosDB. + + +*Type*: `string` + + +```yml +# Examples + +value_map: root = "blobfish" + +value_map: root = 41 + +value_map: root = true + +value_map: root = json("blobfish").depth + +value_map: root = [1, 2, 3] +``` + +=== `patch_condition` + +Patch operation condition. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +patch_condition: from c where not is_defined(c.blobfish) +``` + +=== `auto_id` + +Automatically set the item `id` field to a random UUID v4. If the `id` field is already set, then it will not be overwritten. Setting this to `false` can improve performance, since the messages will not have to be parsed. + + +*Type*: `bool` + +*Default*: `true` + +=== `item_id` + +ID of item to replace or delete. Only used by the Replace and Delete operations +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +item_id: ${! json("id") } +``` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + +== CosmosDB emulator + +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: + +```bash +> docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator +``` + +Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. + +Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy] like so: + +```bash +> mitmproxy -k --mode "reverse:https://localhost:8081" +``` + +Then you can access the CosmosDB UI via `http://localhost:8080/_explorer/index.html` and use `http://localhost:8080` as the CosmosDB endpoint. + + diff --git a/docs/modules/components/pages/outputs/azure_queue_storage.adoc b/docs/modules/components/pages/outputs/azure_queue_storage.adoc new file mode 100644 index 0000000000..829e3c0714 --- /dev/null +++ b/docs/modules/components/pages/outputs/azure_queue_storage.adoc @@ -0,0 +1,263 @@ += azure_queue_storage +:type: output +:status: beta +:categories: ["Services","Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an Azure Storage Queue. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + azure_queue_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + queue_name: "" # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + azure_queue_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + queue_name: "" # No default (required) + ttl: "" + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Only one authentication method is required, `storage_connection_string` or `storage_account` and `storage_access_key`. If both are set then the `storage_connection_string` is given priority. + +In order to set the `queue_name` you can use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here], which are calculated per message of a batch. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `storage_account` + +The storage account to access. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_access_key` + +The storage account access key. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_connection_string` + +A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_sas_token` + +The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. + + +*Type*: `string` + +*Default*: `""` + +=== `queue_name` + +The name of the target Queue Storage queue. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `ttl` + +The TTL of each individual message as a duration string. Defaults to 0, meaning no retention period is set +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +ttl: 60s + +ttl: 5m + +ttl: 36h +``` + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/azure_table_storage.adoc b/docs/modules/components/pages/outputs/azure_table_storage.adoc new file mode 100644 index 0000000000..ab7fe310b4 --- /dev/null +++ b/docs/modules/components/pages/outputs/azure_table_storage.adoc @@ -0,0 +1,371 @@ += azure_table_storage +:type: output +:status: beta +:categories: ["Services","Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores messages in an Azure Table Storage table. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + azure_table_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + table_name: ${! meta("kafka_topic") } # No default (required) + partition_key: "" + row_key: "" + properties: {} + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + azure_table_storage: + storage_account: "" + storage_access_key: "" + storage_connection_string: "" + storage_sas_token: "" + table_name: ${! meta("kafka_topic") } # No default (required) + partition_key: "" + row_key: "" + properties: {} + transaction_type: INSERT + max_in_flight: 64 + timeout: 5s + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Only one authentication method is required, `storage_connection_string` or `storage_account` and `storage_access_key`. If both are set then the `storage_connection_string` is given priority. + +In order to set the `table_name`, `partition_key` and `row_key` you can use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here], which are calculated per message of a batch. + +If the `properties` are not set in the config, all the `json` fields are marshalled and stored in the table, which will be created if it does not exist. + +The `object` and `array` fields are marshaled as strings. e.g.: + +The JSON message: + +```json +{ + "foo": 55, + "bar": { + "baz": "a", + "bez": "b" + }, + "diz": ["a", "b"] +} +``` + +Will store in the table the following properties: + +```yml +foo: '55' +bar: '{ "baz": "a", "bez": "b" }' +diz: '["a", "b"]' +``` + +It's also possible to use function interpolations to get or transform the properties values, e.g.: + +```yml +properties: + device: '${! json("device") }' + timestamp: '${! json("timestamp") }' +``` + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `storage_account` + +The storage account to access. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_access_key` + +The storage account access key. This field is ignored if `storage_connection_string` is set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_connection_string` + +A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. + + +*Type*: `string` + +*Default*: `""` + +=== `storage_sas_token` + +The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. + + +*Type*: `string` + +*Default*: `""` + +=== `table_name` + +The table to store messages into. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +table_name: ${! meta("kafka_topic") } + +table_name: ${! json("table") } +``` + +=== `partition_key` + +The partition key. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +partition_key: ${! json("date") } +``` + +=== `row_key` + +The row key. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +row_key: ${! json("device")}-${!uuid_v4() } +``` + +=== `properties` + +A map of properties to store into the table. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +=== `transaction_type` + +Type of transaction operation. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"INSERT"` + +Options: +`INSERT` +, `INSERT_MERGE` +, `INSERT_REPLACE` +, `UPDATE_MERGE` +, `UPDATE_REPLACE` +, `DELETE` +. + +```yml +# Examples + +transaction_type: ${! json("operation") } + +transaction_type: ${! meta("operation") } + +transaction_type: INSERT +``` + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `64` + +=== `timeout` + +The maximum period to wait on an upload before abandoning it and reattempting. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/beanstalkd.adoc b/docs/modules/components/pages/outputs/beanstalkd.adoc new file mode 100644 index 0000000000..6d0cdb86d1 --- /dev/null +++ b/docs/modules/components/pages/outputs/beanstalkd.adoc @@ -0,0 +1,56 @@ += beanstalkd +:type: output +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Write messages to a Beanstalkd queue. + +Introduced in version 4.7.0. + +```yml +# Config fields, showing default values +output: + label: "" + beanstalkd: + address: 127.0.0.1:11300 # No default (required) + max_in_flight: 64 +``` + +== Fields + +=== `address` + +An address to connect to. + + +*Type*: `string` + + +```yml +# Examples + +address: 127.0.0.1:11300 +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/broker.adoc b/docs/modules/components/pages/outputs/broker.adoc new file mode 100644 index 0000000000..49c998fdbb --- /dev/null +++ b/docs/modules/components/pages/outputs/broker.adoc @@ -0,0 +1,255 @@ += broker +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Allows you to route messages to multiple child outputs using a range of brokering <>. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + broker: + pattern: fan_out + outputs: [] # No default (required) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + broker: + copies: 1 + pattern: fan_out + outputs: [] # No default (required) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +xref:components:processors/about.adoc[Processors] can be listed to apply across individual outputs or all outputs: + +```yaml +output: + broker: + pattern: fan_out + outputs: + - resource: foo + - resource: bar + # Processors only applied to messages sent to bar. + processors: + - resource: bar_processor + + # Processors applied to messages sent to all brokered outputs. + processors: + - resource: general_processor +``` + +== Fields + +=== `copies` + +The number of copies of each configured output to spawn. + + +*Type*: `int` + +*Default*: `1` + +=== `pattern` + +The brokering pattern to use. + + +*Type*: `string` + +*Default*: `"fan_out"` + +Options: +`fan_out` +, `fan_out_fail_fast` +, `fan_out_sequential` +, `fan_out_sequential_fail_fast` +, `round_robin` +, `greedy` +. + +=== `outputs` + +A list of child outputs to broker. + + +*Type*: `array` + + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +== Patterns + +The broker pattern determines the way in which messages are allocated and can be chosen from the following: + +=== `fan_out` + +With the fan out pattern all outputs will be sent every message that passes through Benthos in parallel. + +If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. + +Sometimes it is useful to disable the back pressure or retries of certain fan out outputs and instead drop messages that have failed or were blocked. In this case you can wrap outputs with a xref:components:outputs/drop_on.adoc[`drop_on` output]. + +=== `fan_out_fail_fast` + +The same as the `fan_out` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. + +=== `fan_out_sequential` + +Similar to the fan out pattern except outputs are written to sequentially, meaning an output is only written to once the preceding output has confirmed receipt of the same message. + +If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. + +=== `fan_out_sequential_fail_fast` + +The same as the `fan_out_sequential` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. + +=== `round_robin` + +With the round robin pattern each message will be assigned a single output following their order. If an output applies back pressure it will block all subsequent messages. If an output fails to send a message then the message will be re-attempted with the next input, and so on. + +=== `greedy` + +The greedy pattern results in higher output throughput at the cost of potentially disproportionate message allocations to those outputs. Each message is sent to a single output, which is determined by allowing outputs to claim messages as soon as they are able to process them. This results in certain faster outputs potentially processing more messages at the cost of slower outputs. + diff --git a/docs/modules/components/pages/outputs/cache.adoc b/docs/modules/components/pages/outputs/cache.adoc new file mode 100644 index 0000000000..b8cb76dffc --- /dev/null +++ b/docs/modules/components/pages/outputs/cache.adoc @@ -0,0 +1,139 @@ += cache +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Stores each message in a xref:components:caches/about.adoc[cache]. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + cache: + target: "" # No default (required) + key: ${!count("items")}-${!timestamp_unix_nano()} + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + cache: + target: "" # No default (required) + key: ${!count("items")}-${!timestamp_unix_nano()} + ttl: 60s # No default (optional) + max_in_flight: 64 +``` + +-- +====== + +Caches are configured as xref:components:caches/about.adoc[resources], where there's a wide variety to choose from. + +The `target` field must reference a configured cache resource label like follows: + +```yaml +output: + cache: + target: foo + key: ${!json("document.id")} + +cache_resources: + - label: foo + memcached: + addresses: + - localhost:11211 + default_ttl: 60s +``` + +In order to create a unique `key` value per item you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `target` + +The target cache to store messages in. + + +*Type*: `string` + + +=== `key` + +The key to store messages by, function interpolation should be used in order to derive a unique key for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"${!count(\"items\")}-${!timestamp_unix_nano()}"` + +```yml +# Examples + +key: ${!count("items")}-${!timestamp_unix_nano()} + +key: ${!json("doc.id")} + +key: ${!meta("kafka_key")} +``` + +=== `ttl` + +The TTL of each individual item as a duration string. After this period an item will be eligible for removal during the next compaction. Not all caches support per-key TTLs, and those that do not will fall back to their generally configured TTL setting. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 3.33.0 or newer + +```yml +# Examples + +ttl: 60s + +ttl: 5m + +ttl: 36h +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/cassandra.adoc b/docs/modules/components/pages/outputs/cassandra.adoc new file mode 100644 index 0000000000..b926013fb3 --- /dev/null +++ b/docs/modules/components/pages/outputs/cassandra.adoc @@ -0,0 +1,583 @@ += cassandra +:type: output +:status: beta + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Runs a query against a Cassandra database for each message in order to insert data. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + cassandra: + addresses: [] # No default (required) + timeout: 600ms + query: "" # No default (required) + args_mapping: "" # No default (optional) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + cassandra: + addresses: [] # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + password_authenticator: + enabled: false + username: "" + password: "" + disable_initial_host_lookup: false + max_retries: 3 + backoff: + initial_interval: 1s + max_interval: 5s + timeout: 600ms + query: "" # No default (required) + args_mapping: "" # No default (optional) + consistency: QUORUM + logged_batch: true + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Query arguments can be set using a bloblang array for the fields using the `args_mapping` field. + +When populating timestamp columns the value must either be a string in ISO 8601 format (2006-01-02T15:04:05Z07:00), or an integer representing unix time in seconds. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Examples + +[tabs] +====== +Basic Inserts:: ++ +-- + +If we were to create a table with some basic columns with `CREATE TABLE foo.bar (id int primary key, content text, created_at timestamp);`, and were processing JSON documents of the form `{"id":"342354354","content":"hello world","timestamp":1605219406}` using logged batches, we could populate our table with the following config: + +```yaml +output: + cassandra: + addresses: + - localhost:9042 + query: 'INSERT INTO foo.bar (id, content, created_at) VALUES (?, ?, ?)' + args_mapping: | + root = [ + this.id, + this.content, + this.timestamp + ] + batching: + count: 500 + period: 1s +``` + +-- +Insert JSON Documents:: ++ +-- + +The following example inserts JSON documents into the table `footable` of the keyspace `foospace` using INSERT JSON (https://cassandra.apache.org/doc/latest/cql/json.html#insert-json). + +```yaml +output: + cassandra: + addresses: + - localhost:9042 + query: 'INSERT INTO foospace.footable JSON ?' + args_mapping: 'root = [ this ]' + batching: + count: 500 + period: 1s +``` + +-- +====== + +== Fields + +=== `addresses` + +A list of Cassandra nodes to connect to. Multiple comma separated addresses can be specified on a single line. + + +*Type*: `array` + + +```yml +# Examples + +addresses: + - localhost:9042 + +addresses: + - foo:9042 + - bar:9042 + +addresses: + - foo:9042,bar:9042 +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `password_authenticator` + +Optional configuration of Cassandra authentication parameters. + + +*Type*: `object` + + +=== `password_authenticator.enabled` + +Whether to use password authentication + + +*Type*: `bool` + +*Default*: `false` + +=== `password_authenticator.username` + +The username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `password_authenticator.password` + +The password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `disable_initial_host_lookup` + +If enabled the driver will not attempt to get host info from the system.peers table. This can speed up queries but will mean that data_centre, rack and token information will not be available. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_retries` + +The maximum number of retries before giving up on a request. + + +*Type*: `int` + +*Default*: `3` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `timeout` + +The client connection timeout. + + +*Type*: `string` + +*Default*: `"600ms"` + +=== `query` + +A query to execute for each message. + + +*Type*: `string` + + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that can be used to provide arguments to Cassandra queries. The result of the query must be an array containing a matching number of elements to the query arguments. + + +*Type*: `string` + +Requires version 3.55.0 or newer + +=== `consistency` + +The consistency level to use. + + +*Type*: `string` + +*Default*: `"QUORUM"` + +Options: +`ANY` +, `ONE` +, `TWO` +, `THREE` +, `QUORUM` +, `ALL` +, `LOCAL_QUORUM` +, `EACH_QUORUM` +, `LOCAL_ONE` +. + +=== `logged_batch` + +If enabled the driver will perform a logged batch. Disabling this prompts unlogged batches to be used instead, which are less efficient but necessary for alternative storages that do not support logged batches. + + +*Type*: `bool` + +*Default*: `true` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/discord.adoc b/docs/modules/components/pages/outputs/discord.adoc new file mode 100644 index 0000000000..ba7cb2bb1d --- /dev/null +++ b/docs/modules/components/pages/outputs/discord.adoc @@ -0,0 +1,52 @@ += discord +:type: output +:status: experimental +:categories: ["Services","Social"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Writes messages to a Discord channel. + +```yml +# Config fields, showing default values +output: + label: "" + discord: + channel_id: "" # No default (required) + bot_token: "" # No default (required) +``` + +This output POSTs messages to the `/channels/\{channel_id}/messages` Discord API endpoint authenticated as a bot using token based authentication. + +If the format of a message is a JSON object matching the https://discord.com/developers/docs/resources/channel#message-object[Discord API message type] then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. + + +== Fields + +=== `channel_id` + +A discord channel ID to write messages to. + + +*Type*: `string` + + +=== `bot_token` + +A bot token used for authentication. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/outputs/drop.adoc b/docs/modules/components/pages/outputs/drop.adoc new file mode 100644 index 0000000000..aa076745f5 --- /dev/null +++ b/docs/modules/components/pages/outputs/drop.adoc @@ -0,0 +1,27 @@ += drop +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Drops all messages. + +```yml +# Config fields, showing default values +output: + label: "" + drop: {} +``` + + diff --git a/docs/modules/components/pages/outputs/drop_on.adoc b/docs/modules/components/pages/outputs/drop_on.adoc new file mode 100644 index 0000000000..17991e00aa --- /dev/null +++ b/docs/modules/components/pages/outputs/drop_on.adoc @@ -0,0 +1,132 @@ += drop_on +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Attempts to write messages to a child output and if the write fails for one of a list of configurable reasons the message is dropped (acked) instead of being reattempted (or nacked). + +```yml +# Config fields, showing default values +output: + label: "" + drop_on: + error: false + error_patterns: [] # No default (optional) + back_pressure: 30s # No default (optional) + output: null # No default (required) +``` + +Regular Benthos outputs will apply back pressure when downstream services aren't accessible, and Benthos retries (or nacks) all messages that fail to be delivered. However, in some circumstances, or for certain output types, we instead might want to relax these mechanisms, which is when this output becomes useful. + +== Fields + +=== `error` + +Whether messages should be dropped when the child output returns an error of any type. For example, this could be when an `http_client` output gets a 4XX response code. In order to instead drop only on specific error patterns use the `error_matches` field instead. + + +*Type*: `bool` + +*Default*: `false` + +=== `error_patterns` + +A list of regular expressions (re2) where if the child output returns an error that matches any part of any of these patterns the message will be dropped. + + +*Type*: `array` + +Requires version 4.27.0 or newer + +```yml +# Examples + +error_patterns: + - and that was really bad$ + +error_patterns: + - roughly [0-9]+ issues occurred +``` + +=== `back_pressure` + +An optional duration string that determines the maximum length of time to wait for a given message to be accepted by the child output before the message should be dropped instead. The most common reason for an output to block is when waiting for a lost connection to be re-established. Once a message has been dropped due to back pressure all subsequent messages are dropped immediately until the output is ready to process them again. Note that if `error` is set to `false` and this field is specified then messages dropped due to back pressure will return an error response (are nacked or reattempted). + + +*Type*: `string` + + +```yml +# Examples + +back_pressure: 30s + +back_pressure: 1m +``` + +=== `output` + +A child output to wrap with this drop mechanism. + + +*Type*: `output` + + +== Examples + +[tabs] +====== +Dropping failed HTTP requests:: ++ +-- + +In this example we have a fan_out broker, where we guarantee delivery to our Kafka output, but drop messages if they fail our secondary HTTP client output. + +```yaml +output: + broker: + pattern: fan_out + outputs: + - kafka: + addresses: [ foobar:6379 ] + topic: foo + - drop_on: + error: true + output: + http_client: + url: http://example.com/foo/messages + verb: POST +``` + +-- +Dropping from outputs that cannot connect:: ++ +-- + +Most outputs that attempt to establish and long-lived connection will apply back-pressure when the connection is lost. The following example has a websocket output where if it takes longer than 10 seconds to establish a connection, or recover a lost one, pending messages are dropped. + +```yaml +output: + drop_on: + back_pressure: 10s + output: + websocket: + url: ws://example.com/foo/messages +``` + +-- +====== + + diff --git a/docs/modules/components/pages/outputs/dynamic.adoc b/docs/modules/components/pages/outputs/dynamic.adoc new file mode 100644 index 0000000000..67e4a69f51 --- /dev/null +++ b/docs/modules/components/pages/outputs/dynamic.adoc @@ -0,0 +1,72 @@ += dynamic +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A special broker type where the outputs are identified by unique labels and can be created, changed and removed during runtime via a REST API. + +```yml +# Config fields, showing default values +output: + label: "" + dynamic: + outputs: {} + prefix: "" +``` + +The broker pattern used is always `fan_out`, meaning each message will be delivered to each dynamic output. + +== Fields + +=== `outputs` + +A map of outputs to statically create. + + +*Type*: `object` + +*Default*: `{}` + +=== `prefix` + +A path prefix for HTTP endpoints that are registered. + + +*Type*: `string` + +*Default*: `""` + +== Endpoints + +=== GET `/outputs` + +Returns a JSON object detailing all dynamic outputs, providing information such as their current uptime and configuration. + +=== GET `/outputs/\{id}` + +Returns the configuration of an output. + +=== POST `/outputs/\{id}` + +Creates or updates an output with a configuration provided in the request body (in YAML or JSON format). + +=== DELETE `/outputs/\{id}` + +Stops and removes an output. + +=== GET `/outputs/\{id}/uptime` + +Returns the uptime of an output as a duration string (of the form "72h3m0.5s"). + diff --git a/docs/modules/components/pages/outputs/elasticsearch.adoc b/docs/modules/components/pages/outputs/elasticsearch.adoc new file mode 100644 index 0000000000..4ca54b7213 --- /dev/null +++ b/docs/modules/components/pages/outputs/elasticsearch.adoc @@ -0,0 +1,699 @@ += elasticsearch +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Publishes messages into an Elasticsearch index. If the index does not exist then it is created with a dynamic mapping. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + elasticsearch: + urls: [] # No default (required) + index: "" # No default (required) + id: ${!count("elastic_ids")}-${!timestamp_unix()} + type: "" + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + elasticsearch: + urls: [] # No default (required) + index: "" # No default (required) + action: index + pipeline: "" + id: ${!count("elastic_ids")}-${!timestamp_unix()} + type: "" + routing: "" + sniff: true + healthcheck: true + timeout: 5s + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + max_in_flight: 64 + max_retries: 0 + backoff: + initial_interval: 1s + max_interval: 5s + max_elapsed_time: 30s + basic_auth: + enabled: false + username: "" + password: "" + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + aws: + enabled: false + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + gzip_compression: false +``` + +-- +====== + +Both the `id` and `index` fields can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. When sending batched messages these interpolations are performed per message part. + +== AWS + +It's possible to enable AWS connectivity with this output using the `aws` fields. However, you may need to set `sniff` and `healthcheck` to false for connections to succeed. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - http://localhost:9200 +``` + +=== `index` + +The index to place messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `action` + +The action to take on the document. This field must resolve to one of the following action types: `create`, `index`, `update`, `upsert` or `delete`. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"index"` + +=== `pipeline` + +An optional pipeline id to preprocess incoming documents. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `id` + +The ID for indexed messages. Interpolation should be used in order to create a unique ID for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"${!count(\"elastic_ids\")}-${!timestamp_unix()}"` + +=== `type` + +The document mapping type. This field is required for versions of elasticsearch earlier than 6.0.0, but are invalid for versions 7.0.0 or later. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `routing` + +The routing key to use for the document. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `sniff` + +Prompts Benthos to sniff for brokers to connect to when establishing a connection. + + +*Type*: `bool` + +*Default*: `true` + +=== `healthcheck` + +Whether to enable healthchecks. + + +*Type*: `bool` + +*Default*: `true` + +=== `timeout` + +The maximum time to wait before abandoning a request (and trying again). + + +*Type*: `string` + +*Default*: `"5s"` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `0` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `backoff.max_elapsed_time` + +The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. + + +*Type*: `string` + +*Default*: `"30s"` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `aws` + +Enables and customises connectivity to Amazon Elastic Service. + + +*Type*: `object` + + +=== `aws.enabled` + +Whether to connect to Amazon Elastic Service. + + +*Type*: `bool` + +*Default*: `false` + +=== `aws.region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `aws.credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `aws.credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `gzip_compression` + +Enable gzip compression on the request side. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/outputs/fallback.adoc b/docs/modules/components/pages/outputs/fallback.adoc new file mode 100644 index 0000000000..f0159de195 --- /dev/null +++ b/docs/modules/components/pages/outputs/fallback.adoc @@ -0,0 +1,75 @@ += fallback +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Attempts to send each message to a child output, starting from the first output on the list. If an output attempt fails then the next output in the list is attempted, and so on. + +Introduced in version 3.58.0. + +```yml +# Config fields, showing default values +output: + label: "" + fallback: [] +``` + +This pattern is useful for triggering events in the case where certain output targets have broken. For example, if you had an output type `http_client` but wished to reroute messages whenever the endpoint becomes unreachable you could use this pattern: + +```yaml +output: + fallback: + - http_client: + url: http://foo:4195/post/might/become/unreachable + retries: 3 + retry_period: 1s + - http_client: + url: http://bar:4196/somewhere/else + retries: 3 + retry_period: 1s + processors: + - mapping: 'root = "failed to send this message to foo: " + content()' + - file: + path: /usr/local/benthos/everything_failed.jsonl +``` + +== Metadata + +When a given output fails the message routed to the following output will have a metadata value named `fallback_error` containing a string error message outlining the cause of the failure. The content of this string will depend on the particular output and can be used to enrich the message or provide information used to broker the data to an appropriate output using something like a `switch` output. + +== Batching + +When an output within a fallback sequence uses batching, like so: + +```yaml +output: + fallback: + - aws_dynamodb: + table: foo + string_columns: + id: ${!json("id")} + content: ${!content()} + batching: + count: 10 + period: 1s + - file: + path: /usr/local/benthos/failed_stuff.jsonl +``` + +Benthos makes a best attempt at inferring which specific messages of the batch failed, and only propagates those individual messages to the next fallback tier. + +However, depending on the output and the error returned it is sometimes not possible to determine the individual messages that failed, in which case the whole batch is passed to the next tier in order to preserve at-least-once delivery guarantees. + + diff --git a/docs/modules/components/pages/outputs/file.adoc b/docs/modules/components/pages/outputs/file.adoc new file mode 100644 index 0000000000..4ccf3d49f1 --- /dev/null +++ b/docs/modules/components/pages/outputs/file.adoc @@ -0,0 +1,87 @@ += file +:type: output +:status: stable +:categories: ["Local"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Writes messages to files on disk based on a chosen codec. + +```yml +# Config fields, showing default values +output: + label: "" + file: + path: /tmp/data.txt # No default (required) + codec: lines +``` + +Messages can be written to different files by using xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the path field. However, only one file is ever open at a given time, and therefore when the path changes the previously open file is closed. + +== Fields + +=== `path` + +The file to write to, if the file does not yet exist it will be created. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 3.33.0 or newer + +```yml +# Examples + +path: /tmp/data.txt + +path: /tmp/${! timestamp_unix() }.txt + +path: /tmp/${! json("document.id") }.json +``` + +=== `codec` + +The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. + + +*Type*: `string` + +*Default*: `"lines"` +Requires version 3.33.0 or newer + +|=== +| Option | Summary + +| `all-bytes` +| Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. +| `append` +| Append each message to the output stream without any delimiter or special encoding. +| `lines` +| Append each message to the output stream followed by a line break. +| `delim:x` +| Append each message to the output stream followed by a custom delimiter. + +|=== + +```yml +# Examples + +codec: lines + +codec: "delim:\t" + +codec: delim:foobar +``` + + diff --git a/docs/modules/components/pages/outputs/gcp_bigquery.adoc b/docs/modules/components/pages/outputs/gcp_bigquery.adoc new file mode 100644 index 0000000000..1551013e65 --- /dev/null +++ b/docs/modules/components/pages/outputs/gcp_bigquery.adoc @@ -0,0 +1,414 @@ += gcp_bigquery +:type: output +:status: beta +:categories: ["GCP","Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages as new rows to a Google Cloud BigQuery table. + +Introduced in version 3.55.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + gcp_bigquery: + project: "" + dataset: "" # No default (required) + table: "" # No default (required) + format: NEWLINE_DELIMITED_JSON + max_in_flight: 64 + job_labels: {} + csv: + header: [] + field_delimiter: ',' + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + gcp_bigquery: + project: "" + dataset: "" # No default (required) + table: "" # No default (required) + format: NEWLINE_DELIMITED_JSON + max_in_flight: 64 + write_disposition: WRITE_APPEND + create_disposition: CREATE_IF_NEEDED + ignore_unknown_values: false + max_bad_records: 0 + auto_detect: false + job_labels: {} + csv: + header: [] + field_delimiter: ',' + allow_jagged_rows: false + allow_quoted_newlines: false + encoding: UTF-8 + skip_leading_rows: 1 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +== Credentials + +By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more in xref:guides:cloud/gcp.adoc[]. + +== Format + +This output currently supports only CSV and NEWLINE_DELIMITED_JSON formats. Learn more about how to use GCP BigQuery with them here: +- https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json[`NEWLINE_DELIMITED_JSON`] +- https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv[`CSV`] + +Each message may contain multiple elements separated by newlines. For example a single message containing: + +```json +{"key": "1"} +{"key": "2"} +``` + +Is equivalent to two separate messages: + +```json +{"key": "1"} +``` + +And: + +```json +{"key": "2"} +``` + +The same is true for the CSV format. + +=== CSV + +For the CSV format when the field `csv.header` is specified a header row will be inserted as the first line of each message batch. If this field is not provided then the first message of each message batch must include a header line. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `project` + +The project ID of the dataset to insert data to. If not set, it will be inferred from the credentials or read from the GOOGLE_CLOUD_PROJECT environment variable. + + +*Type*: `string` + +*Default*: `""` + +=== `dataset` + +The BigQuery Dataset ID. + + +*Type*: `string` + + +=== `table` + +The table to insert messages to. + + +*Type*: `string` + + +=== `format` + +The format of each incoming message. + + +*Type*: `string` + +*Default*: `"NEWLINE_DELIMITED_JSON"` + +Options: +`NEWLINE_DELIMITED_JSON` +, `CSV` +. + +=== `max_in_flight` + +The maximum number of message batches to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `write_disposition` + +Specifies how existing data in a destination table is treated. + + +*Type*: `string` + +*Default*: `"WRITE_APPEND"` + +Options: +`WRITE_APPEND` +, `WRITE_EMPTY` +, `WRITE_TRUNCATE` +. + +=== `create_disposition` + +Specifies the circumstances under which destination table will be created. If CREATE_IF_NEEDED is used the GCP BigQuery will create the table if it does not already exist and tables are created atomically on successful completion of a job. The CREATE_NEVER option ensures the table must already exist and will not be automatically created. + + +*Type*: `string` + +*Default*: `"CREATE_IF_NEEDED"` + +Options: +`CREATE_IF_NEEDED` +, `CREATE_NEVER` +. + +=== `ignore_unknown_values` + +Causes values not matching the schema to be tolerated. Unknown values are ignored. For CSV this ignores extra values at the end of a line. For JSON this ignores named values that do not match any column name. If this field is set to false (the default value), records containing unknown values are treated as bad records. The max_bad_records field can be used to customize how bad records are handled. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_bad_records` + +The maximum number of bad records that will be ignored when reading data. + + +*Type*: `int` + +*Default*: `0` + +=== `auto_detect` + +Indicates if we should automatically infer the options and schema for CSV and JSON sources. If the table doesn't exist and this field is set to `false` the output may not be able to insert data and will throw insertion error. Be careful using this field since it delegates to the GCP BigQuery service the schema detection and values like `"no"` may be treated as booleans for the CSV format. + + +*Type*: `bool` + +*Default*: `false` + +=== `job_labels` + +A list of labels to add to the load job. + + +*Type*: `object` + +*Default*: `{}` + +=== `csv` + +Specify how CSV data should be interpretted. + + +*Type*: `object` + + +=== `csv.header` + +A list of values to use as header for each batch of messages. If not specified the first line of each message will be used as header. + + +*Type*: `array` + +*Default*: `[]` + +=== `csv.field_delimiter` + +The separator for fields in a CSV file, used when reading or exporting data. + + +*Type*: `string` + +*Default*: `","` + +=== `csv.allow_jagged_rows` + +Causes missing trailing optional columns to be tolerated when reading CSV data. Missing values are treated as nulls. + + +*Type*: `bool` + +*Default*: `false` + +=== `csv.allow_quoted_newlines` + +Sets whether quoted data sections containing newlines are allowed when reading CSV data. + + +*Type*: `bool` + +*Default*: `false` + +=== `csv.encoding` + +Encoding is the character encoding of data to be read. + + +*Type*: `string` + +*Default*: `"UTF-8"` + +Options: +`UTF-8` +, `ISO-8859-1` +. + +=== `csv.skip_leading_rows` + +The number of rows at the top of a CSV file that BigQuery will skip when reading data. The default value is 1 since Benthos will add the specified header in the first line of each batch sent to BigQuery. + + +*Type*: `int` + +*Default*: `1` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/gcp_cloud_storage.adoc b/docs/modules/components/pages/outputs/gcp_cloud_storage.adoc new file mode 100644 index 0000000000..def7abd674 --- /dev/null +++ b/docs/modules/components/pages/outputs/gcp_cloud_storage.adoc @@ -0,0 +1,338 @@ += gcp_cloud_storage +:type: output +:status: beta +:categories: ["Services","GCP"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends message parts as objects to a Google Cloud Storage bucket. Each object is uploaded with the path specified with the `path` field. + +Introduced in version 3.43.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + gcp_cloud_storage: + bucket: "" # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + content_type: application/octet-stream + collision_mode: overwrite + timeout: 3s + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + gcp_cloud_storage: + bucket: "" # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + content_type: application/octet-stream + content_encoding: "" + collision_mode: overwrite + chunk_size: 16777216 + timeout: 3s + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +In order to have a different path for each object you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries], which are calculated per message of a batch. + +== Metadata + +Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the xref:configuration:metadata.adoc[metadata docs]. + +== Credentials + +By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more in xref:guides:cloud/gcp.adoc[]. + +== Batching + +It's common to want to upload messages to Google Cloud Storage as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an xref:components:processors/archive.adoc[`archive`] and/or xref:components:processors/compress.adoc[`compress`] processor. + +For example, if we wished to upload messages as a .tar.gz archive of documents we could achieve that with the following config: + +```yaml +output: + gcp_cloud_storage: + bucket: TODO + path: ${!count("files")}-${!timestamp_unix_nano()}.tar.gz + batching: + count: 100 + period: 10s + processors: + - archive: + format: tar + - compress: + algorithm: gzip +``` + +Alternatively, if we wished to upload JSON documents as a single large document containing an array of objects we can do that with: + +```yaml +output: + gcp_cloud_storage: + bucket: TODO + path: ${!count("files")}-${!timestamp_unix_nano()}.json + batching: + count: 100 + processors: + - archive: + format: json_array +``` + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `bucket` + +The bucket to upload messages to. + + +*Type*: `string` + + +=== `path` + +The path of each message to upload. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` + +```yml +# Examples + +path: ${!count("files")}-${!timestamp_unix_nano()}.txt + +path: ${!meta("kafka_key")}.json + +path: ${!json("doc.namespace")}/${!json("doc.id")}.json +``` + +=== `content_type` + +The content type to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"application/octet-stream"` + +=== `content_encoding` + +An optional content encoding to set for each object. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `collision_mode` + +Determines how file path collisions should be dealt with. + + +*Type*: `string` + +*Default*: `"overwrite"` +Requires version 3.53.0 or newer + +|=== +| Option | Summary + +| `append` +| Append the message bytes to the original file. +| `error-if-exists` +| Return an error, this is the equivalent of a nack. +| `ignore` +| Do not modify the original file, the new data will be dropped. +| `overwrite` +| Replace the existing file with the new one. + +|=== + +=== `chunk_size` + +An optional chunk size which controls the maximum number of bytes of the object that the Writer will attempt to send to the server in a single request. If ChunkSize is set to zero, chunking will be disabled. + + +*Type*: `int` + +*Default*: `16777216` + +=== `timeout` + +The maximum period to wait on an upload before abandoning it and reattempting. + + +*Type*: `string` + +*Default*: `"3s"` + +```yml +# Examples + +timeout: 1s + +timeout: 500ms +``` + +=== `max_in_flight` + +The maximum number of message batches to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/gcp_pubsub.adoc b/docs/modules/components/pages/outputs/gcp_pubsub.adoc new file mode 100644 index 0000000000..8d426778ce --- /dev/null +++ b/docs/modules/components/pages/outputs/gcp_pubsub.adoc @@ -0,0 +1,367 @@ += gcp_pubsub +:type: output +:status: stable +:categories: ["Services","GCP"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to a GCP Cloud Pub/Sub topic. xref:configuration:metadata.adoc[Metadata] from messages are sent as attributes. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + gcp_pubsub: + project: "" # No default (required) + topic: "" # No default (required) + endpoint: "" + max_in_flight: 64 + count_threshold: 100 + delay_threshold: 10ms + byte_threshold: 1000000 + metadata: + exclude_prefixes: [] + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + gcp_pubsub: + project: "" # No default (required) + topic: "" # No default (required) + endpoint: "" + ordering_key: "" # No default (optional) + max_in_flight: 64 + count_threshold: 100 + delay_threshold: 10ms + byte_threshold: 1000000 + publish_timeout: 1m0s + metadata: + exclude_prefixes: [] + flow_control: + max_outstanding_bytes: -1 + max_outstanding_messages: 1000 + limit_exceeded_behavior: block + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +For information on how to set up credentials, see https://cloud.google.com/docs/authentication/production[this guide]. + +== Troubleshooting + +If you're consistently seeing `Failed to send message to gcp_pubsub: context deadline exceeded` error logs without any further information it is possible that you are encountering https://github.com/benthosdev/benthos/issues/1042, which occurs when metadata values contain characters that are not valid utf-8. This can frequently occur when consuming from Kafka as the key metadata field may be populated with an arbitrary binary value, but this issue is not exclusive to Kafka. + +If you are blocked by this issue then a work around is to delete either the specific problematic keys: + +```yaml +pipeline: + processors: + - mapping: | + meta kafka_key = deleted() +``` + +Or delete all keys with: + +```yaml +pipeline: + processors: + - mapping: meta = deleted() +``` + +== Fields + +=== `project` + +The project ID of the topic to publish to. + + +*Type*: `string` + + +=== `topic` + +The topic to publish to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `endpoint` + +An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +endpoint: us-central1-pubsub.googleapis.com:443 + +endpoint: us-west3-pubsub.googleapis.com:443 +``` + +=== `ordering_key` + +The ordering key to use for publishing messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increasing this may improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `count_threshold` + +Publish a pubsub buffer when it has this many messages + + +*Type*: `int` + +*Default*: `100` + +=== `delay_threshold` + +Publish a non-empty pubsub buffer after this delay has passed. + + +*Type*: `string` + +*Default*: `"10ms"` + +=== `byte_threshold` + +Publish a batch when its size in bytes reaches this value. + + +*Type*: `int` + +*Default*: `1000000` + +=== `publish_timeout` + +The maximum length of time to wait before abandoning a publish attempt for a message. + + +*Type*: `string` + +*Default*: `"1m0s"` + +```yml +# Examples + +publish_timeout: 10s + +publish_timeout: 5m + +publish_timeout: 60m +``` + +=== `metadata` + +Specify criteria for which metadata values are sent as attributes, all are sent by default. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `flow_control` + +For a given topic, configures the PubSub client's internal buffer for messages to be published. + + +*Type*: `object` + + +=== `flow_control.max_outstanding_bytes` + +Maximum size of buffered messages to be published. If less than or equal to zero, this is disabled. + + +*Type*: `int` + +*Default*: `-1` + +=== `flow_control.max_outstanding_messages` + +Maximum number of buffered messages to be published. If less than or equal to zero, this is disabled. + + +*Type*: `int` + +*Default*: `1000` + +=== `flow_control.limit_exceeded_behavior` + +Configures the behavior when trying to publish additional messages while the flow controller is full. The available options are block (default), ignore (disable), and signal_error (publish results will return an error). + + +*Type*: `string` + +*Default*: `"block"` + +Options: +`ignore` +, `block` +, `signal_error` +. + +=== `batching` + +Configures a batching policy on this output. While the PubSub client maintains its own internal buffering mechanism, preparing larger batches of messages can further trade-off some latency for throughput. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/hdfs.adoc b/docs/modules/components/pages/outputs/hdfs.adoc new file mode 100644 index 0000000000..04e65cc5b6 --- /dev/null +++ b/docs/modules/components/pages/outputs/hdfs.adoc @@ -0,0 +1,231 @@ += hdfs +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends message parts as files to a HDFS directory. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + hdfs: + hosts: [] # No default (required) + user: "" + directory: "" # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + hdfs: + hosts: [] # No default (required) + user: "" + directory: "" # No default (required) + path: ${!count("files")}-${!timestamp_unix_nano()}.txt + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +Each file is written with the path specified with the 'path' field, in order to have a different path for each object you should use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `hosts` + +A list of target host addresses to connect to. + + +*Type*: `array` + + +```yml +# Examples + +hosts: localhost:9000 +``` + +=== `user` + +A user ID to connect as. + + +*Type*: `string` + +*Default*: `""` + +=== `directory` + +A directory to store message files within. If the directory does not exist it will be created. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `path` + +The path to upload messages as, interpolation functions should be used in order to generate unique file paths. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/http_client.adoc b/docs/modules/components/pages/outputs/http_client.adoc new file mode 100644 index 0000000000..633f70f79e --- /dev/null +++ b/docs/modules/components/pages/outputs/http_client.adoc @@ -0,0 +1,971 @@ += http_client +:type: output +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an HTTP server. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + http_client: + url: "" # No default (required) + verb: POST + headers: {} + rate_limit: "" # No default (optional) + timeout: 5s + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + http_client: + url: "" # No default (required) + verb: POST + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + dump_request_log_level: "" + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + oauth2: + enabled: false + client_key: "" + client_secret: "" + token_url: "" + scopes: [] + endpoint_params: {} + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + extract_headers: + include_prefixes: [] + include_patterns: [] + rate_limit: "" # No default (optional) + timeout: 5s + retry_period: 1s + max_retry_backoff: 300s + retries: 3 + backoff_on: + - 429 + drop_on: [] + successful_on: [] + proxy_url: "" # No default (optional) + batch_as_multipart: false + propagate_response: false + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + multipart: [] +``` + +-- +====== + +When the number of retries expires the output will reject the message, the behavior after this will depend on the pipeline but usually this simply means the send is attempted again until successful whilst applying back pressure. + +The URL and header values of this type can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. + +The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be disabled by setting the field <> to `false`. + +== Propagate responses + +It's possible to propagate the response from each HTTP request back to the input source by setting `propagate_response` to `true`. Only inputs that support xref:guides:sync_responses.adoc[synchronous responses] are able to make use of these propagated responses. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `url` + +The URL to connect to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `verb` + +A verb to connect with + + +*Type*: `string` + +*Default*: `"POST"` + +```yml +# Examples + +verb: POST + +verb: GET + +verb: DELETE +``` + +=== `headers` + +A map of headers to add to the request. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +headers: + Content-Type: application/octet-stream + traceparent: ${! tracing_span().traceparent } +``` + +=== `metadata` + +Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `dump_request_log_level` + +EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged. + + +*Type*: `string` + +*Default*: `""` +Requires version 4.12.0 or newer + +Options: +`TRACE` +, `DEBUG` +, `INFO` +, `WARN` +, `ERROR` +, `FATAL` +, `` +. + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2` + +Allows you to specify open authentication via OAuth version 2 using the client credentials token flow. + + +*Type*: `object` + + +=== `oauth2.enabled` + +Whether to use OAuth version 2 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth2.client_key` + +A value used to identify the client to the token provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.client_secret` + +A secret used to establish ownership of the client key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.token_url` + +The URL of the token provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.scopes` + +A list of optional requested permissions. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.45.0 or newer + +=== `oauth2.endpoint_params` + +A list of optional endpoint parameters, values should be arrays of strings. + + +*Type*: `object` + +*Default*: `{}` +Requires version 4.21.0 or newer + +```yml +# Examples + +endpoint_params: + bar: + - woof + foo: + - meow + - quack +``` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `extract_headers` + +Specify which response headers should be added to resulting synchronous response messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. This field is not applicable unless `propagate_response` is set to `true`. + + +*Type*: `object` + + +=== `extract_headers.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `extract_headers.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `rate_limit` + +An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by. + + +*Type*: `string` + + +=== `timeout` + +A static timeout to apply to requests. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `retry_period` + +The base period to wait between failed requests. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `max_retry_backoff` + +The maximum period to wait between failed requests. + + +*Type*: `string` + +*Default*: `"300s"` + +=== `retries` + +The maximum number of retry attempts to make. + + +*Type*: `int` + +*Default*: `3` + +=== `backoff_on` + +A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually. + + +*Type*: `array` + +*Default*: `[429]` + +=== `drop_on` + +A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped. + + +*Type*: `array` + +*Default*: `[]` + +=== `successful_on` + +A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field. + + +*Type*: `array` + +*Default*: `[]` + +=== `proxy_url` + +An optional HTTP proxy URL. + + +*Type*: `string` + + +=== `batch_as_multipart` + +Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. If disabled messages in batches will be sent as individual requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `propagate_response` + +Whether responses from the server should be xref:guides:sync_responses.adoc[propagated back] to the input. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `multipart` + +EXPERIMENTAL: Create explicit multipart HTTP requests by specifying an array of parts to add to the request, each part specified consists of content headers and a data field that can be populated dynamically. If this field is populated it will override the default request creation behavior. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.63.0 or newer + +=== `multipart[].content_type` + +The content type of the individual message part. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +content_type: application/bin +``` + +=== `multipart[].content_disposition` + +The content disposition of the individual message part. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +content_disposition: form-data; name="bin"; filename='${! @AttachmentName } +``` + +=== `multipart[].body` + +The body of the individual message part. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +body: ${! this.data.part1 } +``` + + diff --git a/docs/modules/components/pages/outputs/http_server.adoc b/docs/modules/components/pages/outputs/http_server.adoc new file mode 100644 index 0000000000..2315fc40e2 --- /dev/null +++ b/docs/modules/components/pages/outputs/http_server.adoc @@ -0,0 +1,190 @@ += http_server +:type: output +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sets up an HTTP server that will send messages over HTTP(S) GET requests. HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + http_server: + address: "" + path: /get + stream_path: /get/stream + ws_path: /get/ws + allowed_verbs: + - GET +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + http_server: + address: "" + path: /get + stream_path: /get/stream + ws_path: /get/ws + allowed_verbs: + - GET + timeout: 5s + cert_file: "" + key_file: "" + cors: + enabled: false + allowed_origins: [] +``` + +-- +====== + +Sets up an HTTP server that will send messages over HTTP(S) GET requests. If the `address` config field is left blank the xref:components:http/about.adoc[service-wide HTTP server] will be used. + +Three endpoints will be registered at the paths specified by the fields `path`, `stream_path` and `ws_path`. Which allow you to consume a single message batch, a continuous stream of line delimited messages, or a websocket of messages for each request respectively. + +When messages are batched the `path` endpoint encodes the batch according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be overridden by xref:configuration:batching.adoc#post-batch-processing[archiving your batches]. + +Please note, messages are considered delivered as soon as the data is written to the client. There is no concept of at least once delivery on this output. + + +[CAUTION] +.Endpoint caveats +==== +Components within a Benthos config will register their respective endpoints in a non-deterministic order. This means that establishing precedence of endpoints that are registered via multiple `http_server` inputs or outputs (either within brokers or from cohabiting streams) is not possible in a predictable way. + +This ambiguity makes it difficult to ensure that paths which are both a subset of a path registered by a separate component, and end in a slash (`/`) and will therefore match against all extensions of that path, do not prevent the more specific path from matching against requests. + +It is therefore recommended that you ensure paths of separate components do not collide unless they are explicitly non-competing. + +For example, if you were to deploy two separate `http_server` inputs, one with a path `/foo/` and the other with a path `/foo/bar`, it would not be possible to ensure that the path `/foo/` does not swallow requests made to `/foo/bar`. +==== + + +== Fields + +=== `address` + +An alternative address to host from. If left empty the service wide address is used. + + +*Type*: `string` + +*Default*: `""` + +=== `path` + +The path from which discrete messages can be consumed. + + +*Type*: `string` + +*Default*: `"/get"` + +=== `stream_path` + +The path from which a continuous stream of messages can be consumed. + + +*Type*: `string` + +*Default*: `"/get/stream"` + +=== `ws_path` + +The path from which websocket connections can be established. + + +*Type*: `string` + +*Default*: `"/get/ws"` + +=== `allowed_verbs` + +An array of verbs that are allowed for the `path` and `stream_path` HTTP endpoint. + + +*Type*: `array` + +*Default*: `["GET"]` + +=== `timeout` + +The maximum time to wait before a blocking, inactive connection is dropped (only applies to the `path` endpoint). + + +*Type*: `string` + +*Default*: `"5s"` + +=== `cert_file` + +Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. + + +*Type*: `string` + +*Default*: `""` + +=== `key_file` + +Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. + + +*Type*: `string` + +*Default*: `""` + +=== `cors` + +Adds Cross-Origin Resource Sharing headers. Only valid with a custom `address`. + + +*Type*: `object` + +Requires version 3.63.0 or newer + +=== `cors.enabled` + +Whether to allow CORS requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `cors.allowed_origins` + +An explicit list of origins that are allowed for CORS requests. + + +*Type*: `array` + +*Default*: `[]` + + diff --git a/docs/modules/components/pages/outputs/inproc.adoc b/docs/modules/components/pages/outputs/inproc.adoc new file mode 100644 index 0000000000..0e6eb8a121 --- /dev/null +++ b/docs/modules/components/pages/outputs/inproc.adoc @@ -0,0 +1,30 @@ += inproc +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + + +```yml +# Config fields, showing default values +output: + label: "" + inproc: "" +``` + +Sends data directly to Benthos inputs by connecting to a unique ID. This allows you to hook up isolated streams whilst running Benthos in xref:guides:streams_mode/about.adoc[streams mode], it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. + +It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs. + + diff --git a/docs/modules/components/pages/outputs/kafka.adoc b/docs/modules/components/pages/outputs/kafka.adoc new file mode 100644 index 0000000000..c4311586d2 --- /dev/null +++ b/docs/modules/components/pages/outputs/kafka.adoc @@ -0,0 +1,827 @@ += kafka +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +The kafka output type writes a batch of messages to Kafka brokers and waits for acknowledgement before propagating it back to the input. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + kafka: + addresses: [] # No default (required) + topic: "" # No default (required) + target_version: 2.1.0 # No default (optional) + key: "" + partitioner: fnv1a_hash + compression: none + static_headers: {} # No default (optional) + metadata: + exclude_prefixes: [] + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + kafka: + addresses: [] # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + sasl: + mechanism: none + user: "" + password: "" + access_token: "" + token_cache: "" + token_key: "" + topic: "" # No default (required) + client_id: benthos + target_version: 2.1.0 # No default (optional) + rack_id: "" + key: "" + partitioner: fnv1a_hash + partition: "" + custom_topic_creation: + enabled: false + partitions: -1 + replication_factor: -1 + compression: none + static_headers: {} # No default (optional) + metadata: + exclude_prefixes: [] + inject_tracing_map: meta = @.merge(this) # No default (optional) + max_in_flight: 64 + idempotent_write: false + ack_replicas: false + max_msg_bytes: 1000000 + timeout: 5s + retry_as_batch: false + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + max_retries: 0 + backoff: + initial_interval: 3s + max_interval: 10s + max_elapsed_time: 30s +``` + +-- +====== + +The config field `ack_replicas` determines whether we wait for acknowledgement from all replicas or just a single broker. + +Both the `key` and `topic` fields can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. + +xref:configuration:metadata.adoc[Metadata] will be added to each message sent as headers (version 0.11+), but can be restricted using the field <>. + +== Strict ordering and retries + +When strict ordering is required for messages written to topic partitions it is important to ensure that both the field `max_in_flight` is set to `1` and that the field `retry_as_batch` is set to `true`. + +You must also ensure that failed batches are never rerouted back to the same output. This can be done by setting the field `max_retries` to `0` and `backoff.max_elapsed_time` to empty, which will apply back pressure indefinitely until the batch is sent successfully. + +However, this also means that manual intervention will eventually be required in cases where the batch cannot be sent due to configuration problems such as an incorrect `max_msg_bytes` estimate. A less strict but automated alternative would be to route failed batches to a dead letter queue using a xref:components:outputs/fallback.adoc[`fallback` broker], but this would allow subsequent batches to be delivered in the meantime whilst those failed batches are dealt with. + +== Troubleshooting + +If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer xref:components:outputs/kafka_franz.adoc[`kafka_franz` output]. + +- I'm seeing logs that report `Failed to connect to kafka: kafka: client has run out of available brokers to talk to (Is your cluster reachable?)`, but the brokers are definitely reachable. + +Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have <> if applicable. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `addresses` + +A list of broker addresses to connect to. If an item of the list contains commas it will be expanded into multiple addresses. + + +*Type*: `array` + + +```yml +# Examples + +addresses: + - localhost:9092 + +addresses: + - localhost:9041,localhost:9042 + +addresses: + - localhost:9041 + - localhost:9042 +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `sasl` + +Enables SASL authentication. + + +*Type*: `object` + + +=== `sasl.mechanism` + +The SASL authentication mechanism, if left empty SASL authentication is not used. + + +*Type*: `string` + +*Default*: `"none"` + +|=== +| Option | Summary + +| `OAUTHBEARER` +| OAuth Bearer based authentication. +| `PLAIN` +| Plain text authentication. NOTE: When using plain text auth it is extremely likely that you'll also need to <>. +| `SCRAM-SHA-256` +| Authentication using the SCRAM-SHA-256 mechanism. +| `SCRAM-SHA-512` +| Authentication using the SCRAM-SHA-512 mechanism. +| `none` +| Default, no SASL authentication. + +|=== + +=== `sasl.user` + +A PLAIN username. It is recommended that you use environment variables to populate this field. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +user: ${USER} +``` + +=== `sasl.password` + +A PLAIN password. It is recommended that you use environment variables to populate this field. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: ${PASSWORD} +``` + +=== `sasl.access_token` + +A static OAUTHBEARER access token + + +*Type*: `string` + +*Default*: `""` + +=== `sasl.token_cache` + +Instead of using a static `access_token` allows you to query a xref:components:caches/about.adoc[`cache`] resource to fetch OAUTHBEARER tokens from + + +*Type*: `string` + +*Default*: `""` + +=== `sasl.token_key` + +Required when using a `token_cache`, the key to query the cache with for tokens. + + +*Type*: `string` + +*Default*: `""` + +=== `topic` + +The topic to publish messages to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `"benthos"` + +=== `target_version` + +The version of the Kafka protocol to use. This limits the capabilities used by the client and should ideally match the version of your brokers. Defaults to the oldest supported stable version. + + +*Type*: `string` + + +```yml +# Examples + +target_version: 2.1.0 + +target_version: 3.1.0 +``` + +=== `rack_id` + +A rack identifier for this client. + + +*Type*: `string` + +*Default*: `""` + +=== `key` + +The key to publish messages with. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `partitioner` + +The partitioning algorithm to use. + + +*Type*: `string` + +*Default*: `"fnv1a_hash"` + +Options: +`fnv1a_hash` +, `murmur2_hash` +, `random` +, `round_robin` +, `manual` +. + +=== `partition` + +The manually-specified partition to publish messages to, relevant only when the field `partitioner` is set to `manual`. Must be able to parse as a 32-bit integer. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `custom_topic_creation` + +If enabled, topics will be created with the specified number of partitions and replication factor if they do not already exist. + + +*Type*: `object` + + +=== `custom_topic_creation.enabled` + +Whether to enable custom topic creation. + + +*Type*: `bool` + +*Default*: `false` + +=== `custom_topic_creation.partitions` + +The number of partitions to create for new topics. Leave at -1 to use the broker configured default. Must be >= 1. + + +*Type*: `int` + +*Default*: `-1` + +=== `custom_topic_creation.replication_factor` + +The replication factor to use for new topics. Leave at -1 to use the broker configured default. Must be an odd number, and less then or equal to the number of brokers. + + +*Type*: `int` + +*Default*: `-1` + +=== `compression` + +The compression algorithm to use. + + +*Type*: `string` + +*Default*: `"none"` + +Options: +`none` +, `snappy` +, `lz4` +, `gzip` +, `zstd` +. + +=== `static_headers` + +An optional map of static headers that should be added to messages in addition to metadata. + + +*Type*: `object` + + +```yml +# Examples + +static_headers: + first-static-header: value-1 + second-static-header: value-2 +``` + +=== `metadata` + +Specify criteria for which metadata values are sent with messages as headers. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `inject_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 3.45.0 or newer + +```yml +# Examples + +inject_tracing_map: meta = @.merge(this) + +inject_tracing_map: root.meta.span = this +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `idempotent_write` + +Enable the idempotent write producer option. This requires the `IDEMPOTENT_WRITE` permission on `CLUSTER` and can be disabled if this permission is not available. + + +*Type*: `bool` + +*Default*: `false` + +=== `ack_replicas` + +Ensure that messages have been copied across all replicas before acknowledging receipt. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_msg_bytes` + +The maximum size in bytes of messages sent to the target topic. + + +*Type*: `int` + +*Default*: `1000000` + +=== `timeout` + +The maximum period of time to wait for message sends before abandoning the request and retrying. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `retry_as_batch` + +When enabled forces an entire batch of messages to be retried if any individual message fails on a send, otherwise only the individual messages that failed are retried. Disabling this helps to reduce message duplicates during intermittent errors, but also makes it impossible to guarantee strict ordering of messages. + + +*Type*: `bool` + +*Default*: `false` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `0` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"3s"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"10s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `backoff.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. Setting this value to a zeroed duration (such as `0s`) will result in unbounded retries. + + +*Type*: `string` + +*Default*: `"30s"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + + diff --git a/docs/modules/components/pages/outputs/kafka_franz.adoc b/docs/modules/components/pages/outputs/kafka_franz.adoc new file mode 100644 index 0000000000..76c975d643 --- /dev/null +++ b/docs/modules/components/pages/outputs/kafka_franz.adoc @@ -0,0 +1,756 @@ += kafka_franz +:type: output +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A Kafka output using the https://github.com/twmb/franz-go[Franz Kafka client library]. + +Introduced in version 3.61.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + kafka_franz: + seed_brokers: [] # No default (required) + topic: "" # No default (required) + key: "" # No default (optional) + partition: ${! meta("partition") } # No default (optional) + metadata: + include_prefixes: [] + include_patterns: [] + max_in_flight: 10 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + kafka_franz: + seed_brokers: [] # No default (required) + topic: "" # No default (required) + key: "" # No default (optional) + partitioner: "" # No default (optional) + partition: ${! meta("partition") } # No default (optional) + client_id: benthos + rack_id: "" + idempotent_write: true + metadata: + include_prefixes: [] + include_patterns: [] + max_in_flight: 10 + timeout: 10s + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + max_message_bytes: 1MB + compression: "" # No default (optional) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + sasl: [] # No default (optional) +``` + +-- +====== + +Writes a batch of messages to Kafka brokers and waits for acknowledgement before propagating it back to the input. + +This output often out-performs the traditional `kafka` output as well as providing more useful logs and error messages. + + +== Fields + +=== `seed_brokers` + +A list of broker addresses to connect to in order to establish connections. If an item of the list contains commas it will be expanded into multiple addresses. + + +*Type*: `array` + + +```yml +# Examples + +seed_brokers: + - localhost:9092 + +seed_brokers: + - foo:9092 + - bar:9092 + +seed_brokers: + - foo:9092,bar:9092 +``` + +=== `topic` + +A topic to write messages to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `key` + +An optional key to populate for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `partitioner` + +Override the default murmur2 hashing partitioner. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `least_backup` +| Chooses the least backed up partition (the partition with the fewest amount of buffered records). Partitions are selected per batch. +| `manual` +| Manually select a partition for each message, requires the field `partition` to be specified. +| `murmur2_hash` +| Kafka's default hash algorithm that uses a 32-bit murmur2 hash of the key to compute which partition the record will be on. +| `round_robin` +| Round-robin's messages through all available partitions. This algorithm has lower throughput and causes higher CPU load on brokers, but can be useful if you want to ensure an even distribution of records to partitions. + +|=== + +=== `partition` + +An optional explicit partition to set for each message. This field is only relevant when the `partitioner` is set to `manual`. The provided interpolation string must be a valid integer. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +partition: ${! meta("partition") } +``` + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `"benthos"` + +=== `rack_id` + +A rack identifier for this client. + + +*Type*: `string` + +*Default*: `""` + +=== `idempotent_write` + +Enable the idempotent write producer option. This requires the `IDEMPOTENT_WRITE` permission on `CLUSTER` and can be disabled if this permission is not available. + + +*Type*: `bool` + +*Default*: `true` + +=== `metadata` + +Determine which (if any) metadata values should be added to messages as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `max_in_flight` + +The maximum number of batches to be sending in parallel at any given time. + + +*Type*: `int` + +*Default*: `10` + +=== `timeout` + +The maximum period of time to wait for message sends before abandoning the request and retrying + + +*Type*: `string` + +*Default*: `"10s"` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `max_message_bytes` + +The maximum space in bytes than an individual message may take, messages larger than this value will be rejected. This field corresponds to Kafka's `max.message.bytes`. + + +*Type*: `string` + +*Default*: `"1MB"` + +```yml +# Examples + +max_message_bytes: 100MB + +max_message_bytes: 50mib +``` + +=== `compression` + +Optionally set an explicit compression type. The default preference is to use snappy when the broker supports it, and fall back to none if not. + + +*Type*: `string` + + +Options: +`lz4` +, `snappy` +, `gzip` +, `none` +, `zstd` +. + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `sasl` + +Specify one or more methods of SASL authentication. SASL is tried in order; if the broker supports the first mechanism, all connections will use that mechanism. If the first mechanism fails, the client will pick the first supported mechanism. If the broker does not support any client mechanisms, connections will fail. + + +*Type*: `array` + + +```yml +# Examples + +sasl: + - mechanism: SCRAM-SHA-512 + password: bar + username: foo +``` + +=== `sasl[].mechanism` + +The SASL mechanism to use. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `AWS_MSK_IAM` +| AWS IAM based authentication as specified by the 'aws-msk-iam-auth' java library. +| `OAUTHBEARER` +| OAuth Bearer based authentication. +| `PLAIN` +| Plain text authentication. +| `SCRAM-SHA-256` +| SCRAM based authentication as specified in RFC5802. +| `SCRAM-SHA-512` +| SCRAM based authentication as specified in RFC5802. +| `none` +| Disable sasl authentication + +|=== + +=== `sasl[].username` + +A username to provide for PLAIN or SCRAM-* authentication. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].password` + +A password to provide for PLAIN or SCRAM-* authentication. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].token` + +The token to use for a single session's OAUTHBEARER authentication. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].extensions` + +Key/value pairs to add to OAUTHBEARER authentication requests. + + +*Type*: `object` + + +=== `sasl[].aws` + +Contains AWS specific fields for when the `mechanism` is set to `AWS_MSK_IAM`. + + +*Type*: `object` + + +=== `sasl[].aws.region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `sasl[].aws.credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `sasl[].aws.credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `sasl[].aws.credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/outputs/mongodb.adoc b/docs/modules/components/pages/outputs/mongodb.adoc new file mode 100644 index 0000000000..e1cbbe1192 --- /dev/null +++ b/docs/modules/components/pages/outputs/mongodb.adoc @@ -0,0 +1,379 @@ += mongodb +:type: output +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Inserts items into a MongoDB collection. + +Introduced in version 3.43.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + operation: update-one + write_concern: + w: "" + j: false + w_timeout: "" + document_map: "" + filter_map: "" + hint_map: "" + upsert: false + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + operation: update-one + write_concern: + w: "" + j: false + w_timeout: "" + document_map: "" + filter_map: "" + hint_map: "" + upsert: false + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `url` + +The URL of the target MongoDB server. + + +*Type*: `string` + + +```yml +# Examples + +url: mongodb://localhost:27017 +``` + +=== `database` + +The name of the target MongoDB database. + + +*Type*: `string` + + +=== `username` + +The username to connect to the database. + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +The password to connect to the database. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `collection` + +The name of the target collection. + + +*Type*: `string` + + +=== `operation` + +The mongodb operation to perform. + + +*Type*: `string` + +*Default*: `"update-one"` + +Options: +`insert-one` +, `delete-one` +, `delete-many` +, `replace-one` +, `update-one` +. + +=== `write_concern` + +The write concern settings for the mongo connection. + + +*Type*: `object` + + +=== `write_concern.w` + +W requests acknowledgement that write operations propagate to the specified number of mongodb instances. + + +*Type*: `string` + +*Default*: `""` + +=== `write_concern.j` + +J requests acknowledgement from MongoDB that write operations are written to the journal. + + +*Type*: `bool` + +*Default*: `false` + +=== `write_concern.w_timeout` + +The write concern timeout. + + +*Type*: `string` + +*Default*: `""` + +=== `document_map` + +A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The document map is required for the operations insert-one, replace-one and update-one. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +document_map: |- + root.a = this.foo + root.b = this.bar +``` + +=== `filter_map` + +A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +filter_map: |- + root.a = this.foo + root.b = this.bar +``` + +=== `hint_map` + +A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +hint_map: |- + root.a = this.foo + root.b = this.bar +``` + +=== `upsert` + +The upsert setting is optional and only applies for update-one and replace-one operations. If the filter specified in filter_map matches, the document is updated or replaced accordingly, otherwise it is created. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.60.0 or newer + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/mqtt.adoc b/docs/modules/components/pages/outputs/mqtt.adoc new file mode 100644 index 0000000000..773dde6d36 --- /dev/null +++ b/docs/modules/components/pages/outputs/mqtt.adoc @@ -0,0 +1,460 @@ += mqtt +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Pushes messages to an MQTT broker. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + mqtt: + urls: [] # No default (required) + client_id: "" + connect_timeout: 30s + topic: "" # No default (required) + qos: 1 + write_timeout: 3s + retained: false + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + mqtt: + urls: [] # No default (required) + client_id: "" + dynamic_client_id_suffix: "" # No default (optional) + connect_timeout: 30s + will: + enabled: false + qos: 0 + retained: false + topic: "" + payload: "" + user: "" + password: "" + keepalive: 30 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + topic: "" # No default (required) + qos: 1 + write_timeout: 3s + retained: false + retained_interpolated: "" # No default (optional) + max_in_flight: 64 +``` + +-- +====== + +The `topic` field can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages these interpolations are performed per message part. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - tcp://localhost:1883 +``` + +=== `client_id` + +An identifier for the client connection. + + +*Type*: `string` + +*Default*: `""` + +=== `dynamic_client_id_suffix` + +Append a dynamically generated suffix to the specified `client_id` on each run of the pipeline. This can be useful when clustering Benthos producers. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `nanoid` +| append a nanoid of length 21 characters + +|=== + +=== `connect_timeout` + +The maximum amount of time to wait in order to establish a connection before the attempt is abandoned. + + +*Type*: `string` + +*Default*: `"30s"` +Requires version 3.58.0 or newer + +```yml +# Examples + +connect_timeout: 1s + +connect_timeout: 500ms +``` + +=== `will` + +Set last will message in case of Benthos failure + + +*Type*: `object` + + +=== `will.enabled` + +Whether to enable last will messages. + + +*Type*: `bool` + +*Default*: `false` + +=== `will.qos` + +Set QoS for last will message. Valid values are: 0, 1, 2. + + +*Type*: `int` + +*Default*: `0` + +=== `will.retained` + +Set retained for last will message. + + +*Type*: `bool` + +*Default*: `false` + +=== `will.topic` + +Set topic for last will message. + + +*Type*: `string` + +*Default*: `""` + +=== `will.payload` + +Set payload for last will message. + + +*Type*: `string` + +*Default*: `""` + +=== `user` + +A username to connect with. + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +A password to connect with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `keepalive` + +Max seconds of inactivity before a keepalive message is sent. + + +*Type*: `int` + +*Default*: `30` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `topic` + +The topic to publish messages to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `qos` + +The QoS value to set for each message. Has options 0, 1, 2. + + +*Type*: `int` + +*Default*: `1` + +=== `write_timeout` + +The maximum amount of time to wait to write data before the attempt is abandoned. + + +*Type*: `string` + +*Default*: `"3s"` +Requires version 3.58.0 or newer + +```yml +# Examples + +write_timeout: 1s + +write_timeout: 500ms +``` + +=== `retained` + +Set message as retained on the topic. + + +*Type*: `bool` + +*Default*: `false` + +=== `retained_interpolated` + +Override the value of `retained` with an interpolable value, this allows it to be dynamically set based on message contents. The value must resolve to either `true` or `false`. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 3.59.0 or newer + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/nanomsg.adoc b/docs/modules/components/pages/outputs/nanomsg.adoc new file mode 100644 index 0000000000..77f6c92205 --- /dev/null +++ b/docs/modules/components/pages/outputs/nanomsg.adoc @@ -0,0 +1,89 @@ += nanomsg +:type: output +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Send messages over a Nanomsg socket. + +```yml +# Config fields, showing default values +output: + label: "" + nanomsg: + urls: [] # No default (required) + bind: false + socket_type: PUSH + poll_timeout: 5s + max_in_flight: 64 +``` + +Currently only PUSH and PUB sockets are supported. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +=== `bind` + +Whether the URLs listed should be bind (otherwise they are connected to). + + +*Type*: `bool` + +*Default*: `false` + +=== `socket_type` + +The socket type to send with. + + +*Type*: `string` + +*Default*: `"PUSH"` + +Options: +`PUSH` +, `PUB` +. + +=== `poll_timeout` + +The maximum period of time to wait for a message to send before the request is abandoned and reattempted. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/nats.adoc b/docs/modules/components/pages/outputs/nats.adoc new file mode 100644 index 0000000000..c18e9e9021 --- /dev/null +++ b/docs/modules/components/pages/outputs/nats.adoc @@ -0,0 +1,472 @@ += nats +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Publish to an NATS subject. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + nats: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + nats: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + max_in_flight: 64 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) + inject_tracing_map: meta = @.merge(this) # No default (optional) +``` + +-- +====== + +This output will interpolate functions within the subject field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here]. + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `subject` + +The subject to publish to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +subject: foo.bar.baz +``` + +=== `headers` + +Explicit message headers to add to messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +headers: + Content-Type: application/json + Timestamp: ${!meta("Timestamp")} +``` + +=== `metadata` + +Determine which (if any) metadata values should be added to messages as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `inject_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 4.23.0 or newer + +```yml +# Examples + +inject_tracing_map: meta = @.merge(this) + +inject_tracing_map: root.meta.span = this +``` + + diff --git a/docs/modules/components/pages/outputs/nats_jetstream.adoc b/docs/modules/components/pages/outputs/nats_jetstream.adoc new file mode 100644 index 0000000000..a4dbdf87ee --- /dev/null +++ b/docs/modules/components/pages/outputs/nats_jetstream.adoc @@ -0,0 +1,477 @@ += nats_jetstream +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Write messages to a NATS JetStream subject. + +Introduced in version 3.46.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + nats_jetstream: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + max_in_flight: 1024 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + nats_jetstream: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + max_in_flight: 1024 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) + inject_tracing_map: meta = @.merge(this) # No default (optional) +``` + +-- +====== + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `subject` + +A subject to write to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +subject: foo.bar.baz + +subject: ${! meta("kafka_topic") } + +subject: foo.${! json("meta.type") } +``` + +=== `headers` + +Explicit message headers to add to messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` +Requires version 4.1.0 or newer + +```yml +# Examples + +headers: + Content-Type: application/json + Timestamp: ${!meta("Timestamp")} +``` + +=== `metadata` + +Determine which (if any) metadata values should be added to messages as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `1024` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `inject_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 4.23.0 or newer + +```yml +# Examples + +inject_tracing_map: meta = @.merge(this) + +inject_tracing_map: root.meta.span = this +``` + + diff --git a/docs/modules/components/pages/outputs/nats_kv.adoc b/docs/modules/components/pages/outputs/nats_kv.adoc new file mode 100644 index 0000000000..67715fef2c --- /dev/null +++ b/docs/modules/components/pages/outputs/nats_kv.adoc @@ -0,0 +1,402 @@ += nats_kv +:type: output +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Put messages in a NATS key-value bucket. + +Introduced in version 4.12.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + key: foo # No default (required) + max_in_flight: 1024 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + key: foo # No default (required) + max_in_flight: 1024 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) +``` + +-- +====== + +The field `key` supports +xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing +you to create a unique key for each message. + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `bucket` + +The name of the KV bucket. + + +*Type*: `string` + + +```yml +# Examples + +bucket: my_kv_bucket +``` + +=== `key` + +The key for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +key: foo + +key: foo.bar.baz + +key: foo.${! json("meta.type") } +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `1024` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/outputs/nats_stream.adoc b/docs/modules/components/pages/outputs/nats_stream.adoc new file mode 100644 index 0000000000..ca6435dd88 --- /dev/null +++ b/docs/modules/components/pages/outputs/nats_stream.adoc @@ -0,0 +1,410 @@ += nats_stream +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Publish to a NATS Stream subject. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + nats_stream: + urls: [] # No default (required) + cluster_id: "" # No default (required) + subject: "" # No default (required) + client_id: "" + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + nats_stream: + urls: [] # No default (required) + cluster_id: "" # No default (required) + subject: "" # No default (required) + client_id: "" + max_in_flight: 64 + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) + inject_tracing_map: meta = @.merge(this) # No default (optional) +``` + +-- +====== + +[CAUTION] +.Deprecation notice +==== +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +==== + + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `cluster_id` + +The cluster ID to publish to. + + +*Type*: `string` + + +=== `subject` + +The subject to publish to. + + +*Type*: `string` + + +=== `client_id` + +The client ID to connect with. + + +*Type*: `string` + +*Default*: `""` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `inject_tracing_map` + +EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. + + +*Type*: `string` + +Requires version 4.23.0 or newer + +```yml +# Examples + +inject_tracing_map: meta = @.merge(this) + +inject_tracing_map: root.meta.span = this +``` + + diff --git a/docs/modules/components/pages/outputs/nsq.adoc b/docs/modules/components/pages/outputs/nsq.adoc new file mode 100644 index 0000000000..1f259f81cc --- /dev/null +++ b/docs/modules/components/pages/outputs/nsq.adoc @@ -0,0 +1,267 @@ += nsq +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Publish to an NSQ topic. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + nsq: + nsqd_tcp_address: "" # No default (required) + topic: "" # No default (required) + user_agent: "" # No default (optional) + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + nsq: + nsqd_tcp_address: "" # No default (required) + topic: "" # No default (required) + user_agent: "" # No default (optional) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + max_in_flight: 64 +``` + +-- +====== + +The `topic` field can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages these interpolations are performed per message part. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `nsqd_tcp_address` + +The address of the target NSQD server. + + +*Type*: `string` + + +=== `topic` + +The topic to publish to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `user_agent` + +A user agent to assume when connecting. + + +*Type*: `string` + + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/opensearch.adoc b/docs/modules/components/pages/outputs/opensearch.adoc new file mode 100644 index 0000000000..bf3f5e540b --- /dev/null +++ b/docs/modules/components/pages/outputs/opensearch.adoc @@ -0,0 +1,625 @@ += opensearch +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Publishes messages into an Elasticsearch index. If the index does not exist then it is created with a dynamic mapping. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + opensearch: + urls: [] # No default (required) + index: "" # No default (required) + action: "" # No default (required) + id: ${!counter()}-${!timestamp_unix()} # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + opensearch: + urls: [] # No default (required) + index: "" # No default (required) + action: "" # No default (required) + id: ${!counter()}-${!timestamp_unix()} # No default (required) + pipeline: "" + routing: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + max_in_flight: 64 + basic_auth: + enabled: false + username: "" + password: "" + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + aws: + enabled: false + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +Both the `id` and `index` fields can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages these interpolations are performed per message part. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Examples + +[tabs] +====== +Updating Documents:: ++ +-- + +When https://opensearch.org/docs/latest/api-reference/document-apis/update-document/[updating documents] the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors. + +```yaml +output: + processors: + - mapping: | + meta id = this.id + root.doc = this + opensearch: + urls: [ TODO ] + index: foo + id: ${! @id } + action: update +``` + +-- +====== + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - http://localhost:9200 +``` + +=== `index` + +The index to place messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `action` + +The action to take on the document. This field must resolve to one of the following action types: `index`, `update` or `delete`. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `id` + +The ID for indexed messages. Interpolation should be used in order to create a unique ID for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +id: ${!counter()}-${!timestamp_unix()} +``` + +=== `pipeline` + +An optional pipeline id to preprocess incoming documents. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `routing` + +The routing key to use for the document. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `aws` + +Enables and customises connectivity to Amazon Elastic Service. + + +*Type*: `object` + + +=== `aws.enabled` + +Whether to connect to Amazon Elastic Service. + + +*Type*: `bool` + +*Default*: `false` + +=== `aws.region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `aws.credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `aws.credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `aws.credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/outputs/pulsar.adoc b/docs/modules/components/pages/outputs/pulsar.adoc new file mode 100644 index 0000000000..6d1795072d --- /dev/null +++ b/docs/modules/components/pages/outputs/pulsar.adoc @@ -0,0 +1,233 @@ += pulsar +:type: output +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Write messages to an Apache Pulsar server. + +Introduced in version 3.43.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + pulsar: + url: pulsar://localhost:6650 # No default (required) + topic: "" # No default (required) + tls: + root_cas_file: "" + key: "" + ordering_key: "" + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + pulsar: + url: pulsar://localhost:6650 # No default (required) + topic: "" # No default (required) + tls: + root_cas_file: "" + key: "" + ordering_key: "" + max_in_flight: 64 + auth: + oauth2: + enabled: false + audience: "" + issuer_url: "" + private_key_file: "" + token: + enabled: false + token: "" +``` + +-- +====== + +== Fields + +=== `url` + +A URL to connect to. + + +*Type*: `string` + + +```yml +# Examples + +url: pulsar://localhost:6650 + +url: pulsar://pulsar.us-west.example.com:6650 + +url: pulsar+ssl://pulsar.us-west.example.com:6651 +``` + +=== `topic` + +The topic to publish to. + + +*Type*: `string` + + +=== `tls` + +Specify the path to a custom CA certificate to trust broker TLS service. + + +*Type*: `object` + + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `key` + +The key to publish messages with. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `ordering_key` + +The ordering key to publish messages with. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `auth` + +Optional configuration of Pulsar authentication methods. + + +*Type*: `object` + +Requires version 3.60.0 or newer + +=== `auth.oauth2` + +Parameters for Pulsar OAuth2 authentication. + + +*Type*: `object` + + +=== `auth.oauth2.enabled` + +Whether OAuth2 is enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `auth.oauth2.audience` + +OAuth2 audience. + + +*Type*: `string` + +*Default*: `""` + +=== `auth.oauth2.issuer_url` + +OAuth2 issuer URL. + + +*Type*: `string` + +*Default*: `""` + +=== `auth.oauth2.private_key_file` + +The path to a file containing a private key. + + +*Type*: `string` + +*Default*: `""` + +=== `auth.token` + +Parameters for Pulsar Token authentication. + + +*Type*: `object` + + +=== `auth.token.enabled` + +Whether Token Auth is enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `auth.token.token` + +Actual base64 encoded token. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/outputs/pusher.adoc b/docs/modules/components/pages/outputs/pusher.adoc new file mode 100644 index 0000000000..7280e7992d --- /dev/null +++ b/docs/modules/components/pages/outputs/pusher.adoc @@ -0,0 +1,257 @@ += pusher +:type: output +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Output for publishing messages to Pusher API (https://pusher.com) + +Introduced in version 4.3.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + pusher: + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + channel: my_channel # No default (required) + event: "" # No default (required) + appId: "" # No default (required) + key: "" # No default (required) + secret: "" # No default (required) + cluster: "" # No default (required) + secure: true + max_in_flight: 1 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + pusher: + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + channel: my_channel # No default (required) + event: "" # No default (required) + appId: "" # No default (required) + key: "" # No default (required) + secret: "" # No default (required) + cluster: "" # No default (required) + secure: true + max_in_flight: 1 +``` + +-- +====== + +== Fields + +=== `batching` + +maximum batch size is 10 (limit of the pusher library) + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `channel` + +Pusher channel to publish to. Interpolation functions can also be used +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +channel: my_channel + +channel: ${!json("id")} +``` + +=== `event` + +Event to publish to + + +*Type*: `string` + + +=== `appId` + +Pusher app id + + +*Type*: `string` + + +=== `key` + +Pusher key + + +*Type*: `string` + + +=== `secret` + +Pusher secret + + +*Type*: `string` + + +=== `cluster` + +Pusher cluster + + +*Type*: `string` + + +=== `secure` + +Enable SSL encryption + + +*Type*: `bool` + +*Default*: `true` + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `1` + + diff --git a/docs/modules/components/pages/outputs/redis_hash.adoc b/docs/modules/components/pages/outputs/redis_hash.adoc new file mode 100644 index 0000000000..0b17908873 --- /dev/null +++ b/docs/modules/components/pages/outputs/redis_hash.adoc @@ -0,0 +1,378 @@ += redis_hash +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sets Redis hash objects using the HMSET command. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + redis_hash: + url: redis://:6397 # No default (required) + key: ${! @.kafka_key )} # No default (required) + walk_metadata: false + walk_json_object: false + fields: {} + max_in_flight: 64 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + redis_hash: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + key: ${! @.kafka_key )} # No default (required) + walk_metadata: false + walk_json_object: false + fields: {} + max_in_flight: 64 +``` + +-- +====== + +The field `key` supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing you to create a unique key for each message. + +The field `fields` allows you to specify an explicit map of field names to interpolated values, also evaluated per message of a batch: + +```yaml +output: + redis_hash: + url: tcp://localhost:6379 + key: ${!json("id")} + fields: + topic: ${!meta("kafka_topic")} + partition: ${!meta("kafka_partition")} + content: ${!json("document.text")} +``` + +If the field `walk_metadata` is set to `true` then Benthos will walk all metadata fields of messages and add them to the list of hash fields to set. + +If the field `walk_json_object` is set to `true` then Benthos will walk each message as a JSON object, extracting keys and the string representation of their value and adds them to the list of hash fields to set. + +The order of hash field extraction is as follows: + +1. Metadata (if enabled) +2. JSON object (if enabled) +3. Explicit fields + +Where latter stages will overwrite matching field names of a former stage. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `key` + +The key for each message, function interpolations should be used to create a unique key per message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +key: ${! @.kafka_key )} + +key: ${! this.doc.id } + +key: ${! count("msgs") } +``` + +=== `walk_metadata` + +Whether all metadata fields of messages should be walked and added to the list of hash fields to set. + + +*Type*: `bool` + +*Default*: `false` + +=== `walk_json_object` + +Whether to walk each message as a JSON object and add each key/value pair to the list of hash fields to set. + + +*Type*: `bool` + +*Default*: `false` + +=== `fields` + +A map of key/value pairs to set as hash fields. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/redis_list.adoc b/docs/modules/components/pages/outputs/redis_list.adoc new file mode 100644 index 0000000000..3cf474597a --- /dev/null +++ b/docs/modules/components/pages/outputs/redis_list.adoc @@ -0,0 +1,452 @@ += redis_list +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Pushes messages onto the end of a Redis list (which is created if it doesn't already exist) using the RPUSH command. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + redis_list: + url: redis://:6397 # No default (required) + key: some_list # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + redis_list: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + key: some_list # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + command: rpush +``` + +-- +====== + +The field `key` supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing you to create a unique key for each message. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `key` + +The key for each message, function interpolations can be optionally used to create a unique key per message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +key: some_list + +key: ${! @.kafka_key )} + +key: ${! this.doc.id } + +key: ${! count("msgs") } +``` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `command` + +The command used to push elements to the Redis list + + +*Type*: `string` + +*Default*: `"rpush"` +Requires version 4.22.0 or newer + +Options: +`rpush` +, `lpush` +. + + diff --git a/docs/modules/components/pages/outputs/redis_pubsub.adoc b/docs/modules/components/pages/outputs/redis_pubsub.adoc new file mode 100644 index 0000000000..7ef89ae265 --- /dev/null +++ b/docs/modules/components/pages/outputs/redis_pubsub.adoc @@ -0,0 +1,424 @@ += redis_pubsub +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Publishes messages through the Redis PubSub model. It is not possible to guarantee that messages have been received. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + redis_pubsub: + url: redis://:6397 # No default (required) + channel: "" # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + redis_pubsub: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + channel: "" # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +This output will interpolate functions within the channel field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `channel` + +The channel to publish messages to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/redis_streams.adoc b/docs/modules/components/pages/outputs/redis_streams.adoc new file mode 100644 index 0000000000..46d5470258 --- /dev/null +++ b/docs/modules/components/pages/outputs/redis_streams.adoc @@ -0,0 +1,469 @@ += redis_streams +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Pushes messages to a Redis (v5.0+) Stream (which is created if it doesn't already exist) using the XADD command. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + redis_streams: + url: redis://:6397 # No default (required) + stream: "" # No default (required) + body_key: body + max_length: 0 + max_in_flight: 64 + metadata: + exclude_prefixes: [] + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + redis_streams: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + stream: "" # No default (required) + body_key: body + max_length: 0 + max_in_flight: 64 + metadata: + exclude_prefixes: [] + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +It's possible to specify a maximum length of the target stream by setting it to a value greater than 0, in which case this cap is applied only when Redis is able to remove a whole macro node, for efficiency. + +Redis stream entries are key/value pairs, as such it is necessary to specify the key to be set to the body of the message. All metadata fields of the message will also be set as key/value pairs, if there is a key collision between a metadata item and the body then the body takes precedence. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `stream` + +The stream to add messages to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `body_key` + +A key to set the raw body of the message to. + + +*Type*: `string` + +*Default*: `"body"` + +=== `max_length` + +When greater than zero enforces a rough cap on the length of the target stream. + + +*Type*: `int` + +*Default*: `0` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + +=== `metadata` + +Specify criteria for which metadata values are included in the message body. + + +*Type*: `object` + + +=== `metadata.exclude_prefixes` + +Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. + + +*Type*: `array` + +*Default*: `[]` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/reject.adoc b/docs/modules/components/pages/outputs/reject.adoc new file mode 100644 index 0000000000..ce33b020cb --- /dev/null +++ b/docs/modules/components/pages/outputs/reject.adoc @@ -0,0 +1,60 @@ += reject +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Rejects all messages, treating them as though the output destination failed to publish them. + +```yml +# Config fields, showing default values +output: + label: "" + reject: "" +``` + +The routing of messages after this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch. + +To learn when this output could be useful, see [the <>. + +== Examples + +[tabs] +====== +Rejecting Failed Messages:: ++ +-- + + +This input is particularly useful for routing messages that have failed during processing, where instead of routing them to some sort of dead letter queue we wish to push the error upstream. We can do this with a switch broker: + +```yaml +output: + switch: + retry_until_success: false + cases: + - check: '!errored()' + output: + amqp_1: + urls: [ amqps://guest:guest@localhost:5672/ ] + target_address: queue:/the_foos + + - output: + reject: "processing failed due to: ${! error() }" +``` + +-- +====== + + diff --git a/docs/modules/components/pages/outputs/reject_errored.adoc b/docs/modules/components/pages/outputs/reject_errored.adoc new file mode 100644 index 0000000000..50f854e773 --- /dev/null +++ b/docs/modules/components/pages/outputs/reject_errored.adoc @@ -0,0 +1,86 @@ += reject_errored +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Rejects messages that have failed their processing steps, resulting in nack behavior at the input level, otherwise sends them to a child output. + +```yml +# Config fields, showing default values +output: + label: "" + reject_errored: null # No default (required) +``` + +The routing of messages rejected by this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch. + +== Examples + +[tabs] +====== +Rejecting Failed Messages:: ++ +-- + + +The most straight forward use case for this output type is to nack messages that have failed their processing steps. In this example our mapping might fail, in which case the messages that failed are rejected and will be nacked by our input: + +```yaml +input: + nats_jetstream: + urls: [ nats://127.0.0.1:4222 ] + subject: foos.pending + +pipeline: + processors: + - mutation: 'root.age = this.fuzzy.age.int64()' + +output: + reject_errored: + nats_jetstream: + urls: [ nats://127.0.0.1:4222 ] + subject: foos.processed +``` + +-- +DLQing Failed Messages:: ++ +-- + + +Another use case for this output is to send failed messages straight into a dead-letter queue. You use it within a xref:components:outputs/fallback.adoc[fallback output] that allows you to specify where these failed messages should go to next. + +```yaml +pipeline: + processors: + - mutation: 'root.age = this.fuzzy.age.int64()' + +output: + fallback: + - reject_errored: + http_client: + url: http://foo:4195/post/might/become/unreachable + retries: 3 + retry_period: 1s + - http_client: + url: http://bar:4196/somewhere/else + retries: 3 + retry_period: 1s +``` + +-- +====== + + diff --git a/docs/modules/components/pages/outputs/resource.adoc b/docs/modules/components/pages/outputs/resource.adoc new file mode 100644 index 0000000000..e0cf9d8506 --- /dev/null +++ b/docs/modules/components/pages/outputs/resource.adoc @@ -0,0 +1,65 @@ += resource +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Resource is an output type that channels messages to a resource output, identified by its name. + +```yml +# Config fields, showing default values +output: + resource: "" +``` + +Resources allow you to tidy up deeply nested configs. For example, the config: + +```yaml +output: + broker: + pattern: fan_out + outputs: + - kafka: + addresses: [ TODO ] + topic: foo + - gcp_pubsub: + project: bar + topic: baz +``` + +Could also be expressed as: + +```yaml +output: + broker: + pattern: fan_out + outputs: + - resource: foo + - resource: bar + +output_resources: + - label: foo + kafka: + addresses: [ TODO ] + topic: foo + + - label: bar + gcp_pubsub: + project: bar + topic: baz +``` + +You can find out more about resources in xref:configuration:resources.adoc[] + + diff --git a/docs/modules/components/pages/outputs/retry.adoc b/docs/modules/components/pages/outputs/retry.adoc new file mode 100644 index 0000000000..ecd23fc5be --- /dev/null +++ b/docs/modules/components/pages/outputs/retry.adoc @@ -0,0 +1,116 @@ += retry +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Attempts to write messages to a child output and if the write fails for any reason the message is retried either until success or, if the retries or max elapsed time fields are non-zero, either is reached. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + retry: + output: null # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + retry: + max_retries: 0 + backoff: + initial_interval: 500ms + max_interval: 3s + max_elapsed_time: 0s + output: null # No default (required) +``` + +-- +====== + +All messages in Benthos are always retried on an output error, but this would usually involve propagating the error back to the source of the message, whereby it would be reprocessed before reaching the output layer once again. + +This output type is useful whenever we wish to avoid reprocessing a message on the event of a failed send. We might, for example, have a deduplication processor that we want to avoid reapplying to the same message more than once in the pipeline. + +Rather than retrying the same output you may wish to retry the send using a different output target (a dead letter queue). In which case you should instead use the xref:components:outputs/fallback.adoc[`fallback`] output type. + +== Fields + +=== `max_retries` + +The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. + + +*Type*: `int` + +*Default*: `0` + +=== `backoff` + +Control time intervals between retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"500ms"` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"3s"` + +=== `backoff.max_elapsed_time` + +The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. + + +*Type*: `string` + +*Default*: `"0s"` + +=== `output` + +A child output. + + +*Type*: `output` + + + diff --git a/docs/modules/components/pages/outputs/sftp.adoc b/docs/modules/components/pages/outputs/sftp.adoc new file mode 100644 index 0000000000..d2166dba98 --- /dev/null +++ b/docs/modules/components/pages/outputs/sftp.adoc @@ -0,0 +1,161 @@ += sftp +:type: output +:status: beta +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Writes files to an SFTP server. + +Introduced in version 3.39.0. + +```yml +# Config fields, showing default values +output: + label: "" + sftp: + address: "" # No default (required) + path: "" # No default (required) + codec: all-bytes + credentials: + username: "" + password: "" + private_key_file: "" + private_key_pass: "" + max_in_flight: 64 +``` + +In order to have a different path for each object you should use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +== Fields + +=== `address` + +The address of the server to connect to. + + +*Type*: `string` + + +=== `path` + +The file to save the messages to on the server. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `codec` + +The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. + + +*Type*: `string` + +*Default*: `"all-bytes"` + +|=== +| Option | Summary + +| `all-bytes` +| Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. +| `append` +| Append each message to the output stream without any delimiter or special encoding. +| `lines` +| Append each message to the output stream followed by a line break. +| `delim:x` +| Append each message to the output stream followed by a custom delimiter. + +|=== + +```yml +# Examples + +codec: lines + +codec: "delim:\t" + +codec: delim:foobar +``` + +=== `credentials` + +The credentials to use to log into the target server. + + +*Type*: `object` + + +=== `credentials.username` + +The username to connect to the SFTP server. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.password` + +The password for the username to connect to the SFTP server. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.private_key_file` + +The private key for the username to connect to the SFTP server. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.private_key_pass` + +Optional passphrase for private key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `max_in_flight` + +The maximum number of messages to have in flight at a given time. Increase this to improve throughput. + + +*Type*: `int` + +*Default*: `64` + + diff --git a/docs/modules/components/pages/outputs/snowflake_put.adoc b/docs/modules/components/pages/outputs/snowflake_put.adoc new file mode 100644 index 0000000000..7c41e8dfff --- /dev/null +++ b/docs/modules/components/pages/outputs/snowflake_put.adoc @@ -0,0 +1,791 @@ += snowflake_put +:type: output +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to Snowflake stages and, optionally, calls Snowpipe to load this data into one or more tables. + +Introduced in version 4.0.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + snowflake_put: + account: "" # No default (required) + region: us-west-2 # No default (optional) + cloud: aws # No default (optional) + user: "" # No default (required) + password: "" # No default (optional) + private_key_file: "" # No default (optional) + private_key_pass: "" # No default (optional) + role: "" # No default (required) + database: "" # No default (required) + warehouse: "" # No default (required) + schema: "" # No default (required) + stage: "" # No default (required) + path: "" + file_name: "" + file_extension: "" + compression: AUTO + request_id: "" + snowpipe: "" # No default (optional) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + max_in_flight: 1 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + snowflake_put: + account: "" # No default (required) + region: us-west-2 # No default (optional) + cloud: aws # No default (optional) + user: "" # No default (required) + password: "" # No default (optional) + private_key_file: "" # No default (optional) + private_key_pass: "" # No default (optional) + role: "" # No default (required) + database: "" # No default (required) + warehouse: "" # No default (required) + schema: "" # No default (required) + stage: "" # No default (required) + path: "" + file_name: "" + file_extension: "" + upload_parallel_threads: 4 + compression: AUTO + request_id: "" + snowpipe: "" # No default (optional) + client_session_keep_alive: false + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) + max_in_flight: 1 +``` + +-- +====== + +In order to use a different stage and / or Snowpipe for each message, you can use function interpolations as described +xref:configuration:interpolation.adoc#bloblang-queries[here]. When using batching, messages are grouped by the calculated +stage and Snowpipe and are streamed to individual files in their corresponding stage and, optionally, a Snowpipe +`insertFiles` REST API call will be made for each individual file. + +== Credentials + +Two authentication mechanisms are supported: +- User/password +- Key Pair Authentication + +=== User/password + +This is a basic authentication mechanism which allows you to PUT data into a stage. However, it is not compatible with +Snowpipe. + +=== Key pair authentication + +This authentication mechanism allows Snowpipe functionality, but it does require configuring an SSH Private Key +beforehand. Please consult the https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[documentation] +for details on how to set it up and assign the Public Key to your user. + +Note that the Snowflake documentation https://twitter.com/felipehoffa/status/1560811785606684672[used to suggest] +using this command: + +```bash +openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 +``` + +to generate an encrypted SSH private key. However, in this case, it uses an encryption algorithm called +`pbeWithMD5AndDES-CBC`, which is part of the PKCS#5 v1.5 and is considered insecure. Due to this, Benthos does not +support it and, if you wish to use password-protected keys directly, you must use PKCS#5 v2.0 to encrypt them by using +the following command (as the current Snowflake docs suggest): + +```bash +openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 des3 -inform PEM -out rsa_key.p8 +``` + +If you have an existing key encrypted with PKCS#5 v1.5, you can re-encrypt it with PKCS#5 v2.0 using this command: + +```bash +openssl pkcs8 -in rsa_key_original.p8 -topk8 -v2 des3 -out rsa_key.p8 +``` + +Please consult the https://linux.die.net/man/1/pkcs8[pkcs8 command documentation] for details on PKCS#5 algorithms. + +== Batching + +It's common to want to upload messages to Snowflake as batched archives. The easiest way to do this is to batch your +messages at the output level and join the batch of messages with an +xref:components:processors/archive.adoc[`archive`] and/or xref:components:processors/compress.adoc[`compress`] +processor. + +For the optimal batch size, please consult the Snowflake https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html[documentation]. + +== Snowpipe + +Given a table called `BENTHOS_TBL` with one column of type `variant`: + +```sql +CREATE OR REPLACE TABLE BENTHOS_DB.PUBLIC.BENTHOS_TBL(RECORD variant) +``` + +and the following `BENTHOS_PIPE` Snowpipe: + +```sql +CREATE OR REPLACE PIPE BENTHOS_DB.PUBLIC.BENTHOS_PIPE AUTO_INGEST = FALSE AS COPY INTO BENTHOS_DB.PUBLIC.BENTHOS_TBL FROM (SELECT * FROM @%BENTHOS_TBL) FILE_FORMAT = (TYPE = JSON COMPRESSION = AUTO) +``` + +you can configure Benthos to use the implicit table stage `@%BENTHOS_TBL` as the `stage` and +`BENTHOS_PIPE` as the `snowpipe`. In this case, you must set `compression` to `AUTO` and, if +using message batching, you'll need to configure an xref:components:processors/archive.adoc[`archive`] processor +with the `concatenate` format. Since the `compression` is set to `AUTO`, the +https://github.com/snowflakedb/gosnowflake[gosnowflake] client library will compress the messages automatically so you +don't need to add a xref:components:processors/compress.adoc[`compress`] processor for message batches. + +If you add `STRIP_OUTER_ARRAY = TRUE` in your Snowpipe `FILE_FORMAT` +definition, then you must use `json_array` instead of `concatenate` as the archive processor format. + +NOTE: Only Snowpipes with `FILE_FORMAT` `TYPE` `JSON` are currently supported. + +== Snowpipe troubleshooting + +Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html[provides] the `insertReport` +and `loadHistoryScan` REST API endpoints which can be used to get information about recent Snowpipe calls. In +order to query them, you'll first need to generate a valid JWT token for your Snowflake account. There are two methods +for doing so: +- Using the `snowsql` https://docs.snowflake.com/en/user-guide/snowsql.html[utility]: + +```bash +snowsql --private-key-path rsa_key.p8 --generate-jwt -a -u +``` + +- Using the Python `sql-api-generate-jwt` https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python[utility]: + +```bash +python3 sql-api-generate-jwt.py --private_key_file_path=rsa_key.p8 --account= --user= +``` + +Once you successfully generate a JWT token and store it into the `JWT_TOKEN` environment variable, then you can, +for example, query the `insertReport` endpoint using `curl`: + +```bash +curl -H "Authorization: Bearer ${JWT_TOKEN}" "https://.snowflakecomputing.com/v1/data/pipes/../insertReport" +``` + +If you need to pass in a valid `requestId` to any of these Snowpipe REST API endpoints, you can set a +xref:guides:bloblang/functions.adoc#uuid_v4[uuid_v4()] string in a metadata field called +`request_id`, log it via the xref:components:processors/log.adoc[`log`] processor and +then configure `request_id: ${ @request_id }` ). Alternatively, you can xref:components:logger/about.adoc[enable debug logging] + and Benthos will print the Request IDs that it sends to Snowpipe. + +== General troubleshooting + +The underlying https://github.com/snowflakedb/gosnowflake[`gosnowflake` driver] requires write access to +the default directory to use for temporary files. Please consult the https://pkg.go.dev/os#TempDir[`os.TempDir`] +docs for details on how to change this directory via environment variables. + +A silent failure can occur due to https://github.com/snowflakedb/gosnowflake/issues/701[this issue], where the +underlying https://github.com/snowflakedb/gosnowflake[`gosnowflake` driver] doesn't return an error and doesn't +log a failure if it can't figure out the current username. One way to trigger this behavior is by running Benthos in a +Docker container with a non-existent user ID (such as `--user 1000:1000`). + + +== Performance + +This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. + +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. + +== Examples + +[tabs] +====== +Kafka / realtime brokers:: ++ +-- + +Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table[Kafka Connector scheme] and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition. + +```yaml +input: + kafka: + addresses: + - localhost:9092 + topics: + - foo + consumer_group: benthos + batching: + count: 10 + period: 3s + processors: + - mapping: | + meta kafka_start_offset = meta("kafka_offset").from(0) + meta kafka_end_offset = meta("kafka_offset").from(-1) + meta batch_timestamp = if batch_index() == 0 { now() } + - mapping: | + meta batch_timestamp = if batch_index() != 0 { meta("batch_timestamp").from(0) } + +output: + snowflake_put: + account: benthos + user: test@benthos.dev + private_key_file: path_to_ssh_key.pem + role: ACCOUNTADMIN + database: BENTHOS_DB + warehouse: COMPUTE_WH + schema: PUBLIC + stage: "@%BENTHOS_TBL" + path: benthos/BENTHOS_TBL/${! @kafka_partition } + file_name: ${! @kafka_start_offset }_${! @kafka_end_offset }_${! meta("batch_timestamp") } + upload_parallel_threads: 4 + compression: NONE + snowpipe: BENTHOS_PIPE +``` + +-- +No compression:: ++ +-- + +Upload concatenated messages into a `.json` file to a table stage without calling Snowpipe. + +```yaml +output: + snowflake_put: + account: benthos + user: test@benthos.dev + private_key_file: path_to_ssh_key.pem + role: ACCOUNTADMIN + database: BENTHOS_DB + warehouse: COMPUTE_WH + schema: PUBLIC + stage: "@%BENTHOS_TBL" + path: benthos + upload_parallel_threads: 4 + compression: NONE + batching: + count: 10 + period: 3s + processors: + - archive: + format: concatenate +``` + +-- +Parquet format with snappy compression:: ++ +-- + +Upload concatenated messages into a `.parquet` file to a table stage without calling Snowpipe. + +```yaml +output: + snowflake_put: + account: benthos + user: test@benthos.dev + private_key_file: path_to_ssh_key.pem + role: ACCOUNTADMIN + database: BENTHOS_DB + warehouse: COMPUTE_WH + schema: PUBLIC + stage: "@%BENTHOS_TBL" + path: benthos + file_extension: parquet + upload_parallel_threads: 4 + compression: NONE + batching: + count: 10 + period: 3s + processors: + - parquet_encode: + schema: + - name: ID + type: INT64 + - name: CONTENT + type: BYTE_ARRAY + default_compression: snappy +``` + +-- +Automatic compression:: ++ +-- + +Upload concatenated messages compressed automatically into a `.gz` archive file to a table stage without calling Snowpipe. + +```yaml +output: + snowflake_put: + account: benthos + user: test@benthos.dev + private_key_file: path_to_ssh_key.pem + role: ACCOUNTADMIN + database: BENTHOS_DB + warehouse: COMPUTE_WH + schema: PUBLIC + stage: "@%BENTHOS_TBL" + path: benthos + upload_parallel_threads: 4 + compression: AUTO + batching: + count: 10 + period: 3s + processors: + - archive: + format: concatenate +``` + +-- +DEFLATE compression:: ++ +-- + +Upload concatenated messages compressed into a `.deflate` archive file to a table stage and call Snowpipe to load them into a table. + +```yaml +output: + snowflake_put: + account: benthos + user: test@benthos.dev + private_key_file: path_to_ssh_key.pem + role: ACCOUNTADMIN + database: BENTHOS_DB + warehouse: COMPUTE_WH + schema: PUBLIC + stage: "@%BENTHOS_TBL" + path: benthos + upload_parallel_threads: 4 + compression: DEFLATE + snowpipe: BENTHOS_PIPE + batching: + count: 10 + period: 3s + processors: + - archive: + format: concatenate + - mapping: | + root = content().compress("zlib") +``` + +-- +RAW_DEFLATE compression:: ++ +-- + +Upload concatenated messages compressed into a `.raw_deflate` archive file to a table stage and call Snowpipe to load them into a table. + +```yaml +output: + snowflake_put: + account: benthos + user: test@benthos.dev + private_key_file: path_to_ssh_key.pem + role: ACCOUNTADMIN + database: BENTHOS_DB + warehouse: COMPUTE_WH + schema: PUBLIC + stage: "@%BENTHOS_TBL" + path: benthos + upload_parallel_threads: 4 + compression: RAW_DEFLATE + snowpipe: BENTHOS_PIPE + batching: + count: 10 + period: 3s + processors: + - archive: + format: concatenate + - mapping: | + root = content().compress("flate") +``` + +-- +====== + +== Fields + +=== `account` + +Account name, which is the same as the https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used[Account Identifier]. +However, when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator], +the Account Identifier is formatted as `..` and this field needs to be +populated using the `` part. + + +*Type*: `string` + + +=== `region` + +Optional region field which needs to be populated when using +an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] +and it must be set to the `` part of the Account Identifier +(`..`). + + +*Type*: `string` + + +```yml +# Examples + +region: us-west-2 +``` + +=== `cloud` + +Optional cloud platform field which needs to be populated +when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] +and it must be set to the `` part of the Account Identifier +(`..`). + + +*Type*: `string` + + +```yml +# Examples + +cloud: aws + +cloud: gcp + +cloud: azure +``` + +=== `user` + +Username. + + +*Type*: `string` + + +=== `password` + +An optional password. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `private_key_file` + +The path to a file containing the private SSH key. + + +*Type*: `string` + + +=== `private_key_pass` + +An optional private SSH key passphrase. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `role` + +Role. + + +*Type*: `string` + + +=== `database` + +Database. + + +*Type*: `string` + + +=== `warehouse` + +Warehouse. + + +*Type*: `string` + + +=== `schema` + +Schema. + + +*Type*: `string` + + +=== `stage` + +Stage name. Use either one of the + https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html[supported] stage types. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `path` + +Stage path. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +=== `file_name` + +Stage file name. Will be equal to the Request ID if not set or empty. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` +Requires version v4.12.0 or newer + +=== `file_extension` + +Stage file extension. Will be derived from the configured `compression` if not set or empty. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` +Requires version v4.12.0 or newer + +```yml +# Examples + +file_extension: csv + +file_extension: parquet +``` + +=== `upload_parallel_threads` + +Specifies the number of threads to use for uploading files. + + +*Type*: `int` + +*Default*: `4` + +=== `compression` + +Compression type. + + +*Type*: `string` + +*Default*: `"AUTO"` + +|=== +| Option | Summary + +| `AUTO` +| Compression (gzip) is applied automatically by the output and messages must contain plain-text JSON. Default `file_extension`: `gz`. +| `DEFLATE` +| Messages must be pre-compressed using the zlib algorithm (with zlib header, RFC1950). Default `file_extension`: `deflate`. +| `GZIP` +| Messages must be pre-compressed using the gzip algorithm. Default `file_extension`: `gz`. +| `NONE` +| No compression is applied and messages must contain plain-text JSON. Default `file_extension`: `json`. +| `RAW_DEFLATE` +| Messages must be pre-compressed using the flate algorithm (without header, RFC1951). Default `file_extension`: `raw_deflate`. +| `ZSTD` +| Messages must be pre-compressed using the Zstandard algorithm. Default `file_extension`: `zst`. + +|=== + +=== `request_id` + +Request ID. Will be assigned a random UUID (v4) string if not set or empty. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` +Requires version v4.12.0 or newer + +=== `snowpipe` + +An optional Snowpipe name. Use the `` part from `..`. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `client_session_keep_alive` + +Enable Snowflake keepalive mechanism to prevent the client session from expiring after 4 hours (error 390114). + + +*Type*: `bool` + +*Default*: `false` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `1` + + diff --git a/docs/modules/components/pages/outputs/socket.adoc b/docs/modules/components/pages/outputs/socket.adoc new file mode 100644 index 0000000000..ca7244b609 --- /dev/null +++ b/docs/modules/components/pages/outputs/socket.adoc @@ -0,0 +1,95 @@ += socket +:type: output +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Connects to a (tcp/udp/unix) server and sends a continuous stream of data, dividing messages according to the specified codec. + +```yml +# Config fields, showing default values +output: + label: "" + socket: + network: "" # No default (required) + address: /tmp/benthos.sock # No default (required) + codec: lines +``` + +== Fields + +=== `network` + +A network type to connect as. + + +*Type*: `string` + + +Options: +`unix` +, `tcp` +, `udp` +. + +=== `address` + +The address to connect to. + + +*Type*: `string` + + +```yml +# Examples + +address: /tmp/benthos.sock + +address: 127.0.0.1:6000 +``` + +=== `codec` + +The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. + + +*Type*: `string` + +*Default*: `"lines"` + +|=== +| Option | Summary + +| `all-bytes` +| Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. +| `append` +| Append each message to the output stream without any delimiter or special encoding. +| `lines` +| Append each message to the output stream followed by a line break. +| `delim:x` +| Append each message to the output stream followed by a custom delimiter. + +|=== + +```yml +# Examples + +codec: lines + +codec: "delim:\t" + +codec: delim:foobar +``` + + diff --git a/docs/modules/components/pages/outputs/splunk_hec.adoc b/docs/modules/components/pages/outputs/splunk_hec.adoc new file mode 100644 index 0000000000..548b67dabe --- /dev/null +++ b/docs/modules/components/pages/outputs/splunk_hec.adoc @@ -0,0 +1,192 @@ += splunk_hec +:type: output +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Writes messages to a Splunk HTTP Endpoint Collector. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + splunk_hec: + url: "" # No default (required) + token: "" # No default (required) + gzip: false + event_host: "" + event_source: "" + event_sourcetype: "" + event_index: "" + batching_count: 100 + batching_period: 30s + batching_byte_size: 1000000 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + splunk_hec: + url: "" # No default (required) + token: "" # No default (required) + gzip: false + event_host: "" + event_source: "" + event_sourcetype: "" + event_index: "" + batching_count: 100 + batching_period: 30s + batching_byte_size: 1000000 + rate_limit: "" + max_in_flight: 64 + skip_cert_verify: false +``` + +-- +====== + +This output POSTs messages to a Splunk HTTP Endpoint Collector (HEC) using token based authentication. The format of the message must be a [valid event JSON](https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/FormateventsforHTTPEventCollector). Raw is not supported. + + +== Fields + +=== `url` + +Full HTTP Endpoint Collector (HEC) URL, ie. https://foobar.splunkcloud.com/services/collector/event + + +*Type*: `string` + + +=== `token` + +A bot token used for authentication. + + +*Type*: `string` + + +=== `gzip` + +Enable gzip compression + + +*Type*: `bool` + +*Default*: `false` + +=== `event_host` + +Set the host value to assign to the event data. Overrides existing host field if present. + + +*Type*: `string` + +*Default*: `""` + +=== `event_source` + +Set the source value to assign to the event data. Overrides existing source field if present. + + +*Type*: `string` + +*Default*: `""` + +=== `event_sourcetype` + +Set the sourcetype value to assign to the event data. Overrides existing sourcetype field if present. + + +*Type*: `string` + +*Default*: `""` + +=== `event_index` + +Set the index value to assign to the event data. Overrides existing index field if present. + + +*Type*: `string` + +*Default*: `""` + +=== `batching_count` + +A number of messages at which the batch should be flushed. If 0 disables count based batching. + + +*Type*: `int` + +*Default*: `100` + +=== `batching_period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `"30s"` + +=== `batching_byte_size` + +An amount of bytes at which the batch should be flushed. If 0 disables size based batching. Splunk Cloud recommends limiting content length of HEC payload to 1 MB. + + +*Type*: `int` + +*Default*: `1000000` + +=== `rate_limit` + +An optional rate limit resource to restrict API requests with. + + +*Type*: `string` + +*Default*: `""` + +=== `max_in_flight` + +The maximum number of parallel message batches to have in flight at any given time. + + +*Type*: `int` + +*Default*: `64` + +=== `skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/outputs/sql.adoc b/docs/modules/components/pages/outputs/sql.adoc new file mode 100644 index 0000000000..c7e4fbabb5 --- /dev/null +++ b/docs/modules/components/pages/outputs/sql.adoc @@ -0,0 +1,264 @@ += sql +:type: output +:status: deprecated +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +[WARNING] +.Deprecated +==== +This component is deprecated and will be removed in the next major version release. Please consider moving onto <>. +==== +Executes an arbitrary SQL query for each message. + +Introduced in version 3.65.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + sql: + driver: "" # No default (required) + data_source_name: "" # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + sql: + driver: "" # No default (required) + data_source_name: "" # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +== Alternatives + +For basic inserts use the xref:components:outputs/sql.adoc[`sql_insert`] output. For more complex queries use the xref:components:outputs/sql_raw.adoc[`sql_raw`] output. + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `data_source_name` + +Data source name. + + +*Type*: `string` + + +=== `query` + +The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: + +| Driver | Placeholder Style | +|---|---| +| `clickhouse` | Dollar sign | +| `mysql` | Question mark | +| `postgres` | Dollar sign | +| `mssql` | Question mark | +| `sqlite` | Question mark | +| `oracle` | Colon | +| `snowflake` | Question mark | +| `trino` | Question mark | +| `gocosmos` | Colon | + + +*Type*: `string` + + +```yml +# Examples + +query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); +``` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `max_in_flight` + +The maximum number of inserts to run in parallel. + + +*Type*: `int` + +*Default*: `64` + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/sql_insert.adoc b/docs/modules/components/pages/outputs/sql_insert.adoc new file mode 100644 index 0000000000..c6e14103aa --- /dev/null +++ b/docs/modules/components/pages/outputs/sql_insert.adoc @@ -0,0 +1,462 @@ += sql_insert +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Inserts a row into an SQL database for each message. + +Introduced in version 3.59.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + sql_insert: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + sql_insert: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) + prefix: "" # No default (optional) + suffix: ON CONFLICT (name) DO NOTHING # No default (optional) + max_in_flight: 64 + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +== Examples + +[tabs] +====== +Table Insert (MySQL):: ++ +-- + + +Here we insert rows into a database by populating the columns id, name and topic with values extracted from messages and metadata: + +```yaml +output: + sql_insert: + driver: mysql + dsn: foouser:foopassword@tcp(localhost:3306)/foodb + table: footable + columns: [ id, name, topic ] + args_mapping: | + root = [ + this.user.id, + this.user.name, + meta("kafka_topic"), + ] +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `table` + +The table to insert to. + + +*Type*: `string` + + +```yml +# Examples + +table: foo +``` + +=== `columns` + +A list of columns to insert. + + +*Type*: `array` + + +```yml +# Examples + +columns: + - foo + - bar + - baz +``` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of columns specified. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `prefix` + +An optional prefix to prepend to the insert query (before INSERT). + + +*Type*: `string` + + +=== `suffix` + +An optional suffix to append to the insert query. + + +*Type*: `string` + + +```yml +# Examples + +suffix: ON CONFLICT (name) DO NOTHING +``` + +=== `max_in_flight` + +The maximum number of inserts to run in parallel. + + +*Type*: `int` + +*Default*: `64` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/sql_raw.adoc b/docs/modules/components/pages/outputs/sql_raw.adoc new file mode 100644 index 0000000000..2fb6a0ef0b --- /dev/null +++ b/docs/modules/components/pages/outputs/sql_raw.adoc @@ -0,0 +1,440 @@ += sql_raw +:type: output +:status: stable +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes an arbitrary SQL query for each message. + +Introduced in version 3.65.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + sql_raw: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + max_in_flight: 64 + batching: + count: 0 + byte_size: 0 + period: "" + check: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + sql_raw: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + unsafe_dynamic_query: false + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + max_in_flight: 64 + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) + batching: + count: 0 + byte_size: 0 + period: "" + check: "" + processors: [] # No default (optional) +``` + +-- +====== + +== Examples + +[tabs] +====== +Table Insert (MySQL):: ++ +-- + + +Here we insert rows into a database by populating the columns id, name and topic with values extracted from messages and metadata: + +```yaml +output: + sql_raw: + driver: mysql + dsn: foouser:foopassword@tcp(localhost:3306)/foodb + query: "INSERT INTO footable (id, name, topic) VALUES (?, ?, ?);" + args_mapping: | + root = [ + this.user.id, + this.user.name, + meta("kafka_topic"), + ] +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `query` + +The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: + +| Driver | Placeholder Style | +|---|---| +| `clickhouse` | Dollar sign | +| `mysql` | Question mark | +| `postgres` | Dollar sign | +| `mssql` | Question mark | +| `sqlite` | Question mark | +| `oracle` | Colon | +| `snowflake` | Question mark | +| `trino` | Question mark | +| `gocosmos` | Colon | + + +*Type*: `string` + + +```yml +# Examples + +query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); +``` + +=== `unsafe_dynamic_query` + +Whether to enable xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the query. Great care should be made to ensure your queries are defended against injection attacks. + + +*Type*: `bool` + +*Default*: `false` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `max_in_flight` + +The maximum number of inserts to run in parallel. + + +*Type*: `int` + +*Default*: `64` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + +=== `batching` + +Allows you to configure a xref:configuration:batching.adoc[batching policy]. + + +*Type*: `object` + + +```yml +# Examples + +batching: + byte_size: 5000 + count: 0 + period: 1s + +batching: + count: 10 + period: 1s + +batching: + check: this.contains("END BATCH") + count: 0 + period: 1m +``` + +=== `batching.count` + +A number of messages at which the batch should be flushed. If `0` disables count based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.byte_size` + +An amount of bytes at which the batch should be flushed. If `0` disables size based batching. + + +*Type*: `int` + +*Default*: `0` + +=== `batching.period` + +A period in which an incomplete batch should be flushed regardless of its size. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +period: 1s + +period: 1m + +period: 500ms +``` + +=== `batching.check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "end_of_transaction" +``` + +=== `batching.processors` + +A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. + + +*Type*: `array` + + +```yml +# Examples + +processors: + - archive: + format: concatenate + +processors: + - archive: + format: lines + +processors: + - archive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/outputs/stdout.adoc b/docs/modules/components/pages/outputs/stdout.adoc new file mode 100644 index 0000000000..543bf9fbd8 --- /dev/null +++ b/docs/modules/components/pages/outputs/stdout.adoc @@ -0,0 +1,64 @@ += stdout +:type: output +:status: stable +:categories: ["Local"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Prints messages to stdout as a continuous stream of data. + +```yml +# Config fields, showing default values +output: + label: "" + stdout: + codec: lines +``` + +== Fields + +=== `codec` + +The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. + + +*Type*: `string` + +*Default*: `"lines"` +Requires version 3.46.0 or newer + +|=== +| Option | Summary + +| `all-bytes` +| Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. +| `append` +| Append each message to the output stream without any delimiter or special encoding. +| `lines` +| Append each message to the output stream followed by a line break. +| `delim:x` +| Append each message to the output stream followed by a custom delimiter. + +|=== + +```yml +# Examples + +codec: lines + +codec: "delim:\t" + +codec: delim:foobar +``` + + diff --git a/docs/modules/components/pages/outputs/subprocess.adoc b/docs/modules/components/pages/outputs/subprocess.adoc new file mode 100644 index 0000000000..caae2baaec --- /dev/null +++ b/docs/modules/components/pages/outputs/subprocess.adoc @@ -0,0 +1,68 @@ += subprocess +:type: output +:status: beta +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a command, runs it as a subprocess, and writes messages to it over stdin. + +```yml +# Config fields, showing default values +output: + label: "" + subprocess: + name: "" # No default (required) + args: [] + codec: lines +``` + +Messages are written according to a specified codec. The process is expected to terminate gracefully when stdin is closed. + +If the subprocess exits unexpectedly then Benthos will log anything printed to stderr and will log the exit code, and will attempt to execute the command again until success. + +The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. + +== Fields + +=== `name` + +The command to execute as a subprocess. + + +*Type*: `string` + + +=== `args` + +A list of arguments to provide the command. + + +*Type*: `array` + +*Default*: `[]` + +=== `codec` + +The way in which messages should be written to the subprocess. + + +*Type*: `string` + +*Default*: `"lines"` + +Options: +`lines` +. + + diff --git a/docs/modules/components/pages/outputs/switch.adoc b/docs/modules/components/pages/outputs/switch.adoc new file mode 100644 index 0000000000..cebfc0a1b0 --- /dev/null +++ b/docs/modules/components/pages/outputs/switch.adoc @@ -0,0 +1,206 @@ += switch +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +The switch output type allows you to route messages to different outputs based on their contents. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + switch: + retry_until_success: false + cases: [] # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + switch: + retry_until_success: false + strict_mode: false + cases: [] # No default (required) +``` + +-- +====== + +Messages that do not pass the check of a single output case are effectively dropped. In order to prevent this outcome set the field <> to `true`, in which case messages that do not pass at least one case are considered failed and will be nacked and/or reprocessed depending on your input. + +== Examples + +[tabs] +====== +Basic Multiplexing:: ++ +-- + + +The most common use for a switch output is to multiplex messages across a range of output destinations. The following config checks the contents of the field `type` of messages and sends `foo` type messages to an `amqp_1` output, `bar` type messages to a `gcp_pubsub` output, and everything else to a `redis_streams` output. + +Outputs can have their own processors associated with them, and in this example the `redis_streams` output has a processor that enforces the presence of a type field before sending it. + +```yaml +output: + switch: + cases: + - check: this.type == "foo" + output: + amqp_1: + urls: [ amqps://guest:guest@localhost:5672/ ] + target_address: queue:/the_foos + + - check: this.type == "bar" + output: + gcp_pubsub: + project: dealing_with_mike + topic: mikes_bars + + - output: + redis_streams: + url: tcp://localhost:6379 + stream: everything_else + processors: + - mapping: | + root = this + root.type = this.type | "unknown" +``` + +-- +Control Flow:: ++ +-- + + +The `continue` field allows messages that have passed a case to be tested against the next one also. This can be useful when combining non-mutually-exclusive case checks. + +In the following example a message that passes both the check of the first case as well as the second will be routed to both. + +```yaml +output: + switch: + cases: + - check: 'this.user.interests.contains("walks").catch(false)' + output: + amqp_1: + urls: [ amqps://guest:guest@localhost:5672/ ] + target_address: queue:/people_what_think_good + continue: true + + - check: 'this.user.dislikes.contains("videogames").catch(false)' + output: + gcp_pubsub: + project: people + topic: that_i_dont_want_to_hang_with +``` + +-- +====== + +== Fields + +=== `retry_until_success` + +If a selected output fails to send a message this field determines whether it is reattempted indefinitely. If set to false the error is instead propagated back to the input level. + +If a message can be routed to >1 outputs it is usually best to set this to true in order to avoid duplicate messages being routed to an output. + + +*Type*: `bool` + +*Default*: `false` + +=== `strict_mode` + +This field determines whether an error should be reported if no condition is met. If set to true, an error is propagated back to the input level. The default behavior is false, which will drop the message. + + +*Type*: `bool` + +*Default*: `false` + +=== `cases` + +A list of switch cases, outlining outputs that can be routed to. + + +*Type*: `array` + + +```yml +# Examples + +cases: + - check: this.urls.contains("http://benthos.dev") + continue: true + output: + cache: + key: ${!json("id")} + target: foo + - output: + s3: + bucket: bar + path: ${!json("id")} +``` + +=== `cases[].check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should be routed to the case output. If left empty the case always passes. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "foo" + +check: this.contents.urls.contains("https://benthos.dev/") +``` + +=== `cases[].output` + +An xref:components:outputs/about.adoc[output] for messages that pass the check to be routed to. + + +*Type*: `output` + + +=== `cases[].continue` + +Indicates whether, if this case passes for a message, the next case should also be tested. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/outputs/sync_response.adoc b/docs/modules/components/pages/outputs/sync_response.adoc new file mode 100644 index 0000000000..babadda200 --- /dev/null +++ b/docs/modules/components/pages/outputs/sync_response.adoc @@ -0,0 +1,51 @@ += sync_response +:type: output +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Returns the final message payload back to the input origin of the message, where it is dealt with according to that specific input type. + +```yml +# Config fields, showing default values +output: + label: "" + sync_response: {} +``` + +For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this output even when combining input types that might not have support for sync responses. An example of an input able to utilize this is the `http_server`. + +It is safe to combine this output with others using broker types. For example, with the `http_server` input we could send the payload to a Kafka topic and also send a modified payload back with: + +```yaml +input: + http_server: + path: /post +output: + broker: + pattern: fan_out + outputs: + - kafka: + addresses: [ TODO:9092 ] + topic: foo_topic + - sync_response: {} + processors: + - mapping: 'root = content().uppercase()' +``` + +Using the above example and posting the message 'hello world' to the endpoint `/post` Benthos would send it unchanged to the topic `foo_topic` and also respond with 'HELLO WORLD'. + +For more information please read xref:guides:sync_responses.adoc[synchronous responses]. + + diff --git a/docs/modules/components/pages/outputs/websocket.adoc b/docs/modules/components/pages/outputs/websocket.adoc new file mode 100644 index 0000000000..6da8e243a2 --- /dev/null +++ b/docs/modules/components/pages/outputs/websocket.adoc @@ -0,0 +1,404 @@ += websocket +:type: output +:status: stable +:categories: ["Network"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends messages to an HTTP server via a websocket connection. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +output: + label: "" + websocket: + url: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +output: + label: "" + websocket: + url: "" # No default (required) + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} +``` + +-- +====== + +== Fields + +=== `url` + +The URL to connect to. + + +*Type*: `string` + + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + + diff --git a/docs/modules/components/pages/outputs/zmq4.adoc b/docs/modules/components/pages/outputs/zmq4.adoc new file mode 100644 index 0000000000..87ffe9263b --- /dev/null +++ b/docs/modules/components/pages/outputs/zmq4.adoc @@ -0,0 +1,105 @@ +//// +THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + lib/output/zmq4.go +//// += zmq4 +:categories: ["Network"] +:status: stable +:type: output + +Writes messages to a ZeroMQ socket. + +[tabs] +===== +.common:: ++ +-- +[,yml] +---- +# Common config fields, showing default values +output: + label: "" + zmq4: + urls: [] + bind: true + socket_type: "" +---- + +-- +.advanced:: ++ +-- +[,yml] +---- +# All config fields, showing default values +output: + label: "" + zmq4: + urls: [] + bind: true + socket_type: "" + high_water_mark: 0 + poll_timeout: 5s +---- + +-- +===== + +By default {page-component-title} does not build with components that require linking to external libraries. If you wish to build {page-component-title} locally with this component then set the build tag `x_benthos_extra`: + +[,shell] +---- +# With go +go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest + +# Using make +make TAGS=x_benthos_extra +---- + +There is a specific docker tag postfix `-cgo` for C builds containing this component. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + +Type: `array` + +[,yml] +---- +# Examples + +urls: + - tcp://localhost:5556 +---- + +=== `bind` + +Whether to bind to the specified URLs (otherwise they are connected to). + +Type: `bool` + +Default: `true` + +=== `socket_type` + +The socket type to connect as. + +Type: `string` + +Options: `PUSH`, `PUB`. + +=== `high_water_mark` + +The message high water mark to use. + +Type: `int` + +Default: `0` + +=== `poll_timeout` + +The poll timeout to use. + +Type: `string` + +Default: `"5s"` diff --git a/docs/modules/components/pages/processors/archive.adoc b/docs/modules/components/pages/processors/archive.adoc new file mode 100644 index 0000000000..3d99683ab4 --- /dev/null +++ b/docs/modules/components/pages/processors/archive.adoc @@ -0,0 +1,109 @@ += archive +:type: processor +:status: stable +:categories: ["Parsing","Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Archives all the messages of a batch into a single message according to the selected archive format. + +```yml +# Config fields, showing default values +label: "" +archive: + format: "" # No default (required) + path: "" +``` + +Some archive formats (such as tar, zip) treat each archive item (message part) as a file with a path. Since message parts only contain raw data a unique path must be generated for each part. This can be done by using function interpolations on the 'path' field as described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. For types that aren't file based (such as binary) the file field is ignored. + +The resulting archived message adopts the metadata of the _first_ message part of the batch. + +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `format` + +The archiving format to apply. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `binary` +| Archive messages to a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format]. +| `concatenate` +| Join the raw contents of each message into a single binary message. +| `json_array` +| Attempt to parse each message as a JSON document and append the result to an array, which becomes the contents of the resulting message. +| `lines` +| Join the raw contents of each message and insert a line break between each one. +| `tar` +| Archive messages to a unix standard tape archive. +| `zip` +| Archive messages to a zip file. + +|=== + +=== `path` + +The path to set for each message in the archive (when applicable). +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +path: ${!count("files")}-${!timestamp_unix_nano()}.txt + +path: ${!meta("kafka_key")}-${!json("id")}.json +``` + +== Examples + +[tabs] +====== +Tar Archive:: ++ +-- + + +If we had JSON messages in a batch each of the form: + +```json +{"doc":{"id":"foo","body":"hello world 1"}} +``` + +And we wished to tar archive them, setting their filenames to their respective unique IDs (with the extension `.json`), our config might look like +this: + +```yaml +pipeline: + processors: + - archive: + format: tar + path: ${!json("doc.id")}.json +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/avro.adoc b/docs/modules/components/pages/processors/avro.adoc new file mode 100644 index 0000000000..4f5a513c2a --- /dev/null +++ b/docs/modules/components/pages/processors/avro.adoc @@ -0,0 +1,101 @@ += avro +:type: processor +:status: beta +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs Avro based operations on messages based on a schema. + +```yml +# Config fields, showing default values +label: "" +avro: + operator: "" # No default (required) + encoding: textual + schema: "" + schema_path: "" +``` + +WARNING: If you are consuming or generating messages using a schema registry service then it is likely this processor will fail as those services require messages to be prefixed with the identifier of the schema version being used. Instead, try the xref:components:processors/schema_registry_encode.adoc[`schema_registry_encode`] and xref:components:processors/schema_registry_decode.adoc[`schema_registry_decode`] processors. + +== Operators + +=== `to_json` + +Converts Avro documents into a JSON structure. This makes it easier to +manipulate the contents of the document within Benthos. The encoding field +specifies how the source documents are encoded. + +=== `from_json` + +Attempts to convert JSON documents into Avro documents according to the +specified encoding. + +== Fields + +=== `operator` + +The <> to execute + + +*Type*: `string` + + +Options: +`to_json` +, `from_json` +. + +=== `encoding` + +An Avro encoding format to use for conversions to and from a schema. + + +*Type*: `string` + +*Default*: `"textual"` + +Options: +`textual` +, `binary` +, `single` +. + +=== `schema` + +A full Avro schema to use. + + +*Type*: `string` + +*Default*: `""` + +=== `schema_path` + +The path of a schema document to apply. Use either this or the `schema` field. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +schema_path: file://path/to/spec.avsc + +schema_path: http://localhost:8081/path/to/spec/versions/1 +``` + + diff --git a/docs/modules/components/pages/processors/awk.adoc b/docs/modules/components/pages/processors/awk.adoc new file mode 100644 index 0000000000..a9b6f3b129 --- /dev/null +++ b/docs/modules/components/pages/processors/awk.adoc @@ -0,0 +1,407 @@ += awk +:type: processor +:status: stable +:categories: ["Mapping"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes an AWK program on messages. This processor is very powerful as it offers a range of <> for querying and mutating message contents and metadata. + +```yml +# Config fields, showing default values +label: "" +awk: + codec: "" # No default (required) + program: "" # No default (required) +``` + +Works by feeding message contents as the program input based on a chosen <> and replaces the contents of each message with the result. If the result is empty (nothing is printed by the program) then the original message contents remain unchanged. + +Comes with a wide range of <> for accessing message metadata, json fields, printing logs, etc. These functions can be overridden by functions within the program. + +Check out the <> in order to see how this processor can be used. + +This processor uses https://github.com/benhoyt/goawk[GoAWK], in order to understand the differences in how the program works you can read more about it in https://github.com/benhoyt/goawk#differences-from-awk[goawk.differences]. + +== Fields + +=== `codec` + +A <> defines how messages should be inserted into the AWK program as variables. The codec does not change which <> are available. The `text` codec is the closest to a typical AWK use case. + + +*Type*: `string` + + +Options: +`none` +, `text` +, `json` +. + +=== `program` + +An AWK program to execute + + +*Type*: `string` + + +== Examples + +[tabs] +====== +JSON Mapping and Arithmetic:: ++ +-- + + +Because AWK is a full programming language it's much easier to map documents and perform arithmetic with it than with other Benthos processors. For example, if we were expecting documents of the form: + +```json +{"doc":{"val1":5,"val2":10},"id":"1","type":"add"} +{"doc":{"val1":5,"val2":10},"id":"2","type":"multiply"} +``` + +And we wished to perform the arithmetic specified in the `type` field, +on the values `val1` and `val2` and, finally, map the result into the +document, giving us the following resulting documents: + +```json +{"doc":{"result":15,"val1":5,"val2":10},"id":"1","type":"add"} +{"doc":{"result":50,"val1":5,"val2":10},"id":"2","type":"multiply"} +``` + +We can do that with the following: + +```yaml +pipeline: + processors: + - awk: + codec: none + program: | + function map_add_vals() { + json_set_int("doc.result", json_get("doc.val1") + json_get("doc.val2")); + } + function map_multiply_vals() { + json_set_int("doc.result", json_get("doc.val1") * json_get("doc.val2")); + } + function map_unknown(type) { + json_set("error","unknown document type"); + print_log("Document type not recognised: " type, "ERROR"); + } + { + type = json_get("type"); + if (type == "add") + map_add_vals(); + else if (type == "multiply") + map_multiply_vals(); + else + map_unknown(type); + } +``` + +-- +Stuff With Arrays:: ++ +-- + + +It's possible to iterate JSON arrays by appending an index value to the path, this can be used to do things like removing duplicates from arrays. For example, given the following input document: + +```json +{"path":{"to":{"foos":["one","two","three","two","four"]}}} +``` + +We could create a new array `foos_unique` from `foos` giving us the result: + +```json +{"path":{"to":{"foos":["one","two","three","two","four"],"foos_unique":["one","two","three","four"]}}} +``` + +With the following config: + +```yaml +pipeline: + processors: + - awk: + codec: none + program: | + { + array_path = "path.to.foos" + array_len = json_length(array_path) + + for (i = 0; i < array_len; i++) { + ele = json_get(array_path "." i) + if ( ! ( ele in seen ) ) { + json_append(array_path "_unique", ele) + seen[ele] = 1 + } + } + } +``` + +-- +====== + +== Codecs + +The chosen codec determines how the contents of the message are fed into the +program. Codecs only impact the input string and variables initialized for your +program, they do not change the range of custom functions available. + +=== `none` + +An empty string is fed into the program. Functions can still be used in order to +extract and mutate metadata and message contents. + +This is useful for when your program only uses functions and doesn't need the +full text of the message to be parsed by the program, as it is significantly +faster. + +=== `text` + +The full contents of the message are fed into the program as a string, allowing +you to reference tokenized segments of the message with variables ($0, $1, etc). +Custom functions can still be used with this codec. + +This is the default codec as it behaves most similar to typical usage of the awk +command line tool. + +=== `json` + +An empty string is fed into the program, and variables are automatically +initialized before execution of your program by walking the flattened JSON +structure. Each value is converted into a variable by taking its full path, +e.g. the object: + +```json +{ + "foo": { + "bar": { + "value": 10 + }, + "created_at": "2018-12-18T11:57:32" + } +} +``` + +Would result in the following variable declarations: + +``` +foo_bar_value = 10 +foo_created_at = "2018-12-18T11:57:32" +``` + +Custom functions can also still be used with this codec. + +== AWK functions + +=== `json_get` + +Signature: `json_get(path)` + +Attempts to find a JSON value in the input message payload by a +xref:configuration:field_paths.adoc[dot separated path] and returns it as a string. + +=== `json_set` + +Signature: `json_set(path, value)` + +Attempts to set a JSON value in the input message payload identified by a +xref:configuration:field_paths.adoc[dot separated path], the value argument will be interpreted +as a string. + +In order to set non-string values use one of the following typed varieties: + +- `json_set_int(path, value)` +- `json_set_float(path, value)` +- `json_set_bool(path, value)` + +=== `json_append` + +Signature: `json_append(path, value)` + +Attempts to append a value to an array identified by a +xref:configuration:field_paths.adoc[dot separated path]. If the target does not +exist it will be created. If the target exists but is not already an array then +it will be converted into one, with its original contents set to the first +element of the array. + +The value argument will be interpreted as a string. In order to append +non-string values use one of the following typed varieties: + +- `json_append_int(path, value)` +- `json_append_float(path, value)` +- `json_append_bool(path, value)` + +=== `json_delete` + +Signature: `json_delete(path)` + +Attempts to delete a JSON field from the input message payload identified by a +xref:configuration:field_paths.adoc[dot separated path]. + +=== `json_length` + +Signature: `json_length(path)` + +Returns the size of the string or array value of JSON field from the input +message payload identified by a xref:configuration:field_paths.adoc[dot separated path]. + +If the target field does not exist, or is not a string or array type, then zero +is returned. In order to explicitly check the type of a field use `json_type`. + +=== `json_type` + +Signature: `json_type(path)` + +Returns the type of a JSON field from the input message payload identified by a +xref:configuration:field_paths.adoc[dot separated path]. + +Possible values are: "string", "int", "float", "bool", "undefined", "null", +"array", "object". + +=== `create_json_object` + +Signature: `create_json_object(key1, val1, key2, val2, ...)` + +Generates a valid JSON object of key value pair arguments. The arguments are +variadic, meaning any number of pairs can be listed. The value will always +resolve to a string regardless of the value type. E.g. the following call: + +`create_json_object("a", "1", "b", 2, "c", "3")` + +Would result in this string: + +`\{"a":"1","b":"2","c":"3"}` + +=== `create_json_array` + +Signature: `create_json_array(val1, val2, ...)` + +Generates a valid JSON array of value arguments. The arguments are variadic, +meaning any number of values can be listed. The value will always resolve to a +string regardless of the value type. E.g. the following call: + +`create_json_array("1", 2, "3")` + +Would result in this string: + +`["1","2","3"]` + +=== `metadata_set` + +Signature: `metadata_set(key, value)` + +Set a metadata key for the message to a value. The value will always resolve to +a string regardless of the value type. + +=== `metadata_get` + +Signature: `metadata_get(key) string` + +Get the value of a metadata key from the message. + +=== `timestamp_unix` + +Signature: `timestamp_unix() int` + +Returns the current unix timestamp (the number of seconds since 01-01-1970). + +=== `timestamp_unix` + +Signature: `timestamp_unix(date) int` + +Attempts to parse a date string by detecting its format and returns the +equivalent unix timestamp (the number of seconds since 01-01-1970). + +=== `timestamp_unix` + +Signature: `timestamp_unix(date, format) int` + +Attempts to parse a date string according to a format and returns the equivalent +unix timestamp (the number of seconds since 01-01-1970). + +The format is defined by showing how the reference time, defined to be +`Mon Jan 2 15:04:05 -0700 MST 2006` would be displayed if it were the value. + +=== `timestamp_unix_nano` + +Signature: `timestamp_unix_nano() int` + +Returns the current unix timestamp in nanoseconds (the number of nanoseconds +since 01-01-1970). + +=== `timestamp_unix_nano` + +Signature: `timestamp_unix_nano(date) int` + +Attempts to parse a date string by detecting its format and returns the +equivalent unix timestamp in nanoseconds (the number of nanoseconds since +01-01-1970). + +=== `timestamp_unix_nano` + +Signature: `timestamp_unix_nano(date, format) int` + +Attempts to parse a date string according to a format and returns the equivalent +unix timestamp in nanoseconds (the number of nanoseconds since 01-01-1970). + +The format is defined by showing how the reference time, defined to be +`Mon Jan 2 15:04:05 -0700 MST 2006` would be displayed if it were the value. + +=== `timestamp_format` + +Signature: `timestamp_format(unix, format) string` + +Formats a unix timestamp. The format is defined by showing how the reference +time, defined to be `Mon Jan 2 15:04:05 -0700 MST 2006` would be displayed if it +were the value. + +The format is optional, and if omitted RFC3339 (`2006-01-02T15:04:05Z07:00`) +will be used. + +=== `timestamp_format_nano` + +Signature: `timestamp_format_nano(unixNano, format) string` + +Formats a unix timestamp in nanoseconds. The format is defined by showing how +the reference time, defined to be `Mon Jan 2 15:04:05 -0700 MST 2006` would be +displayed if it were the value. + +The format is optional, and if omitted RFC3339 (`2006-01-02T15:04:05Z07:00`) +will be used. + +=== `print_log` + +Signature: `print_log(message, level)` + +Prints a Benthos log message at a particular log level. The log level is +optional, and if omitted the level `INFO` will be used. + +=== `base64_encode` + +Signature: `base64_encode(data)` + +Encodes the input data to a base64 string. + +=== `base64_decode` + +Signature: `base64_decode(data)` + +Attempts to base64-decode the input data and returns the decoded string if +successful. It will emit an error otherwise. + + + diff --git a/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc b/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc new file mode 100644 index 0000000000..ef9dccec62 --- /dev/null +++ b/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc @@ -0,0 +1,216 @@ += aws_dynamodb_partiql +:type: processor +:status: experimental +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a PartiQL expression against a DynamoDB table for each message. + +Introduced in version 3.48.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +aws_dynamodb_partiql: + query: "" # No default (required) + args_mapping: "" +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +aws_dynamodb_partiql: + query: "" # No default (required) + unsafe_dynamic_query: false + args_mapping: "" + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" +``` + +-- +====== + +Both writes or reads are supported, when the query is a read the contents of the message will be replaced with the result. This processor is more efficient when messages are pre-batched as the whole batch will be executed in a single call. + +== Examples + +[tabs] +====== +Insert:: ++ +-- + +The following example inserts rows into the table footable with the columns foo, bar and baz populated with values extracted from messages: + +```yaml +pipeline: + processors: + - aws_dynamodb_partiql: + query: "INSERT INTO footable VALUE {'foo':'?','bar':'?','baz':'?'}" + args_mapping: | + root = [ + { "S": this.foo }, + { "S": meta("kafka_topic") }, + { "S": this.document.content }, + ] +``` + +-- +====== + +== Fields + +=== `query` + +A PartiQL query to execute for each message. + + +*Type*: `string` + + +=== `unsafe_dynamic_query` + +Whether to enable dynamic queries that support interpolation functions. + + +*Type*: `bool` + +*Default*: `false` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that, for each message, creates a list of arguments to use with the query. + + +*Type*: `string` + +*Default*: `""` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/processors/aws_lambda.adoc b/docs/modules/components/pages/processors/aws_lambda.adoc new file mode 100644 index 0000000000..3f4c330b83 --- /dev/null +++ b/docs/modules/components/pages/processors/aws_lambda.adoc @@ -0,0 +1,270 @@ += aws_lambda +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Invokes an AWS lambda for each message. The contents of the message is the payload of the request, and the result of the invocation will become the new contents of the message. + +Introduced in version 3.36.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +aws_lambda: + parallel: false + function: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +aws_lambda: + parallel: false + function: "" # No default (required) + rate_limit: "" + region: "" + endpoint: "" + credentials: + profile: "" + id: "" + secret: "" + token: "" + from_ec2_role: false + role: "" + role_external_id: "" + timeout: 5s + retries: 3 +``` + +-- +====== + +The `rate_limit` field can be used to specify a rate limit xref:components:rate_limits/about.adoc[resource] to cap the rate of requests across parallel components service wide. + +In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the xref:components:processors/branch.adoc[`branch` processor]. + +== Error handling + +When Benthos is unable to connect to the AWS endpoint or is otherwise unable to invoke the target lambda function it will retry the request according to the configured number of retries. Once these attempts have been exhausted the failed message will continue through the pipeline with it's contents unchanged, but flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. + +However, if the invocation of the function is successful but the function itself throws an error, then the message will have it's contents updated with a JSON payload describing the reason for the failure, and a metadata field `lambda_function_error` will be added to the message allowing you to detect and handle function errors with a xref:components:processors/branch.adoc[`branch`]: + +```yaml +pipeline: + processors: + - branch: + processors: + - aws_lambda: + function: foo + result_map: | + root = if meta().exists("lambda_function_error") { + throw("Invocation failed due to %v: %v".format(this.errorType, this.errorMessage)) + } else { + this + } +output: + switch: + retry_until_success: false + cases: + - check: errored() + output: + reject: ${! error() } + - output: + resource: somewhere_else +``` + +== Credentials + +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. + +== Examples + +[tabs] +====== +Branched Invoke:: ++ +-- + + +This example uses a xref:components:processors/branch.adoc[`branch` processor] to map a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged. + +```yaml +pipeline: + processors: + - branch: + request_map: '{"id":this.doc.id,"username":this.user.name}' + processors: + - aws_lambda: + function: trigger_user_update +``` + +-- +====== + +== Fields + +=== `parallel` + +Whether messages of a batch should be dispatched in parallel. + + +*Type*: `bool` + +*Default*: `false` + +=== `function` + +The function to invoke. + + +*Type*: `string` + + +=== `rate_limit` + +An optional xref:components:rate_limits/about.adoc[`rate_limit`] to throttle invocations by. + + +*Type*: `string` + +*Default*: `""` + +=== `region` + +The AWS region to target. + + +*Type*: `string` + +*Default*: `""` + +=== `endpoint` + +Allows you to specify a custom endpoint for the AWS API. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials` + +Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]. + + +*Type*: `object` + + +=== `credentials.profile` + +A profile from `~/.aws/credentials` to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.id` + +The ID of credentials to use. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.secret` + +The secret for the credentials being used. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.token` + +The token for the credentials being used, required when using short term credentials. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.from_ec2_role` + +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. + + +*Type*: `bool` + +*Default*: `false` +Requires version 4.2.0 or newer + +=== `credentials.role` + +A role ARN to assume. + + +*Type*: `string` + +*Default*: `""` + +=== `credentials.role_external_id` + +An external ID to provide when assuming a role. + + +*Type*: `string` + +*Default*: `""` + +=== `timeout` + +The maximum period of time to wait before abandoning an invocation. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `retries` + +The maximum number of retry attempts for each message. + + +*Type*: `int` + +*Default*: `3` + + diff --git a/docs/modules/components/pages/processors/azure_cosmosdb.adoc b/docs/modules/components/pages/processors/azure_cosmosdb.adoc new file mode 100644 index 0000000000..d5b6cb5899 --- /dev/null +++ b/docs/modules/components/pages/processors/azure_cosmosdb.adoc @@ -0,0 +1,420 @@ += azure_cosmosdb +:type: processor +:status: experimental +:categories: ["Azure"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]. + +Introduced in version v4.25.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +azure_cosmosdb: + endpoint: https://localhost:8081 # No default (optional) + account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) + connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) + database: testdb # No default (required) + container: testcontainer # No default (required) + partition_keys_map: root = "blobfish" # No default (required) + operation: Create + item_id: ${! json("id") } # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +azure_cosmosdb: + endpoint: https://localhost:8081 # No default (optional) + account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) + connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) + database: testdb # No default (required) + container: testcontainer # No default (required) + partition_keys_map: root = "blobfish" # No default (required) + operation: Create + patch_operations: [] # No default (optional) + patch_condition: from c where not is_defined(c.blobfish) # No default (optional) + auto_id: true + item_id: ${! json("id") } # No default (optional) + enable_content_response_on_write: true +``` + +-- +====== + +When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. + +The `partition_keys` field must resolve to the same value(s) across the entire message batch. + + +== Credentials + +You can use one of the following authentication mechanisms: + +- Set the `endpoint` field and the `account_key` field +- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set the `connection_string` field + + +== Metadata + +This component adds the following metadata fields to each message: +``` +- activity_id +- request_charge +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + + +== Batching + +CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here]). + + +== Examples + +[tabs] +====== +Patch documents:: ++ +-- + +Query documents from a container and patch them. + +```yaml +input: + azure_cosmosdb: + endpoint: http://localhost:8080 + account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + database: blobbase + container: blobfish + partition_keys_map: root = "AbyssalPlain" + query: SELECT * FROM blobfish + + processors: + - mapping: | + root = "" + meta habitat = json("habitat") + meta id = this.id + - azure_cosmosdb: + endpoint: http://localhost:8080 + account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + database: testdb + container: blobfish + partition_keys_map: root = json("habitat") + item_id: ${! meta("id") } + operation: Patch + patch_operations: + # Add a new /diet field + - operation: Add + path: /diet + value_map: root = json("diet") + # Remove the first location from the /locations array field + - operation: Remove + path: /locations/0 + # Add new location at the end of the /locations array field + - operation: Add + path: /locations/- + value_map: root = "Challenger Deep" + # Return the updated document + enable_content_response_on_write: true +``` + +-- +====== + +== Fields + +=== `endpoint` + +CosmosDB endpoint. + + +*Type*: `string` + + +```yml +# Examples + +endpoint: https://localhost:8081 +``` + +=== `account_key` + +Account key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +```yml +# Examples + +account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== +``` + +=== `connection_string` + +Connection string. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +```yml +# Examples + +connection_string: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; +``` + +=== `database` + +Database. + + +*Type*: `string` + + +```yml +# Examples + +database: testdb +``` + +=== `container` + +Container. + + +*Type*: `string` + + +```yml +# Examples + +container: testcontainer +``` + +=== `partition_keys_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided. + + +*Type*: `string` + + +```yml +# Examples + +partition_keys_map: root = "blobfish" + +partition_keys_map: root = 41 + +partition_keys_map: root = true + +partition_keys_map: root = null + +partition_keys_map: root = json("blobfish").depth +``` + +=== `operation` + +Operation. + + +*Type*: `string` + +*Default*: `"Create"` + +|=== +| Option | Summary + +| `Create` +| Create operation. +| `Delete` +| Delete operation. +| `Patch` +| Patch operation. +| `Read` +| Read operation. +| `Replace` +| Replace operation. +| `Upsert` +| Upsert operation. + +|=== + +=== `patch_operations` + +Patch operations to be performed when `operation: Patch` . + + +*Type*: `array` + + +=== `patch_operations[].operation` + +Operation. + + +*Type*: `string` + +*Default*: `"Add"` + +|=== +| Option | Summary + +| `Add` +| Add patch operation. +| `Increment` +| Increment patch operation. +| `Remove` +| Remove patch operation. +| `Replace` +| Replace patch operation. +| `Set` +| Set patch operation. + +|=== + +=== `patch_operations[].path` + +Path. + + +*Type*: `string` + + +```yml +# Examples + +path: /foo/bar/baz +``` + +=== `patch_operations[].value_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a value of any type that is supported by CosmosDB. + + +*Type*: `string` + + +```yml +# Examples + +value_map: root = "blobfish" + +value_map: root = 41 + +value_map: root = true + +value_map: root = json("blobfish").depth + +value_map: root = [1, 2, 3] +``` + +=== `patch_condition` + +Patch operation condition. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +patch_condition: from c where not is_defined(c.blobfish) +``` + +=== `auto_id` + +Automatically set the item `id` field to a random UUID v4. If the `id` field is already set, then it will not be overwritten. Setting this to `false` can improve performance, since the messages will not have to be parsed. + + +*Type*: `bool` + +*Default*: `true` + +=== `item_id` + +ID of item to replace or delete. Only used by the Replace and Delete operations +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +item_id: ${! json("id") } +``` + +=== `enable_content_response_on_write` + +Enable content response on write operations. To save some bandwidth, set this to false if you don't need to receive the updated message(s) from the server, in which case the processor will not modify the content of the messages which are fed into it. Applies to every operation except Read. + + +*Type*: `bool` + +*Default*: `true` + + +== CosmosDB emulator + +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: + +```bash +> docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator +``` + +Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. + +Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy] like so: + +```bash +> mitmproxy -k --mode "reverse:https://localhost:8081" +``` + +Then you can access the CosmosDB UI via `http://localhost:8080/_explorer/index.html` and use `http://localhost:8080` as the CosmosDB endpoint. + + diff --git a/docs/modules/components/pages/processors/bloblang.adoc b/docs/modules/components/pages/processors/bloblang.adoc new file mode 100644 index 0000000000..f0d541108d --- /dev/null +++ b/docs/modules/components/pages/processors/bloblang.adoc @@ -0,0 +1,127 @@ += bloblang +:type: processor +:status: stable +:categories: ["Mapping","Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping on messages. + +```yml +# Config fields, showing default values +label: "" +bloblang: "" +``` + +Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information see xref:guides:bloblang/about.adoc[]. + +If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `from ""`, where the path must be absolute, or relative from the location that Benthos is executed from. + +== Component rename + +This processor was recently renamed to the xref:components:processors/mapping.adoc[`mapping` processor] in order to make the purpose of the processor more prominent. It is still valid to use the existing `bloblang` name but eventually it will be deprecated and replaced by the new name in example configs. + +== Examples + +[tabs] +====== +Mapping:: ++ +-- + + +Given JSON documents containing an array of fans: + +```json +{ + "id":"foo", + "description":"a show about foo", + "fans":[ + {"name":"bev","obsession":0.57}, + {"name":"grace","obsession":0.21}, + {"name":"ali","obsession":0.89}, + {"name":"vic","obsession":0.43} + ] +} +``` + +We can reduce the fans to only those with an obsession score above 0.5, giving us: + +```json +{ + "id":"foo", + "description":"a show about foo", + "fans":[ + {"name":"bev","obsession":0.57}, + {"name":"ali","obsession":0.89} + ] +} +``` + +With the following config: + +```yaml +pipeline: + processors: + - bloblang: | + root = this + root.fans = this.fans.filter(fan -> fan.obsession > 0.5) +``` + +-- +More Mapping:: ++ +-- + + +When receiving JSON documents of the form: + +```json +{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +} +``` + +We could collapse the location names from the state of Washington into a field `Cities`: + +```json +{"Cities": "Bellevue, Olympia, Seattle"} +``` + +With the following config: + +```yaml +pipeline: + processors: + - bloblang: | + root.Cities = this.locations. + filter(loc -> loc.state == "WA"). + map_each(loc -> loc.name). + sort().join(", ") +``` + +-- +====== + +== Error handling + +Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use +xref:configuration:error_handling.adoc[standard processor error handling patterns]. + +However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behavior, which you can read about in xref:guides:bloblang/about#error-handling.adoc[Error handling]. + diff --git a/docs/modules/components/pages/processors/bounds_check.adoc b/docs/modules/components/pages/processors/bounds_check.adoc new file mode 100644 index 0000000000..20ef2586b3 --- /dev/null +++ b/docs/modules/components/pages/processors/bounds_check.adoc @@ -0,0 +1,91 @@ += bounds_check +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Removes messages (and batches) that do not fit within certain size boundaries. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +bounds_check: + max_part_size: 1073741824 + min_part_size: 1 +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +bounds_check: + max_part_size: 1073741824 + min_part_size: 1 + max_parts: 100 + min_parts: 1 +``` + +-- +====== + +== Fields + +=== `max_part_size` + +The maximum size of a message to allow (in bytes) + + +*Type*: `int` + +*Default*: `1073741824` + +=== `min_part_size` + +The minimum size of a message to allow (in bytes) + + +*Type*: `int` + +*Default*: `1` + +=== `max_parts` + +The maximum size of message batches to allow (in message count) + + +*Type*: `int` + +*Default*: `100` + +=== `min_parts` + +The minimum size of message batches to allow (in message count) + + +*Type*: `int` + +*Default*: `1` + + diff --git a/docs/modules/components/pages/processors/branch.adoc b/docs/modules/components/pages/processors/branch.adoc new file mode 100644 index 0000000000..cdcb96f22b --- /dev/null +++ b/docs/modules/components/pages/processors/branch.adoc @@ -0,0 +1,217 @@ += branch +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +The `branch` processor allows you to create a new request message via a xref:guides:bloblang/about.adoc[Bloblang mapping], execute a list of processors on the request messages, and, finally, map the result back into the source message using another mapping. + +```yml +# Config fields, showing default values +label: "" +branch: + request_map: "" + processors: [] # No default (required) + result_map: "" +``` + +This is useful for preserving the original message contents when using processors that would otherwise replace the entire contents. + +== Metadata + +Metadata fields that are added to messages during branch processing will not be automatically copied into the resulting message. In order to do this you should explicitly declare in your `result_map` either a wholesale copy with `meta = metadata()`, or selective copies with `meta foo = metadata("bar")` and so on. It is also possible to reference the metadata of the origin message in the `result_map` using the xref:guides:bloblang/about.adoc#metadata[`@` operator]. + +== Error handling + +If the `request_map` fails the child processors will not be executed. If the child processors themselves result in an (uncaught) error then the `result_map` will not be executed. If the `result_map` fails the message will remain unchanged. Under any of these conditions standard xref:configuration:error_handling.adoc[error handling methods] can be used in order to filter, DLQ or recover the failed messages. + +== Conditional branching + +If the root of your request map is set to `deleted()` then the branch processors are skipped for the given message, this allows you to conditionally branch messages. + +== Fields + +=== `request_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata). + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +request_map: |- + root = { + "id": this.doc.id, + "content": this.doc.body.text + } + +request_map: |- + root = if this.type == "foo" { + this.foo.request + } else { + deleted() + } +``` + +=== `processors` + +A list of processors to apply to mapped requests. When processing message batches the resulting batch must match the size and ordering of the input batch, therefore filtering, grouping should not be performed within these processors. + + +*Type*: `array` + + +=== `result_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata). + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +result_map: |- + meta foo_code = metadata("code") + root.foo_result = this + +result_map: |- + meta = metadata() + root.bar.body = this.body + root.bar.id = this.user.id + +result_map: root.raw_result = content().string() + +result_map: |- + root.enrichments.foo = if metadata("request_failed") != null { + throw(metadata("request_failed")) + } else { + this + } + +result_map: |- + # Retain only the updated metadata fields which were present in the origin message + meta = metadata().filter(v -> @.get(v.key) != null) +``` + +== Examples + +[tabs] +====== +HTTP Request:: ++ +-- + + +This example strips the request message into an empty body, grabs an HTTP payload, and places the result back into the original message at the path `image.pull_count`: + +```yaml +pipeline: + processors: + - branch: + request_map: 'root = ""' + processors: + - http: + url: https://hub.docker.com/v2/repositories/jeffail/benthos + verb: GET + headers: + Content-Type: application/json + result_map: root.image.pull_count = this.pull_count + +# Example input: {"id":"foo","some":"pre-existing data"} +# Example output: {"id":"foo","some":"pre-existing data","image":{"pull_count":1234}} +``` + +-- +Non Structured Results:: ++ +-- + + +When the result of your branch processors is unstructured and you wish to simply set a resulting field to the raw output use the content function to obtain the raw bytes of the resulting message and then coerce it into your value type of choice: + +```yaml +pipeline: + processors: + - branch: + request_map: 'root = this.document.id' + processors: + - cache: + resource: descriptions_cache + key: ${! content() } + operator: get + result_map: root.document.description = content().string() + +# Example input: {"document":{"id":"foo","content":"hello world"}} +# Example output: {"document":{"id":"foo","content":"hello world","description":"this is a cool doc"}} +``` + +-- +Lambda Function:: ++ +-- + + +This example maps a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged. + +```yaml +pipeline: + processors: + - branch: + request_map: '{"id":this.doc.id,"username":this.user.name}' + processors: + - aws_lambda: + function: trigger_user_update + +# Example input: {"doc":{"id":"foo","body":"hello world"},"user":{"name":"fooey"}} +# Output matches the input, which is unchanged +``` + +-- +Conditional Caching:: ++ +-- + + +This example caches a document by a message ID only when the type of the document is a foo: + +```yaml +pipeline: + processors: + - branch: + request_map: | + meta id = this.id + root = if this.type == "foo" { + this.document + } else { + deleted() + } + processors: + - cache: + resource: TODO + operator: set + key: ${! @id } + value: ${! content() } +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/cache.adoc b/docs/modules/components/pages/processors/cache.adoc new file mode 100644 index 0000000000..1915f454cc --- /dev/null +++ b/docs/modules/components/pages/processors/cache.adoc @@ -0,0 +1,235 @@ += cache +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs operations against a xref:components:caches/about.adoc[cache resource] for each message, allowing you to store or retrieve data within message payloads. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +cache: + resource: "" # No default (required) + operator: "" # No default (required) + key: "" # No default (required) + value: "" # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +cache: + resource: "" # No default (required) + operator: "" # No default (required) + key: "" # No default (required) + value: "" # No default (optional) + ttl: 60s # No default (optional) +``` + +-- +====== + +For use cases where you wish to cache the result of processors consider using the xref:components:processors/cached.adoc[`cached` processor] instead. + +This processor will interpolate functions within the `key` and `value` fields individually for each message. This allows you to specify dynamic keys and values based on the contents of the message payloads and metadata. You can find a list of functions in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. + +== Examples + +[tabs] +====== +Deduplication:: ++ +-- + + +Deduplication can be done using the add operator with a key extracted from the message payload, since it fails when a key already exists we can remove the duplicates using a xref:components:processors/mapping.adoc[`mapping` processor]: + +```yaml +pipeline: + processors: + - cache: + resource: foocache + operator: add + key: '${! json("message.id") }' + value: "storeme" + - mapping: root = if errored() { deleted() } + +cache_resources: + - label: foocache + redis: + url: tcp://TODO:6379 +``` + +-- +Deduplication Batch-Wide:: ++ +-- + + +Sometimes it's necessary to deduplicate a batch of messages (also known as a window) by a single identifying value. This can be done by introducing a xref:components:processors/branch.adoc[`branch` processor], which executes the cache only once on behalf of the batch, in this case with a value make from a field extracted from the first and last messages of the batch: + +```yaml +pipeline: + processors: + # Try and add one message to a cache that identifies the whole batch + - branch: + request_map: | + root = if batch_index() == 0 { + json("id").from(0) + json("meta.tail_id").from(-1) + } else { deleted() } + processors: + - cache: + resource: foocache + operator: add + key: ${! content() } + value: t + # Delete all messages if we failed + - mapping: | + root = if errored().from(0) { + deleted() + } +``` + +-- +Hydration:: ++ +-- + + +It's possible to enrich payloads with content previously stored in a cache by using the xref:components:processors/branch.adoc[`branch`] processor: + +```yaml +pipeline: + processors: + - branch: + processors: + - cache: + resource: foocache + operator: get + key: '${! json("message.document_id") }' + result_map: 'root.message.document = this' + + # NOTE: If the data stored in the cache is not valid JSON then use + # something like this instead: + # result_map: 'root.message.document = content().string()' + +cache_resources: + - label: foocache + memcached: + addresses: [ "TODO:11211" ] +``` + +-- +====== + +== Fields + +=== `resource` + +The xref:components:caches/about.adoc[`cache` resource] to target with this processor. + + +*Type*: `string` + + +=== `operator` + +The <> to perform with the cache. + + +*Type*: `string` + + +Options: +`set` +, `add` +, `get` +, `delete` +. + +=== `key` + +A key to use with the cache. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `value` + +A value to use with the cache (when applicable). +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `ttl` + +The TTL of each individual item as a duration string. After this period an item will be eligible for removal during the next compaction. Not all caches support per-key TTLs, those that do will have a configuration field `default_ttl`, and those that do not will fall back to their generally configured TTL setting. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 3.33.0 or newer + +```yml +# Examples + +ttl: 60s + +ttl: 5m + +ttl: 36h +``` + +== Operators + +=== `set` + +Set a key in the cache to a value. If the key already exists the contents are +overridden. + +=== `add` + +Set a key in the cache to a value. If the key already exists the action fails +with a 'key already exists' error, which can be detected with +xref:configuration:error_handling.adoc[processor error handling]. + +=== `get` + +Retrieve the contents of a cached key and replace the original message payload +with the result. If the key does not exist the action fails with an error, which +can be detected with xref:configuration:error_handling.adoc[processor error handling]. + +=== `delete` + +Delete a key and its contents from the cache. If the key does not exist the +action is a no-op and will not fail with an error. + diff --git a/docs/modules/components/pages/processors/cached.adoc b/docs/modules/components/pages/processors/cached.adoc new file mode 100644 index 0000000000..4e4c0665e9 --- /dev/null +++ b/docs/modules/components/pages/processors/cached.adoc @@ -0,0 +1,162 @@ += cached +:type: processor +:status: experimental +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Cache the result of applying one or more processors to messages identified by a key. If the key already exists within the cache the contents of the message will be replaced with the cached result instead of applying the processors. This component is therefore useful in situations where an expensive set of processors need only be executed periodically. + +Introduced in version 4.3.0. + +```yml +# Config fields, showing default values +label: "" +cached: + cache: "" # No default (required) + skip_on: errored() # No default (optional) + key: my_foo_result # No default (required) + ttl: "" # No default (optional) + processors: [] # No default (required) +``` + +The format of the data when stored within the cache is a custom and versioned schema chosen to balance performance and storage space. It is therefore not possible to point this processor to a cache that is pre-populated with data that this processor has not created itself. + +== Examples + +[tabs] +====== +Cached Enrichment:: ++ +-- + +In the following example we want to we enrich messages consumed from Kafka with data specific to the origin topic partition, we do this by placing an `http` processor within a `branch`, where the HTTP URL contains interpolation functions with the topic and partition in the path. + +However, it would be inefficient to make this HTTP request for every single message as the result is consistent for all data of a given topic partition. We can solve this by placing our enrichment call within a `cached` processor where the key contains the topic and partition, resulting in messages that originate from the same topic/partition combination using the cached result of the prior. + +```yaml +pipeline: + processors: + - branch: + processors: + - cached: + key: '${! meta("kafka_topic") }-${! meta("kafka_partition") }' + cache: foo_cache + processors: + - mapping: 'root = ""' + - http: + url: http://example.com/enrichment/${! meta("kafka_topic") }/${! meta("kafka_partition") } + verb: GET + result_map: 'root.enrichment = this' + +cache_resources: + - label: foo_cache + memory: + # Disable compaction so that cached items never expire + compaction_interval: "" +``` + +-- +Periodic Global Enrichment:: ++ +-- + +In the following example we enrich all messages with the same data obtained from a static URL with an `http` processor within a `branch`. However, we expect the data from this URL to change roughly every 10 minutes, so we configure a `cached` processor with a static key (since this request is consistent for all messages) and a TTL of `10m`. + +```yaml +pipeline: + processors: + - branch: + request_map: 'root = ""' + processors: + - cached: + key: static_foo + cache: foo_cache + ttl: 10m + processors: + - http: + url: http://example.com/get/foo.json + verb: GET + result_map: 'root.foo = this' + +cache_resources: + - label: foo_cache + memory: {} +``` + +-- +====== + +== Fields + +=== `cache` + +The cache resource to read and write processor results from. + + +*Type*: `string` + + +=== `skip_on` + +A condition that can be used to skip caching the results from the processors. + + +*Type*: `string` + + +```yml +# Examples + +skip_on: errored() +``` + +=== `key` + +A key to be resolved for each message, if the key already exists in the cache then the cached result is used, otherwise the processors are applied and the result is cached under this key. The key could be static and therefore apply generally to all messages or it could be an interpolated expression that is potentially unique for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +key: my_foo_result + +key: ${! this.document.id } + +key: ${! meta("kafka_key") } + +key: ${! meta("kafka_topic") } +``` + +=== `ttl` + +An optional expiry period to set for each cache entry. Some caches only have a general TTL and will therefore ignore this setting. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `processors` + +The list of processors whose result will be cached. + + +*Type*: `array` + + + diff --git a/docs/modules/components/pages/processors/catch.adoc b/docs/modules/components/pages/processors/catch.adoc new file mode 100644 index 0000000000..36b09add1e --- /dev/null +++ b/docs/modules/components/pages/processors/catch.adoc @@ -0,0 +1,45 @@ += catch +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Applies a list of child processors _only_ when a previous processing step has failed. + +```yml +# Config fields, showing default values +label: "" +catch: [] +``` + +Behaves similarly to the xref:components:processors/for_each.adoc[`for_each`] processor, where a list of child processors are applied to individual messages of a batch. However, processors are only applied to messages that failed a processing step prior to the catch. + +For example, with the following config: + +```yaml +pipeline: + processors: + - resource: foo + - catch: + - resource: bar + - resource: baz +``` + +If the processor `foo` fails for a particular message, that message will be fed into the processors `bar` and `baz`. Messages that do not fail for the processor `foo` will skip these processors. + +When messages leave the catch block their fail flags are cleared. This processor is useful for when it's possible to recover failed messages, or when special actions (such as logging/metrics) are required before dropping them. + +More information about error handling can be found in xref:configuration:error_handling.adoc[]. + + diff --git a/docs/modules/components/pages/processors/command.adoc b/docs/modules/components/pages/processors/command.adoc new file mode 100644 index 0000000000..b1ba858958 --- /dev/null +++ b/docs/modules/components/pages/processors/command.adoc @@ -0,0 +1,117 @@ += command +:type: processor +:status: experimental +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a command for each message. + +Introduced in version 4.21.0. + +```yml +# Config fields, showing default values +label: "" +command: + name: bash # No default (required) + args_mapping: '[ "-c", this.script_path ]' # No default (optional) +``` + +The specified command is executed for each message processed, with the raw bytes of the message being fed into the stdin of the command process, and the resulting message having its contents replaced with the stdout of it. + +== Performance + +Since this processor executes a new process for each message performance will likely be an issue for high throughput streams. If this is the case then consider using the xref:components:processors/subprocess.adoc[`subprocess` processor] instead as it keeps the underlying process alive long term and uses codecs to insert and extract inputs and outputs to it via stdin/stdout. + +== Error handling + +If a non-zero error code is returned by the command then an error containing the entirety of stderr (or a generic message if nothing is written) is set on the message. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about xref:configuration:error_handling.adoc[these patterns]. + +If the command is successful but stderr is written to then a metadata field `command_stderr` is populated with its contents. + + +== Fields + +=== `name` + +The name of the command to execute. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +name: bash + +name: go + +name: ${! @command } +``` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that, when specified, should resolve into an array of arguments to pass to the command. Command arguments are expressed this way in order to support dynamic behavior. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: '[ "-c", this.script_path ]' +``` + +== Examples + +[tabs] +====== +Cron Scheduled Command:: ++ +-- + +This example uses a xref:components:inputs/generate.adoc[`generate` input] to trigger a command on a cron schedule: + +```yaml +input: + generate: + interval: '0,30 */2 * * * *' + mapping: 'root = ""' # Empty string as we do not need to pipe anything to stdin + processors: + - command: + name: df + args_mapping: '[ "-h" ]' +``` + +-- +Dynamic Command Execution:: ++ +-- + +This example config takes structured messages of the form `{"command":"echo","args":["foo"]}` and uses their contents to execute the contained command and arguments dynamically, replacing its contents with the command result printed to stdout: + +```yaml +pipeline: + processors: + - command: + name: ${! this.command } + args_mapping: 'this.args' +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/compress.adoc b/docs/modules/components/pages/processors/compress.adoc new file mode 100644 index 0000000000..5a1b6c1143 --- /dev/null +++ b/docs/modules/components/pages/processors/compress.adoc @@ -0,0 +1,58 @@ += compress +:type: processor +:status: stable +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Compresses messages according to the selected algorithm. Supported compression algorithms are: [flate gzip lz4 pgzip snappy zlib] + +```yml +# Config fields, showing default values +label: "" +compress: + algorithm: "" # No default (required) + level: -1 +``` + +The 'level' field might not apply to all algorithms. + +== Fields + +=== `algorithm` + +The compression algorithm to use. + + +*Type*: `string` + + +Options: +`flate` +, `gzip` +, `lz4` +, `pgzip` +, `snappy` +, `zlib` +. + +=== `level` + +The level of compression to use. May not be applicable to all algorithms. + + +*Type*: `int` + +*Default*: `-1` + + diff --git a/docs/modules/components/pages/processors/couchbase.adoc b/docs/modules/components/pages/processors/couchbase.adoc new file mode 100644 index 0000000000..7ff26ea327 --- /dev/null +++ b/docs/modules/components/pages/processors/couchbase.adoc @@ -0,0 +1,205 @@ += couchbase +:type: processor +:status: experimental +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs operations against Couchbase for each message, allowing you to store or retrieve data within message payloads. + +Introduced in version 4.11.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +couchbase: + url: couchbase://localhost:11210 # No default (required) + username: "" # No default (optional) + password: "" # No default (optional) + bucket: "" # No default (required) + id: ${! json("id") } # No default (required) + content: "" # No default (optional) + operation: get +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +couchbase: + url: couchbase://localhost:11210 # No default (required) + username: "" # No default (optional) + password: "" # No default (optional) + bucket: "" # No default (required) + collection: _default + transcoder: legacy + timeout: 15s + id: ${! json("id") } # No default (required) + content: "" # No default (optional) + operation: get +``` + +-- +====== + +When inserting, replacing or upserting documents, each must have the `content` property set. + +== Fields + +=== `url` + +Couchbase connection string. + + +*Type*: `string` + + +```yml +# Examples + +url: couchbase://localhost:11210 +``` + +=== `username` + +Username to connect to the cluster. + + +*Type*: `string` + + +=== `password` + +Password to connect to the cluster. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `bucket` + +Couchbase bucket. + + +*Type*: `string` + + +=== `collection` + +Bucket collection. + + +*Type*: `string` + +*Default*: `"_default"` + +=== `transcoder` + +Couchbase transcoder to use. + + +*Type*: `string` + +*Default*: `"legacy"` + +|=== +| Option | Summary + +| `json` +| JSONTranscoder implements the default transcoding behavior and applies JSON transcoding to all values. This will apply the following behavior to the value: binary ([]byte) -> error. default -> JSON value, JSON Flags. +| `legacy` +| LegacyTranscoder implements the behavior for a backward-compatible transcoder. This transcoder implements behavior matching that of gocb v1.This will apply the following behavior to the value: binary ([]byte) -> binary bytes, Binary expectedFlags. string -> string bytes, String expectedFlags. default -> JSON value, JSON expectedFlags. +| `raw` +| RawBinaryTranscoder implements passthrough behavior of raw binary data. This transcoder does not apply any serialization. This will apply the following behavior to the value: binary ([]byte) -> binary bytes, binary expectedFlags. default -> error. +| `rawjson` +| RawJSONTranscoder implements passthrough behavior of JSON data. This transcoder does not apply any serialization. It will forward data across the network without incurring unnecessary parsing costs. This will apply the following behavior to the value: binary ([]byte) -> JSON bytes, JSON expectedFlags. string -> JSON bytes, JSON expectedFlags. default -> error. +| `rawstring` +| RawStringTranscoder implements passthrough behavior of raw string data. This transcoder does not apply any serialization. This will apply the following behavior to the value: string -> string bytes, string expectedFlags. default -> error. + +|=== + +=== `timeout` + +Operation timeout. + + +*Type*: `string` + +*Default*: `"15s"` + +=== `id` + +Document id. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +id: ${! json("id") } +``` + +=== `content` + +Document content. + + +*Type*: `string` + + +=== `operation` + +Couchbase operation to perform. + + +*Type*: `string` + +*Default*: `"get"` + +|=== +| Option | Summary + +| `get` +| fetch a document. +| `insert` +| insert a new document. +| `remove` +| delete a document. +| `replace` +| replace the contents of a document. +| `upsert` +| creates a new document if it does not exist, if it does exist then it updates it. + +|=== + + diff --git a/docs/modules/components/pages/processors/decompress.adoc b/docs/modules/components/pages/processors/decompress.adoc new file mode 100644 index 0000000000..7366b088ae --- /dev/null +++ b/docs/modules/components/pages/processors/decompress.adoc @@ -0,0 +1,47 @@ += decompress +:type: processor +:status: stable +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Decompresses messages according to the selected algorithm. Supported decompression algorithms are: [bzip2 flate gzip lz4 pgzip snappy zlib] + +```yml +# Config fields, showing default values +label: "" +decompress: + algorithm: "" # No default (required) +``` + +== Fields + +=== `algorithm` + +The decompression algorithm to use. + + +*Type*: `string` + + +Options: +`bzip2` +, `flate` +, `gzip` +, `lz4` +, `pgzip` +, `snappy` +, `zlib` +. + + diff --git a/docs/modules/components/pages/processors/dedupe.adoc b/docs/modules/components/pages/processors/dedupe.adoc new file mode 100644 index 0000000000..0316f862b0 --- /dev/null +++ b/docs/modules/components/pages/processors/dedupe.adoc @@ -0,0 +1,105 @@ += dedupe +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Deduplicates messages by storing a key value in a cache using the `add` operator. If the key already exists within the cache it is dropped. + +```yml +# Config fields, showing default values +label: "" +dedupe: + cache: "" # No default (required) + key: ${! meta("kafka_key") } # No default (required) + drop_on_err: true +``` + +Caches must be configured as resources, for more information check out the xref:components:caches/about.adoc[cache documentation]. + +When using this processor with an output target that might fail you should always wrap the output within an indefinite xref:components:outputs/retry.adoc[`retry`] block. This ensures that during outages your messages aren't reprocessed after failures, which would result in messages being dropped. + +== Batch deduplication + +This processor enacts on individual messages only, in order to perform a deduplication on behalf of a batch (or window) of messages instead use the xref:components:processors/cache.adoc#examples[`cache` processor]. + +== Delivery guarantees + +Performing deduplication on a stream using a distributed cache voids any at-least-once guarantees that it previously had. This is because the cache will preserve message signatures even if the message fails to leave the Benthos pipeline, which would cause message loss in the event of an outage at the output sink followed by a restart of the Benthos instance (or a server crash, etc). + +This problem can be mitigated by using an in-memory cache and distributing messages to horizontally scaled Benthos pipelines partitioned by the deduplication key. However, in situations where at-least-once delivery guarantees are important it is worth avoiding deduplication in favour of implement idempotent behavior at the edge of your stream pipelines. + +== Fields + +=== `cache` + +The xref:components:caches/about.adoc[`cache` resource] to target with this processor. + + +*Type*: `string` + + +=== `key` + +An interpolated string yielding the key to deduplicate by for each message. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +key: ${! meta("kafka_key") } + +key: ${! content().hash("xxhash64") } +``` + +=== `drop_on_err` + +Whether messages should be dropped when the cache returns a general error such as a network issue. + + +*Type*: `bool` + +*Default*: `true` + +== Examples + +[tabs] +====== +Deduplicate based on Kafka key:: ++ +-- + +The following configuration demonstrates a pipeline that deduplicates messages based on the Kafka key. + +```yaml +pipeline: + processors: + - dedupe: + cache: keycache + key: ${! meta("kafka_key") } + +cache_resources: + - label: keycache + memory: + default_ttl: 60s +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/for_each.adoc b/docs/modules/components/pages/processors/for_each.adoc new file mode 100644 index 0000000000..1c0fa66d8f --- /dev/null +++ b/docs/modules/components/pages/processors/for_each.adoc @@ -0,0 +1,30 @@ += for_each +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message. + +```yml +# Config fields, showing default values +label: "" +for_each: [] +``` + +This is useful for forcing batch wide processors such as xref:components:processors/dedupe.adoc[`dedupe`] or interpolations such as the `value` field of the `metadata` processor to execute on individual message parts of a batch instead. + +Please note that most processors already process per message of a batch, and this processor is not needed in those cases. + + diff --git a/docs/modules/components/pages/processors/gcp_bigquery_select.adoc b/docs/modules/components/pages/processors/gcp_bigquery_select.adoc new file mode 100644 index 0000000000..1515499bf8 --- /dev/null +++ b/docs/modules/components/pages/processors/gcp_bigquery_select.adoc @@ -0,0 +1,158 @@ += gcp_bigquery_select +:type: processor +:status: experimental +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a `SELECT` query against BigQuery and replaces messages with the rows returned. + +Introduced in version 3.64.0. + +```yml +# Config fields, showing default values +label: "" +gcp_bigquery_select: + project: "" # No default (required) + table: bigquery-public-data.samples.shakespeare # No default (required) + columns: [] # No default (required) + where: type = ? and created_at > ? # No default (optional) + job_labels: {} + args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) + prefix: "" # No default (optional) + suffix: "" # No default (optional) +``` + +== Examples + +[tabs] +====== +Word count:: ++ +-- + + +Given a stream of English terms, enrich the messages with the word count from Shakespeare's public works: + +```yaml +pipeline: + processors: + - branch: + processors: + - gcp_bigquery_select: + project: test-project + table: bigquery-public-data.samples.shakespeare + columns: + - word + - sum(word_count) as total_count + where: word = ? + suffix: | + GROUP BY word + ORDER BY total_count DESC + LIMIT 10 + args_mapping: root = [ this.term ] + result_map: | + root.count = this.get("0.total_count") +``` + +-- +====== + +== Fields + +=== `project` + +GCP project where the query job will execute. + + +*Type*: `string` + + +=== `table` + +Fully-qualified BigQuery table name to query. + + +*Type*: `string` + + +```yml +# Examples + +table: bigquery-public-data.samples.shakespeare +``` + +=== `columns` + +A list of columns to query. + + +*Type*: `array` + + +=== `where` + +An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks (`?`). + + +*Type*: `string` + + +```yml +# Examples + +where: type = ? and created_at > ? + +where: user_id = ? +``` + +=== `job_labels` + +A list of labels to add to the query job. + + +*Type*: `object` + +*Default*: `{}` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ "article", now().ts_format("2006-01-02") ] +``` + +=== `prefix` + +An optional prefix to prepend to the select query (before SELECT). + + +*Type*: `string` + + +=== `suffix` + +An optional suffix to append to the select query. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/processors/grok.adoc b/docs/modules/components/pages/processors/grok.adoc new file mode 100644 index 0000000000..b502b8ea8a --- /dev/null +++ b/docs/modules/components/pages/processors/grok.adoc @@ -0,0 +1,156 @@ += grok +:type: processor +:status: stable +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Parses messages into a structured format by attempting to apply a list of Grok expressions, the first expression to result in at least one value replaces the original message with a JSON object containing the values. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +grok: + expressions: [] # No default (required) + pattern_definitions: {} + pattern_paths: [] +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +grok: + expressions: [] # No default (required) + pattern_definitions: {} + pattern_paths: [] + named_captures_only: true + use_default_patterns: true + remove_empty_values: true +``` + +-- +====== + +Type hints within patterns are respected, therefore with the pattern `%\{WORD:first},%{INT:second:int}` and a payload of `foo,1` the resulting payload would be `\{"first":"foo","second":1}`. + +== Performance + +This processor currently uses the https://golang.org/s/re2syntax[Go RE2] regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information, see https://swtch.com/~rsc/regexp/regexp1.html. + +== Examples + +[tabs] +====== +VPC Flow Logs:: ++ +-- + + +Grok can be used to parse unstructured logs such as VPC flow logs that look like this: + +```text +2 123456789010 eni-1235b8ca123456789 172.31.16.139 172.31.16.21 20641 22 6 20 4249 1418530010 1418530070 ACCEPT OK +``` + +Into structured objects that look like this: + +```json +{"accountid":"123456789010","action":"ACCEPT","bytes":4249,"dstaddr":"172.31.16.21","dstport":22,"end":1418530070,"interfaceid":"eni-1235b8ca123456789","logstatus":"OK","packets":20,"protocol":6,"srcaddr":"172.31.16.139","srcport":20641,"start":1418530010,"version":2} +``` + +With the following config: + +```yaml +pipeline: + processors: + - grok: + expressions: + - '%{VPCFLOWLOG}' + pattern_definitions: + VPCFLOWLOG: '%{NUMBER:version:int} %{NUMBER:accountid} %{NOTSPACE:interfaceid} %{NOTSPACE:srcaddr} %{NOTSPACE:dstaddr} %{NOTSPACE:srcport:int} %{NOTSPACE:dstport:int} %{NOTSPACE:protocol:int} %{NOTSPACE:packets:int} %{NOTSPACE:bytes:int} %{NUMBER:start:int} %{NUMBER:end:int} %{NOTSPACE:action} %{NOTSPACE:logstatus}' +``` + +-- +====== + +== Fields + +=== `expressions` + +One or more Grok expressions to attempt against incoming messages. The first expression to match at least one value will be used to form a result. + + +*Type*: `array` + + +=== `pattern_definitions` + +A map of pattern definitions that can be referenced within `patterns`. + + +*Type*: `object` + +*Default*: `{}` + +=== `pattern_paths` + +A list of paths to load Grok patterns from. This field supports wildcards, including super globs (double star). + + +*Type*: `array` + +*Default*: `[]` + +=== `named_captures_only` + +Whether to only capture values from named patterns. + + +*Type*: `bool` + +*Default*: `true` + +=== `use_default_patterns` + +Whether to use a <>. + + +*Type*: `bool` + +*Default*: `true` + +=== `remove_empty_values` + +Whether to remove values that are empty from the resulting structure. + + +*Type*: `bool` + +*Default*: `true` + +== Default patterns + +For summary of the default patterns on offer, see https://github.com/Jeffail/grok/blob/master/patterns.go#L5. + diff --git a/docs/modules/components/pages/processors/group_by.adoc b/docs/modules/components/pages/processors/group_by.adoc new file mode 100644 index 0000000000..b33ec93cc8 --- /dev/null +++ b/docs/modules/components/pages/processors/group_by.adoc @@ -0,0 +1,98 @@ += group_by +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Splits a xref:configuration:batching.adoc[batch of messages] into N batches, where each resulting batch contains a group of messages determined by a xref:guides:bloblang/about.adoc[Bloblang query]. + +```yml +# Config fields, showing default values +label: "" +group_by: [] # No default (required) +``` + +Once the groups are established a list of processors are applied to their respective grouped batch, which can be used to label the batch as per their grouping. Messages that do not pass the check of any specified group are placed in their own group. + +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `[].check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message belongs to a given group. + + +*Type*: `string` + + +```yml +# Examples + +check: this.type == "foo" + +check: this.contents.urls.contains("https://benthos.dev/") + +check: "true" +``` + +=== `[].processors` + +A list of xref:components:processors/about.adoc[processors] to execute on the newly formed group. + + +*Type*: `array` + +*Default*: `[]` + +== Examples + +[tabs] +====== +Grouped Processing:: ++ +-- + +Imagine we have a batch of messages that we wish to split into a group of foos and everything else, which should be sent to different output destinations based on those groupings. We also need to send the foos as a tar gzip archive. For this purpose we can use the `group_by` processor with a xref:components:outputs/switch.adoc[`switch`] output: + +```yaml +pipeline: + processors: + - group_by: + - check: content().contains("this is a foo") + processors: + - archive: + format: tar + - compress: + algorithm: gzip + - mapping: 'meta grouping = "foo"' + +output: + switch: + cases: + - check: meta("grouping") == "foo" + output: + gcp_pubsub: + project: foo_prod + topic: only_the_foos + - output: + gcp_pubsub: + project: somewhere_else + topic: no_foos_here +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/group_by_value.adoc b/docs/modules/components/pages/processors/group_by_value.adoc new file mode 100644 index 0000000000..3a706dad5f --- /dev/null +++ b/docs/modules/components/pages/processors/group_by_value.adoc @@ -0,0 +1,68 @@ += group_by_value +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Splits a batch of messages into N batches, where each resulting batch contains a group of messages determined by a xref:configuration:interpolation.adoc#bloblang-queries[function interpolated string] evaluated per message. + +```yml +# Config fields, showing default values +label: "" +group_by_value: + value: ${! meta("kafka_key") } # No default (required) +``` + +This allows you to group messages using arbitrary fields within their content or metadata, process them individually, and send them to unique locations as per their group. + +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `value` + +The interpolated string to group based on. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +value: ${! meta("kafka_key") } + +value: ${! json("foo.bar") }-${! meta("baz") } +``` + +== Examples + +If we were consuming Kafka messages and needed to group them by their key, archive the groups, and send them to S3 with the key as part of the path we could achieve that with the following: + +```yaml +pipeline: + processors: + - group_by_value: + value: ${! meta("kafka_key") } + - archive: + format: tar + - compress: + algorithm: gzip +output: + aws_s3: + bucket: TODO + path: docs/${! meta("kafka_key") }/${! count("files") }-${! timestamp_unix_nano() }.tar.gz +``` + diff --git a/docs/modules/components/pages/processors/http.adoc b/docs/modules/components/pages/processors/http.adoc new file mode 100644 index 0000000000..53060f5707 --- /dev/null +++ b/docs/modules/components/pages/processors/http.adoc @@ -0,0 +1,822 @@ += http +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs an HTTP request using a message batch as the request body, and replaces the original message parts with the body of the response. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +http: + url: "" # No default (required) + verb: POST + headers: {} + rate_limit: "" # No default (optional) + timeout: 5s + parallel: false +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +http: + url: "" # No default (required) + verb: POST + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + dump_request_log_level: "" + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + oauth2: + enabled: false + client_key: "" + client_secret: "" + token_url: "" + scopes: [] + endpoint_params: {} + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + extract_headers: + include_prefixes: [] + include_patterns: [] + rate_limit: "" # No default (optional) + timeout: 5s + retry_period: 1s + max_retry_backoff: 300s + retries: 3 + backoff_on: + - 429 + drop_on: [] + successful_on: [] + proxy_url: "" # No default (optional) + batch_as_multipart: false + parallel: false +``` + +-- +====== + +The `rate_limit` field can be used to specify a rate limit xref:components:rate_limits/about.adoc[resource] to cap the rate of requests across all parallel components service wide. + +The URL and header values of this type can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. + +In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the xref:components:processors/branch.adoc[`branch` processor]. + +== Response codes + +Benthos considers any response code between 200 and 299 inclusive to indicate a successful response, you can add more success status codes with the field `successful_on`. + +When a request returns a response code within the `backoff_on` field it will be retried after increasing intervals. + +When a request returns a response code within the `drop_on` field it will not be reattempted and is immediately considered a failed request. + +== Add metadata + +If the request returns an error response code this processor sets a metadata field `http_status_code` on the resulting message. + +Use the field `extract_headers` to specify rules for which other headers should be copied into the resulting message from the response. + +== Error handling + +When all retry attempts for a message are exhausted the processor cancels the attempt. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about xref:configuration:error_handling.adoc[these patterns]. + +== Examples + +[tabs] +====== +Branched Request:: ++ +-- + +This example uses a xref:components:processors/branch.adoc[`branch` processor] to strip the request message into an empty body, grab an HTTP payload, and place the result back into the original message at the path `repo.status`: + +```yaml +pipeline: + processors: + - branch: + request_map: 'root = ""' + processors: + - http: + url: https://hub.docker.com/v2/repositories/jeffail/benthos + verb: GET + headers: + Content-Type: application/json + result_map: 'root.repo.status = this' +``` + +-- +====== + +== Fields + +=== `url` + +The URL to connect to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `verb` + +A verb to connect with + + +*Type*: `string` + +*Default*: `"POST"` + +```yml +# Examples + +verb: POST + +verb: GET + +verb: DELETE +``` + +=== `headers` + +A map of headers to add to the request. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +headers: + Content-Type: application/octet-stream + traceparent: ${! tracing_span().traceparent } +``` + +=== `metadata` + +Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `dump_request_log_level` + +EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged. + + +*Type*: `string` + +*Default*: `""` +Requires version 4.12.0 or newer + +Options: +`TRACE` +, `DEBUG` +, `INFO` +, `WARN` +, `ERROR` +, `FATAL` +, `` +. + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2` + +Allows you to specify open authentication via OAuth version 2 using the client credentials token flow. + + +*Type*: `object` + + +=== `oauth2.enabled` + +Whether to use OAuth version 2 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth2.client_key` + +A value used to identify the client to the token provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.client_secret` + +A secret used to establish ownership of the client key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.token_url` + +The URL of the token provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth2.scopes` + +A list of optional requested permissions. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.45.0 or newer + +=== `oauth2.endpoint_params` + +A list of optional endpoint parameters, values should be arrays of strings. + + +*Type*: `object` + +*Default*: `{}` +Requires version 4.21.0 or newer + +```yml +# Examples + +endpoint_params: + bar: + - woof + foo: + - meow + - quack +``` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `extract_headers` + +Specify which response headers should be added to resulting messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. + + +*Type*: `object` + + +=== `extract_headers.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `extract_headers.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `rate_limit` + +An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by. + + +*Type*: `string` + + +=== `timeout` + +A static timeout to apply to requests. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `retry_period` + +The base period to wait between failed requests. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `max_retry_backoff` + +The maximum period to wait between failed requests. + + +*Type*: `string` + +*Default*: `"300s"` + +=== `retries` + +The maximum number of retry attempts to make. + + +*Type*: `int` + +*Default*: `3` + +=== `backoff_on` + +A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually. + + +*Type*: `array` + +*Default*: `[429]` + +=== `drop_on` + +A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped. + + +*Type*: `array` + +*Default*: `[]` + +=== `successful_on` + +A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field. + + +*Type*: `array` + +*Default*: `[]` + +=== `proxy_url` + +An optional HTTP proxy URL. + + +*Type*: `string` + + +=== `batch_as_multipart` + +Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. + + +*Type*: `bool` + +*Default*: `false` + +=== `parallel` + +When processing batched messages, whether to send messages of the batch in parallel, otherwise they are sent serially. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/processors/insert_part.adoc b/docs/modules/components/pages/processors/insert_part.adoc new file mode 100644 index 0000000000..46679577d1 --- /dev/null +++ b/docs/modules/components/pages/processors/insert_part.adoc @@ -0,0 +1,55 @@ += insert_part +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Insert a new message into a batch at an index. If the specified index is greater than the length of the existing batch it will be appended to the end. + +```yml +# Config fields, showing default values +label: "" +insert_part: + index: -1 + content: "" +``` + +The index can be negative, and if so the message will be inserted from the end counting backwards starting from -1. E.g. if index = -1 then the new message will become the last of the batch, if index = -2 then the new message will be inserted before the last message, and so on. If the negative index is greater than the length of the existing batch it will be inserted at the beginning. + +The new message will have metadata copied from the first pre-existing message of the batch. + +This processor will interpolate functions within the 'content' field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here]. + +== Fields + +=== `index` + +The index within the batch to insert the message at. + + +*Type*: `int` + +*Default*: `-1` + +=== `content` + +The content of the message being inserted. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/processors/javascript.adoc b/docs/modules/components/pages/processors/javascript.adoc new file mode 100644 index 0000000000..0165e334b9 --- /dev/null +++ b/docs/modules/components/pages/processors/javascript.adoc @@ -0,0 +1,228 @@ += javascript +:type: processor +:status: experimental +:categories: ["Mapping"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a provided JavaScript code block or file for each message. + +Introduced in version 4.14.0. + +```yml +# Config fields, showing default values +label: "" +javascript: + code: "" # No default (optional) + file: "" # No default (optional) + global_folders: [] +``` + +The https://github.com/dop251/goja[execution engine] behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. + +Imports via `require` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be found on https://github.com/dop251/goja#known-incompatibilities-and-caveats[GitHub]. + +This processor is implemented using the https://github.com/dop251/goja[github.com/dop251/goja] library. + +== Fields + +=== `code` + +An inline JavaScript program to run. One of `code` or `file` must be defined. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `file` + +A file containing a JavaScript program to run. One of `code` or `file` must be defined. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +=== `global_folders` + +List of folders that will be used to load modules from if the requested JS module is not found elsewhere. + + +*Type*: `array` + +*Default*: `[]` + +== Examples + +[tabs] +====== +Simple mutation:: ++ +-- + +In this example we define a simple function that performs a basic mutation against messages, treating their contents as raw strings. + +```yaml +pipeline: + processors: + - javascript: + code: 'benthos.v0_msg_set_string(benthos.v0_msg_as_string() + "hello world");' +``` + +-- +Structured mutation:: ++ +-- + +In this example we define a function that performs basic mutations against a structured message. Note that we encapsulate the logic within an anonymous function that is called for each invocation, this is required in order to avoid duplicate variable declarations in the global state. + +```yaml +pipeline: + processors: + - javascript: + code: | + (() => { + let thing = benthos.v0_msg_as_structured(); + thing.num_keys = Object.keys(thing).length; + delete thing["b"]; + benthos.v0_msg_set_structured(thing); + })(); +``` + +-- +====== + +== Runtime + +In order to optimize code execution JS runtimes are created on demand (in order to support parallel execution) and are reused across invocations. Therefore, it is important to understand that global state created by your programs will outlive individual invocations. In order for your programs to avoid failing after the first invocation ensure that you do not define variables at the global scope. + +Although technically possible, it is recommended that you do not rely on the global state for maintaining state across invocations as the pooling nature of the runtimes will prevent deterministic behavior. We aim to support deterministic strategies for mutating global state in the future. + +== Functions + +### `benthos.v0_fetch` + +Executes an HTTP request synchronously and returns the result as an object of the form `{"status":200,"body":"foo"}`. + +#### Parameters + +**`url`** <string> The URL to fetch +**`headers`** <object(string,string)> An object of string/string key/value pairs to add the request as headers. +**`method`** <string> The method of the request. +**`body`** <(optional) string> A body to send. + +#### Examples + +```javascript +let result = benthos.v0_fetch("http://example.com", {}, "GET", "") +benthos.v0_msg_set_structured(result); +``` + +### `benthos.v0_msg_as_string` + +Obtain the raw contents of the processed message as a string. + +#### Examples + +```javascript +let contents = benthos.v0_msg_as_string(); +``` + +### `benthos.v0_msg_as_structured` + +Obtain the root of the processed message as a structured value. If the message is not valid JSON or has not already been expanded into a structured form this function will throw an error. + +#### Examples + +```javascript +let foo = benthos.v0_msg_as_structured().foo; +``` + +### `benthos.v0_msg_exists_meta` + +Check that a metadata key exists. + +#### Parameters + +**`name`** <string> The metadata key to search for. + +#### Examples + +```javascript +if (benthos.v0_msg_exists_meta("kafka_key")) {} +``` + +### `benthos.v0_msg_get_meta` + +Get the value of a metadata key from the processed message. + +#### Parameters + +**`name`** <string> The metadata key to search for. + +#### Examples + +```javascript +let key = benthos.v0_msg_get_meta("kafka_key"); +``` + +### `benthos.v0_msg_set_meta` + +Set a metadata key on the processed message to a value. + +#### Parameters + +**`name`** <string> The metadata key to set. +**`value`** <anything> The value to set it to. + +#### Examples + +```javascript +benthos.v0_msg_set_meta("thing", "hello world"); +``` + +### `benthos.v0_msg_set_string` + +Set the contents of the processed message to a given string. + +#### Parameters + +**`value`** <string> The value to set it to. + +#### Examples + +```javascript +benthos.v0_msg_set_string("hello world"); +``` + +### `benthos.v0_msg_set_structured` + +Set the root of the processed message to a given value of any type. + +#### Parameters + +**`value`** <anything> The value to set it to. + +#### Examples + +```javascript +benthos.v0_msg_set_structured({ + "foo": "a thing", + "bar": "something else", + "baz": 1234 +}); +``` + + + diff --git a/docs/modules/components/pages/processors/jmespath.adoc b/docs/modules/components/pages/processors/jmespath.adoc new file mode 100644 index 0000000000..7eea222a77 --- /dev/null +++ b/docs/modules/components/pages/processors/jmespath.adoc @@ -0,0 +1,84 @@ += jmespath +:type: processor +:status: stable +:categories: ["Mapping"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a http://jmespath.org/[JMESPath query] on JSON documents and replaces the message with the resulting document. + +```yml +# Config fields, showing default values +label: "" +jmespath: + query: "" # No default (required) +``` + +[TIP] +.Try out Bloblang +==== +For better performance and improved capabilities try native Benthos mapping with the xref:components:processors/mapping.adoc[`mapping` processor]. +==== + + +== Fields + +=== `query` + +The JMESPath query to apply to messages. + + +*Type*: `string` + + +== Examples + +[tabs] +====== +Mapping:: ++ +-- + + +When receiving JSON documents of the form: + +```json +{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +} +``` + +We could collapse the location names from the state of Washington into a field `Cities`: + +```json +{"Cities": "Bellevue, Olympia, Seattle"} +``` + +With the following config: + +```yaml +pipeline: + processors: + - jmespath: + query: "locations[?state == 'WA'].name | sort(@) | {Cities: join(', ', @)}" +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/jq.adoc b/docs/modules/components/pages/processors/jq.adoc new file mode 100644 index 0000000000..ae0f3fb642 --- /dev/null +++ b/docs/modules/components/pages/processors/jq.adoc @@ -0,0 +1,139 @@ += jq +:type: processor +:status: stable +:categories: ["Mapping"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Transforms and filters messages using jq queries. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +jq: + query: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +jq: + query: "" # No default (required) + raw: false + output_raw: false +``` + +-- +====== + +[TIP] +.Try out Bloblang +==== +For better performance and improved capabilities try out native Benthos mapping with the xref:components:processors/mapping.adoc[`mapping` processor]. +==== + +The provided query is executed on each message, targeting either the contents as a structured JSON value or as a raw string using the field `raw`, and the message is replaced with the query result. + +Message metadata is also accessible within the query from the variable `$metadata`. + +This processor uses the https://github.com/itchyny/gojq[gojq library], and therefore does not require jq to be installed as a dependency. However, this also means there are some https://github.com/itchyny/gojq#difference-to-jq[differences in how these queries are executed] versus the jq cli. + +If the query does not emit any value then the message is filtered, if the query returns multiple values then the resulting message will be an array containing all values. + +The full query syntax is described in https://stedolan.github.io/jq/manual/[jq's documentation]. + +== Error handling + +Queries can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. + +== Fields + +=== `query` + +The jq query to filter and transform messages with. + + +*Type*: `string` + + +=== `raw` + +Whether to process the input as a raw string instead of as JSON. + + +*Type*: `bool` + +*Default*: `false` + +=== `output_raw` + +Whether to output raw text (unquoted) instead of JSON strings when the emitted values are string types. + + +*Type*: `bool` + +*Default*: `false` + +== Examples + +[tabs] +====== +Mapping:: ++ +-- + + +When receiving JSON documents of the form: + +```json +{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +} +``` + +We could collapse the location names from the state of Washington into a field `Cities`: + +```json +{"Cities": "Bellevue, Olympia, Seattle"} +``` + +With the following config: + +```yaml +pipeline: + processors: + - jq: + query: '{Cities: .locations | map(select(.state == "WA").name) | sort | join(", ") }' +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/json_schema.adoc b/docs/modules/components/pages/processors/json_schema.adoc new file mode 100644 index 0000000000..be5db5afe1 --- /dev/null +++ b/docs/modules/components/pages/processors/json_schema.adoc @@ -0,0 +1,98 @@ += json_schema +:type: processor +:status: stable +:categories: ["Mapping"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Checks messages against a provided JSONSchema definition but does not change the payload under any circumstances. If a message does not match the schema it can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +```yml +# Config fields, showing default values +label: "" +json_schema: + schema: "" # No default (optional) + schema_path: "" # No default (optional) +``` + +Please refer to the https://json-schema.org/[JSON Schema website] for information and tutorials regarding the syntax of the schema. + +== Fields + +=== `schema` + +A schema to apply. Use either this or the `schema_path` field. + + +*Type*: `string` + + +=== `schema_path` + +The path of a schema document to apply. Use either this or the `schema` field. + + +*Type*: `string` + + +== Examples + +With the following JSONSchema document: + +```json +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + } +} +``` + +And the following Benthos configuration: + +```yaml +pipeline: + processors: + - json_schema: + schema_path: "file://path_to_schema.json" + - catch: + - log: + level: ERROR + message: "Schema validation failed due to: ${!error()}" + - mapping: 'root = deleted()' # Drop messages that fail +``` + +If a payload being processed looked like: + +```json +{"firstName":"John","lastName":"Doe","age":-21} +``` + +Then a log message would appear explaining the fault and the payload would be +dropped. + diff --git a/docs/modules/components/pages/processors/log.adoc b/docs/modules/components/pages/processors/log.adoc new file mode 100644 index 0000000000..eb813057d0 --- /dev/null +++ b/docs/modules/components/pages/processors/log.adoc @@ -0,0 +1,102 @@ += log +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Prints a log event for each message. Messages always remain unchanged. The log message can be set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries] which allows you to log the contents and metadata of messages. + +```yml +# Config fields, showing default values +label: "" +log: + level: INFO + fields_mapping: |- # No default (optional) + root.reason = "cus I wana" + root.id = this.id + root.age = this.user.age.number() + root.kafka_topic = meta("kafka_topic") + message: "" +``` + +The `level` field determines the log level of the printed events and can be any of the following values: TRACE, DEBUG, INFO, WARN, ERROR. + +== Structured fields + +It's also possible add custom fields to logs when the format is set to a structured form such as `json` or `logfmt` with the config field <>: + +```yaml +pipeline: + processors: + - log: + level: DEBUG + message: hello world + fields_mapping: | + root.reason = "cus I wana" + root.id = this.id + root.age = this.user.age + root.kafka_topic = meta("kafka_topic") +``` + + +== Fields + +=== `level` + +The log level to use. + + +*Type*: `string` + +*Default*: `"INFO"` + +Options: +`FATAL` +, `ERROR` +, `WARN` +, `INFO` +, `DEBUG` +, `TRACE` +, `ALL` +. + +=== `fields_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that can be used to specify extra fields to add to the log. If log fields are also added with the `fields` field then those values will override matching keys from this mapping. + + +*Type*: `string` + + +```yml +# Examples + +fields_mapping: |- + root.reason = "cus I wana" + root.id = this.id + root.age = this.user.age.number() + root.kafka_topic = meta("kafka_topic") +``` + +=== `message` + +The message to print. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + + diff --git a/docs/modules/components/pages/processors/mapping.adoc b/docs/modules/components/pages/processors/mapping.adoc new file mode 100644 index 0000000000..86719310dd --- /dev/null +++ b/docs/modules/components/pages/processors/mapping.adoc @@ -0,0 +1,141 @@ += mapping +:type: processor +:status: stable +:categories: ["Mapping","Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping on messages, creating a new document that replaces (or filters) the original message. + +Introduced in version 4.5.0. + +```yml +# Config fields, showing default values +label: "" +mapping: "" # No default (required) +``` + +Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information, see xref:guides:bloblang/about.adoc[]. + +If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `from ""`, where the path must be absolute, or relative from the location that Benthos is executed from. + +Note: This processor is equivalent to the xref:components:processors/bloblang.adoc#component-rename[Bloblang] one. The latter will be deprecated in a future release. + +== Input document immutability + +Mapping operates by creating an entirely new object during assignments, this has the advantage of treating the original referenced document as immutable and therefore queryable at any stage of your mapping. For example, with the following mapping: + +```coffeescript +root.id = this.id +root.invitees = this.invitees.filter(i -> i.mood >= 0.5) +root.rejected = this.invitees.filter(i -> i.mood < 0.5) +``` + +Notice that we mutate the value of `invitees` in the resulting document by filtering out objects with a lower mood. However, even after doing so we're still able to reference the unchanged original contents of this value from the input document in order to populate a second field. Within this mapping we also have the flexibility to reference the mutable mapped document by using the keyword `root` (i.e. `root.invitees`) on the right-hand side instead. + +Mapping documents is advantageous in situations where the result is a document with a dramatically different shape to the input document, since we are effectively rebuilding the document in its entirety and might as well keep a reference to the unchanged input document throughout. However, in situations where we are only performing minor alterations to the input document, the rest of which is unchanged, it might be more efficient to use the xref:components:processors/mutation.adoc[`mutation` processor] instead. + +== Error handling + +Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. + +However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired xref:guides:bloblang/about.adoc#error-handling[fallback behavior]. + + +== Examples + +[tabs] +====== +Mapping:: ++ +-- + + +Given JSON documents containing an array of fans: + +```json +{ + "id":"foo", + "description":"a show about foo", + "fans":[ + {"name":"bev","obsession":0.57}, + {"name":"grace","obsession":0.21}, + {"name":"ali","obsession":0.89}, + {"name":"vic","obsession":0.43} + ] +} +``` + +We can reduce the documents down to just the ID and only those fans with an obsession score above 0.5, giving us: + +```json +{ + "id":"foo", + "fans":[ + {"name":"bev","obsession":0.57}, + {"name":"ali","obsession":0.89} + ] +} +``` + +With the following config: + +```yaml +pipeline: + processors: + - mapping: | + root.id = this.id + root.fans = this.fans.filter(fan -> fan.obsession > 0.5) +``` + +-- +More Mapping:: ++ +-- + + +When receiving JSON documents of the form: + +```json +{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +} +``` + +We could collapse the location names from the state of Washington into a field `Cities`: + +```json +{"Cities": "Bellevue, Olympia, Seattle"} +``` + +With the following config: + +```yaml +pipeline: + processors: + - mapping: | + root.Cities = this.locations. + filter(loc -> loc.state == "WA"). + map_each(loc -> loc.name). + sort().join(", ") +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/metric.adoc b/docs/modules/components/pages/processors/metric.adoc new file mode 100644 index 0000000000..badd945e29 --- /dev/null +++ b/docs/modules/components/pages/processors/metric.adoc @@ -0,0 +1,187 @@ += metric +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Emit custom metrics by extracting values from messages. + +```yml +# Config fields, showing default values +label: "" +metric: + type: "" # No default (required) + name: "" # No default (required) + labels: {} # No default (optional) + value: "" +``` + +This processor works by evaluating an xref:configuration:interpolation.adoc#bloblang-queries[interpolated field `value`] for each message and updating a emitted metric according to the <>. + +Custom metrics such as these are emitted along with Benthos internal metrics, where you can customize where metrics are sent, which metric names are emitted and rename them as/when appropriate. For more information see the xref:components:metrics/about.adoc[metrics docs]. + +== Fields + +=== `type` + +The metric <> to create. + + +*Type*: `string` + + +Options: +`counter` +, `counter_by` +, `gauge` +, `timing` +. + +=== `name` + +The name of the metric to create, this must be unique across all Benthos components otherwise it will overwrite those other metrics. + + +*Type*: `string` + + +=== `labels` + +A map of label names and values that can be used to enrich metrics. Labels are not supported by some metric destinations, in which case the metrics series are combined. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + + +```yml +# Examples + +labels: + topic: ${! meta("kafka_topic") } + type: ${! json("doc.type") } +``` + +=== `value` + +For some metric types specifies a value to set, increment. Certain metrics exporters such as Prometheus support floating point values, but those that do not will cast a floating point value into an integer. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +*Default*: `""` + +== Examples + +[tabs] +====== +Counter:: ++ +-- + +In this example we emit a counter metric called `Foos`, which increments for every message processed, and we label the metric with some metadata about where the message came from and a field from the document that states what type it is. We also configure our metrics to emit to CloudWatch, and explicitly only allow our custom metric and some internal Benthos metrics to emit. + +```yaml +pipeline: + processors: + - metric: + name: Foos + type: counter + labels: + topic: ${! meta("kafka_topic") } + partition: ${! meta("kafka_partition") } + type: ${! json("document.type").or("unknown") } + +metrics: + mapping: | + root = if ![ + "Foos", + "input_received", + "output_sent" + ].contains(this) { deleted() } + aws_cloudwatch: + namespace: ProdConsumer +``` + +-- +Gauge:: ++ +-- + +In this example we emit a gauge metric called `FooSize`, which is given a value extracted from JSON messages at the path `foo.size`. We then also configure our Prometheus metric exporter to only emit this custom metric and nothing else. We also label the metric with some metadata. + +```yaml +pipeline: + processors: + - metric: + name: FooSize + type: gauge + labels: + topic: ${! meta("kafka_topic") } + value: ${! json("foo.size") } + +metrics: + mapping: 'if this != "FooSize" { deleted() }' + prometheus: {} +``` + +-- +====== + +== Types + +=== `counter` + +Increments a counter by exactly 1, the contents of `value` are ignored +by this type. + +=== `counter_by` + +If the contents of `value` can be parsed as a positive integer value +then the counter is incremented by this value. + +For example, the following configuration will increment the value of the +`count.custom.field` metric by the contents of `field.some.value`: + +```yaml +pipeline: + processors: + - metric: + type: counter_by + name: CountCustomField + value: ${!json("field.some.value")} +``` + +=== `gauge` + +If the contents of `value` can be parsed as a positive integer value +then the gauge is set to this value. + +For example, the following configuration will set the value of the +`gauge.custom.field` metric to the contents of `field.some.value`: + +```yaml +pipeline: + processors: + - metric: + type: gauge + name: GaugeCustomField + value: ${!json("field.some.value")} +``` + +=== `timing` + +Equivalent to `gauge` where instead the metric is a timing. It is recommended that timing values are recorded in nanoseconds in order to be consistent with standard Benthos timing metrics, as in some cases these values are automatically converted into other units such as when exporting timings as histograms with Prometheus metrics. + diff --git a/docs/modules/components/pages/processors/mongodb.adoc b/docs/modules/components/pages/processors/mongodb.adoc new file mode 100644 index 0000000000..bf7a471dc7 --- /dev/null +++ b/docs/modules/components/pages/processors/mongodb.adoc @@ -0,0 +1,268 @@ += mongodb +:type: processor +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs operations against MongoDB for each message, allowing you to store or retrieve data within message payloads. + +Introduced in version 3.43.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + operation: insert-one + write_concern: + w: "" + j: false + w_timeout: "" + document_map: "" + filter_map: "" + hint_map: "" + upsert: false +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +mongodb: + url: mongodb://localhost:27017 # No default (required) + database: "" # No default (required) + username: "" + password: "" + collection: "" # No default (required) + operation: insert-one + write_concern: + w: "" + j: false + w_timeout: "" + document_map: "" + filter_map: "" + hint_map: "" + upsert: false + json_marshal_mode: canonical +``` + +-- +====== + +== Fields + +=== `url` + +The URL of the target MongoDB server. + + +*Type*: `string` + + +```yml +# Examples + +url: mongodb://localhost:27017 +``` + +=== `database` + +The name of the target MongoDB database. + + +*Type*: `string` + + +=== `username` + +The username to connect to the database. + + +*Type*: `string` + +*Default*: `""` + +=== `password` + +The password to connect to the database. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `collection` + +The name of the target collection. + + +*Type*: `string` + + +=== `operation` + +The mongodb operation to perform. + + +*Type*: `string` + +*Default*: `"insert-one"` + +Options: +`insert-one` +, `delete-one` +, `delete-many` +, `replace-one` +, `update-one` +, `find-one` +. + +=== `write_concern` + +The write concern settings for the mongo connection. + + +*Type*: `object` + + +=== `write_concern.w` + +W requests acknowledgement that write operations propagate to the specified number of mongodb instances. + + +*Type*: `string` + +*Default*: `""` + +=== `write_concern.j` + +J requests acknowledgement from MongoDB that write operations are written to the journal. + + +*Type*: `bool` + +*Default*: `false` + +=== `write_concern.w_timeout` + +The write concern timeout. + + +*Type*: `string` + +*Default*: `""` + +=== `document_map` + +A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The document map is required for the operations insert-one, replace-one and update-one. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +document_map: |- + root.a = this.foo + root.b = this.bar +``` + +=== `filter_map` + +A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +filter_map: |- + root.a = this.foo + root.b = this.bar +``` + +=== `hint_map` + +A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +hint_map: |- + root.a = this.foo + root.b = this.bar +``` + +=== `upsert` + +The upsert setting is optional and only applies for update-one and replace-one operations. If the filter specified in filter_map matches, the document is updated or replaced accordingly, otherwise it is created. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.60.0 or newer + +=== `json_marshal_mode` + +The json_marshal_mode setting is optional and controls the format of the output message. + + +*Type*: `string` + +*Default*: `"canonical"` +Requires version 3.60.0 or newer + +|=== +| Option | Summary + +| `canonical` +| A string format that emphasizes type preservation at the expense of readability and interoperability. That is, conversion from canonical to BSON will generally preserve type information except in certain specific cases. +| `relaxed` +| A string format that emphasizes readability and interoperability at the expense of type preservation. That is, conversion from relaxed format to BSON can lose type information. + +|=== + + diff --git a/docs/modules/components/pages/processors/msgpack.adoc b/docs/modules/components/pages/processors/msgpack.adoc new file mode 100644 index 0000000000..b30c7431b4 --- /dev/null +++ b/docs/modules/components/pages/processors/msgpack.adoc @@ -0,0 +1,49 @@ += msgpack +:type: processor +:status: beta +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Converts messages to or from the https://msgpack.org/[MessagePack] format. + +Introduced in version 3.59.0. + +```yml +# Config fields, showing default values +label: "" +msgpack: + operator: "" # No default (required) +``` + +== Fields + +=== `operator` + +The operation to perform on messages. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `from_json` +| Convert JSON messages to MessagePack format +| `to_json` +| Convert MessagePack messages to JSON format + +|=== + + diff --git a/docs/modules/components/pages/processors/mutation.adoc b/docs/modules/components/pages/processors/mutation.adoc new file mode 100644 index 0000000000..1de35dd09a --- /dev/null +++ b/docs/modules/components/pages/processors/mutation.adoc @@ -0,0 +1,145 @@ += mutation +:type: processor +:status: stable +:categories: ["Mapping","Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping and directly transforms the contents of messages, mutating (or deleting) them. + +Introduced in version 4.5.0. + +```yml +# Config fields, showing default values +label: "" +mutation: "" # No default (required) +``` + +Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information, see xref:guides:bloblang/about.adoc[]. + +If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `from ""`, where the path must be absolute, or relative from the location that Benthos is executed from. + +== Input document mutability + +A mutation is a mapping that transforms input documents directly, this has the advantage of reducing the need to copy the data fed into the mapping. However, this also means that the referenced document is mutable and therefore changes throughout the mapping. For example, with the following Bloblang: + +```coffeescript +root.rejected = this.invitees.filter(i -> i.mood < 0.5) +root.invitees = this.invitees.filter(i -> i.mood >= 0.5) +``` + +Notice that we create a field `rejected` by copying the array field `invitees` and filtering out objects with a high mood. We then overwrite the field `invitees` by filtering out objects with a low mood, resulting in two array fields that are each a subset of the original. If we were to reverse the ordering of these assignments like so: + +```coffeescript +root.invitees = this.invitees.filter(i -> i.mood >= 0.5) +root.rejected = this.invitees.filter(i -> i.mood < 0.5) +``` + +Then the new field `rejected` would be empty as we have already mutated `invitees` to exclude the objects that it would be populated by. We can solve this problem either by carefully ordering our assignments or by capturing the original array using a variable (`let invitees = this.invitees`). + +Mutations are advantageous over a standard mapping in situations where the result is a document with mostly the same shape as the input document, since we can avoid unnecessarily copying data from the referenced input document. However, in situations where we are creating an entirely new document shape it can be more convenient to use the traditional xref:components:processors/mapping.adoc[`mapping` processor] instead. + +== Error handling + +Bloblang mappings can fail, in which case the error is logged and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. + +However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired xref:guides:bloblang/about.adoc#error-handling[fallback behavior]. + + +== Examples + +[tabs] +====== +Mapping:: ++ +-- + + +Given JSON documents containing an array of fans: + +```json +{ + "id":"foo", + "description":"a show about foo", + "fans":[ + {"name":"bev","obsession":0.57}, + {"name":"grace","obsession":0.21}, + {"name":"ali","obsession":0.89}, + {"name":"vic","obsession":0.43} + ] +} +``` + +We can reduce the documents down to just the ID and only those fans with an obsession score above 0.5, giving us: + +```json +{ + "id":"foo", + "fans":[ + {"name":"bev","obsession":0.57}, + {"name":"ali","obsession":0.89} + ] +} +``` + +With the following config: + +```yaml +pipeline: + processors: + - mutation: | + root.description = deleted() + root.fans = this.fans.filter(fan -> fan.obsession > 0.5) +``` + +-- +More Mapping:: ++ +-- + + +When receiving JSON documents of the form: + +```json +{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +} +``` + +We could collapse the location names from the state of Washington into a field `Cities`: + +```json +{"Cities": "Bellevue, Olympia, Seattle"} +``` + +With the following config: + +```yaml +pipeline: + processors: + - mutation: | + root.Cities = this.locations. + filter(loc -> loc.state == "WA"). + map_each(loc -> loc.name). + sort().join(", ") +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/nats_kv.adoc b/docs/modules/components/pages/processors/nats_kv.adoc new file mode 100644 index 0000000000..717ce3370b --- /dev/null +++ b/docs/modules/components/pages/processors/nats_kv.adoc @@ -0,0 +1,482 @@ += nats_kv +:type: processor +:status: beta +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Perform operations on a NATS key-value bucket. + +Introduced in version 4.12.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + operation: "" # No default (required) + key: foo # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +nats_kv: + urls: [] # No default (required) + bucket: my_kv_bucket # No default (required) + operation: "" # No default (required) + key: foo # No default (required) + revision: "42" # No default (optional) + timeout: 5s + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) +``` + +-- +====== + +== KV operations + +The NATS KV processor supports a multitude of KV operations via the <> field. Along with `get`, `put`, and `delete`, this processor supports atomic operations like `update` and `create`, as well as utility operations like `purge`, `history`, and `keys`. + +== Metadata + +This processor adds the following metadata fields to each message, depending on the chosen `operation`: + +=== get, get_revision +``` text +- nats_kv_key +- nats_kv_bucket +- nats_kv_revision +- nats_kv_delta +- nats_kv_operation +- nats_kv_created +``` + +=== create, update, delete, purge +``` text +- nats_kv_key +- nats_kv_bucket +- nats_kv_revision +- nats_kv_operation +``` + +=== keys +``` text +- nats_kv_bucket +``` + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `bucket` + +The name of the KV bucket. + + +*Type*: `string` + + +```yml +# Examples + +bucket: my_kv_bucket +``` + +=== `operation` + +The operation to perform on the KV bucket. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `create` +| Adds the key/value pair if it does not exist. Returns an error if it already exists. +| `delete` +| Deletes the key/value pair, but keeps historical values. +| `get` +| Returns the latest value for `key`. +| `get_revision` +| Returns the value of `key` for the specified `revision`. +| `history` +| Returns historical values of `key` as an array of objects containing the following fields: `key`, `value`, `bucket`, `revision`, `delta`, `operation`, `created`. +| `keys` +| Returns the keys in the `bucket` which match the `keys_filter` as an array of strings. +| `purge` +| Deletes the key/value pair and all historical values. +| `put` +| Places a new value for the key into the store. +| `update` +| Updates the value for `key` only if the `revision` matches the latest revision. + +|=== + +=== `key` + +The key for each message. Supports https://docs.nats.io/nats-concepts/subjects#wildcards[wildcards] for the `history` and `keys` operations. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +key: foo + +key: foo.bar.baz + +key: foo.* + +key: foo.> + +key: foo.${! json("meta.type") } +``` + +=== `revision` + +The revision of the key to operate on. Used for `get_revision` and `update` operations. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +revision: "42" + +revision: ${! @nats_kv_revision } +``` + +=== `timeout` + +The maximum period to wait on an operation before aborting and returning an error. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/processors/nats_request_reply.adoc b/docs/modules/components/pages/processors/nats_request_reply.adoc new file mode 100644 index 0000000000..ffea8cc3f4 --- /dev/null +++ b/docs/modules/components/pages/processors/nats_request_reply.adoc @@ -0,0 +1,487 @@ += nats_request_reply +:type: processor +:status: experimental +:categories: ["Services"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sends a message to a NATS subject and expects a reply, from a NATS subscriber acting as a responder, back. + +Introduced in version 4.27.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +nats_request_reply: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + timeout: 3s +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +nats_request_reply: + urls: [] # No default (required) + subject: foo.bar.baz # No default (required) + inbox_prefix: _INBOX_joe # No default (optional) + headers: {} + metadata: + include_prefixes: [] + include_patterns: [] + timeout: 3s + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + auth: + nkey_file: ./seed.nk # No default (optional) + user_credentials_file: ./user.creds # No default (optional) + user_jwt: "" # No default (optional) + user_nkey_seed: "" # No default (optional) +``` + +-- +====== + +== Metadata + +This input adds the following metadata fields to each message: + +```text +- nats_subject +- nats_sequence_stream +- nats_sequence_consumer +- nats_num_delivered +- nats_num_pending +- nats_domain +- nats_timestamp_unix_nano +``` + +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. + +== Connection name + +When monitoring and managing a production NATS system, it is often useful to +know which connection a message was send/received from. This can be achieved by +setting the connection name option when creating a NATS connection. + +Benthos will automatically set the connection name based off the label of the given +NATS component, so that monitoring tools between NATS and benthos can stay in sync. + + +== Authentication + +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. + +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file + +The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured +with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey +configured in the `nkey_file` field. + +https://docs.nats.io/developing-with-nats/security/nkey[More details]. + +=== User credentials + +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +which is configured to use this authentication scheme. + +The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. + +Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain +the plain text NKey Seed. + +https://docs.nats.io/developing-with-nats/security/creds[More details]. + +== Fields + +=== `urls` + +A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. + + +*Type*: `array` + + +```yml +# Examples + +urls: + - nats://127.0.0.1:4222 + +urls: + - nats://username:password@127.0.0.1:4222 +``` + +=== `subject` + +A subject to write to. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +subject: foo.bar.baz + +subject: ${! meta("kafka_topic") } + +subject: foo.${! json("meta.type") } +``` + +=== `inbox_prefix` + +Set an explicit inbox prefix for the response subject + + +*Type*: `string` + + +```yml +# Examples + +inbox_prefix: _INBOX_joe +``` + +=== `headers` + +Explicit message headers to add to messages. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + +*Default*: `{}` + +```yml +# Examples + +headers: + Content-Type: application/json + Timestamp: ${!meta("Timestamp")} +``` + +=== `metadata` + +Determine which (if any) metadata values should be added to messages as headers. + + +*Type*: `object` + + +=== `metadata.include_prefixes` + +Provide a list of explicit metadata key prefixes to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_prefixes: + - foo_ + - bar_ + +include_prefixes: + - kafka_ + +include_prefixes: + - content- +``` + +=== `metadata.include_patterns` + +Provide a list of explicit metadata key regular expression (re2) patterns to match against. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +include_patterns: + - .* + +include_patterns: + - _timestamp_unix$ +``` + +=== `timeout` + +A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as 300ms, -1.5h or 2h45m. Valid time units are ns, us (or µs), ms, s, m, h. + + +*Type*: `string` + +*Default*: `"3s"` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `auth` + +Optional configuration of NATS authentication parameters. + + +*Type*: `object` + + +=== `auth.nkey_file` + +An optional file containing a NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +nkey_file: ./seed.nk +``` + +=== `auth.user_credentials_file` + +An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. + + +*Type*: `string` + + +```yml +# Examples + +user_credentials_file: ./user.creds +``` + +=== `auth.user_jwt` + +An optional plain text user JWT (given along with the corresponding user NKey Seed). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + +=== `auth.user_nkey_seed` + +An optional plain text user NKey Seed (given along with the corresponding user JWT). +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/processors/noop.adoc b/docs/modules/components/pages/processors/noop.adoc new file mode 100644 index 0000000000..ffbf9bd258 --- /dev/null +++ b/docs/modules/components/pages/processors/noop.adoc @@ -0,0 +1,25 @@ += noop +:type: processor +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Noop is a processor that does nothing, the message passes through unchanged. Why? Sometimes doing nothing is the braver option. + +```yml +# Config fields, showing default values +label: "" +noop: {} +``` + + diff --git a/docs/modules/components/pages/processors/parallel.adoc b/docs/modules/components/pages/processors/parallel.adoc new file mode 100644 index 0000000000..d139492bee --- /dev/null +++ b/docs/modules/components/pages/processors/parallel.adoc @@ -0,0 +1,51 @@ += parallel +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message (similar to the xref:components:processors/for_each.adoc[`for_each`] processor), but where each message is processed in parallel. + +```yml +# Config fields, showing default values +label: "" +parallel: + cap: 0 + processors: [] # No default (required) +``` + +The field `cap`, if greater than zero, caps the maximum number of parallel processing threads. + +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching in xref:configuration:batching.adoc[]. + +== Fields + +=== `cap` + +The maximum number of messages to have processing at a given time. + + +*Type*: `int` + +*Default*: `0` + +=== `processors` + +A list of child processors to apply. + + +*Type*: `array` + + + diff --git a/docs/modules/components/pages/processors/parquet.adoc b/docs/modules/components/pages/processors/parquet.adoc new file mode 100644 index 0000000000..cb6645f435 --- /dev/null +++ b/docs/modules/components/pages/processors/parquet.adoc @@ -0,0 +1,155 @@ += parquet +:type: processor +:status: deprecated +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +[WARNING] +.Deprecated +==== +This component is deprecated and will be removed in the next major version release. Please consider moving onto <>. +==== +Converts batches of documents to or from https://parquet.apache.org/docs/[Parquet files]. + +Introduced in version 3.62.0. + +```yml +# Config fields, showing default values +label: "" +parquet: + operator: "" # No default (required) + compression: snappy + schema_file: schemas/foo.json # No default (optional) + schema: |- # No default (optional) + { + "Tag": "name=root, repetitiontype=REQUIRED", + "Fields": [ + {"Tag":"name=name,inname=NameIn,type=BYTE_ARRAY,convertedtype=UTF8, repetitiontype=REQUIRED"}, + {"Tag":"name=age,inname=Age,type=INT32,repetitiontype=REQUIRED"} + ] + } +``` + +== Alternatives + +This processor is now deprecated, it's recommended that you use the new xref:components:processors/parquet_decode.adoc[`parquet_decode`] and xref:components:processors/parquet_encode.adoc[`parquet_encode`] processors as they provide a number of advantages, the most important of which is better error messages for when schemas are mismatched or files could not be consumed. + +== Troubleshooting + +This processor is experimental and the error messages that it provides are often vague and unhelpful. An error message of the form `interface \{} is nil, not ` implies that a field of the given type was expected but not found in the processed message when writing parquet files. + +Unfortunately the name of the field will sometimes be missing from the error, in which case it's worth double checking the schema you provided to make sure that there are no typos in the field names, and if that doesn't reveal the issue it can help to mark fields as OPTIONAL in the schema and gradually change them back to REQUIRED until the error returns. + +== Define the schema + +The schema must be specified as a JSON string, containing an object that describes the fields expected at the root of each document. Each field can itself have more fields defined, allowing for nested structures: + +```json +{ + "Tag": "name=root, repetitiontype=REQUIRED", + "Fields": [ + {"Tag": "name=name, inname=NameIn, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"}, + {"Tag": "name=age, inname=Age, type=INT32, repetitiontype=REQUIRED"}, + {"Tag": "name=id, inname=Id, type=INT64, repetitiontype=REQUIRED"}, + {"Tag": "name=weight, inname=Weight, type=FLOAT, repetitiontype=REQUIRED"}, + { + "Tag": "name=favPokemon, inname=FavPokemon, type=LIST, repetitiontype=OPTIONAL", + "Fields": [ + {"Tag": "name=name, inname=PokeName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"}, + {"Tag": "name=coolness, inname=Coolness, type=FLOAT, repetitiontype=REQUIRED"} + ] + } + ] +} +``` + +A schema can be derived from a source file using https://github.com/xitongsys/parquet-go/tree/master/tool/parquet-tools: + +```sh +./parquet-tools -cmd schema -file foo.parquet +``` + +== Fields + +=== `operator` + +Determines whether the processor converts messages into a parquet file or expands parquet files into messages. Converting into JSON allows subsequent processors and mappings to convert the data into any other format. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `from_json` +| Compress a batch of JSON documents into a file. +| `to_json` +| Expand a file into one or more JSON messages. + +|=== + +=== `compression` + +The type of compression to use when writing parquet files, this field is ignored when consuming parquet files. + + +*Type*: `string` + +*Default*: `"snappy"` + +Options: +`uncompressed` +, `snappy` +, `gzip` +, `lz4` +, `zstd` +. + +=== `schema_file` + +A file path containing a schema used to describe the parquet files being generated or consumed, the format of the schema is a JSON document detailing the tag and fields of documents. The schema can be found at: https://pkg.go.dev/github.com/xitongsys/parquet-go#readme-json. Either a `schema_file` or `schema` field must be specified when creating Parquet files via the `from_json` operator. + + +*Type*: `string` + + +```yml +# Examples + +schema_file: schemas/foo.json +``` + +=== `schema` + +A schema used to describe the parquet files being generated or consumed, the format of the schema is a JSON document detailing the tag and fields of documents. The schema can be found at: https://pkg.go.dev/github.com/xitongsys/parquet-go#readme-json. Either a `schema_file` or `schema` field must be specified when creating Parquet files via the `from_json` operator. + + +*Type*: `string` + + +```yml +# Examples + +schema: |- + { + "Tag": "name=root, repetitiontype=REQUIRED", + "Fields": [ + {"Tag":"name=name,inname=NameIn,type=BYTE_ARRAY,convertedtype=UTF8, repetitiontype=REQUIRED"}, + {"Tag":"name=age,inname=Age,type=INT32,repetitiontype=REQUIRED"} + ] + } +``` + + diff --git a/docs/modules/components/pages/processors/parquet_decode.adoc b/docs/modules/components/pages/processors/parquet_decode.adoc new file mode 100644 index 0000000000..f7f5ab745a --- /dev/null +++ b/docs/modules/components/pages/processors/parquet_decode.adoc @@ -0,0 +1,61 @@ += parquet_decode +:type: processor +:status: experimental +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Decodes https://parquet.apache.org/docs/[Parquet files] into a batch of structured messages. + +Introduced in version 4.4.0. + +```yml +# Config fields, showing default values +label: "" +parquet_decode: {} +``` + +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. + +== Examples + +[tabs] +====== +Reading Parquet Files from AWS S3:: ++ +-- + +In this example we consume files from AWS S3 as they're written by listening onto an SQS queue for upload events. We make sure to use the `to_the_end` scanner which means files are read into memory in full, which then allows us to use a `parquet_decode` processor to expand each file into a batch of messages. Finally, we write the data out to local files as newline delimited JSON. + +```yaml +input: + aws_s3: + bucket: TODO + prefix: foos/ + scanner: + to_the_end: {} + sqs: + url: TODO + processors: + - parquet_decode: {} + +output: + file: + codec: lines + path: './foos/${! meta("s3_key") }.jsonl' +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/parquet_encode.adoc b/docs/modules/components/pages/processors/parquet_encode.adoc new file mode 100644 index 0000000000..1ebcddf83d --- /dev/null +++ b/docs/modules/components/pages/processors/parquet_encode.adoc @@ -0,0 +1,195 @@ += parquet_encode +:type: processor +:status: experimental +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Encodes https://parquet.apache.org/docs/[Parquet files] from a batch of structured messages. + +Introduced in version 4.4.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +parquet_encode: + schema: [] # No default (required) + default_compression: uncompressed +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +parquet_encode: + schema: [] # No default (required) + default_compression: uncompressed + default_encoding: DELTA_LENGTH_BYTE_ARRAY +``` + +-- +====== + +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. + + +== Examples + +[tabs] +====== +Writing Parquet Files to AWS S3:: ++ +-- + +In this example we use the batching mechanism of an `aws_s3` output to collect a batch of messages in memory, which then converts it to a parquet file and uploads it. + +```yaml +output: + aws_s3: + bucket: TODO + path: 'stuff/${! timestamp_unix() }-${! uuid_v4() }.parquet' + batching: + count: 1000 + period: 10s + processors: + - parquet_encode: + schema: + - name: id + type: INT64 + - name: weight + type: DOUBLE + - name: content + type: BYTE_ARRAY + default_compression: zstd +``` + +-- +====== + +== Fields + +=== `schema` + +Parquet schema. + + +*Type*: `array` + + +=== `schema[].name` + +The name of the column. + + +*Type*: `string` + + +=== `schema[].type` + +The type of the column, only applicable for leaf columns with no child fields. Some logical types can be specified here such as UTF8. + + +*Type*: `string` + + +Options: +`BOOLEAN` +, `INT32` +, `INT64` +, `FLOAT` +, `DOUBLE` +, `BYTE_ARRAY` +, `UTF8` +. + +=== `schema[].repeated` + +Whether the field is repeated. + + +*Type*: `bool` + +*Default*: `false` + +=== `schema[].optional` + +Whether the field is optional. + + +*Type*: `bool` + +*Default*: `false` + +=== `schema[].fields` + +A list of child fields. + + +*Type*: `array` + + +```yml +# Examples + +fields: + - name: foo + type: INT64 + - name: bar + type: BYTE_ARRAY +``` + +=== `default_compression` + +The default compression type to use for fields. + + +*Type*: `string` + +*Default*: `"uncompressed"` + +Options: +`uncompressed` +, `snappy` +, `gzip` +, `brotli` +, `zstd` +, `lz4raw` +. + +=== `default_encoding` + +The default encoding type to use for fields. A custom default encoding is only necessary when consuming data with libraries that do not support `DELTA_LENGTH_BYTE_ARRAY` and is therefore best left unset where possible. + + +*Type*: `string` + +*Default*: `"DELTA_LENGTH_BYTE_ARRAY"` +Requires version 4.11.0 or newer + +Options: +`DELTA_LENGTH_BYTE_ARRAY` +, `PLAIN` +. + + diff --git a/docs/modules/components/pages/processors/parse_log.adoc b/docs/modules/components/pages/processors/parse_log.adoc new file mode 100644 index 0000000000..4babc61756 --- /dev/null +++ b/docs/modules/components/pages/processors/parse_log.adoc @@ -0,0 +1,140 @@ += parse_log +:type: processor +:status: stable +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Parses common log <> into <>. This is easier and often much faster than xref:components:processors/grok.adoc[`grok`]. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +parse_log: + format: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +parse_log: + format: "" # No default (required) + best_effort: true + allow_rfc3339: true + default_year: current + default_timezone: UTC +``` + +-- +====== + +== Fields + +=== `format` + +A common log <> to parse. + + +*Type*: `string` + + +Options: +`syslog_rfc5424` +, `syslog_rfc3164` +. + +=== `best_effort` + +Still returns partially parsed messages even if an error occurs. + + +*Type*: `bool` + +*Default*: `true` + +=== `allow_rfc3339` + +Also accept timestamps in rfc3339 format while parsing. Applicable to format `syslog_rfc3164`. + + +*Type*: `bool` + +*Default*: `true` + +=== `default_year` + +Sets the strategy used to set the year for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. When set to `current` the current year will be set, when set to an integer that value will be used. Leave this field empty to not set a default year at all. + + +*Type*: `string` + +*Default*: `"current"` + +=== `default_timezone` + +Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the https://golang.org/pkg/time/#LoadLocation[time.LoadLocation] format. + + +*Type*: `string` + +*Default*: `"UTC"` + +== Codecs + +Currently the only supported structured data codec is `json`. + +== Formats + +=== `syslog_rfc5424` + +Attempts to parse a log following the https://tools.ietf.org/html/rfc5424[Syslog rfc5424] spec. The resulting structured document may contain any of the following fields: + +- `message` (string) +- `timestamp` (string, RFC3339) +- `facility` (int) +- `severity` (int) +- `priority` (int) +- `version` (int) +- `hostname` (string) +- `procid` (string) +- `appname` (string) +- `msgid` (string) +- `structureddata` (object) + +=== `syslog_rfc3164` + +Attempts to parse a log following the https://tools.ietf.org/html/rfc3164[Syslog rfc3164] spec. The resulting structured document may contain any of the following fields: + +- `message` (string) +- `timestamp` (string, RFC3339) +- `facility` (int) +- `severity` (int) +- `priority` (int) +- `hostname` (string) +- `procid` (string) +- `appname` (string) +- `msgid` (string) + + diff --git a/docs/modules/components/pages/processors/processors.adoc b/docs/modules/components/pages/processors/processors.adoc new file mode 100644 index 0000000000..651b39d9f2 --- /dev/null +++ b/docs/modules/components/pages/processors/processors.adoc @@ -0,0 +1,53 @@ += processors +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A processor grouping several sub-processors. + +```yml +# Config fields, showing default values +label: "" +processors: [] +``` + +This processor is useful in situations where you want to collect several processors under a single resource identifier, whether it is for making your configuration easier to read and navigate, or for improving the testability of your configuration. The behavior of child processors will match exactly the behavior they would have under any other processors block. + +== Examples + +[tabs] +====== +Grouped Processing:: ++ +-- + +Imagine we have a collection of processors who cover a specific functionality. We could use this processor to group them together and make it easier to read and mock during testing by giving the whole block a label: + +```yaml +pipeline: + processors: + - label: my_super_feature + processors: + - log: + message: "Let's do something cool" + - archive: + format: json_array + - mapping: root.items = this +``` + +-- +====== + + diff --git a/docs/modules/components/pages/processors/protobuf.adoc b/docs/modules/components/pages/processors/protobuf.adoc new file mode 100644 index 0000000000..a300be16e0 --- /dev/null +++ b/docs/modules/components/pages/processors/protobuf.adoc @@ -0,0 +1,198 @@ += protobuf +:type: processor +:status: stable +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + + +Performs conversions to or from a protobuf message. This processor uses reflection, meaning conversions can be made directly from the target .proto files. + + +```yml +# Config fields, showing default values +label: "" +protobuf: + operator: "" # No default (required) + message: "" # No default (required) + discard_unknown: false + use_proto_names: false + import_paths: [] +``` + +The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json[https://developers.google.com/protocol-buffers/docs/proto3#json] + +Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at https://github.com/benthosdev/benthos-plugin-example[https://github.com/benthosdev/benthos-plugin-example] + +== Operators + +=== `to_json` + +Converts protobuf messages into a generic JSON structure. This makes it easier to manipulate the contents of the document within Benthos. + +=== `from_json` + +Attempts to create a target protobuf message from a generic JSON structure. + + +== Examples + +[tabs] +====== +JSON to Protobuf:: ++ +-- + + +If we have the following protobuf definition within a directory called `testing/schema`: + +```protobuf +syntax = "proto3"; +package testing; + +import "google/protobuf/timestamp.proto"; + +message Person { + string first_name = 1; + string last_name = 2; + string full_name = 3; + int32 age = 4; + int32 id = 5; // Unique ID number for this person. + string email = 6; + + google.protobuf.Timestamp last_updated = 7; +} +``` + +And a stream of JSON documents of the form: + +```json +{ + "firstName": "caleb", + "lastName": "quaye", + "email": "caleb@myspace.com" +} +``` + +We can convert the documents into protobuf messages with the following config: + +```yaml +pipeline: + processors: + - protobuf: + operator: from_json + message: testing.Person + import_paths: [ testing/schema ] +``` + +-- +Protobuf to JSON:: ++ +-- + + +If we have the following protobuf definition within a directory called `testing/schema`: + +```protobuf +syntax = "proto3"; +package testing; + +import "google/protobuf/timestamp.proto"; + +message Person { + string first_name = 1; + string last_name = 2; + string full_name = 3; + int32 age = 4; + int32 id = 5; // Unique ID number for this person. + string email = 6; + + google.protobuf.Timestamp last_updated = 7; +} +``` + +And a stream of protobuf messages of the type `Person`, we could convert them into JSON documents of the format: + +```json +{ + "firstName": "caleb", + "lastName": "quaye", + "email": "caleb@myspace.com" +} +``` + +With the following config: + +```yaml +pipeline: + processors: + - protobuf: + operator: to_json + message: testing.Person + import_paths: [ testing/schema ] +``` + +-- +====== + +== Fields + +=== `operator` + +The <> to execute + + +*Type*: `string` + + +Options: +`to_json` +, `from_json` +. + +=== `message` + +The fully qualified name of the protobuf message to convert to/from. + + +*Type*: `string` + + +=== `discard_unknown` + +If `true`, the `from_json` operator discards fields that are unknown to the schema. + + +*Type*: `bool` + +*Default*: `false` + +=== `use_proto_names` + +If `true`, the `to_json` operator deserializes fields exactly as named in schema file. + + +*Type*: `bool` + +*Default*: `false` + +=== `import_paths` + +A list of directories containing .proto files, including all definitions required for parsing the target message. If left empty the current directory is used. Each directory listed will be walked with all found .proto files imported. + + +*Type*: `array` + +*Default*: `[]` + + diff --git a/docs/modules/components/pages/processors/rate_limit.adoc b/docs/modules/components/pages/processors/rate_limit.adoc new file mode 100644 index 0000000000..6c53e1502b --- /dev/null +++ b/docs/modules/components/pages/processors/rate_limit.adoc @@ -0,0 +1,37 @@ += rate_limit +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Throttles the throughput of a pipeline according to a specified xref:components:rate_limits/about.adoc[`rate_limit`] resource. Rate limits are shared across components and therefore apply globally to all processing pipelines. + +```yml +# Config fields, showing default values +label: "" +rate_limit: + resource: "" # No default (required) +``` + +== Fields + +=== `resource` + +The target xref:components:rate_limits/about.adoc[`rate_limit` resource]. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/processors/redis.adoc b/docs/modules/components/pages/processors/redis.adoc new file mode 100644 index 0000000000..1be4134e70 --- /dev/null +++ b/docs/modules/components/pages/processors/redis.adoc @@ -0,0 +1,403 @@ += redis +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs actions against Redis that aren't possible using a xef:components:processors/cache.adoc[`cache`] processor. Actions are +performed for each message and the message contents are replaced with the result. In order to merge the result into the original message compose this processor within a xref:components:processors/branch.adoc[`branch` processor]. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +redis: + url: redis://:6397 # No default (required) + command: scard # No default (optional) + args_mapping: root = [ this.key ] # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +redis: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + command: scard # No default (optional) + args_mapping: root = [ this.key ] # No default (optional) + retries: 3 + retry_period: 500ms +``` + +-- +====== + +== Examples + +[tabs] +====== +Querying Cardinality:: ++ +-- + +If given payloads containing a metadata field `set_key` it's possible to query and store the cardinality of the set for each message using a xref:components:processors/branch.adoc[`branch` processor] in order to augment rather than replace the message contents: + +```yaml +pipeline: + processors: + - branch: + processors: + - redis: + url: TODO + command: scard + args_mapping: 'root = [ meta("set_key") ]' + result_map: 'root.cardinality = this' +``` + +-- +Running Total:: ++ +-- + +If we have JSON data containing number of friends visited during covid 19: + +```json +{"name":"ash","month":"feb","year":2019,"friends_visited":10} +{"name":"ash","month":"apr","year":2019,"friends_visited":-2} +{"name":"bob","month":"feb","year":2019,"friends_visited":3} +{"name":"bob","month":"apr","year":2019,"friends_visited":1} +``` + +We can add a field that contains the running total number of friends visited: + +```json +{"name":"ash","month":"feb","year":2019,"friends_visited":10,"total":10} +{"name":"ash","month":"apr","year":2019,"friends_visited":-2,"total":8} +{"name":"bob","month":"feb","year":2019,"friends_visited":3,"total":3} +{"name":"bob","month":"apr","year":2019,"friends_visited":1,"total":4} +``` + +Using the `incrby` command: + +```yaml +pipeline: + processors: + - branch: + processors: + - redis: + url: TODO + command: incrby + args_mapping: 'root = [ this.name, this.friends_visited ]' + result_map: 'root.total = this' +``` + +-- +====== + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `command` + +The command to execute. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + +Requires version 4.3.0 or newer + +```yml +# Examples + +command: scard + +command: incrby + +command: ${! meta("command") } +``` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis command. + + +*Type*: `string` + +Requires version 4.3.0 or newer + +```yml +# Examples + +args_mapping: root = [ this.key ] + +args_mapping: root = [ meta("kafka_key"), this.count ] +``` + +=== `retries` + +The maximum number of retries before abandoning a request. + + +*Type*: `int` + +*Default*: `3` + +=== `retry_period` + +The time to wait before consecutive retry attempts. + + +*Type*: `string` + +*Default*: `"500ms"` + + diff --git a/docs/modules/components/pages/processors/redis_script.adoc b/docs/modules/components/pages/processors/redis_script.adoc new file mode 100644 index 0000000000..f25b2f6783 --- /dev/null +++ b/docs/modules/components/pages/processors/redis_script.adoc @@ -0,0 +1,389 @@ += redis_script +:type: processor +:status: beta +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Performs actions against Redis using https://redis.io/docs/manual/programmability/eval-intro/[LUA scripts]. + +Introduced in version 4.11.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +redis_script: + url: redis://:6397 # No default (required) + script: return redis.call('set', KEYS[1], ARGV[1]) # No default (required) + args_mapping: root = [ this.key ] # No default (required) + keys_mapping: root = [ this.key ] # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +redis_script: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + script: return redis.call('set', KEYS[1], ARGV[1]) # No default (required) + args_mapping: root = [ this.key ] # No default (required) + keys_mapping: root = [ this.key ] # No default (required) + retries: 3 + retry_period: 500ms +``` + +-- +====== + +Actions are performed for each message and the message contents are replaced with the result. + +In order to merge the result into the original message compose this processor within a xref:components:processors/branch.adoc[`branch` processor]. + +== Examples + +[tabs] +====== +Running a script:: ++ +-- + +The following example will use a script execution to get next element from a sorted set and set its score with timestamp unix nano value. + +```yaml +pipeline: + processors: + - redis_script: + url: TODO + script: | + local value = redis.call("ZRANGE", KEYS[1], '0', '0') + + if next(elements) == nil then + return '' + end + + redis.call("ZADD", "XX", KEYS[1], ARGV[1], value) + + return value + keys_mapping: 'root = [ meta("key") ]' + args_mapping: 'root = [ timestamp_unix_nano() ]' +``` + +-- +====== + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `script` + +A script to use for the target operator. It has precedence over the 'command' field. + + +*Type*: `string` + + +```yml +# Examples + +script: return redis.call('set', KEYS[1], ARGV[1]) +``` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis script. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.key ] + +args_mapping: root = [ meta("kafka_key"), "hardcoded_value" ] +``` + +=== `keys_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of keys matching in size to the number of arguments required for the specified Redis script. + + +*Type*: `string` + + +```yml +# Examples + +keys_mapping: root = [ this.key ] + +keys_mapping: root = [ meta("kafka_key"), this.count ] +``` + +=== `retries` + +The maximum number of retries before abandoning a request. + + +*Type*: `int` + +*Default*: `3` + +=== `retry_period` + +The time to wait before consecutive retry attempts. + + +*Type*: `string` + +*Default*: `"500ms"` + + diff --git a/docs/modules/components/pages/processors/resource.adoc b/docs/modules/components/pages/processors/resource.adoc new file mode 100644 index 0000000000..cc6328cfa1 --- /dev/null +++ b/docs/modules/components/pages/processors/resource.adoc @@ -0,0 +1,53 @@ += resource +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Resource is a processor type that runs a processor resource identified by its label. + +```yml +# Config fields, showing default values +resource: "" +``` + +This processor allows you to reference the same configured processor resource in multiple places, and can also tidy up large nested configs. For example, the config: + +```yaml +pipeline: + processors: + - mapping: | + root.message = this + root.meta.link_count = this.links.length() + root.user.age = this.user.age.number() +``` + +Is equivalent to: + +```yaml +pipeline: + processors: + - resource: foo_proc + +processor_resources: + - label: foo_proc + mapping: | + root.message = this + root.meta.link_count = this.links.length() + root.user.age = this.user.age.number() +``` + +You can find out more about resources in xref:configuration:resources.adoc[] + + diff --git a/docs/modules/components/pages/processors/retry.adoc b/docs/modules/components/pages/processors/retry.adoc new file mode 100644 index 0000000000..0106df1752 --- /dev/null +++ b/docs/modules/components/pages/processors/retry.adoc @@ -0,0 +1,188 @@ += retry +:type: processor +:status: beta +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Attempts to execute a series of child processors until success. + +Introduced in version 4.27.0. + +```yml +# Config fields, showing default values +label: "" +retry: + backoff: + initial_interval: 500ms + max_interval: 10s + max_elapsed_time: 1m + processors: [] # No default (required) + parallel: false +``` + +Executes child processors and if a resulting message is errored then, after a specified backoff period, the same original message will be attempted again through those same processors. If the child processors result in more than one message then the retry mechanism will kick in if _any_ of the resulting messages are errored. + +It is important to note that any mutations performed on the message during these child processors will be discarded for the next retry, and therefore it is safe to assume that each execution of the child processors will always be performed on the data as it was when it first reached the retry processor. + +By default the retry backoff has a specified <>, if this time period is reached during retries and an error still occurs these errored messages will proceed through to the next processor after the retry (or your outputs). Normal xref:configuration:error_handling.adoc[error handling patterns] can be used on these messages. + +In order to avoid permanent loops any error associated with messages as they first enter a retry processor will be cleared. + +[CAUTION] +.Batching +==== +If you wish to wrap a batch-aware series of processors then take a look at the <>. +==== + + +== Examples + +[tabs] +====== +Stop ignoring me Taz:: ++ +-- + + +Here we have a config where I generate animal noises and send them to Taz via HTTP. Taz has a tendency to stop his servers whenever I dispatch my animals upon him, and therefore these HTTP requests sometimes fail. However, I have the retry processor and with this super power I can specify a back off policy and it will ensure that for each animal noise the HTTP processor is attempted until either it succeeds or my Benthos instance is stopped. + +I even go as far as to zero-out the maximum elapsed time field, which means that for each animal noise I will wait indefinitely, because I really really want Taz to receive every single animal noise that he is entitled to. + +```yaml +input: + generate: + interval: 1s + mapping: 'root.noise = [ "woof", "meow", "moo", "quack" ].index(random_int(min: 0, max: 3))' + +pipeline: + processors: + - retry: + backoff: + initial_interval: 100ms + max_interval: 5s + max_elapsed_time: 0s + processors: + - http: + url: 'http://example.com/try/not/to/dox/taz' + verb: POST + +output: + # Drop everything because it's junk data, I don't want it lol + drop: {} +``` + +-- +====== + +== Fields + +=== `backoff` + +Determine time intervals and cut offs for retry attempts. + + +*Type*: `object` + + +=== `backoff.initial_interval` + +The initial period to wait between retry attempts. + + +*Type*: `string` + +*Default*: `"500ms"` + +```yml +# Examples + +initial_interval: 50ms + +initial_interval: 1s +``` + +=== `backoff.max_interval` + +The maximum period to wait between retry attempts + + +*Type*: `string` + +*Default*: `"10s"` + +```yml +# Examples + +max_interval: 5s + +max_interval: 1m +``` + +=== `backoff.max_elapsed_time` + +The maximum overall period of time to spend on retry attempts before the request is aborted. Setting this value to a zeroed duration (such as `0s`) will result in unbounded retries. + + +*Type*: `string` + +*Default*: `"1m"` + +```yml +# Examples + +max_elapsed_time: 1m + +max_elapsed_time: 1h +``` + +=== `processors` + +A list of xref:components:processors/about.adoc[processors] to execute on each message. + + +*Type*: `array` + + +=== `parallel` + +When processing batches of messages these batches are ignored and the processors apply to each message sequentially. However, when this field is set to `true` each message will be processed in parallel. Caution should be made to ensure that batch sizes do not surpass a point where this would cause resource (CPU, memory, API limits) contention. + + +*Type*: `bool` + +*Default*: `false` + +== Batching + +When messages are batched the child processors of a retry are executed for each individual message in isolation, performed serially by default but in parallel when the field <> is set to `true`. This is an intentional limitation of the retry processor and is done in order to ensure that errors are correctly associated with a given input message. Otherwise, the archiving, expansion, grouping, filtering and so on of the child processors could obfuscate this relationship. + +If the target behavior of your retried processors is "batch aware", in that you wish to perform some processing across the entire batch of messages and repeat it in the event of errors, you can use an xref:components:processors/archive.adoc[`archive` processor] to collapse the batch into an individual message. Then, within these child processors either perform your batch aware processing on the archive, or use an xref:components:processors/unarchive.adoc[`unarchive` processor] in order to expand the single message back out into a batch. + +For example, if the retry processor were being used to wrap an HTTP request where the payload data is a batch archived into a JSON array it should look something like this: + +```yaml +pipeline: + processors: + - archive: + format: json_array + - retry: + processors: + - http: + url: example.com/nope + verb: POST + - unarchive: + format: json_array +``` + + diff --git a/docs/modules/components/pages/processors/schema_registry_decode.adoc b/docs/modules/components/pages/processors/schema_registry_decode.adoc new file mode 100644 index 0000000000..fa376a6222 --- /dev/null +++ b/docs/modules/components/pages/processors/schema_registry_decode.adoc @@ -0,0 +1,429 @@ += schema_registry_decode +:type: processor +:status: beta +:categories: ["Parsing","Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Automatically decodes and validates messages with schemas from a Confluent Schema Registry service. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +schema_registry_decode: + url: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +schema_registry_decode: + avro_raw_json: false + url: "" # No default (required) + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} + tls: + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] +``` + +-- +====== + +Decodes messages automatically from a schema stored within a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. + +== Avro JSON format + +This processor creates documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: + +- if its type is `null`, then it is encoded as a JSON `null`; +- otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. + +For example, the union schema `["null","string","Foo"]`, where `Foo` is a record name, would encode: + +- `null` as `null`; +- the string `"a"` as `\{"string": "a"}`; and +- a `Foo` instance as `\{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. + +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field <> to `true`. + +== Protobuf format + +This processor decodes protobuf messages to JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json + + +== Fields + +=== `avro_raw_json` + +Whether Avro messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json. + + +*Type*: `bool` + +*Default*: `false` + +=== `url` + +The base URL of the schema registry service. + + +*Type*: `string` + + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + +Requires version 4.7.0 or newer + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + +Requires version 4.7.0 or newer + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + +Requires version 4.7.0 or newer + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + + diff --git a/docs/modules/components/pages/processors/schema_registry_encode.adoc b/docs/modules/components/pages/processors/schema_registry_encode.adoc new file mode 100644 index 0000000000..10ff2149f9 --- /dev/null +++ b/docs/modules/components/pages/processors/schema_registry_encode.adoc @@ -0,0 +1,482 @@ += schema_registry_encode +:type: processor +:status: beta +:categories: ["Parsing","Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Automatically encodes and validates messages with schemas from a Confluent Schema Registry service. + +Introduced in version 3.58.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +schema_registry_encode: + url: "" # No default (required) + subject: foo # No default (required) + refresh_period: 10m +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +schema_registry_encode: + url: "" # No default (required) + subject: foo # No default (required) + refresh_period: 10m + avro_raw_json: false + oauth: + enabled: false + consumer_key: "" + consumer_secret: "" + access_token: "" + access_token_secret: "" + basic_auth: + enabled: false + username: "" + password: "" + jwt: + enabled: false + private_key_file: "" + signing_method: "" + claims: {} + headers: {} + tls: + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] +``` + +-- +====== + +Encodes messages automatically from schemas obtains from a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by polling the service for the latest schema version for target subjects. + +If a message fails to encode under the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. + +== Avro JSON format + +By default this processor expects documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: + +- if its type is `null`, then it is encoded as a JSON `null`; +- otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. + +For example, the union schema `["null","string","Foo"]`, where `Foo` is a record name, would encode: + +- `null` as `null`; +- the string `"a"` as `\{"string": "a"}`; and +- a `Foo` instance as `\{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. + +However, it is possible to instead consume documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field <> to `true`. + +=== Known issues + +Important! There is an outstanding issue in the https://github.com/linkedin/goavro[avro serializing library] that benthos uses which means it https://github.com/linkedin/goavro/issues/252[doesn't encode logical types correctly]. It's still possible to encode logical types that are in-line with the spec if `avro_raw_json` is set to true, though now of course non-logical types will not be in-line with the spec. + +== Protobuf format + +This processor encodes protobuf messages either from any format parsed within Benthos (encoded as JSON by default), or from raw JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json + +=== Multiple message support + +When a target subject presents a protobuf schema that contains multiple messages it becomes ambiguous which message definition a given input data should be encoded against. In such scenarios Benthos will attempt to encode the data against each of them and select the first to successfully match against the data, this process currently *ignores all nested message definitions*. In order to speed up this exhaustive search the last known successful message will be attempted first for each subsequent input. + +We will be considering alternative approaches in future so please https://redpanda.com/slack[get in touch] with thoughts and feedback. + + +== Fields + +=== `url` + +The base URL of the schema registry service. + + +*Type*: `string` + + +=== `subject` + +The schema subject to derive schemas from. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +subject: foo + +subject: ${! meta("kafka_topic") } +``` + +=== `refresh_period` + +The period after which a schema is refreshed for each subject, this is done by polling the schema registry service. + + +*Type*: `string` + +*Default*: `"10m"` + +```yml +# Examples + +refresh_period: 60s + +refresh_period: 1h +``` + +=== `avro_raw_json` + +Whether messages encoded in Avro format should be parsed as normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be parsed as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between standard json and avro json. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.59.0 or newer + +=== `oauth` + +Allows you to specify open authentication via OAuth version 1. + + +*Type*: `object` + +Requires version 4.7.0 or newer + +=== `oauth.enabled` + +Whether to use OAuth version 1 in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `oauth.consumer_key` + +A value used to identify the client to the service provider. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.consumer_secret` + +A secret used to establish ownership of the consumer key. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token` + +A value used to gain access to the protected resources on behalf of the user. + + +*Type*: `string` + +*Default*: `""` + +=== `oauth.access_token_secret` + +A secret provided in order to establish ownership of a given access token. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth` + +Allows you to specify basic authentication. + + +*Type*: `object` + +Requires version 4.7.0 or newer + +=== `basic_auth.enabled` + +Whether to use basic authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `basic_auth.username` + +A username to authenticate as. + + +*Type*: `string` + +*Default*: `""` + +=== `basic_auth.password` + +A password to authenticate with. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `jwt` + +BETA: Allows you to specify JWT authentication. + + +*Type*: `object` + +Requires version 4.7.0 or newer + +=== `jwt.enabled` + +Whether to use JWT authentication in requests. + + +*Type*: `bool` + +*Default*: `false` + +=== `jwt.private_key_file` + +A file with the PEM encoded via PKCS1 or PKCS8 as private key. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.signing_method` + +A method used to sign the token such as RS256, RS384, RS512 or EdDSA. + + +*Type*: `string` + +*Default*: `""` + +=== `jwt.claims` + +A value used to identify the claims that issued the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `jwt.headers` + +Add optional key/value headers to the JWT. + + +*Type*: `object` + +*Default*: `{}` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + + +*Type*: `object` + + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + + diff --git a/docs/modules/components/pages/processors/select_parts.adoc b/docs/modules/components/pages/processors/select_parts.adoc new file mode 100644 index 0000000000..4471276cb9 --- /dev/null +++ b/docs/modules/components/pages/processors/select_parts.adoc @@ -0,0 +1,46 @@ += select_parts +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Cherry pick a set of messages from a batch by their index. Indexes larger than the number of messages are simply ignored. + +```yml +# Config fields, showing default values +label: "" +select_parts: + parts: [] +``` + +The selected parts are added to the new message batch in the same order as the selection array. E.g. with 'parts' set to [ 2, 0, 1 ] and the message parts [ '0', '1', '2', '3' ], the output will be [ '2', '0', '1' ]. + +If none of the selected parts exist in the input batch (resulting in an empty output message) the batch is dropped entirely. + +Message indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. E.g. if index = -1 then the selected part will be the last part of the message, if index = -2 then the part before the last element with be selected, and so on. + +This processor is only applicable to xref:configuration:batching.adoc[batched messages]. + +== Fields + +=== `parts` + +An array of message indexes of a batch. Indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. + + +*Type*: `array` + +*Default*: `[]` + + diff --git a/docs/modules/components/pages/processors/sentry_capture.adoc b/docs/modules/components/pages/processors/sentry_capture.adoc new file mode 100644 index 0000000000..26a3eb989e --- /dev/null +++ b/docs/modules/components/pages/processors/sentry_capture.adoc @@ -0,0 +1,157 @@ += sentry_capture +:type: processor +:status: experimental + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Captures log events from messages and submits them to https://sentry.io/[Sentry]. + +Introduced in version 4.16.0. + +```yml +# Config fields, showing default values +label: "" +sentry_capture: + dsn: "" + message: webhook event received # No default (required) + context: 'root = {"order": {"product_id": "P93174", "quantity": 5}}' # No default (optional) + tags: {} # No default (optional) + environment: "" + release: "" + level: INFO + transport_mode: async + flush_timeout: 5s + sampling_rate: 1 +``` + +== Fields + +=== `dsn` + +The DSN address to send sentry events to. If left empty, then SENTRY_DSN is used. + + +*Type*: `string` + +*Default*: `""` + +=== `message` + +A message to set on the sentry event +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + +```yml +# Examples + +message: webhook event received + +message: 'failed to find product in database: ${! error() }' +``` + +=== `context` + +A mapping that must evaluate to an object-of-objects or `deleted()`. If this mapping produces a value, then it is set on a sentry event as additional context. + + +*Type*: `string` + + +```yml +# Examples + +context: 'root = {"order": {"product_id": "P93174", "quantity": 5}}' + +context: root = deleted() +``` + +=== `tags` + +Sets key/value string tags on an event. Unlike context, these are indexed and searchable on Sentry but have length limitations. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `object` + + +=== `environment` + +The environment to be sent with events. If left empty, then SENTRY_ENVIRONMENT is used. + + +*Type*: `string` + +*Default*: `""` + +=== `release` + +The version of the code deployed to an environment. If left empty, then the Sentry client will attempt to detect the release from the environment. + + +*Type*: `string` + +*Default*: `""` + +=== `level` + +Sets the level on sentry events similar to logging levels. + + +*Type*: `string` + +*Default*: `"INFO"` + +Options: +`DEBUG` +, `INFO` +, `WARN` +, `ERROR` +, `FATAL` +. + +=== `transport_mode` + +Determines how events are sent. A sync transport will block when sending each event until a response is received from the Sentry server. The recommended async transport will enqueue events in a buffer and send them in the background. + + +*Type*: `string` + +*Default*: `"async"` + +Options: +`async` +, `sync` +. + +=== `flush_timeout` + +The duration to wait when closing the processor to flush any remaining enqueued events. + + +*Type*: `string` + +*Default*: `"5s"` + +=== `sampling_rate` + +The rate at which events are sent to the server. A value of 0 disables capturing sentry events entirely. A value of 1 results in sending all events to Sentry. Any value in between results sending some percentage of events. + + +*Type*: `float` + +*Default*: `1` + + diff --git a/docs/modules/components/pages/processors/sleep.adoc b/docs/modules/components/pages/processors/sleep.adoc new file mode 100644 index 0000000000..3c68edf489 --- /dev/null +++ b/docs/modules/components/pages/processors/sleep.adoc @@ -0,0 +1,38 @@ += sleep +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Sleep for a period of time specified as a duration string for each message. This processor will interpolate functions within the `duration` field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here]. + +```yml +# Config fields, showing default values +label: "" +sleep: + duration: "" # No default (required) +``` + +== Fields + +=== `duration` + +The duration of time to sleep for each execution. +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/processors/split.adoc b/docs/modules/components/pages/processors/split.adoc new file mode 100644 index 0000000000..71529cd47e --- /dev/null +++ b/docs/modules/components/pages/processors/split.adoc @@ -0,0 +1,52 @@ += split +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Breaks message batches (synonymous with multiple part messages) into smaller batches. The size of the resulting batches are determined either by a discrete size or, if the field `byte_size` is non-zero, then by total size in bytes (which ever limit is reached first). + +```yml +# Config fields, showing default values +label: "" +split: + size: 1 + byte_size: 0 +``` + +This processor is for breaking batches down into smaller ones. In order to break a single message out into multiple messages use the xref:components:processors/unarchive.adoc[`unarchive` processor]. + +If there is a remainder of messages after splitting a batch the remainder is also sent as a single batch. For example, if your target size was 10, and the processor received a batch of 95 message parts, the result would be 9 batches of 10 messages followed by a batch of 5 messages. + +== Fields + +=== `size` + +The target number of messages. + + +*Type*: `int` + +*Default*: `1` + +=== `byte_size` + +An optional target of total message bytes. + + +*Type*: `int` + +*Default*: `0` + + diff --git a/docs/modules/components/pages/processors/sql.adoc b/docs/modules/components/pages/processors/sql.adoc new file mode 100644 index 0000000000..4a57f9cc68 --- /dev/null +++ b/docs/modules/components/pages/processors/sql.adoc @@ -0,0 +1,161 @@ += sql +:type: processor +:status: deprecated +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +[WARNING] +.Deprecated +==== +This component is deprecated and will be removed in the next major version release. Please consider moving onto <>. +==== +Runs an arbitrary SQL query against a database and (optionally) returns the result as an array of objects, one for each row returned. + +Introduced in version 3.65.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +sql: + driver: "" # No default (required) + data_source_name: "" # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + result_codec: none +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +sql: + driver: "" # No default (required) + data_source_name: "" # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + unsafe_dynamic_query: false + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + result_codec: none +``` + +-- +====== + +If the query fails to execute then the message will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +== Alternatives + +For basic inserts or select queries use either the xref:components:processors/sql_insert.adoc[`sql_insert`] or the xref:components:processors/sql_select.adoc[`sql_select`] processor. For more complex queries use the xref:components:processors/sql_raw.adoc[`sql_raw`] processor. + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `data_source_name` + +Data source name. + + +*Type*: `string` + + +=== `query` + +The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: + +| Driver | Placeholder Style | +|---|---| +| `clickhouse` | Dollar sign | +| `mysql` | Question mark | +| `postgres` | Dollar sign | +| `mssql` | Question mark | +| `sqlite` | Question mark | +| `oracle` | Colon | +| `snowflake` | Question mark | +| `trino` | Question mark | +| `gocosmos` | Colon | + + +*Type*: `string` + + +```yml +# Examples + +query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); +``` + +=== `unsafe_dynamic_query` + +Whether to enable xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the query. Great care should be made to ensure your queries are defended against injection attacks. + + +*Type*: `bool` + +*Default*: `false` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `result_codec` + +Result codec. + + +*Type*: `string` + +*Default*: `"none"` + + diff --git a/docs/modules/components/pages/processors/sql_insert.adoc b/docs/modules/components/pages/processors/sql_insert.adoc new file mode 100644 index 0000000000..76d3b987b9 --- /dev/null +++ b/docs/modules/components/pages/processors/sql_insert.adoc @@ -0,0 +1,339 @@ += sql_insert +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Inserts rows into an SQL database for each message, and leaves the message unchanged. + +Introduced in version 3.59.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +sql_insert: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +sql_insert: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) + prefix: "" # No default (optional) + suffix: ON CONFLICT (name) DO NOTHING # No default (optional) + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) +``` + +-- +====== + +If the insert fails to execute then the message will still remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +== Examples + +[tabs] +====== +Table Insert (MySQL):: ++ +-- + + +Here we insert rows into a database by populating the columns id, name and topic with values extracted from messages and metadata: + +```yaml +pipeline: + processors: + - sql_insert: + driver: mysql + dsn: foouser:foopassword@tcp(localhost:3306)/foodb + table: footable + columns: [ id, name, topic ] + args_mapping: | + root = [ + this.user.id, + this.user.name, + meta("kafka_topic"), + ] +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `table` + +The table to insert to. + + +*Type*: `string` + + +```yml +# Examples + +table: foo +``` + +=== `columns` + +A list of columns to insert. + + +*Type*: `array` + + +```yml +# Examples + +columns: + - foo + - bar + - baz +``` + +=== `args_mapping` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of columns specified. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `prefix` + +An optional prefix to prepend to the insert query (before INSERT). + + +*Type*: `string` + + +=== `suffix` + +An optional suffix to append to the insert query. + + +*Type*: `string` + + +```yml +# Examples + +suffix: ON CONFLICT (name) DO NOTHING +``` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/processors/sql_raw.adoc b/docs/modules/components/pages/processors/sql_raw.adoc new file mode 100644 index 0000000000..7a4248c1b7 --- /dev/null +++ b/docs/modules/components/pages/processors/sql_raw.adoc @@ -0,0 +1,345 @@ += sql_raw +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Runs an arbitrary SQL query against a database and (optionally) returns the result as an array of objects, one for each row returned. + +Introduced in version 3.65.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +sql_raw: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + exec_only: false +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +sql_raw: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) + unsafe_dynamic_query: false + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + exec_only: false + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) +``` + +-- +====== + +If the query fails to execute then the message will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +== Examples + +[tabs] +====== +Table Insert (MySQL):: ++ +-- + +The following example inserts rows into the table footable with the columns foo, bar and baz populated with values extracted from messages. + +```yaml +pipeline: + processors: + - sql_raw: + driver: mysql + dsn: foouser:foopassword@tcp(localhost:3306)/foodb + query: "INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?);" + args_mapping: '[ document.foo, document.bar, meta("kafka_topic") ]' + exec_only: true +``` + +-- +Table Query (PostgreSQL):: ++ +-- + +Here we query a database for columns of footable that share a `user_id` with the message field `user.id`. A xref:components:processors/branch.adoc[`branch` processor] is used in order to insert the resulting array into the original message at the path `foo_rows`. + +```yaml +pipeline: + processors: + - branch: + processors: + - sql_raw: + driver: postgres + dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable + query: "SELECT * FROM footable WHERE user_id = $1;" + args_mapping: '[ this.user.id ]' + result_map: 'root.foo_rows = this' +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `query` + +The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: + +| Driver | Placeholder Style | +|---|---| +| `clickhouse` | Dollar sign | +| `mysql` | Question mark | +| `postgres` | Dollar sign | +| `mssql` | Question mark | +| `sqlite` | Question mark | +| `oracle` | Colon | +| `snowflake` | Question mark | +| `trino` | Question mark | +| `gocosmos` | Colon | + + +*Type*: `string` + + +```yml +# Examples + +query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); + +query: SELECT * FROM footable WHERE user_id = $1; +``` + +=== `unsafe_dynamic_query` + +Whether to enable xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the query. Great care should be made to ensure your queries are defended against injection attacks. + + +*Type*: `bool` + +*Default*: `false` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `exec_only` + +Whether the query result should be discarded. When set to `true` the message contents will remain unchanged, which is useful in cases where you are executing inserts, updates, etc. + + +*Type*: `bool` + +*Default*: `false` + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/processors/sql_select.adoc b/docs/modules/components/pages/processors/sql_select.adoc new file mode 100644 index 0000000000..e1a7f657a1 --- /dev/null +++ b/docs/modules/components/pages/processors/sql_select.adoc @@ -0,0 +1,356 @@ += sql_select +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Runs an SQL select query against a database and returns the result as an array of objects, one for each row returned, containing a key for each column queried and its value. + +Introduced in version 3.59.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +sql_select: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + where: meow = ? and woof = ? # No default (optional) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +sql_select: + driver: "" # No default (required) + dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) + table: foo # No default (required) + columns: [] # No default (required) + where: meow = ? and woof = ? # No default (optional) + args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) + prefix: "" # No default (optional) + suffix: "" # No default (optional) + init_files: [] # No default (optional) + init_statement: | # No default (optional) + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; + conn_max_idle_time: "" # No default (optional) + conn_max_life_time: "" # No default (optional) + conn_max_idle: 2 + conn_max_open: 0 # No default (optional) +``` + +-- +====== + +If the query fails to execute then the message will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. + +== Examples + +[tabs] +====== +Table Query (PostgreSQL):: ++ +-- + + +Here we query a database for columns of footable that share a `user_id` +with the message `user.id`. A xref:components:processors/branch.adoc[`branch` processor] +is used in order to insert the resulting array into the original message at the +path `foo_rows`: + +```yaml +pipeline: + processors: + - branch: + processors: + - sql_select: + driver: postgres + dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable + table: footable + columns: [ '*' ] + where: user_id = ? + args_mapping: '[ this.user.id ]' + result_map: 'root.foo_rows = this' +``` + +-- +====== + +== Fields + +=== `driver` + +A database <> to use. + + +*Type*: `string` + + +Options: +`mysql` +, `postgres` +, `clickhouse` +, `mssql` +, `sqlite` +, `oracle` +, `snowflake` +, `trino` +, `gocosmos` +. + +=== `dsn` + +A Data Source Name to identify the target database. + +==== Drivers + +The following is a list of supported drivers, their placeholder style, and their respective DSN formats: + +|=== +| Driver | Data Source Name Format + +| `clickhouse` +| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] + +| `mysql` +| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + +| `postgres` +| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` + +| `mssql` +| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` + +| `sqlite` +| `file:/path/to/filename.db[?param&=value1&...]` + +| `oracle` +| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` + +| `snowflake` +| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` + +| `trino` +| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] + +| `gocosmos` +| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] +|=== + +Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. + +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. + +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. + + +*Type*: `string` + + +```yml +# Examples + +dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + +dsn: foouser:foopassword@tcp(localhost:3306)/foodb + +dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable + +dsn: oracle://foouser:foopass@localhost:1521/service_name +``` + +=== `table` + +The table to query. + + +*Type*: `string` + + +```yml +# Examples + +table: foo +``` + +=== `columns` + +A list of columns to query. + + +*Type*: `array` + + +```yml +# Examples + +columns: + - '*' + +columns: + - foo + - bar + - baz +``` + +=== `where` + +An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks, and will automatically be converted to dollar syntax when the postgres or clickhouse drivers are used. + + +*Type*: `string` + + +```yml +# Examples + +where: meow = ? and woof = ? + +where: user_id = ? +``` + +=== `args_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. + + +*Type*: `string` + + +```yml +# Examples + +args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] + +args_mapping: root = [ meta("user.id") ] +``` + +=== `prefix` + +An optional prefix to prepend to the query (before SELECT). + + +*Type*: `string` + + +=== `suffix` + +An optional suffix to append to the select query. + + +*Type*: `string` + + +=== `init_files` + +An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). + +Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `array` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_files: + - ./init/*.sql + +init_files: + - ./foo.sql + - ./bar.sql +``` + +=== `init_statement` + +An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. + +If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. + +If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. + + +*Type*: `string` + +Requires version 4.10.0 or newer + +```yml +# Examples + +init_statement: |2 + CREATE TABLE IF NOT EXISTS some_table ( + foo varchar(50) not null, + bar integer, + baz varchar(50), + primary key (foo) + ) WITHOUT ROWID; +``` + +=== `conn_max_idle_time` + +An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. + + +*Type*: `string` + + +=== `conn_max_life_time` + +An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. + + +*Type*: `string` + + +=== `conn_max_idle` + +An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. + + +*Type*: `int` + +*Default*: `2` + +=== `conn_max_open` + +An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/processors/subprocess.adoc b/docs/modules/components/pages/processors/subprocess.adoc new file mode 100644 index 0000000000..7359502c58 --- /dev/null +++ b/docs/modules/components/pages/processors/subprocess.adoc @@ -0,0 +1,145 @@ += subprocess +:type: processor +:status: stable +:categories: ["Integration"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a command as a subprocess and, for each message, will pipe its contents to the stdin stream of the process followed by a newline. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +subprocess: + name: cat # No default (required) + args: [] +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +subprocess: + name: cat # No default (required) + args: [] + max_buffer: 65536 + codec_send: lines + codec_recv: lines +``` + +-- +====== + +[NOTE] +==== +This processor keeps the subprocess alive and requires very specific behavior from the command executed. If you wish to simply execute a command for each message take a look at the xref:components:processors/command.adoc[`command` processor] instead. +==== + +The subprocess must then either return a line over stdout or stderr. If a response is returned over stdout then its contents will replace the message. If a response is instead returned from stderr it will be logged and the message will continue unchanged and will be xref:configuration:error_handling.adoc[marked as failed]. + +Rather than separating data by a newline it's possible to specify alternative <> and <> values, which allow binary messages to be encoded for logical separation. + +The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. + +The field `max_buffer` defines the maximum response size able to be read from the subprocess. This value should be set significantly above the real expected maximum response size. + +== Subprocess requirements + +It is required that subprocesses flush their stdout and stderr pipes for each line. Benthos will attempt to keep the process alive for as long as the pipeline is running. If the process exits early it will be restarted. + +== Messages containing line breaks + +If a message contains line breaks each line of the message is piped to the subprocess and flushed, and a response is expected from the subprocess before another line is fed in. + +== Fields + +=== `name` + +The command to execute as a subprocess. + + +*Type*: `string` + + +```yml +# Examples + +name: cat + +name: sed + +name: awk +``` + +=== `args` + +A list of arguments to provide the command. + + +*Type*: `array` + +*Default*: `[]` + +=== `max_buffer` + +The maximum expected response size. + + +*Type*: `int` + +*Default*: `65536` + +=== `codec_send` + +Determines how messages written to the subprocess are encoded, which allows them to be logically separated. + + +*Type*: `string` + +*Default*: `"lines"` +Requires version 3.37.0 or newer + +Options: +`lines` +, `length_prefixed_uint32_be` +, `netstring` +. + +=== `codec_recv` + +Determines how messages read from the subprocess are decoded, which allows them to be logically separated. + + +*Type*: `string` + +*Default*: `"lines"` +Requires version 3.37.0 or newer + +Options: +`lines` +, `length_prefixed_uint32_be` +, `netstring` +. + + diff --git a/docs/modules/components/pages/processors/switch.adoc b/docs/modules/components/pages/processors/switch.adoc new file mode 100644 index 0000000000..4a7e99ed63 --- /dev/null +++ b/docs/modules/components/pages/processors/switch.adoc @@ -0,0 +1,104 @@ += switch +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Conditionally processes messages based on their contents. + +```yml +# Config fields, showing default values +label: "" +switch: [] # No default (required) +``` + +For each switch case a xref:guides:bloblang/about.adoc[Bloblang query] is checked and, if the result is true (or the check is empty) the child processors are executed on the message. + +== Fields + +=== `[].check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should have the processors of this case executed on it. If left empty the case always passes. If the check mapping throws an error the message will be flagged xref:configuration:error_handling.adoc[as having failed] and will not be tested against any other cases. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: this.type == "foo" + +check: this.contents.urls.contains("https://benthos.dev/") +``` + +=== `[].processors` + +A list of xref:components:processors/about.adoc[processors] to execute on a message. + + +*Type*: `array` + +*Default*: `[]` + +=== `[].fallthrough` + +Indicates whether, if this case passes for a message, the next case should also be executed. + + +*Type*: `bool` + +*Default*: `false` + +== Examples + +[tabs] +====== +I Hate George:: ++ +-- + + +We have a system where we're counting a metric for all messages that pass through our system. However, occasionally we get messages from George where he's rambling about dumb stuff we don't care about. + +For Georges messages we want to instead emit a metric that gauges how angry he is about being ignored and then we drop it. + +```yaml +pipeline: + processors: + - switch: + - check: this.user.name.first != "George" + processors: + - metric: + type: counter + name: MessagesWeCareAbout + + - processors: + - metric: + type: gauge + name: GeorgesAnger + value: ${! json("user.anger") } + - mapping: root = deleted() +``` + +-- +====== + +== Batching + +When a switch processor executes on a xref:configuration:batching.adoc[batch of messages] they are checked individually and can be matched independently against cases. During processing the messages matched against a case are processed as a batch, although the ordering of messages during case processing cannot be guaranteed to match the order as received. + +At the end of switch processing the resulting batch will follow the same ordering as the batch was received. If any child processors have split or otherwise grouped messages this grouping will be lost as the result of a switch is always a single batch. In order to perform conditional grouping and/or splitting use the xref:components:processors/group_by.adoc[`group_by` processor]. + diff --git a/docs/modules/components/pages/processors/sync_response.adoc b/docs/modules/components/pages/processors/sync_response.adoc new file mode 100644 index 0000000000..6e4a86b52e --- /dev/null +++ b/docs/modules/components/pages/processors/sync_response.adoc @@ -0,0 +1,30 @@ += sync_response +:type: processor +:status: stable +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Adds the payload in its current state as a synchronous response to the input source, where it is dealt with according to that specific input type. + +```yml +# Config fields, showing default values +label: "" +sync_response: {} +``` + +For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this processor even when combining input types that might not have support for sync responses. An example of an input able to utilize this is the `http_server`. + +For more information please read xref:guides:sync_responses.adoc[synchronous responses]. + + diff --git a/docs/modules/components/pages/processors/try.adoc b/docs/modules/components/pages/processors/try.adoc new file mode 100644 index 0000000000..7805213869 --- /dev/null +++ b/docs/modules/components/pages/processors/try.adoc @@ -0,0 +1,66 @@ += try +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a list of child processors on messages only if no prior processors have failed (or the errors have been cleared). + +```yml +# Config fields, showing default values +label: "" +try: [] +``` + +This processor behaves similarly to the xref:components:processors/for_each.adoc[`for_each`] processor, where a list of child processors are applied to individual messages of a batch. However, if a message has failed any prior processor (before or during the try block) then that message will skip all following processors. + +For example, with the following config: + +```yaml +pipeline: + processors: + - resource: foo + - try: + - resource: bar + - resource: baz + - resource: buz +``` + +If the processor `bar` fails for a particular message, that message will skip the processors `baz` and `buz`. Similarly, if `bar` succeeds but `baz` does not then `buz` will be skipped. If the processor `foo` fails for a message then none of `bar`, `baz` or `buz` are executed on that message. + +This processor is useful for when child processors depend on the successful output of previous processors. This processor can be followed with a xref:components:processors/catch.adoc[catch] processor for defining child processors to be applied only to failed messages. + +More information about error handing can be found in xref:configuration:error_handling.adoc[]. + +== Nest within a catch block + +In some cases it might be useful to nest a try block within a catch block, since the xref:components:processors/catch.adoc[`catch` processor] only clears errors _after_ executing its child processors this means a nested try processor will not execute unless the errors are explicitly cleared beforehand. + +This can be done by inserting an empty catch block before the try block like as follows: + +```yaml +pipeline: + processors: + - resource: foo + - catch: + - log: + level: ERROR + message: "Foo failed due to: ${! error() }" + - catch: [] # Clear prior error + - try: + - resource: bar + - resource: baz +``` + + diff --git a/docs/modules/components/pages/processors/unarchive.adoc b/docs/modules/components/pages/processors/unarchive.adoc new file mode 100644 index 0000000000..db68f7dcb5 --- /dev/null +++ b/docs/modules/components/pages/processors/unarchive.adoc @@ -0,0 +1,68 @@ += unarchive +:type: processor +:status: stable +:categories: ["Parsing","Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Unarchives messages according to the selected archive format into multiple messages within a xref:configuration:batching.adoc[batch]. + +```yml +# Config fields, showing default values +label: "" +unarchive: + format: "" # No default (required) +``` + +When a message is unarchived the new messages replace the original message in the batch. Messages that are selected but fail to unarchive (invalid format) will remain unchanged in the message batch but will be flagged as having failed, allowing you to xref:configuration:error_handling.adoc[error handle them]. + +== Metadata + +The metadata found on the messages handled by this processor will be copied into the resulting messages. For the unarchive formats that contain file information (tar, zip), a metadata field is also added to each message called `archive_filename` with the extracted filename. + + +== Fields + +=== `format` + +The unarchiving format to apply. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `binary` +| Extract messages from a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format]. +| `csv` +| Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message. +| `csv:x` +| Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message using a custom delimiter. The custom delimiter must be a single character, e.g. the format "csv:\t" would consume a tab delimited file. +| `json_array` +| Attempt to parse a message as a JSON array, and extract each element into its own message. +| `json_documents` +| Attempt to parse a message as a stream of concatenated JSON documents. Each parsed document is expanded into a new message. +| `json_map` +| Attempt to parse the message as a JSON map and for each element of the map expands its contents into a new message. A metadata field is added to each message called `archive_key` with the relevant key from the top-level map. +| `lines` +| Extract the lines of a message each into their own message. +| `tar` +| Extract messages from a unix standard tape archive. +| `zip` +| Extract messages from a zip file. + +|=== + + diff --git a/docs/modules/components/pages/processors/wasm.adoc b/docs/modules/components/pages/processors/wasm.adoc new file mode 100644 index 0000000000..7265634f71 --- /dev/null +++ b/docs/modules/components/pages/processors/wasm.adoc @@ -0,0 +1,60 @@ += wasm +:type: processor +:status: experimental +:categories: ["Utility"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a function exported by a WASM module for each message. + +Introduced in version 4.11.0. + +```yml +# Config fields, showing default values +label: "" +wasm: + module_path: "" # No default (required) + function: process +``` + +This processor uses https://github.com/tetratelabs/wazero[Wazero] to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. + +This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in https://github.com/benthosdev/benthos/tree/main/public/wasm/README.md[the codebase]. + +These examples, as well as the processor itself, is a work in progress. + +== Parallelism + +It's not currently possible to execute a single WASM runtime across parallel threads with this processor. Therefore, in order to support parallel processing this processor implements pooling of module runtimes. Ideally your WASM module shouldn't depend on any global state, but if it does then you need to ensure the processor xref:configuration:processing_pipelines.adoc[is only run on a single thread]. + + +== Fields + +=== `module_path` + +The path of the target WASM module to execute. + + +*Type*: `string` + + +=== `function` + +The name of the function exported by the target WASM module to run for each message. + + +*Type*: `string` + +*Default*: `"process"` + + diff --git a/docs/modules/components/pages/processors/while.adoc b/docs/modules/components/pages/processors/while.adoc new file mode 100644 index 0000000000..04ee6c09f2 --- /dev/null +++ b/docs/modules/components/pages/processors/while.adoc @@ -0,0 +1,107 @@ += while +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A processor that checks a xref:guides:bloblang/about.adoc[Bloblang query] against each batch of messages and executes child processors on them for as long as the query resolves to true. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +while: + at_least_once: false + check: "" + processors: [] # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +while: + at_least_once: false + max_loops: 0 + check: "" + processors: [] # No default (required) +``` + +-- +====== + +The field `at_least_once`, if true, ensures that the child processors are always executed at least one time (like a do .. while loop.) + +The field `max_loops`, if greater than zero, caps the number of loops for a message batch to this value. + +If following a loop execution the number of messages in a batch is reduced to zero the loop is exited regardless of the condition result. If following a loop execution there are more than 1 message batches the query is checked against the first batch only. + +The conditions of this processor are applied across entire message batches. You can find out more about batching xref:configuration:batching.adoc[in this doc]. + +== Fields + +=== `at_least_once` + +Whether to always run the child processors at least one time. + + +*Type*: `bool` + +*Default*: `false` + +=== `max_loops` + +An optional maximum number of loops to execute. Helps protect against accidentally creating infinite loops. + + +*Type*: `int` + +*Default*: `0` + +=== `check` + +A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether the while loop should execute again. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +check: errored() + +check: this.urls.unprocessed.length() > 0 +``` + +=== `processors` + +A list of child processors to execute on each loop. + + +*Type*: `array` + + + diff --git a/docs/modules/components/pages/processors/workflow.adoc b/docs/modules/components/pages/processors/workflow.adoc new file mode 100644 index 0000000000..af6f597255 --- /dev/null +++ b/docs/modules/components/pages/processors/workflow.adoc @@ -0,0 +1,390 @@ += workflow +:type: processor +:status: stable +:categories: ["Composition"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Executes a topology of xref:components:processors/branch.adoc[`branch` processors], performing them in parallel where possible. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +workflow: + meta_path: meta.workflow + order: [] + branches: {} +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +workflow: + meta_path: meta.workflow + order: [] + branch_resources: [] + branches: {} +``` + +-- +====== + +== Why use a workflow + +=== Performance + +Most of the time the best way to compose processors is also the simplest, just configure them in series. This is because processors are often CPU bound, low-latency, and you can gain vertical scaling by increasing the number of processor pipeline threads, allowing Benthos to process xref:configuration:processing_pipelines.adoc[multiple messages in parallel]. + +However, some processors such as xref:components:processors/http.adoc[`http`], xref:components:processors/aws_lambda.adoc[`aws_lambda`] or xref:components:processors/cache.adoc[`cache`] interact with external services and therefore spend most of their time waiting for a response. These processors tend to be high-latency and low CPU activity, which causes messages to process slowly. + +When a processing pipeline contains multiple network processors that aren't dependent on each other we can benefit from performing these processors in parallel for each individual message, reducing the overall message processing latency. + +=== Simplifying processor topology + +A workflow is often expressed as a https://en.wikipedia.org/wiki/Directed_acyclic_graph[DAG] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. + +For example, if we had processing stages A, B, C and D, where stage A could result in either stage B or C being next, always followed by D, it might look something like this: + +```text + /--> B --\ +A --| |--> D + \--> C --/ +``` + +This flow would be easy to express in a standard Benthos config, we could simply use a xref:components:processors/switch.adoc[`switch` processor] to route to either B or C depending on a condition on the result of A. However, this method of flow control quickly becomes unfeasible as the DAG gets more complicated, imagine expressing this flow using switch processors: + +```text + /--> B -------------|--> D + / / +A --| /--> E --| + \--> C --| \ + \----------|--> F +``` + +And imagine doing so knowing that the diagram is subject to change over time. Yikes! Instead, with a workflow we can either trust it to automatically resolve the DAG or express it manually as simply as `order: [ [ A ], [ B, C ], [ E ], [ D, F ] ]`, and the conditional logic for determining if a stage is executed is defined as part of the branch itself. + +== Examples + +[tabs] +====== +Automatic Ordering:: ++ +-- + + +When the field `order` is omitted a best attempt is made to determine a dependency tree between branches based on their request and result mappings. In the following example the branches foo and bar will be executed first in parallel, and afterwards the branch baz will be executed. + +```yaml +pipeline: + processors: + - workflow: + meta_path: meta.workflow + branches: + foo: + request_map: 'root = ""' + processors: + - http: + url: TODO + result_map: 'root.foo = this' + + bar: + request_map: 'root = this.body' + processors: + - aws_lambda: + function: TODO + result_map: 'root.bar = this' + + baz: + request_map: | + root.fooid = this.foo.id + root.barstuff = this.bar.content + processors: + - cache: + resource: TODO + operator: set + key: ${! json("fooid") } + value: ${! json("barstuff") } +``` + +-- +Conditional Branches:: ++ +-- + + +Branches of a workflow are skipped when the `request_map` assigns `deleted()` to the root. In this example the branch A is executed when the document type is "foo", and branch B otherwise. Branch C is executed afterwards and is skipped unless either A or B successfully provided a result at `tmp.result`. + +```yaml +pipeline: + processors: + - workflow: + branches: + A: + request_map: | + root = if this.document.type != "foo" { + deleted() + } + processors: + - http: + url: TODO + result_map: 'root.tmp.result = this' + + B: + request_map: | + root = if this.document.type == "foo" { + deleted() + } + processors: + - aws_lambda: + function: TODO + result_map: 'root.tmp.result = this' + + C: + request_map: | + root = if this.tmp.result != null { + deleted() + } + processors: + - http: + url: TODO_SOMEWHERE_ELSE + result_map: 'root.tmp.result = this' +``` + +-- +Resources:: ++ +-- + + +The `order` field can be used in order to refer to <>, this can sometimes make your pipeline configuration cleaner, as well as allowing you to reuse branch configurations in order places. It's also possible to mix and match branches configured within the workflow and configured as resources. + +```yaml +pipeline: + processors: + - workflow: + order: [ [ foo, bar ], [ baz ] ] + branches: + bar: + request_map: 'root = this.body' + processors: + - aws_lambda: + function: TODO + result_map: 'root.bar = this' + +processor_resources: + - label: foo + branch: + request_map: 'root = ""' + processors: + - http: + url: TODO + result_map: 'root.foo = this' + + - label: baz + branch: + request_map: | + root.fooid = this.foo.id + root.barstuff = this.bar.content + processors: + - cache: + resource: TODO + operator: set + key: ${! json("fooid") } + value: ${! json("barstuff") } +``` + +-- +====== + +== Fields + +=== `meta_path` + +A xref:configuration:field_paths.adoc[dot path] indicating where to store and reference <> about the workflow execution. + + +*Type*: `string` + +*Default*: `"meta.workflow"` + +=== `order` + +An explicit declaration of branch ordered tiers, which describes the order in which parallel tiers of branches should be executed. Branches should be identified by the name as they are configured in the field `branches`. It's also possible to specify branch processors configured <>. + + +*Type*: `two-dimensional array` + +*Default*: `[]` + +```yml +# Examples + +order: + - - foo + - bar + - - baz + +order: + - - foo + - - bar + - - baz +``` + +=== `branch_resources` + +An optional list of xref:components:processors/branch.adoc[`branch` processor] names that are configured as <>. These resources will be included in the workflow with any branches configured inline within the <> field. The order and parallelism in which branches are executed is automatically resolved based on the mappings of each branch. When using resources with an explicit order it is not necessary to list resources in this field. + + +*Type*: `array` + +*Default*: `[]` +Requires version 3.38.0 or newer + +=== `branches` + +An object of named xref:components:processors/branch.adoc[`branch` processors] that make up the workflow. The order and parallelism in which branches are executed can either be made explicit with the field `order`, or if omitted an attempt is made to automatically resolve an ordering based on the mappings of each branch. + + +*Type*: `object` + +*Default*: `{}` + +=== `branches..request_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata). + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +request_map: |- + root = { + "id": this.doc.id, + "content": this.doc.body.text + } + +request_map: |- + root = if this.type == "foo" { + this.foo.request + } else { + deleted() + } +``` + +=== `branches..processors` + +A list of processors to apply to mapped requests. When processing message batches the resulting batch must match the size and ordering of the input batch, therefore filtering, grouping should not be performed within these processors. + + +*Type*: `array` + + +=== `branches..result_map` + +A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata). + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +result_map: |- + meta foo_code = metadata("code") + root.foo_result = this + +result_map: |- + meta = metadata() + root.bar.body = this.body + root.bar.id = this.user.id + +result_map: root.raw_result = content().string() + +result_map: |- + root.enrichments.foo = if metadata("request_failed") != null { + throw(metadata("request_failed")) + } else { + this + } + +result_map: |- + # Retain only the updated metadata fields which were present in the origin message + meta = metadata().filter(v -> @.get(v.key) != null) +``` + +== Structured metadata + +When the field `meta_path` is non-empty the workflow processor creates an object describing which workflows were successful, skipped or failed for each message and stores the object within the message at the end. + +The object is of the following form: + +```json +{ + "succeeded": [ "foo" ], + "skipped": [ "bar" ], + "failed": { + "baz": "the error message from the branch" + } +} +``` + +If a message already has a meta object at the given path when it is processed then the object is used in order to determine which branches have already been performed on the message (or skipped) and can therefore be skipped on this run. + +This is a useful pattern when replaying messages that have failed some branches previously. For example, given the above example object the branches foo and bar would automatically be skipped, and baz would be reattempted. + +The previous meta object will also be preserved in the field `.previous` when the new meta object is written, preserving a full record of all workflow executions. + +If a field `.apply` exists in the meta object for a message and is an array then it will be used as an explicit list of stages to apply, all other stages will be skipped. + +== Resources + +It's common to configure processors (and other components) xref:configuration:resources.adoc[as resources] in order to keep the pipeline configuration cleaner. With the workflow processor you can include branch processors configured as resources within your workflow either by specifying them by name in the field `order`, if Benthos doesn't find a branch within the workflow configuration of that name it'll refer to the resources. + +Alternatively, if you do not wish to have an explicit ordering, you can add resource names to the field `branch_resources` and they will be included in the workflow with automatic DAG resolution along with any branches configured in the `branches` field. + +=== Resource error conditions + +There are two error conditions that could potentially occur when resources included in your workflow are mutated, and if you are planning to mutate resources in your workflow it is important that you understand them. + +The first error case is that a resource in the workflow is removed and not replaced, when this happens the workflow will still be executed but the individual branch will fail. This should only happen if you explicitly delete a branch resource, as any mutation operation will create the new resource before removing the old one. + +The second error case is when automatic DAG resolution is being used and a resource in the workflow is changed in a way that breaks the DAG (circular dependencies, etc). When this happens it is impossible to execute the workflow and therefore the processor will fail, which is possible to capture and handle using xref:configuration:error_handling.adoc[standard error handling patterns]. + +== Error handling + +The recommended approach to handle failures within a workflow is to query against the <> it provides, as it provides granular information about exactly which branches failed and which ones succeeded and therefore aren't necessary to perform again. + +For example, if our meta object is stored at the path `meta.workflow` and we wanted to check whether a message has failed for any branch we can do that using a xref:guides:bloblang/about.adoc[Bloblang query] like `this.meta.workflow.failed.length() | 0 > 0`, or to check whether a specific branch failed we can use `this.exists("meta.workflow.failed.foo")`. + +However, if structured metadata is disabled by setting the field `meta_path` to empty then the workflow processor instead adds a general error flag to messages when any executed branch fails. In this case it's possible to handle failures using xref:configuration:error_handling.adoc[standard error handling patterns]. + + + diff --git a/docs/modules/components/pages/processors/xml.adoc b/docs/modules/components/pages/processors/xml.adoc new file mode 100644 index 0000000000..b3a4dc79db --- /dev/null +++ b/docs/modules/components/pages/processors/xml.adoc @@ -0,0 +1,113 @@ += xml +:type: processor +:status: beta +:categories: ["Parsing"] + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Parses messages as an XML document, performs a mutation on the data, and then overwrites the previous contents with the new value. + +```yml +# Config fields, showing default values +label: "" +xml: + operator: "" + cast: false +``` + +== Operators + +=== `to_json` + +Converts an XML document into a JSON structure, where elements appear as keys of an object according to the following rules: + +- If an element contains attributes they are parsed by prefixing a hyphen, `-`, to the attribute label. +- If the element is a simple element and has attributes, the element value is given the key `#text`. +- XML comments, directives, and process instructions are ignored. +- When elements are repeated the resulting JSON value is an array. + +For example, given the following XML: + +```xml + + This is a title + This is a description + foo1 + foo2 + foo3 + +``` + +The resulting JSON structure would look like this: + +```json +{ + "root":{ + "title":"This is a title", + "description":{ + "#text":"This is a description", + "-tone":"boring" + }, + "elements":[ + {"#text":"foo1","-id":"1"}, + {"#text":"foo2","-id":"2"}, + "foo3" + ] + } +} +``` + +With cast set to true, the resulting JSON structure would look like this: + +```json +{ + "root":{ + "title":"This is a title", + "description":{ + "#text":"This is a description", + "-tone":"boring" + }, + "elements":[ + {"#text":"foo1","-id":1}, + {"#text":"foo2","-id":2}, + "foo3" + ] + } +} +``` + +== Fields + +=== `operator` + +An XML <> to apply to messages. + + +*Type*: `string` + +*Default*: `""` + +Options: +`to_json` +. + +=== `cast` + +Whether to try to cast values that are numbers and booleans to the right type. Default: all values are strings. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/rate_limits/local.adoc b/docs/modules/components/pages/rate_limits/local.adoc new file mode 100644 index 0000000000..9174875056 --- /dev/null +++ b/docs/modules/components/pages/rate_limits/local.adoc @@ -0,0 +1,47 @@ += local +:type: rate_limit +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +The local rate limit is a simple X every Y type rate limit that can be shared across any number of components within the pipeline but does not support distributed rate limits across multiple running instances of Benthos. + +```yml +# Config fields, showing default values +label: "" +local: + count: 1000 + interval: 1s +``` + +== Fields + +=== `count` + +The maximum number of requests to allow for a given period of time. + + +*Type*: `int` + +*Default*: `1000` + +=== `interval` + +The time window to limit requests by. + + +*Type*: `string` + +*Default*: `"1s"` + + diff --git a/docs/modules/components/pages/rate_limits/redis.adoc b/docs/modules/components/pages/rate_limits/redis.adoc new file mode 100644 index 0000000000..3a218155d4 --- /dev/null +++ b/docs/modules/components/pages/rate_limits/redis.adoc @@ -0,0 +1,312 @@ += redis +:type: rate_limit +:status: experimental + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +A rate limit implementation using Redis. It works by using a simple token bucket algorithm to limit the number of requests to a given count within a given time period. The rate limit is shared across all instances of Benthos that use the same Redis instance, which must all have a consistent count and interval. + +Introduced in version 4.12.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +label: "" +redis: + url: redis://:6397 # No default (required) + count: 1000 + interval: 1s + key: "" # No default (required) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +label: "" +redis: + url: redis://:6397 # No default (required) + kind: simple + master: "" + tls: + enabled: false + skip_cert_verify: false + enable_renegotiation: false + root_cas: "" + root_cas_file: "" + client_certs: [] + count: 1000 + interval: 1s + key: "" # No default (required) +``` + +-- +====== + +== Fields + +=== `url` + +The URL of the target Redis server. Database is optional and is supplied as the URL path. + + +*Type*: `string` + + +```yml +# Examples + +url: redis://:6397 + +url: redis://localhost:6379 + +url: redis://foousername:foopassword@redisplace:6379 + +url: redis://:foopassword@redisplace:6379 + +url: redis://localhost:6379/1 + +url: redis://localhost:6379/1,redis://localhost:6380/1 +``` + +=== `kind` + +Specifies a simple, cluster-aware, or failover-aware redis client. + + +*Type*: `string` + +*Default*: `"simple"` + +Options: +`simple` +, `cluster` +, `failover` +. + +=== `master` + +Name of the redis master when `kind` is `failover` + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +master: mymaster +``` + +=== `tls` + +Custom TLS settings can be used to override system defaults. + +**Troubleshooting** + +Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. + + +*Type*: `object` + + +=== `tls.enabled` + +Whether custom TLS settings are enabled. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.skip_cert_verify` + +Whether to skip server side certificate verification. + + +*Type*: `bool` + +*Default*: `false` + +=== `tls.enable_renegotiation` + +Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. + + +*Type*: `bool` + +*Default*: `false` +Requires version 3.45.0 or newer + +=== `tls.root_cas` + +An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas: |- + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +=== `tls.root_cas_file` + +An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +root_cas_file: ./root_cas.pem +``` + +=== `tls.client_certs` + +A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. + + +*Type*: `array` + +*Default*: `[]` + +```yml +# Examples + +client_certs: + - cert: foo + key: bar + +client_certs: + - cert_file: ./example.pem + key_file: ./example.key +``` + +=== `tls.client_certs[].cert` + +A plain text certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key` + +A plain text certificate key to use. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].cert_file` + +The path of a certificate to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].key_file` + +The path of a certificate key to use. + + +*Type*: `string` + +*Default*: `""` + +=== `tls.client_certs[].password` + +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +password: foo + +password: ${KEY_PASSWORD} +``` + +=== `count` + +The maximum number of messages to allow for a given period of time. + + +*Type*: `int` + +*Default*: `1000` + +=== `interval` + +The time window to limit requests by. + + +*Type*: `string` + +*Default*: `"1s"` + +=== `key` + +The key to use for the rate limit. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/scanners/avro.adoc b/docs/modules/components/pages/scanners/avro.adoc new file mode 100644 index 0000000000..f0c2a5732b --- /dev/null +++ b/docs/modules/components/pages/scanners/avro.adoc @@ -0,0 +1,72 @@ += avro +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consume a stream of Avro OCF datum. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +avro: {} +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +avro: + raw_json: false +``` + +-- +====== + +== Avro JSON format + +This scanner yields documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: + +- if its type is `null`, then it is encoded as a JSON `null`; +- otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. + +For example, the union schema `["null","string","Foo"]`, where `Foo` is a record name, would encode: + +- `null` as `null`; +- the string `"a"` as `{"string": "a"}`; and +- a `Foo` instance as `{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. + +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field <> to `true`. + + +== Fields + +=== `raw_json` + +Whether messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/scanners/chunker.adoc b/docs/modules/components/pages/scanners/chunker.adoc new file mode 100644 index 0000000000..eaa53067cb --- /dev/null +++ b/docs/modules/components/pages/scanners/chunker.adoc @@ -0,0 +1,35 @@ += chunker +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Split an input stream into chunks of a given number of bytes. + +```yml +# Config fields, showing default values +chunker: + size: 0 # No default (required) +``` + +== Fields + +=== `size` + +The size of each chunk in bytes. + + +*Type*: `int` + + + diff --git a/docs/modules/components/pages/scanners/csv.adoc b/docs/modules/components/pages/scanners/csv.adoc new file mode 100644 index 0000000000..21377528b9 --- /dev/null +++ b/docs/modules/components/pages/scanners/csv.adoc @@ -0,0 +1,73 @@ += csv +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consume comma-separated values row by row, including support for custom delimiters. + +```yml +# Config fields, showing default values +csv: + custom_delimiter: "" # No default (optional) + parse_header_row: true + lazy_quotes: false + continue_on_error: false +``` + +== Metadata + +This scanner adds the following metadata to each message: + +- `csv_row` The index of each row, beginning at 0. + + + +== Fields + +=== `custom_delimiter` + +Use a provided custom delimiter instead of the default comma. + + +*Type*: `string` + + +=== `parse_header_row` + +Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, each message will consist of an array of values from the corresponding CSV row. + + +*Type*: `bool` + +*Default*: `true` + +=== `lazy_quotes` + +If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field. + + +*Type*: `bool` + +*Default*: `false` + +=== `continue_on_error` + +If a row fails to parse due to any error emit an empty message marked with the error and then continue consuming subsequent rows when possible. This can sometimes be useful in situations where input data contains individual rows which are malformed. However, when a row encounters a parsing error it is impossible to guarantee that following rows are valid, as this indicates that the input data is unreliable and could potentially emit misaligned rows. + + +*Type*: `bool` + +*Default*: `false` + + diff --git a/docs/modules/components/pages/scanners/decompress.adoc b/docs/modules/components/pages/scanners/decompress.adoc new file mode 100644 index 0000000000..e24d16b3ec --- /dev/null +++ b/docs/modules/components/pages/scanners/decompress.adoc @@ -0,0 +1,46 @@ += decompress +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Decompress the stream of bytes according to an algorithm, before feeding it into a child scanner. + +```yml +# Config fields, showing default values +decompress: + algorithm: "" # No default (required) + into: + to_the_end: {} +``` + +== Fields + +=== `algorithm` + +One of `gzip`, `pgzip`, `zlib`, `bzip2`, `flate`, `snappy`, `lz4`, `zstd`. + + +*Type*: `string` + + +=== `into` + +The child scanner to feed the decompressed stream into. + + +*Type*: `scanner` + +*Default*: `{"to_the_end":{}}` + + diff --git a/docs/modules/components/pages/scanners/json_documents.adoc b/docs/modules/components/pages/scanners/json_documents.adoc new file mode 100644 index 0000000000..c2f55cbc6e --- /dev/null +++ b/docs/modules/components/pages/scanners/json_documents.adoc @@ -0,0 +1,26 @@ += json_documents +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consumes a stream of one or more JSON documents. + +Introduced in version 4.27.0. + +```yml +# Config fields, showing default values +json_documents: {} +``` + + diff --git a/docs/modules/components/pages/scanners/lines.adoc b/docs/modules/components/pages/scanners/lines.adoc new file mode 100644 index 0000000000..c3ca59c46b --- /dev/null +++ b/docs/modules/components/pages/scanners/lines.adoc @@ -0,0 +1,45 @@ += lines +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Split an input stream into a message per line of data. + +```yml +# Config fields, showing default values +lines: + custom_delimiter: "" # No default (optional) + max_buffer_size: 65536 +``` + +== Fields + +=== `custom_delimiter` + +Use a provided custom delimiter for detecting the end of a line rather than a single line break. + + +*Type*: `string` + + +=== `max_buffer_size` + +Set the maximum buffer size for storing line data, this limits the maximum size that a line can be without causing an error. + + +*Type*: `int` + +*Default*: `65536` + + diff --git a/docs/modules/components/pages/scanners/re_match.adoc b/docs/modules/components/pages/scanners/re_match.adoc new file mode 100644 index 0000000000..ae5314438a --- /dev/null +++ b/docs/modules/components/pages/scanners/re_match.adoc @@ -0,0 +1,51 @@ += re_match +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Split an input stream into segments matching against a regular expression. + +```yml +# Config fields, showing default values +re_match: + pattern: (?m)^\d\d:\d\d:\d\d # No default (required) + max_buffer_size: 65536 +``` + +== Fields + +=== `pattern` + +The pattern to match against. + + +*Type*: `string` + + +```yml +# Examples + +pattern: (?m)^\d\d:\d\d:\d\d +``` + +=== `max_buffer_size` + +Set the maximum buffer size for storing line data, this limits the maximum size that a message can be without causing an error. + + +*Type*: `int` + +*Default*: `65536` + + diff --git a/docs/modules/components/pages/scanners/skip_bom.adoc b/docs/modules/components/pages/scanners/skip_bom.adoc new file mode 100644 index 0000000000..f4509a0e3e --- /dev/null +++ b/docs/modules/components/pages/scanners/skip_bom.adoc @@ -0,0 +1,37 @@ += skip_bom +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Skip one or more byte order marks for each opened child scanner. + +```yml +# Config fields, showing default values +skip_bom: + into: + to_the_end: {} +``` + +== Fields + +=== `into` + +The child scanner to feed the resulting stream into. + + +*Type*: `scanner` + +*Default*: `{"to_the_end":{}}` + + diff --git a/docs/modules/components/pages/scanners/switch.adoc b/docs/modules/components/pages/scanners/switch.adoc new file mode 100644 index 0000000000..4b57af1c90 --- /dev/null +++ b/docs/modules/components/pages/scanners/switch.adoc @@ -0,0 +1,89 @@ += switch +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Select a child scanner dynamically for source data based on factors such as the filename. + +```yml +# Config fields, showing default values +switch: [] # No default (required) +``` + +This scanner outlines a list of potential child scanner candidates to be chosen, and for each source of data the first candidate to pass will be selected. A candidate without any conditions acts as a catch-all and will pass for every source, it is recommended to always have a catch-all scanner at the end of your list. If a given source of data does not pass a candidate an error is returned and the data is rejected. + +== Fields + +=== `[].re_match_name` + +A regular expression to test against the name of each source of data fed into the scanner (filename or equivalent). If this pattern matches the child scanner is selected. + + +*Type*: `string` + + +=== `[].scanner` + +The scanner to activate if this candidate passes. + + +*Type*: `scanner` + + +== Examples + +[tabs] +====== +Switch based on file name:: ++ +-- + +In this example a file input chooses a scanner based on the extension of each file + +```yaml +input: + file: + paths: [ ./data/* ] + scanner: + switch: + - re_match_name: '\.avro$' + scanner: { avro: {} } + + - re_match_name: '\.csv$' + scanner: { csv: {} } + + - re_match_name: '\.csv.gz$' + scanner: + decompress: + algorithm: gzip + into: + csv: {} + + - re_match_name: '\.tar$' + scanner: { tar: {} } + + - re_match_name: '\.tar.gz$' + scanner: + decompress: + algorithm: gzip + into: + tar: {} + + - scanner: { to_the_end: {} } +``` + +-- +====== + + diff --git a/docs/modules/components/pages/scanners/tar.adoc b/docs/modules/components/pages/scanners/tar.adoc new file mode 100644 index 0000000000..b99218456f --- /dev/null +++ b/docs/modules/components/pages/scanners/tar.adoc @@ -0,0 +1,32 @@ += tar +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Consume a tar archive file by file. + +```yml +# Config fields, showing default values +tar: {} +``` + +== Metadata + +This scanner adds the following metadata to each message: + +- `tar_name` + + + + diff --git a/docs/modules/components/pages/scanners/to_the_end.adoc b/docs/modules/components/pages/scanners/to_the_end.adoc new file mode 100644 index 0000000000..bdde09ff8e --- /dev/null +++ b/docs/modules/components/pages/scanners/to_the_end.adoc @@ -0,0 +1,30 @@ += to_the_end +:type: scanner +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Read the input stream all the way until the end and deliver it as a single message. + +```yml +# Config fields, showing default values +to_the_end: {} +``` + +[CAUTION] +==== +Some sources of data may not have a logical end, therefore caution should be made to exclusively use this scanner when the end of an input stream is clearly defined (and well within memory). +==== + + + diff --git a/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc b/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc new file mode 100644 index 0000000000..12957d01fc --- /dev/null +++ b/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc @@ -0,0 +1,97 @@ += gcp_cloudtrace +:type: tracer +:status: experimental + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Send tracing events to a https://cloud.google.com/trace[Google Cloud Trace]. + +Introduced in version 4.2.0. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +tracer: + gcp_cloudtrace: + project: "" # No default (required) + sampling_ratio: 1 + flush_interval: "" # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +tracer: + gcp_cloudtrace: + project: "" # No default (required) + sampling_ratio: 1 + tags: {} + flush_interval: "" # No default (optional) +``` + +-- +====== + +== Fields + +=== `project` + +The google project with Cloud Trace API enabled. If this is omitted then the Google Cloud SDK will attempt auto-detect it from the environment. + + +*Type*: `string` + + +=== `sampling_ratio` + +Sets the ratio of traces to sample. Tuning the sampling ratio is recommended for high-volume production workloads. + + +*Type*: `float` + +*Default*: `1` + +```yml +# Examples + +sampling_ratio: 1 +``` + +=== `tags` + +A map of tags to add to tracing spans. + + +*Type*: `object` + +*Default*: `{}` + +=== `flush_interval` + +The period of time between each flush of tracing spans. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/tracers/jaeger.adoc b/docs/modules/components/pages/tracers/jaeger.adoc new file mode 100644 index 0000000000..8e237c88d4 --- /dev/null +++ b/docs/modules/components/pages/tracers/jaeger.adoc @@ -0,0 +1,132 @@ += jaeger +:type: tracer +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Send tracing events to a https://www.jaegertracing.io/[Jaeger] agent or collector. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +tracer: + jaeger: + agent_address: "" + collector_url: "" + sampler_type: const + flush_interval: "" # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +tracer: + jaeger: + agent_address: "" + collector_url: "" + sampler_type: const + sampler_param: 1 + tags: {} + flush_interval: "" # No default (optional) +``` + +-- +====== + +== Fields + +=== `agent_address` + +The address of a Jaeger agent to send tracing events to. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +agent_address: jaeger-agent:6831 +``` + +=== `collector_url` + +The URL of a Jaeger collector to send tracing events to. If set, this will override `agent_address`. + + +*Type*: `string` + +*Default*: `""` +Requires version 3.38.0 or newer + +```yml +# Examples + +collector_url: https://jaeger-collector:14268/api/traces +``` + +=== `sampler_type` + +The sampler type to use. + + +*Type*: `string` + +*Default*: `"const"` + +|=== +| Option | Summary + +| `const` +| Sample a percentage of traces. 1 or more means all traces are sampled, 0 means no traces are sampled and anything in between means a percentage of traces are sampled. Tuning the sampling rate is recommended for high-volume production workloads. + +|=== + +=== `sampler_param` + +A parameter to use for sampling. This field is unused for some sampling types. + + +*Type*: `float` + +*Default*: `1` + +=== `tags` + +A map of tags to add to tracing spans. + + +*Type*: `object` + +*Default*: `{}` + +=== `flush_interval` + +The period of time between each flush of tracing spans. + + +*Type*: `string` + + + diff --git a/docs/modules/components/pages/tracers/none.adoc b/docs/modules/components/pages/tracers/none.adoc new file mode 100644 index 0000000000..61f407eec2 --- /dev/null +++ b/docs/modules/components/pages/tracers/none.adoc @@ -0,0 +1,25 @@ += none +:type: tracer +:status: stable + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Do not send tracing events anywhere. + +```yml +# Config fields, showing default values +tracer: + none: {} +``` + + diff --git a/docs/modules/components/pages/tracers/open_telemetry_collector.adoc b/docs/modules/components/pages/tracers/open_telemetry_collector.adoc new file mode 100644 index 0000000000..cf6736edcc --- /dev/null +++ b/docs/modules/components/pages/tracers/open_telemetry_collector.adoc @@ -0,0 +1,164 @@ += open_telemetry_collector +:type: tracer +:status: experimental + + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the corresponding source file under internal/impl/. +//// + + +component_type_dropdown::[] + + +Send tracing events to an https://opentelemetry.io/docs/collector/[Open Telemetry collector]. + + +[tabs] +====== +Common:: ++ +-- + +```yml +# Common config fields, showing default values +tracer: + open_telemetry_collector: + http: [] # No default (required) + grpc: [] # No default (required) + sampling: + enabled: false + ratio: 0.85 # No default (optional) +``` + +-- +Advanced:: ++ +-- + +```yml +# All config fields, showing default values +tracer: + open_telemetry_collector: + http: [] # No default (required) + grpc: [] # No default (required) + tags: {} + sampling: + enabled: false + ratio: 0.85 # No default (optional) +``` + +-- +====== + +== Fields + +=== `http` + +A list of http collectors. + + +*Type*: `array` + + +=== `http[].address` + +The endpoint of a collector to send tracing events to. + + +*Type*: `string` + + +```yml +# Examples + +address: localhost:4318 +``` + +=== `http[].secure` + +Connect to the collector over HTTPS + + +*Type*: `bool` + +*Default*: `false` + +=== `grpc` + +A list of grpc collectors. + + +*Type*: `array` + + +=== `grpc[].address` + +The endpoint of a collector to send tracing events to. + + +*Type*: `string` + + +```yml +# Examples + +address: localhost:4317 +``` + +=== `grpc[].secure` + +Connect to the collector with client transport security + + +*Type*: `bool` + +*Default*: `false` + +=== `tags` + +A map of tags to add to all tracing spans. + + +*Type*: `object` + +*Default*: `{}` + +=== `sampling` + +Settings for trace sampling. Sampling is recommended for high-volume production workloads. + + +*Type*: `object` + +Requires version 4.25.0 or newer + +=== `sampling.enabled` + +Whether to enable sampling. + + +*Type*: `bool` + +*Default*: `false` + +=== `sampling.ratio` + +Sets the ratio of traces to sample. + + +*Type*: `float` + + +```yml +# Examples + +ratio: 0.85 + +ratio: 0.5 +``` + + diff --git a/docs/modules/configuration/pages/templating.adoc b/docs/modules/configuration/pages/templating.adoc new file mode 100644 index 0000000000..34b3dabfba --- /dev/null +++ b/docs/modules/configuration/pages/templating.adoc @@ -0,0 +1,326 @@ += Templating +:description: Learn how templates work. + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/template/docs.adoc +//// + +[WARNING] +.Experimental +==== +Templates are an experimental feature and therefore subject to change outside of major version releases. +==== + +Templates are a way to define new {page-component-title} components (similar to plugins) that are implemented by generating a {page-component-title} config snippet from pre-defined parameter fields. This is useful when a common pattern of {page-component-title} configuration is used but with varying parameters each time. + +A template is defined in a YAML file that can be imported when {page-component-title} runs using the flag `-t`: + +[source,bash] +---- +benthos -t "./templates/*.yaml" -c ./config.yaml +---- + +The template describes the type of the component and configuration fields that can be used to customize it, followed by a xref:guides:bloblang/about.adoc[Bloblang mapping] that translates an object containing those fields into a benthos config structure. This allows you to use logic to generate more complex configurations: + +[tabs] +====== +Template:: ++ +-- + +[source,yaml] +---- +name: aws_sqs_list +type: input + +fields: + - name: urls + type: string + kind: list + - name: region + type: string + default: us-east-1 + +mapping: | + root.broker.inputs = this.urls.map_each(url -> { + "aws_sqs": { + "url": url, + "region": this.region, + } + }) +---- +-- +Config:: ++ +-- + +[source,yaml] +---- +input: + aws_sqs_list: + urls: + - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 + - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 + +pipeline: + processors: + - mapping: | + root.id = uuid_v4() + root.foo = this.inner.foo + root.body = this.outer +---- +-- +Result:: ++ +-- + +[source,yaml] +---- +input: + broker: + inputs: + - aws_sqs: + url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 + region: us-east-1 + - aws_sqs: + url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 + region: us-east-1 + +pipeline: + processors: + - mapping: | + root.id = uuid_v4() + root.foo = this.inner.foo + root.body = this.outer +---- +-- +====== + +You can see more examples of templates at https://github.com/benthosdev/benthos/tree/main/config/template_examples^. + +== Fields + +The schema of a template file is as follows: + +=== `name` + +The name of the component this template will create. + + +*Type*: `string` + + +=== `type` + +The type of the component this template will create. + + +*Type*: `string` + + +Options: +`cache` +, `input` +, `output` +, `processor` +, `rate_limit` +. + +=== `status` + +The stability of the template describing the likelihood that the configuration spec of the template, or it's behavior, will change. + + +*Type*: `string` + +*Default*: `"stable"` + +|=== +| Option | Summary + +| `stable` +| This template is stable and will therefore not change in a breaking way outside of major version releases. +| `beta` +| This template is beta and will therefore not change in a breaking way unless a major problem is found. +| `experimental` +| This template is experimental and therefore subject to breaking changes outside of major version releases. + +|=== + +=== `categories` + +An optional list of tags, which are used for arbitrarily grouping components in documentation. + + +*Type*: list of `string` + +*Default*: `[]` + +=== `summary` + +A short summary of the component. + + +*Type*: `string` + +*Default*: `""` + +=== `description` + +A longer form description of the component and how to use it. + + +*Type*: `string` + +*Default*: `""` + +=== `fields` + +The configuration fields of the template, fields specified here will be parsed from a Benthos config and will be accessible from the template mapping. + + +*Type*: list of `object` + + +=== `fields[].name` + +The name of the field. + + +*Type*: `string` + + +=== `fields[].description` + +A description of the field. + + +*Type*: `string` + +*Default*: `""` + +=== `fields[].type` + +The scalar type of the field. + + +*Type*: `string` + + +|=== +| Option | Summary + +| `string` +| standard string type +| `int` +| standard integer type +| `float` +| standard float type +| `bool` +| a boolean true/false +| `unknown` +| allows for nesting arbitrary configuration inside of a field + +|=== + +=== `fields[].kind` + +The kind of the field. + + +*Type*: `string` + +*Default*: `"scalar"` + +Options: +`scalar` +, `map` +, `list` +. + +=== `fields[].default` + +An optional default value for the field. If a default value is not specified then a configuration without the field is considered incorrect. + + +*Type*: `unknown` + + +=== `fields[].advanced` + +Whether this field is considered advanced. + + +*Type*: `bool` + +*Default*: `false` + +=== `mapping` + +A xref:guides:bloblang/about.adoc[Bloblang] mapping that translates the fields of the template into a valid Benthos configuration for the target component type. + + +*Type*: `string` + + +=== `metrics_mapping` + +An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that allows you to rename or prevent certain metrics paths from being exported. For more information check out the xref:components:metrics/about.adoc#metric-mapping[metrics documentation]. When metric paths are created, renamed and dropped a trace log is written, enabling TRACE level logging is therefore a good way to diagnose path mappings. + +Invocations of this mapping are able to reference a variable $label in order to obtain the value of the label provided to the template config. This allows you to match labels with the root of the config. + + +*Type*: `string` + +*Default*: `""` + +```yml +# Examples + +metrics_mapping: this.replace("input", "source").replace("output", "sink") + +metrics_mapping: |- + root = if ![ + "input_received", + "input_latency", + "output_sent" + ].contains(this) { deleted() } +``` + +=== `tests` + +Optional unit test definitions for the template that verify certain configurations produce valid configs. These tests are executed with the command `benthos template lint`. + + +*Type*: list of `object` + +*Default*: `[]` + +=== `tests[].name` + +A name to identify the test. + + +*Type*: `string` + + +=== `tests[].config` + +A configuration to run this test with, the config resulting from applying the template with this config will be linted. + + +*Type*: `object` + + +=== `tests[].expected` + +An optional configuration describing the expected result of applying the template, when specified the result will be diffed and any mismatching fields will be reported as a test error. + + +*Type*: `object` + + diff --git a/docs/modules/configuration/pages/unit_testing.adoc b/docs/modules/configuration/pages/unit_testing.adoc new file mode 100644 index 0000000000..b38f0fc448 --- /dev/null +++ b/docs/modules/configuration/pages/unit_testing.adoc @@ -0,0 +1,619 @@ += Unit Testing +:json-pointer-url: https://tools.ietf.org/html/rfc6901 +:bloblang-url: xref:guides:bloblang/about.adoc +:logger-url: xref:components:logger/about.adoc +:processors-mapping-url: xref:components:processors/mapping.adoc + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/config/test/docs.adoc +//// + +The {page-component-title} service offers a command `benthos test` for running unit tests on sections of a configuration file. This makes it easy to protect your config files from regressions over time. + +== Writing a test + +Let's imagine we have a configuration file `foo.yaml` containing some processors: + +```yaml +input: + kafka: + addresses: [ TODO ] + topics: [ foo, bar ] + consumer_group: foogroup + +pipeline: + processors: + - mapping: '"%vend".format(content().uppercase().string())' + +output: + aws_s3: + bucket: TODO + path: '${! meta("kafka_topic") }/${! json("message.id") }.json' +``` + +One way to write our unit tests for this config is to accompany it with a file of the same name and extension but suffixed with `_benthos_test`, which in this case would be `foo_benthos_test.yaml`. + +```yml +tests: + - name: example test + target_processors: '/pipeline/processors' + environment: {} + input_batch: + - content: 'example content' + metadata: + example_key: example metadata value + output_batches: + - + - content_equals: EXAMPLE CONTENTend + metadata_equals: + example_key: example metadata value +``` + +Under `tests` we have a list of any number of unit tests to execute for the config file. Each test is run in complete isolation, including any resources defined by the config file. Tests should be allocated a unique `name` that identifies the feature being tested. + +The field `target_processors` is either the label of a processor to test, or a {json-pointer-url}[JSON Pointer] that identifies the position of a processor, or list of processors, within the file which should be executed by the test. For example a value of `foo` would target a processor with the label `foo`, and a value of `/input/processors` would target all processors within the input section of the config. + +The field `environment` allows you to define an object of key/value pairs that set environment variables to be evaluated during the parsing of the target config file. These are unique to each test, allowing you to test different environment variable interpolation combinations. + +The field `input_batch` lists one or more messages to be fed into the targeted processors as a batch. Each message of the batch may have its raw content defined as well as metadata key/value pairs. + +For the common case where the messages are in JSON format, you can use `json_content` instead of `content` to specify the message structurally rather than verbatim. + +The field `output_batches` lists any number of batches of messages which are expected to result from the target processors. Each batch lists any number of messages, each one defining <> to describe the expected contents of the message. + +If the number of batches defined does not match the resulting number of batches the test will fail. If the number of messages defined in each batch does not match the number in the resulting batches the test will fail. If any condition of a message fails then the test fails. + +=== Inline tests + +Sometimes it's more convenient to define your tests within the config being tested. This is fine, simply add the `tests` field to the end of the config being tested. + +=== Bloblang tests + +Sometimes when working with large {bloblang-url}[Bloblang mappings] it's preferred to have the full mapping in a separate file to your {page-component-title} configuration. In this case it's possible to write unit tests that target and execute the mapping directly with the field `target_mapping`, which when specified is interpreted as either an absolute path or a path relative to the test definition file that points to a file containing only a Bloblang mapping. + +For example, if we were to have a file `cities.blobl` containing a mapping: + +```coffeescript +root.Cities = this.locations. + filter(loc -> loc.state == "WA"). + map_each(loc -> loc.name). + sort().join(", ") +``` + +We can accompany it with a test file `cities_test.yaml` containing a regular test definition: + +```yml +tests: + - name: test cities mapping + target_mapping: './cities.blobl' + environment: {} + input_batch: + - content: | + { + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] + } + output_batches: + - + - json_equals: {"Cities": "Bellevue, Olympia, Seattle"} +``` + +And execute this test the same way we execute other {page-component-title} tests (`benthos test ./dir/cities_test.yaml`, `benthos test ./dir/...`, etc). + +=== Fragmented tests + +Sometimes the number of tests you need to define in order to cover a config file is so vast that it's necessary to split them across multiple test definition files. This is possible but {page-component-title} still requires a way to detect the configuration file being targeted by these fragmented test definition files. In order to do this we must prefix our `target_processors` field with the path of the target relative to the definition file. + +The syntax of `target_processors` in this case is a full {json-pointer-url}[JSON Pointer] that should look something like `target.yaml#/pipeline/processors`. For example, if we saved our test definition above in an arbitrary location like `./tests/first.yaml` and wanted to target our original `foo.yaml` config file, we could do that with the following: + +```yml +tests: + - name: example test + target_processors: '../foo.yaml#/pipeline/processors' + environment: {} + input_batch: + - content: 'example content' + metadata: + example_key: example metadata value + output_batches: + - + - content_equals: EXAMPLE CONTENTend + metadata_equals: + example_key: example metadata value +``` + +== Input Definitions + +=== `content` + +Sets the raw content of the message. + +=== `json_content` + +```yml +json_content: + foo: foo value + bar: [ element1, 10 ] +``` + +Sets the raw content of the message to a JSON document matching the structure of the value. + +=== `file_content` + +```yml +file_content: ./foo/bar.txt +``` + +Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. + +=== `metadata` + +A map of key/value pairs that sets the metadata values of the message. + +== Output Conditions + +=== `bloblang` + +```yml +bloblang: 'this.age > 10 && @foo.length() > 0' +``` + +Executes a {bloblang-url}[Bloblang expression] on a message, if the result is anything other than a boolean equalling `true` the test fails. + +=== `content_equals` + +```yml +content_equals: example content +``` + +Checks the full raw contents of a message against a value. + +=== `content_matches` + +```yml +content_matches: "^foo [a-z]+ bar$" +``` + +Checks whether the full raw contents of a message matches a regular expression (re2). + +=== `metadata_equals` + +```yml +metadata_equals: + example_key: example metadata value +``` + +Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. + +=== `file_equals` + +```yml +file_equals: ./foo/bar.txt +``` + +Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. + +=== `file_json_equals` + +```yml +file_json_equals: ./foo/bar.json +``` + +Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. + +=== `json_equals` + +```yml +json_equals: { "key": "value" } +``` + +Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. + +You can also structure the condition content as YAML and it will be converted to the equivalent JSON document for testing: + +```yml +json_equals: + key: value +``` + +=== `json_contains` + +```yml +json_contains: { "key": "value" } +``` + +Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. + +== Running tests + +Executing tests for a specific config can be done by pointing the subcommand `test` at either the config to be tested or its test definition, e.g. `benthos test ./config.yaml` and `benthos test ./config_benthos_test.yaml` are equivalent. + +The `test` subcommand also supports wildcard patterns e.g. `benthos test ./foo/*.yaml` will execute all tests within matching files. In order to walk a directory tree and execute all tests found you can use the shortcut `./...`, e.g. `benthos test ./...` will execute all tests found in the current directory, any child directories, and so on. + +If you want to allow components to write logs at a provided level to stdout when running the tests, you can use +`benthos test --log `. Please consult the {logger-url}[logger docs] for further details. + +== Mocking processors + +BETA: This feature is currently in a BETA phase, which means breaking changes could be made if a fundamental issue with the feature is found. + +Sometimes you'll want to write tests for a series of processors, where one or more of them are networked (or otherwise stateful). Rather than creating and managing mocked services you can define mock versions of those processors in the test definition. For example, if we have a config with the following processors: + +```yaml +pipeline: + processors: + - mapping: 'root = "simon says: " + content()' + - label: get_foobar_api + http: + url: http://example.com/foobar + verb: GET + - mapping: 'root = content().uppercase()' +``` + +Rather than create a fake service for the `http` processor to interact with we can define a mock in our test definition that replaces it with a {processors-mapping-url}[`mapping` processor]. Mocks are configured as a map of labels that identify a processor to replace and the config to replace it with: + +```yaml +tests: + - name: mocks the http proc + target_processors: '/pipeline/processors' + mocks: + get_foobar_api: + mapping: 'root = content().string() + " this is some mock content"' + input_batch: + - content: "hello world" + output_batches: + - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" +``` + +With the above test definition the `http` processor will be swapped out for `mapping: 'root = content().string() + " this is some mock content"'`. For the purposes of mocking it is recommended that you use a {processors-mapping-url}[`mapping` processor] that simply mutates the message in a way that you would expect the mocked processor to. + +NOTE: It's not currently possible to mock components that are imported as separate resource files (using `--resource`/`-r`). It is recommended that you mock these by maintaining separate definitions for test purposes (`-r "./test/*.yaml"`). + +=== More granular mocking + +It is also possible to target specific fields within the test config by {json-pointer-url}[JSON pointers] as an alternative to labels. The following test definition would create the same mock as the previous: + +```yaml +tests: + - name: mocks the http proc + target_processors: '/pipeline/processors' + mocks: + /pipeline/processors/1: + mapping: 'root = content().string() + " this is some mock content"' + input_batch: + - content: "hello world" + output_batches: + - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" +``` + +== Fields + +The schema of a template file is as follows: + +=== `tests` + +A list of one or more unit tests to execute. + + +*Type*: list of `object` + + +=== `tests[].name` + +The name of the test, this should be unique and give a rough indication of what behavior is being tested. + + +*Type*: `string` + + +=== `tests[].environment` + +An optional map of environment variables to set for the duration of the test. + + +*Type*: map of `string` + + +=== `tests[].target_processors` + +A [JSON Pointer][json-pointer] that identifies the specific processors which should be executed by the test. The target can either be a single processor or an array of processors. Alternatively a resource label can be used to identify a processor. + +It is also possible to target processors in a separate file by prefixing the target with a path relative to the test file followed by a # symbol. + + +*Type*: `string` + +*Default*: `"/pipeline/processors"` + +```yml +# Examples + +target_processors: foo_processor + +target_processors: /pipeline/processors/0 + +target_processors: target.yaml#/pipeline/processors + +target_processors: target.yaml#/pipeline/processors +``` + +=== `tests[].target_mapping` + +A file path relative to the test definition path of a Bloblang file to execute as an alternative to testing processors with the `target_processors` field. This allows you to define unit tests for Bloblang mappings directly. + + +*Type*: `string` + +*Default*: `""` + +=== `tests[].mocks` + +An optional map of processors to mock. Keys should contain either a label or a JSON pointer of a processor that should be mocked. Values should contain a processor definition, which will replace the mocked processor. Most of the time you'll want to use a [`mapping` processor][processors.mapping] here, and use it to create a result that emulates the target processor. + + +*Type*: map of `unknown` + + +```yml +# Examples + +mocks: + get_foobar_api: + mapping: root = content().string() + " this is some mock content" + +mocks: + /pipeline/processors/1: + mapping: root = content().string() + " this is some mock content" +``` + +=== `tests[].input_batch` + +Define a batch of messages to feed into your test, specify either an `input_batch` or a series of `input_batches`. + + +*Type*: list of `object` + + +=== `tests[].input_batch[].content` + +The raw content of the input message. + + +*Type*: `string` + + +=== `tests[].input_batch[].json_content` + +Sets the raw content of the message to a JSON document matching the structure of the value. + + +*Type*: `unknown` + + +```yml +# Examples + +json_content: + bar: + - element1 + - 10 + foo: foo value +``` + +=== `tests[].input_batch[].file_content` + +Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. + + +*Type*: `string` + + +```yml +# Examples + +file_content: ./foo/bar.txt +``` + +=== `tests[].input_batch[].metadata` + +A map of metadata key/values to add to the input message. + + +*Type*: map of `unknown` + + +=== `tests[].input_batches` + +Define a series of batches of messages to feed into your test, specify either an `input_batch` or a series of `input_batches`. + + +*Type*: `object` + + +=== `tests[].input_batches[][].content` + +The raw content of the input message. + + +*Type*: `string` + + +=== `tests[].input_batches[][].json_content` + +Sets the raw content of the message to a JSON document matching the structure of the value. + + +*Type*: `unknown` + + +```yml +# Examples + +json_content: + bar: + - element1 + - 10 + foo: foo value +``` + +=== `tests[].input_batches[][].file_content` + +Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. + + +*Type*: `string` + + +```yml +# Examples + +file_content: ./foo/bar.txt +``` + +=== `tests[].input_batches[][].metadata` + +A map of metadata key/values to add to the input message. + + +*Type*: map of `unknown` + + +=== `tests[].output_batches` + +List of output batches. + + +*Type*: `object` + + +=== `tests[].output_batches[][].bloblang` + +Executes a Bloblang mapping on the output message, if the result is anything other than a boolean equalling `true` the test fails. + + +*Type*: `string` + + +```yml +# Examples + +bloblang: this.age > 10 && @foo.length() > 0 +``` + +=== `tests[].output_batches[][].content_equals` + +Checks the full raw contents of a message against a value. + + +*Type*: `string` + + +=== `tests[].output_batches[][].content_matches` + +Checks whether the full raw contents of a message matches a regular expression (re2). + + +*Type*: `string` + + +```yml +# Examples + +content_matches: ^foo [a-z]+ bar$ +``` + +=== `tests[].output_batches[][].metadata_equals` + +Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. + + +*Type*: map of `unknown` + + +```yml +# Examples + +metadata_equals: + example_key: example metadata value +``` + +=== `tests[].output_batches[][].file_equals` + +Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. + + +*Type*: `string` + + +```yml +# Examples + +file_equals: ./foo/bar.txt +``` + +=== `tests[].output_batches[][].file_json_equals` + +Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. + + +*Type*: `string` + + +```yml +# Examples + +file_json_equals: ./foo/bar.json +``` + +=== `tests[].output_batches[][].json_equals` + +Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. + + +*Type*: `unknown` + + +```yml +# Examples + +json_equals: + key: value +``` + +=== `tests[].output_batches[][].json_contains` + +Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. + + +*Type*: `unknown` + + +```yml +# Examples + +json_contains: + key: value +``` + +=== `tests[].output_batches[][].file_json_contains` + +Checks that both the message and the file contents are valid JSON documents, and that the message is a superset of the condition. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. + + +*Type*: `string` + + +```yml +# Examples + +file_json_contains: ./foo/bar.json +``` + diff --git a/docs/modules/guides/pages/bloblang/functions.adoc b/docs/modules/guides/pages/bloblang/functions.adoc new file mode 100644 index 0000000000..d2cb9878a0 --- /dev/null +++ b/docs/modules/guides/pages/bloblang/functions.adoc @@ -0,0 +1,728 @@ += Bloblang Functions +:description: A list of Bloblang functions + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/bloblang/query/functions.go + internal/docs/bloblang.go +//// + + +Functions can be placed anywhere and allow you to extract information from your environment, generate values, or access data from the underlying message being mapped: + +```coffeescript +root.doc.id = uuid_v4() +root.doc.received_at = now() +root.doc.host = hostname() +``` + +Functions support both named and nameless style arguments: + +```coffeescript +root.values_one = range(start: 0, stop: this.max, step: 2) +root.values_two = range(0, this.max, 2) +``` + +== General + +=== `counter` + +[CAUTION] +.Experimental +==== +This function is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Returns a non-negative integer that increments each time it is resolved, yielding the minimum (`1` by default) as the first value. Each instantiation of `counter` has its own independent count. Once the maximum integer (or `max` argument) is reached the counter resets back to the minimum. + +==== Parameters + +*`min`* <query expression, default `1`> The minimum value of the counter, this is the first value that will be yielded. If this parameter is dynamic it will be resolved only once during the lifetime of the mapping. +*`max`* <query expression, default `9223372036854775807`> The maximum value of the counter, once this value is yielded the counter will reset back to the min. If this parameter is dynamic it will be resolved only once during the lifetime of the mapping. +*`set`* <(optional) query expression> An optional mapping that when specified will be executed each time the counter is resolved. When this mapping resolves to a non-negative integer value it will cause the counter to reset to this value and yield it. If this mapping is omitted or doesn't resolve to anything then the counter will increment and yield the value as normal. If this mapping resolves to `null` then the counter is not incremented and the current value is yielded. If this mapping resolves to a deletion then the counter is reset to the `min` value. + +==== Examples + + +```coffeescript +root.id = counter() + +# In: {} +# Out: {"id":1} + +# In: {} +# Out: {"id":2} +``` + +It's possible to increment a counter multiple times within a single mapping invocation using a map. + +```coffeescript + +map foos { + root = counter() +} + +root.meow_id = null.apply("foos") +root.woof_id = null.apply("foos") + + +# In: {} +# Out: {"meow_id":1,"woof_id":2} + +# In: {} +# Out: {"meow_id":3,"woof_id":4} +``` + +By specifying an optional `set` parameter it is possible to dynamically reset the counter based on input data. + +```coffeescript +root.consecutive_doggos = counter(min: 1, set: if !this.sound.lowercase().contains("woof") { 0 }) + +# In: {"sound":"woof woof"} +# Out: {"consecutive_doggos":1} + +# In: {"sound":"woofer wooooo"} +# Out: {"consecutive_doggos":2} + +# In: {"sound":"meow"} +# Out: {"consecutive_doggos":0} + +# In: {"sound":"uuuuh uh uh woof uhhhhhh"} +# Out: {"consecutive_doggos":1} +``` + +The `set` parameter can also be utilized to peek at the counter without mutating it by returning `null`. + +```coffeescript +root.things = counter(set: if this.id == null { null }) + +# In: {"id":"a"} +# Out: {"things":1} + +# In: {"id":"b"} +# Out: {"things":2} + +# In: {"what":"just checking"} +# Out: {"things":2} + +# In: {"id":"c"} +# Out: {"things":3} +``` + +=== `deleted` + +A function that returns a result indicating that the mapping target should be deleted. Deleting, also known as dropping, messages will result in them being acknowledged as successfully processed to inputs in a Benthos pipeline. For more information about error handling patterns read xref:configuration:error_handling.adoc[]. + +==== Examples + + +```coffeescript +root = this +root.bar = deleted() + +# In: {"bar":"bar_value","baz":"baz_value","foo":"foo value"} +# Out: {"baz":"baz_value","foo":"foo value"} +``` + +Since the result is a value it can be used to do things like remove elements of an array within `map_each`. + +```coffeescript +root.new_nums = this.nums.map_each(num -> if num < 10 { deleted() } else { num - 10 }) + +# In: {"nums":[3,11,4,17]} +# Out: {"new_nums":[1,7]} +``` + +=== `ksuid` + +Generates a new ksuid each time it is invoked and prints a string representation. + +==== Examples + + +```coffeescript +root.id = ksuid() +``` + +=== `nanoid` + +Generates a new nanoid each time it is invoked and prints a string representation. + +==== Parameters + +*`length`* <(optional) integer> An optional length. +*`alphabet`* <(optional) string> An optional custom alphabet to use for generating IDs. When specified the field `length` must also be present. + +==== Examples + + +```coffeescript +root.id = nanoid() +``` + +It is possible to specify an optional length parameter. + +```coffeescript +root.id = nanoid(54) +``` + +It is also possible to specify an optional custom alphabet after the length parameter. + +```coffeescript +root.id = nanoid(54, "abcde") +``` + +=== `random_int` + + +Generates a non-negative pseudo-random 64-bit integer. An optional integer argument can be provided in order to seed the random number generator. + +Optional `min` and `max` arguments can be provided in order to only generate numbers within a range. Neither of these parameters can be set via a dynamic expression (i.e. from values taken from mapped data). Instead, for dynamic ranges extract a min and max manually using a modulo operator (`random_int() % a + b`). + +==== Parameters + +*`seed`* <query expression, default `{"Value":0}`> A seed to use, if a query is provided it will only be resolved once during the lifetime of the mapping. +*`min`* <integer, default `0`> The minimum value the random generated number will have. The default value is 0. +*`max`* <integer, default `9223372036854775806`> The maximum value the random generated number will have. The default value is 9223372036854775806 (math.MaxInt64 - 1). + +==== Examples + + +```coffeescript +root.first = random_int() +root.second = random_int(1) +root.third = random_int(max:20) +root.fourth = random_int(min:10, max:20) +root.fifth = random_int(timestamp_unix_nano(), 5, 20) +root.sixth = random_int(seed:timestamp_unix_nano(), max:20) + +``` + +It is possible to specify a dynamic seed argument, in which case the argument will only be resolved once during the lifetime of the mapping. + +```coffeescript +root.first = random_int(timestamp_unix_nano()) +``` + +=== `range` + +The `range` function creates an array of integers following a range between a start, stop and optional step integer argument. If the step argument is omitted then it defaults to 1. A negative step can be provided as long as stop < start. + +==== Parameters + +*`start`* <integer> The start value. +*`stop`* <integer> The stop value. +*`step`* <integer, default `1`> The step value. + +==== Examples + + +```coffeescript +root.a = range(0, 10) +root.b = range(start: 0, stop: this.max, step: 2) # Using named params +root.c = range(0, -this.max, -2) + +# In: {"max":10} +# Out: {"a":[0,1,2,3,4,5,6,7,8,9],"b":[0,2,4,6,8],"c":[0,-2,-4,-6,-8]} +``` + +=== `snowflake_id` + +Generate a new snowflake ID each time it is invoked and prints a string representation. I.e.: 1559229974454472704 + +==== Parameters + +*`node_id`* <integer, default `1`> It is possible to specify the node_id. + +==== Examples + + +```coffeescript +root.id = snowflake_id() +``` + +It is possible to specify the node_id. + +```coffeescript +root.id = snowflake_id(2) +``` + +=== `throw` + +Throws an error similar to a regular mapping error. This is useful for abandoning a mapping entirely given certain conditions. + +==== Parameters + +*`why`* <string> A string explanation for why an error was thrown, this will be added to the resulting error message. + +==== Examples + + +```coffeescript +root.doc.type = match { + this.exists("header.id") => "foo" + this.exists("body.data") => "bar" + _ => throw("unknown type") +} +root.doc.contents = (this.body.content | this.thing.body) + +# In: {"header":{"id":"first"},"thing":{"body":"hello world"}} +# Out: {"doc":{"contents":"hello world","type":"foo"}} + +# In: {"nothing":"matches"} +# Out: Error("failed assignment (line 1): unknown type") +``` + +=== `ulid` + +[CAUTION] +.Experimental +==== +This function is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Generate a random ULID. + +==== Parameters + +*`encoding`* <string, default `"crockford"`> The format to encode a ULID into. Valid options are: crockford, hex +*`random_source`* <string, default `"secure_random"`> The source of randomness to use for generating ULIDs. "secure_random" is recommended for most use cases. "fast_random" can be used if security is not a concern. + +==== Examples + + +Using the defaults of Crockford Base32 encoding and secure random source + +```coffeescript +root.id = ulid() +``` + +ULIDs can be hex-encoded too. + +```coffeescript +root.id = ulid("hex") +``` + +They can be generated using a fast, but unsafe, random source for use cases that are not security-sensitive. + +```coffeescript +root.id = ulid("crockford", "fast_random") +``` + +=== `uuid_v4` + +Generates a new RFC-4122 UUID each time it is invoked and prints a string representation. + +==== Examples + + +```coffeescript +root.id = uuid_v4() +``` + +== Message Info + +=== `batch_index` + +Returns the index of the mapped message within a batch. This is useful for applying maps only on certain messages of a batch. + +==== Examples + + +```coffeescript +root = if batch_index() > 0 { deleted() } +``` + +=== `batch_size` + +Returns the size of the message batch. + +==== Examples + + +```coffeescript +root.foo = batch_size() +``` + +=== `content` + +Returns the full raw contents of the mapping target message as a byte array. When mapping to a JSON field the value should be encoded using the method xref:guides:bloblang/methods.adoc#encode[`encode`], or cast to a string directly using the method xref:guides:bloblang/methods.adoc#string[`string`], otherwise it will be base64 encoded by default. + +==== Examples + + +```coffeescript +root.doc = content().string() + +# In: {"foo":"bar"} +# Out: {"doc":"{\"foo\":\"bar\"}"} +``` + +=== `error` + +If an error has occurred during the processing of a message this function returns the reported cause of the error as a string, otherwise `null`. For more information about error handling patterns read xref:configuration:error_handling.adoc[]. + +==== Examples + + +```coffeescript +root.doc.error = error() +``` + +=== `errored` + +Returns a boolean value indicating whether an error has occurred during the processing of a message. For more information about error handling patterns read xref:configuration:error_handling.adoc[]. + +==== Examples + + +```coffeescript +root.doc.status = if errored() { 400 } else { 200 } +``` + +=== `json` + +Returns the value of a field within a JSON message located by a [dot path][field_paths] argument. This function always targets the entire source JSON document regardless of the mapping context. + +==== Parameters + +*`path`* <string, default `""`> An optional [dot path][field_paths] identifying a field to obtain. + +==== Examples + + +```coffeescript +root.mapped = json("foo.bar") + +# In: {"foo":{"bar":"hello world"}} +# Out: {"mapped":"hello world"} +``` + +The path argument is optional and if omitted the entire JSON payload is returned. + +```coffeescript +root.doc = json() + +# In: {"foo":{"bar":"hello world"}} +# Out: {"doc":{"foo":{"bar":"hello world"}}} +``` + +=== `metadata` + +Returns the value of a metadata key from the input message, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map, in order to query metadata mutations made within a mapping use the xref:guides:bloblang/about.adoc#metadata[`@` operator]. This function supports extracting metadata from other messages of a batch with the `from` method. + +==== Parameters + +*`key`* <string, default `""`> An optional key of a metadata value to obtain. + +==== Examples + + +```coffeescript +root.topic = metadata("kafka_topic") +``` + +The key parameter is optional and if omitted the entire metadata contents are returned as an object. + +```coffeescript +root.all_metadata = metadata() +``` + +=== `tracing_id` + +[CAUTION] +.Experimental +==== +This function is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Provides the message trace id. The returned value will be zeroed if the message does not contain a span. + +==== Examples + + +```coffeescript +meta trace_id = tracing_id() +``` + +=== `tracing_span` + +[CAUTION] +.Experimental +==== +This function is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Provides the message tracing span xref:components:tracers/about.adoc[(created via Open Telemetry APIs)] as an object serialized via text map formatting. The returned value will be `null` if the message does not have a span. + +==== Examples + + +```coffeescript +root.headers.traceparent = tracing_span().traceparent + +# In: {"some_stuff":"just can't be explained by science"} +# Out: {"headers":{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}} +``` + +== Environment + +=== `env` + +Returns the value of an environment variable, or `null` if the environment variable does not exist. + +==== Parameters + +*`name`* <string> The name of an environment variable. +*`no_cache`* <bool, default `false`> Force the variable lookup to occur for each mapping invocation. + +==== Examples + + +```coffeescript +root.thing.key = env("key").or("default value") +``` + +```coffeescript +root.thing.key = env(this.thing.key_name) +``` + +When the name parameter is static this function will only resolve once and yield the same result for each invocation as an optimization, this means that updates to env vars during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the variable lookup to be performed for each execution of the mapping. + +```coffeescript +root.thing.key = env(name: "key", no_cache: true) +``` + +=== `file` + +Reads a file and returns its contents. Relative paths are resolved from the directory of the process executing the mapping. In order to read files relative to the mapping file use the newer <> + +==== Parameters + +*`path`* <string> The path of the target file. +*`no_cache`* <bool, default `false`> Force the file to be read for each mapping invocation. + +==== Examples + + +```coffeescript +root.doc = file(env("BENTHOS_TEST_BLOBLANG_FILE")).parse_json() + +# In: {} +# Out: {"doc":{"foo":"bar"}} +``` + +When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimization, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping. + +```coffeescript +root.doc = file(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json() + +# In: {} +# Out: {"doc":{"foo":"bar"}} +``` + +=== `file_rel` + +Reads a file and returns its contents. Relative paths are resolved from the directory of the mapping. + +==== Parameters + +*`path`* <string> The path of the target file. +*`no_cache`* <bool, default `false`> Force the file to be read for each mapping invocation. + +==== Examples + + +```coffeescript +root.doc = file_rel(env("BENTHOS_TEST_BLOBLANG_FILE")).parse_json() + +# In: {} +# Out: {"doc":{"foo":"bar"}} +``` + +When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimization, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping. + +```coffeescript +root.doc = file_rel(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json() + +# In: {} +# Out: {"doc":{"foo":"bar"}} +``` + +=== `hostname` + +Returns a string matching the hostname of the machine running Benthos. + +==== Examples + + +```coffeescript +root.thing.host = hostname() +``` + +=== `now` + +Returns the current timestamp as a string in RFC 3339 format with the local timezone. Use the method `ts_format` in order to change the format and timezone. + +==== Examples + + +```coffeescript +root.received_at = now() +``` + +```coffeescript +root.received_at = now().ts_format("Mon Jan 2 15:04:05 -0700 MST 2006", "UTC") +``` + +=== `timestamp_unix` + +Returns the current unix timestamp in seconds. + +==== Examples + + +```coffeescript +root.received_at = timestamp_unix() +``` + +=== `timestamp_unix_micro` + +Returns the current unix timestamp in microseconds. + +==== Examples + + +```coffeescript +root.received_at = timestamp_unix_micro() +``` + +=== `timestamp_unix_milli` + +Returns the current unix timestamp in milliseconds. + +==== Examples + + +```coffeescript +root.received_at = timestamp_unix_milli() +``` + +=== `timestamp_unix_nano` + +Returns the current unix timestamp in nanoseconds. + +==== Examples + + +```coffeescript +root.received_at = timestamp_unix_nano() +``` + +== Fake Data Generation + +=== `fake` + +[NOTE] +.Beta +==== +This function is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Takes in a string that maps to a https://github.com/go-faker/faker[faker] function and returns the result from that faker function. Returns an error if the given string doesn't match a supported faker function. Supported functions: `latitude`, `longitude`, `unix_time`, `date`, `time_string`, `month_name`, `year_string`, `day_of_week`, `day_of_month`, `timestamp`, `century`, `timezone`, `time_period`, `email`, `mac_address`, `domain_name`, `url`, `username`, `ipv4`, `ipv6`, `password`, `jwt`, `word`, `sentence`, `paragraph`, `cc_type`, `cc_number`, `currency`, `amount_with_currency`, `title_male`, `title_female`, `first_name`, `first_name_male`, `first_name_female`, `last_name`, `name`, `gender`, `chinese_first_name`, `chinese_last_name`, `chinese_name`, `phone_number`, `toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the https://github.com/go-faker/faker[faker] docs for details on these functions. + +==== Parameters + +*`function`* <string, default `""`> The name of the function to use to generate the value. + +==== Examples + + +Use `time_string` to generate a time in the format `00:00:00`: + +```coffeescript +root.time = fake("time_string") +``` + +Use `email` to generate a string in email address format: + +```coffeescript +root.email = fake("email") +``` + +Use `jwt` to generate a JWT token: + +```coffeescript +root.jwt = fake("jwt") +``` + +Use `uuid_hyphenated` to generate a hyphenated UUID: + +```coffeescript +root.uuid = fake("uuid_hyphenated") +``` + +== Deprecated + +=== `count` + +The `count` function is a counter starting at 1 which increments after each time it is called. Count takes an argument which is an identifier for the counter, allowing you to specify multiple unique counters in your configuration. + +==== Parameters + +*`name`* <string> An identifier for the counter. + +==== Examples + + +```coffeescript +root = this +root.id = count("bloblang_function_example") + +# In: {"message":"foo"} +# Out: {"id":1,"message":"foo"} + +# In: {"message":"bar"} +# Out: {"id":2,"message":"bar"} +``` + +=== `meta` + +Returns the value of a metadata key from the input message as a string, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map. In order to query metadata mutations made within a mapping use the <>. This function supports extracting metadata from other messages of a batch with the `from` method. + +==== Parameters + +*`key`* <string, default `""`> An optional key of a metadata value to obtain. + +==== Examples + + +```coffeescript +root.topic = meta("kafka_topic") +``` + +The key parameter is optional and if omitted the entire metadata contents are returned as an object. + +```coffeescript +root.all_metadata = meta() +``` + +=== `root_meta` + +Returns the value of a metadata key from the new message being created as a string, or `null` if the key does not exist. Changes made to metadata during a mapping will be reflected by this function. + +==== Parameters + +*`key`* <string, default `""`> An optional key of a metadata value to obtain. + +==== Examples + + +```coffeescript +root.topic = root_meta("kafka_topic") +``` + +The key parameter is optional and if omitted the entire metadata contents are returned as an object. + +```coffeescript +root.all_metadata = root_meta() +``` + diff --git a/docs/modules/guides/pages/bloblang/methods.adoc b/docs/modules/guides/pages/bloblang/methods.adoc new file mode 100644 index 0000000000..008217c034 --- /dev/null +++ b/docs/modules/guides/pages/bloblang/methods.adoc @@ -0,0 +1,3893 @@ += Bloblang Methods +:description: A list of Bloblang methods + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/bloblang/query/methods.go + internal/bloblang/query/methods_strings.go + internal/docs/bloblang.go +//// + + +Methods provide most of the power in Bloblang as they allow you to augment values and can be added to any expression (including other methods): + +```coffeescript +root.doc.id = this.thing.id.string().catch(uuid_v4()) +root.doc.reduced_nums = this.thing.nums.map_each(num -> if num < 10 { + deleted() +} else { + num - 10 +}) +root.has_good_taste = ["pikachu","mewtwo","magmar"].contains(this.user.fav_pokemon) +``` + +Methods support both named and nameless style arguments: + +```coffeescript +root.foo_one = this.(bar | baz).trim().replace_all(old: "dog", new: "cat") +root.foo_two = this.(bar | baz).trim().replace_all("dog", "cat") +``` + +== General + +=== `apply` + +Apply a declared mapping to a target value. + +==== Parameters + +*`mapping`* <string> The mapping to apply. + +==== Examples + + +```coffeescript +map thing { + root.inner = this.first +} + +root.foo = this.doc.apply("thing") + +# In: {"doc":{"first":"hello world"}} +# Out: {"foo":{"inner":"hello world"}} +``` + +```coffeescript +map create_foo { + root.name = "a foo" + root.purpose = "to be a foo" +} + +root = this +root.foo = null.apply("create_foo") + +# In: {"id":"1234"} +# Out: {"foo":{"name":"a foo","purpose":"to be a foo"},"id":"1234"} +``` + +=== `catch` + +If the result of a target query fails (due to incorrect types, failed parsing, etc) the argument is returned instead. + +==== Parameters + +*`fallback`* <query expression> A value to yield, or query to execute, if the target query fails. + +==== Examples + + +```coffeescript +root.doc.id = this.thing.id.string().catch(uuid_v4()) +``` + +The fallback argument can be a mapping, allowing you to capture the error string and yield structured data back. + +```coffeescript +root.url = this.url.parse_url().catch(err -> {"error":err,"input":this.url}) + +# In: {"url":"invalid %&# url"} +# Out: {"url":{"error":"field `this.url`: parse \"invalid %&\": invalid URL escape \"%&\"","input":"invalid %&# url"}} +``` + +When the input document is not structured attempting to reference structured fields with `this` will result in an error. Therefore, a convenient way to delete non-structured data is with a catch. + +```coffeescript +root = this.catch(deleted()) + +# In: {"doc":{"foo":"bar"}} +# Out: {"doc":{"foo":"bar"}} + +# In: not structured data +# Out: +``` + +=== `exists` + +Checks that a field, identified via a xref:configuration:field_paths.adoc[dot path], exists in an object. + +==== Parameters + +*`path`* <string> A xref:configuration:field_paths.adoc[dot path] to a field. + +==== Examples + + +```coffeescript +root.result = this.foo.exists("bar.baz") + +# In: {"foo":{"bar":{"baz":"yep, I exist"}}} +# Out: {"result":true} + +# In: {"foo":{"bar":{}}} +# Out: {"result":false} + +# In: {"foo":{}} +# Out: {"result":false} +``` + +=== `from` + +Modifies a target query such that certain functions are executed from the perspective of another message in the batch. This allows you to mutate events based on the contents of other messages. Functions that support this behavior are `content`, `json` and `meta`. + +==== Parameters + +*`index`* <integer> The message index to use as a perspective. + +==== Examples + + +For example, the following map extracts the contents of the JSON field `foo` specifically from message index `1` of a batch, effectively overriding the field `foo` for all messages of a batch to that of message 1: + +```coffeescript +root = this +root.foo = json("foo").from(1) +``` + +=== `from_all` + +Modifies a target query such that certain functions are executed from the perspective of each message in the batch, and returns the set of results as an array. Functions that support this behavior are `content`, `json` and `meta`. + +==== Examples + + +```coffeescript +root = this +root.foo_summed = json("foo").from_all().sum() +``` + +=== `or` + +If the result of the target query fails or resolves to `null`, returns the argument instead. This is an explicit method alternative to the coalesce pipe operator `|`. + +==== Parameters + +*`fallback`* <query expression> A value to yield, or query to execute, if the target query fails or resolves to `null`. + +==== Examples + + +```coffeescript +root.doc.id = this.thing.id.or(uuid_v4()) +``` + +== String Manipulation + +=== `capitalize` + +Takes a string value and returns a copy with all Unicode letters that begin words mapped to their Unicode title case. + +==== Examples + + +```coffeescript +root.title = this.title.capitalize() + +# In: {"title":"the foo bar"} +# Out: {"title":"The Foo Bar"} +``` + +=== `compare_argon2` + +Checks whether a string matches a hashed secret using Argon2. + +==== Parameters + +*`hashed_secret`* <string> The hashed secret to compare with the input. This must be a fully-qualified string which encodes the Argon2 options used to generate the hash. + +==== Examples + + +```coffeescript +root.match = this.secret.compare_argon2("$argon2id$v=19$m=4096,t=3,p=1$c2FsdHktbWNzYWx0ZmFjZQ$RMUMwgtS32/mbszd+ke4o4Ej1jFpYiUqY6MHWa69X7Y") + +# In: {"secret":"there-are-many-blobs-in-the-sea"} +# Out: {"match":true} +``` + +```coffeescript +root.match = this.secret.compare_argon2("$argon2id$v=19$m=4096,t=3,p=1$c2FsdHktbWNzYWx0ZmFjZQ$RMUMwgtS32/mbszd+ke4o4Ej1jFpYiUqY6MHWa69X7Y") + +# In: {"secret":"will-i-ever-find-love"} +# Out: {"match":false} +``` + +=== `compare_bcrypt` + +Checks whether a string matches a hashed secret using bcrypt. + +==== Parameters + +*`hashed_secret`* <string> The hashed secret value to compare with the input. + +==== Examples + + +```coffeescript +root.match = this.secret.compare_bcrypt("$2y$10$Dtnt5NNzVtMCOZONT705tOcS8It6krJX8bEjnDJnwxiFKsz1C.3Ay") + +# In: {"secret":"there-are-many-blobs-in-the-sea"} +# Out: {"match":true} +``` + +```coffeescript +root.match = this.secret.compare_bcrypt("$2y$10$Dtnt5NNzVtMCOZONT705tOcS8It6krJX8bEjnDJnwxiFKsz1C.3Ay") + +# In: {"secret":"will-i-ever-find-love"} +# Out: {"match":false} +``` + +=== `contains` + +Checks whether a string contains a substring and returns a boolean result. + +==== Parameters + +*`value`* <unknown> A value to test against elements of the target. + +==== Examples + + +```coffeescript +root.has_foo = this.thing.contains("foo") + +# In: {"thing":"this foo that"} +# Out: {"has_foo":true} + +# In: {"thing":"this bar that"} +# Out: {"has_foo":false} +``` + +=== `escape_html` + +Escapes a string so that special characters like `<` to become `<`. It escapes only five such characters: `<`, `>`, `&`, `'` and `"` so that it can be safely placed within an HTML entity. + +==== Examples + + +```coffeescript +root.escaped = this.value.escape_html() + +# In: {"value":"foo & bar"} +# Out: {"escaped":"foo & bar"} +``` + +=== `escape_url_query` + +Escapes a string so that it can be safely placed within a URL query. + +==== Examples + + +```coffeescript +root.escaped = this.value.escape_url_query() + +# In: {"value":"foo & bar"} +# Out: {"escaped":"foo+%26+bar"} +``` + +=== `filepath_join` + +Joins an array of path elements into a single file path. The separator depends on the operating system of the machine. + +==== Examples + + +```coffeescript +root.path = this.path_elements.filepath_join() + +# In: {"path_elements":["/foo/","bar.txt"]} +# Out: {"path":"/foo/bar.txt"} +``` + +=== `filepath_split` + +Splits a file path immediately following the final Separator, separating it into a directory and file name component returned as a two element array of strings. If there is no Separator in the path, the first element will be empty and the second will contain the path. The separator depends on the operating system of the machine. + +==== Examples + + +```coffeescript +root.path_sep = this.path.filepath_split() + +# In: {"path":"/foo/bar.txt"} +# Out: {"path_sep":["/foo/","bar.txt"]} + +# In: {"path":"baz.txt"} +# Out: {"path_sep":["","baz.txt"]} +``` + +=== `format` + +Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go https://pkg.go.dev/fmt[`fmt` package documentation] for the list of valid format verbs. + +==== Examples + + +```coffeescript +root.foo = "%s(%v): %v".format(this.name, this.age, this.fingers) + +# In: {"name":"lance","age":37,"fingers":13} +# Out: {"foo":"lance(37): 13"} +``` + +=== `has_prefix` + +Checks whether a string has a prefix argument and returns a bool. + +==== Parameters + +*`value`* <string> The string to test. + +==== Examples + + +```coffeescript +root.t1 = this.v1.has_prefix("foo") +root.t2 = this.v2.has_prefix("foo") + +# In: {"v1":"foobar","v2":"barfoo"} +# Out: {"t1":true,"t2":false} +``` + +=== `has_suffix` + +Checks whether a string has a suffix argument and returns a bool. + +==== Parameters + +*`value`* <string> The string to test. + +==== Examples + + +```coffeescript +root.t1 = this.v1.has_suffix("foo") +root.t2 = this.v2.has_suffix("foo") + +# In: {"v1":"foobar","v2":"barfoo"} +# Out: {"t1":false,"t2":true} +``` + +=== `index_of` + +Returns the starting index of the argument substring in a string target, or `-1` if the target doesn't contain the argument. + +==== Parameters + +*`value`* <string> A string to search for. + +==== Examples + + +```coffeescript +root.index = this.thing.index_of("bar") + +# In: {"thing":"foobar"} +# Out: {"index":3} +``` + +```coffeescript +root.index = content().index_of("meow") + +# In: the cat meowed, the dog woofed +# Out: {"index":8} +``` + +=== `length` + +Returns the length of a string. + +==== Examples + + +```coffeescript +root.foo_len = this.foo.length() + +# In: {"foo":"hello world"} +# Out: {"foo_len":11} +``` + +=== `lowercase` + +Convert a string value into lowercase. + +==== Examples + + +```coffeescript +root.foo = this.foo.lowercase() + +# In: {"foo":"HELLO WORLD"} +# Out: {"foo":"hello world"} +``` + +=== `quote` + +Quotes a target string using escape sequences (`\t`, `\n`, `\xFF`, `\u0100`) for control characters and non-printable characters. + +==== Examples + + +```coffeescript +root.quoted = this.thing.quote() + +# In: {"thing":"foo\nbar"} +# Out: {"quoted":"\"foo\\nbar\""} +``` + +=== `replace_all` + +Replaces all occurrences of the first argument in a target string with the second argument. + +==== Parameters + +*`old`* <string> A string to match against. +*`new`* <string> A string to replace with. + +==== Examples + + +```coffeescript +root.new_value = this.value.replace_all("foo","dog") + +# In: {"value":"The foo ate my homework"} +# Out: {"new_value":"The dog ate my homework"} +``` + +=== `replace_all_many` + +For each pair of strings in an argument array, replaces all occurrences of the first item of the pair with the second. This is a more compact way of chaining a series of `replace_all` methods. + +==== Parameters + +*`values`* <array> An array of values, each even value will be replaced with the following odd value. + +==== Examples + + +```coffeescript +root.new_value = this.value.replace_all_many([ + "", "<b>", + "", "</b>", + "", "<i>", + "", "</i>", +]) + +# In: {"value":"Hello World"} +# Out: {"new_value":"<i>Hello</i> <b>World</b>"} +``` + +=== `reverse` + +Returns the target string in reverse order. + +==== Examples + + +```coffeescript +root.reversed = this.thing.reverse() + +# In: {"thing":"backwards"} +# Out: {"reversed":"sdrawkcab"} +``` + +```coffeescript +root = content().reverse() + +# In: {"thing":"backwards"} +# Out: }"sdrawkcab":"gniht"{ +``` + +=== `slice` + +Extract a slice from a string by specifying two indices, a low and high bound, which selects a half-open range that includes the first character, but excludes the last one. If the second index is omitted then it defaults to the length of the input sequence. + +==== Parameters + +*`low`* <integer> The low bound, which is the first element of the selection, or if negative selects from the end. +*`high`* <(optional) integer> An optional high bound. + +==== Examples + + +```coffeescript +root.beginning = this.value.slice(0, 2) +root.end = this.value.slice(4) + +# In: {"value":"foo bar"} +# Out: {"beginning":"fo","end":"bar"} +``` + +A negative low index can be used, indicating an offset from the end of the sequence. If the low index is greater than the length of the sequence then an empty result is returned. + +```coffeescript +root.last_chunk = this.value.slice(-4) +root.the_rest = this.value.slice(0, -4) + +# In: {"value":"foo bar"} +# Out: {"last_chunk":" bar","the_rest":"foo"} +``` + +=== `slug` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its https://pkg.go.dev/github.com/gosimple/slug[docs] for more information. + +Introduced in version 4.2.0. + + +==== Parameters + +*`lang`* <(optional) string, default `"en"`> + +==== Examples + + +Creates a slug from an English string + +```coffeescript +root.slug = this.value.slug() + +# In: {"value":"Gopher & Benthos"} +# Out: {"slug":"gopher-and-benthos"} +``` + +Creates a slug from a French string + +```coffeescript +root.slug = this.value.slug("fr") + +# In: {"value":"Gaufre & Poisson d'Eau Profonde"} +# Out: {"slug":"gaufre-et-poisson-deau-profonde"} +``` + +=== `split` + +Split a string value into an array of strings by splitting it on a string separator. + +==== Parameters + +*`delimiter`* <string> The delimiter to split with. + +==== Examples + + +```coffeescript +root.new_value = this.value.split(",") + +# In: {"value":"foo,bar,baz"} +# Out: {"new_value":["foo","bar","baz"]} +``` + +=== `strip_html` + +Attempts to remove all HTML tags from a target string. + +==== Parameters + +*`preserve`* <(optional) array> An optional array of element types to preserve in the output. + +==== Examples + + +```coffeescript +root.stripped = this.value.strip_html() + +# In: {"value":"

the plain old text

"} +# Out: {"stripped":"the plain old text"} +``` + +It's also possible to provide an explicit list of element types to preserve in the output. + +```coffeescript +root.stripped = this.value.strip_html(["article"]) + +# In: {"value":"

the plain old text

"} +# Out: {"stripped":"
the plain old text
"} +``` + +=== `trim` + +Remove all leading and trailing characters from a string that are contained within an argument cutset. If no arguments are provided then whitespace is removed. + +==== Parameters + +*`cutset`* <(optional) string> An optional string of characters to trim from the target value. + +==== Examples + + +```coffeescript +root.title = this.title.trim("!?") +root.description = this.description.trim() + +# In: {"description":" something happened and its amazing! ","title":"!!!watch out!?"} +# Out: {"description":"something happened and its amazing!","title":"watch out"} +``` + +=== `trim_prefix` + +Remove the provided leading prefix substring from a string. If the string does not have the prefix substring, it is returned unchanged. + +Introduced in version 4.12.0. + + +==== Parameters + +*`prefix`* <string> The leading prefix substring to trim from the string. + +==== Examples + + +```coffeescript +root.name = this.name.trim_prefix("foobar_") +root.description = this.description.trim_prefix("foobar_") + +# In: {"description":"unchanged","name":"foobar_blobton"} +# Out: {"description":"unchanged","name":"blobton"} +``` + +=== `trim_suffix` + +Remove the provided trailing suffix substring from a string. If the string does not have the suffix substring, it is returned unchanged. + +Introduced in version 4.12.0. + + +==== Parameters + +*`suffix`* <string> The trailing suffix substring to trim from the string. + +==== Examples + + +```coffeescript +root.name = this.name.trim_suffix("_foobar") +root.description = this.description.trim_suffix("_foobar") + +# In: {"description":"unchanged","name":"blobton_foobar"} +# Out: {"description":"unchanged","name":"blobton"} +``` + +=== `unescape_html` + +Unescapes a string so that entities like `<` become `<`. It unescapes a larger range of entities than `escape_html` escapes. For example, `á` unescapes to `á`, as does `á` and `&xE1;`. + +==== Examples + + +```coffeescript +root.unescaped = this.value.unescape_html() + +# In: {"value":"foo & bar"} +# Out: {"unescaped":"foo & bar"} +``` + +=== `unescape_url_query` + +Expands escape sequences from a URL query string. + +==== Examples + + +```coffeescript +root.unescaped = this.value.unescape_url_query() + +# In: {"value":"foo+%26+bar"} +# Out: {"unescaped":"foo & bar"} +``` + +=== `unquote` + +Unquotes a target string, expanding any escape sequences (`\t`, `\n`, `\xFF`, `\u0100`) for control characters and non-printable characters. + +==== Examples + + +```coffeescript +root.unquoted = this.thing.unquote() + +# In: {"thing":"\"foo\\nbar\""} +# Out: {"unquoted":"foo\nbar"} +``` + +=== `uppercase` + +Convert a string value into uppercase. + +==== Examples + + +```coffeescript +root.foo = this.foo.uppercase() + +# In: {"foo":"hello world"} +# Out: {"foo":"HELLO WORLD"} +``` + +== Regular Expressions + +=== `re_find_all` + +Returns an array containing all successive matches of a regular expression in a string. + +==== Parameters + +*`pattern`* <string> The pattern to match against. + +==== Examples + + +```coffeescript +root.matches = this.value.re_find_all("a.") + +# In: {"value":"paranormal"} +# Out: {"matches":["ar","an","al"]} +``` + +=== `re_find_all_object` + +Returns an array of objects containing all matches of the regular expression and the matches of its subexpressions. The key of each match value is the name of the group when specified, otherwise it is the index of the matching group, starting with the expression as a whole at 0. + +==== Parameters + +*`pattern`* <string> The pattern to match against. + +==== Examples + + +```coffeescript +root.matches = this.value.re_find_all_object("a(?Px*)b") + +# In: {"value":"-axxb-ab-"} +# Out: {"matches":[{"0":"axxb","foo":"xx"},{"0":"ab","foo":""}]} +``` + +```coffeescript +root.matches = this.value.re_find_all_object("(?m)(?P\\w+):\\s+(?P\\w+)$") + +# In: {"value":"option1: value1\noption2: value2\noption3: value3"} +# Out: {"matches":[{"0":"option1: value1","key":"option1","value":"value1"},{"0":"option2: value2","key":"option2","value":"value2"},{"0":"option3: value3","key":"option3","value":"value3"}]} +``` + +=== `re_find_all_submatch` + +Returns an array of arrays containing all successive matches of the regular expression in a string and the matches, if any, of its subexpressions. + +==== Parameters + +*`pattern`* <string> The pattern to match against. + +==== Examples + + +```coffeescript +root.matches = this.value.re_find_all_submatch("a(x*)b") + +# In: {"value":"-axxb-ab-"} +# Out: {"matches":[["axxb","xx"],["ab",""]]} +``` + +=== `re_find_object` + +Returns an object containing the first match of the regular expression and the matches of its subexpressions. The key of each match value is the name of the group when specified, otherwise it is the index of the matching group, starting with the expression as a whole at 0. + +==== Parameters + +*`pattern`* <string> The pattern to match against. + +==== Examples + + +```coffeescript +root.matches = this.value.re_find_object("a(?Px*)b") + +# In: {"value":"-axxb-ab-"} +# Out: {"matches":{"0":"axxb","foo":"xx"}} +``` + +```coffeescript +root.matches = this.value.re_find_object("(?P\\w+):\\s+(?P\\w+)") + +# In: {"value":"option1: value1"} +# Out: {"matches":{"0":"option1: value1","key":"option1","value":"value1"}} +``` + +=== `re_match` + +Checks whether a regular expression matches against any part of a string and returns a boolean. + +==== Parameters + +*`pattern`* <string> The pattern to match against. + +==== Examples + + +```coffeescript +root.matches = this.value.re_match("[0-9]") + +# In: {"value":"there are 10 puppies"} +# Out: {"matches":true} + +# In: {"value":"there are ten puppies"} +# Out: {"matches":false} +``` + +=== `re_replace_all` + +Replaces all occurrences of the argument regular expression in a string with a value. Inside the value $ signs are interpreted as submatch expansions, e.g. `$1` represents the text of the first submatch. + +==== Parameters + +*`pattern`* <string> The pattern to match against. +*`value`* <string> The value to replace with. + +==== Examples + + +```coffeescript +root.new_value = this.value.re_replace_all("ADD ([0-9]+)","+($1)") + +# In: {"value":"foo ADD 70"} +# Out: {"new_value":"foo +(70)"} +``` + +== Number Manipulation + +=== `abs` + +Returns the absolute value of an int64 or float64 number. As a special case, when an integer is provided that is the minimum value it is converted to the maximum value. + +==== Examples + + +```coffeescript + +root.outs = this.ins.map_each(ele -> ele.abs()) + + +# In: {"ins":[9,-18,1.23,-4.56]} +# Out: {"outs":[9,18,1.23,4.56]} +``` + +=== `ceil` + +Returns the least integer value greater than or equal to a number. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned. + +==== Examples + + +```coffeescript +root.new_value = this.value.ceil() + +# In: {"value":5.3} +# Out: {"new_value":6} + +# In: {"value":-5.9} +# Out: {"new_value":-5} +``` + +=== `float32` + + +Converts a numerical type into a 32-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`strconv.ParseFloat` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.out = this.in.float32() + + +# In: {"in":"6.674282313423543523453425345e-11"} +# Out: {"out":6.674283e-11} +``` + +=== `float64` + + +Converts a numerical type into a 64-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`strconv.ParseFloat` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.out = this.in.float64() + + +# In: {"in":"6.674282313423543523453425345e-11"} +# Out: {"out":6.674282313423544e-11} +``` + +=== `floor` + +Returns the greatest integer value less than or equal to the target number. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned. + +==== Examples + + +```coffeescript +root.new_value = this.value.floor() + +# In: {"value":5.7} +# Out: {"new_value":5} +``` + +=== `int16` + + +Converts a numerical type into a 16-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 16-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.int16() +root.b = this.b.round().int16() +root.c = this.c.int16() +root.d = this.d.int16().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":-12} +``` + +```coffeescript + +root = this.int16() + + +# In: "0xDE" +# Out: 222 +``` + +=== `int32` + + +Converts a numerical type into a 32-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 32-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.int32() +root.b = this.b.round().int32() +root.c = this.c.int32() +root.d = this.d.int32().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":-12} +``` + +```coffeescript + +root = this.int32() + + +# In: "0xDEAD" +# Out: 57005 +``` + +=== `int64` + + +Converts a numerical type into a 64-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 64-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.int64() +root.b = this.b.round().int64() +root.c = this.c.int64() +root.d = this.d.int64().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":-12} +``` + +```coffeescript + +root = this.int64() + + +# In: "0xDEADBEEF" +# Out: 3735928559 +``` + +=== `int8` + + +Converts a numerical type into a 8-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 8-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.int8() +root.b = this.b.round().int8() +root.c = this.c.int8() +root.d = this.d.int8().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":-12} +``` + +```coffeescript + +root = this.int8() + + +# In: "0xD" +# Out: 13 +``` + +=== `log` + +Returns the natural logarithm of a number. + +==== Examples + + +```coffeescript +root.new_value = this.value.log().round() + +# In: {"value":1} +# Out: {"new_value":0} + +# In: {"value":2.7183} +# Out: {"new_value":1} +``` + +=== `log10` + +Returns the decimal logarithm of a number. + +==== Examples + + +```coffeescript +root.new_value = this.value.log10() + +# In: {"value":100} +# Out: {"new_value":2} + +# In: {"value":1000} +# Out: {"new_value":3} +``` + +=== `max` + +Returns the largest numerical value found within an array. All values must be numerical and the array must not be empty, otherwise an error is returned. + +==== Examples + + +```coffeescript +root.biggest = this.values.max() + +# In: {"values":[0,3,2.5,7,5]} +# Out: {"biggest":7} +``` + +```coffeescript +root.new_value = [0,this.value].max() + +# In: {"value":-1} +# Out: {"new_value":0} + +# In: {"value":7} +# Out: {"new_value":7} +``` + +=== `min` + +Returns the smallest numerical value found within an array. All values must be numerical and the array must not be empty, otherwise an error is returned. + +==== Examples + + +```coffeescript +root.smallest = this.values.min() + +# In: {"values":[0,3,-2.5,7,5]} +# Out: {"smallest":-2.5} +``` + +```coffeescript +root.new_value = [10,this.value].min() + +# In: {"value":2} +# Out: {"new_value":2} + +# In: {"value":23} +# Out: {"new_value":10} +``` + +=== `round` + +Rounds numbers to the nearest integer, rounding half away from zero. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned. + +==== Examples + + +```coffeescript +root.new_value = this.value.round() + +# In: {"value":5.3} +# Out: {"new_value":5} + +# In: {"value":5.9} +# Out: {"new_value":6} +``` + +=== `uint16` + + +Converts a numerical type into a 16-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 16-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.uint16() +root.b = this.b.round().uint16() +root.c = this.c.uint16() +root.d = this.d.uint16().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":0} +``` + +```coffeescript + +root = this.uint16() + + +# In: "0xDE" +# Out: 222 +``` + +=== `uint32` + + +Converts a numerical type into a 32-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 32-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.uint32() +root.b = this.b.round().uint32() +root.c = this.c.uint32() +root.d = this.d.uint32().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":0} +``` + +```coffeescript + +root = this.uint32() + + +# In: "0xDEAD" +# Out: 57005 +``` + +=== `uint64` + + +Converts a numerical type into a 64-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 64-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.uint64() +root.b = this.b.round().uint64() +root.c = this.c.uint64() +root.d = this.d.uint64().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":0} +``` + +```coffeescript + +root = this.uint64() + + +# In: "0xDEADBEEF" +# Out: 3735928559 +``` + +=== `uint8` + + +Converts a numerical type into a 8-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). + +If the value is a string then an attempt will be made to parse it as a 8-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. + +==== Examples + + +```coffeescript + +root.a = this.a.uint8() +root.b = this.b.round().uint8() +root.c = this.c.uint8() +root.d = this.d.uint8().catch(0) + + +# In: {"a":12,"b":12.34,"c":"12","d":-12} +# Out: {"a":12,"b":12,"c":12,"d":0} +``` + +```coffeescript + +root = this.uint8() + + +# In: "0xD" +# Out: 13 +``` + +== Timestamp Manipulation + +=== `parse_duration` + +Attempts to parse a string as a duration and returns an integer of nanoseconds. A duration string is a possibly signed sequence of decimal numbers, each with an optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + +==== Examples + + +```coffeescript +root.delay_for_ns = this.delay_for.parse_duration() + +# In: {"delay_for":"50us"} +# Out: {"delay_for_ns":50000} +``` + +```coffeescript +root.delay_for_s = this.delay_for.parse_duration() / 1000000000 + +# In: {"delay_for":"2h"} +# Out: {"delay_for_s":7200} +``` + +=== `parse_duration_iso8601` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to parse a string using ISO-8601 rules as a duration and returns an integer of nanoseconds. A duration string is represented by the format "P[n]Y[n]M[n]DT[n]H[n]M[n]S" or "P[n]W". In these representations, the "[n]" is replaced by the value for each of the date and time elements that follow the "[n]". For example, "P3Y6M4DT12H30M5S" represents a duration of "three years, six months, four days, twelve hours, thirty minutes, and five seconds". The last field of the format allows fractions with one decimal place, so "P3.5S" will return 3500000000ns. Any additional decimals will be truncated. + +==== Examples + + +Arbitrary ISO-8601 duration string to nanoseconds: + +```coffeescript +root.delay_for_ns = this.delay_for.parse_duration_iso8601() + +# In: {"delay_for":"P3Y6M4DT12H30M5S"} +# Out: {"delay_for_ns":110839937000000000} +``` + +Two hours ISO-8601 duration string to seconds: + +```coffeescript +root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000 + +# In: {"delay_for":"PT2H"} +# Out: {"delay_for_s":7200} +``` + +Two and a half seconds ISO-8601 duration string to seconds: + +```coffeescript +root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000 + +# In: {"delay_for":"PT2.5S"} +# Out: {"delay_for_s":2.5} +``` + +=== `ts_add_iso8601` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Parse parameter string as ISO 8601 period and add it to value with high precision for units larger than an hour. + +==== Parameters + +*`duration`* <string> Duration in ISO 8601 format + +=== `ts_format` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to format a timestamp value as a string according to a specified format, or RFC 3339 by default. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. + +The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the <> method. + +==== Parameters + +*`format`* <string, default `"2006-01-02T15:04:05.999999999Z07:00"`> The output format to use. +*`tz`* <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. + +==== Examples + + +```coffeescript +root.something_at = (this.created_at + 300).ts_format() +``` + +An optional string argument can be used in order to specify the output format of the timestamp. The format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. + +```coffeescript +root.something_at = (this.created_at + 300).ts_format("2006-Jan-02 15:04:05") +``` + +A second optional string argument can also be used in order to specify a timezone, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. + +```coffeescript +root.something_at = this.created_at.ts_format(format: "2006-Jan-02 15:04:05", tz: "UTC") + +# In: {"created_at":1597405526} +# Out: {"something_at":"2020-Aug-14 11:45:26"} + +# In: {"created_at":"2020-08-14T11:50:26.371Z"} +# Out: {"something_at":"2020-Aug-14 11:50:26"} +``` + +And `ts_format` supports up to nanosecond precision with floating point timestamp values. + +```coffeescript +root.something_at = this.created_at.ts_format("2006-Jan-02 15:04:05.999999", "UTC") + +# In: {"created_at":1597405526.123456} +# Out: {"something_at":"2020-Aug-14 11:45:26.123456"} + +# In: {"created_at":"2020-08-14T11:50:26.371Z"} +# Out: {"something_at":"2020-Aug-14 11:50:26.371"} +``` + +=== `ts_parse` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as <>. + +The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the <> method. + +==== Parameters + +*`format`* <string> The format of the target string. + +==== Examples + + +```coffeescript +root.doc.timestamp = this.doc.timestamp.ts_parse("2006-Jan-02") + +# In: {"doc":{"timestamp":"2020-Aug-14"}} +# Out: {"doc":{"timestamp":"2020-08-14T00:00:00Z"}} +``` + +=== `ts_round` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns the result of rounding a timestamp to the nearest multiple of the argument duration (nanoseconds). The rounding behavior for halfway values is to round up. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +Introduced in version 4.2.0. + + +==== Parameters + +*`duration`* <integer> A duration measured in nanoseconds to round by. + +==== Examples + + +Use the method `parse_duration` to convert a duration string into an integer argument. + +```coffeescript +root.created_at_hour = this.created_at.ts_round("1h".parse_duration()) + +# In: {"created_at":"2020-08-14T05:54:23Z"} +# Out: {"created_at_hour":"2020-08-14T06:00:00Z"} +``` + +=== `ts_strftime` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to format a timestamp value as a string according to a specified strftime-compatible format. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. + +==== Parameters + +*`format`* <string> The output format to use. +*`tz`* <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used. + +==== Examples + + +The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strftime[man 3 strftime] for the list of format specifiers. + +```coffeescript +root.something_at = (this.created_at + 300).ts_strftime("%Y-%b-%d %H:%M:%S") +``` + +A second optional string argument can also be used in order to specify a timezone, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. + +```coffeescript +root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S", "UTC") + +# In: {"created_at":1597405526} +# Out: {"something_at":"2020-Aug-14 11:45:26"} + +# In: {"created_at":"2020-08-14T11:50:26.371Z"} +# Out: {"something_at":"2020-Aug-14 11:50:26"} +``` + +As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. + +```coffeescript +root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S.%f", "UTC") + +# In: {"created_at":1597405526} +# Out: {"something_at":"2020-Aug-14 11:45:26.000000"} + +# In: {"created_at":"2020-08-14T11:50:26.371Z"} +# Out: {"something_at":"2020-Aug-14 11:50:26.371000"} +``` + +=== `ts_strptime` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into <>. + +==== Parameters + +*`format`* <string> The format of the target string. + +==== Examples + + +The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strptime[man 3 strptime] for the list of format specifiers. + +```coffeescript +root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d") + +# In: {"doc":{"timestamp":"2020-Aug-14"}} +# Out: {"doc":{"timestamp":"2020-08-14T00:00:00Z"}} +``` + +As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. + +```coffeescript +root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d %H:%M:%S.%f") + +# In: {"doc":{"timestamp":"2020-Aug-14 11:50:26.371000"}} +# Out: {"doc":{"timestamp":"2020-08-14T11:50:26.371Z"}} +``` + +=== `ts_sub` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns the difference in nanoseconds between the target timestamp (t1) and the timestamp provided as a parameter (t2). The <> method can be used in order to parse different timestamp formats. + +Introduced in version 4.23.0. + + +==== Parameters + +*`t2`* <timestamp> The second timestamp to be subtracted from the method target. + +==== Examples + + +Use the `.abs()` method in order to calculate an absolute duration between two timestamps. + +```coffeescript +root.between = this.started_at.ts_sub("2020-08-14T05:54:23Z").abs() + +# In: {"started_at":"2020-08-13T05:54:23Z"} +# Out: {"between":86400000000000} +``` + +=== `ts_sub_iso8601` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Parse parameter string as ISO 8601 period and subtract it from value with high precision for units larger than an hour. + +==== Parameters + +*`duration`* <string> Duration in ISO 8601 format + +=== `ts_tz` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns the result of converting a timestamp to a specified timezone. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +Introduced in version 4.3.0. + + +==== Parameters + +*`tz`* <string> The timezone to change to. If set to "UTC" then the timezone will be UTC. If set to "Local" then the local timezone will be used. Otherwise, the argument is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York". + +==== Examples + + +```coffeescript +root.created_at_utc = this.created_at.ts_tz("UTC") + +# In: {"created_at":"2021-02-03T17:05:06+01:00"} +# Out: {"created_at_utc":"2021-02-03T16:05:06Z"} +``` + +=== `ts_unix` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +==== Examples + + +```coffeescript +root.created_at_unix = this.created_at.ts_unix() + +# In: {"created_at":"2009-11-10T23:00:00Z"} +# Out: {"created_at_unix":1257894000} +``` + +=== `ts_unix_micro` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +==== Examples + + +```coffeescript +root.created_at_unix = this.created_at.ts_unix_micro() + +# In: {"created_at":"2009-11-10T23:00:00Z"} +# Out: {"created_at_unix":1257894000000000} +``` + +=== `ts_unix_milli` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +==== Examples + + +```coffeescript +root.created_at_unix = this.created_at.ts_unix_milli() + +# In: {"created_at":"2009-11-10T23:00:00Z"} +# Out: {"created_at_unix":1257894000000} +``` + +=== `ts_unix_nano` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +==== Examples + + +```coffeescript +root.created_at_unix = this.created_at.ts_unix_nano() + +# In: {"created_at":"2009-11-10T23:00:00Z"} +# Out: {"created_at_unix":1257894000000000000} +``` + +== Type Coercion + +=== `bool` + +Attempt to parse a value into a boolean. An optional argument can be provided, in which case if the value cannot be parsed the argument will be returned instead. If the value is a number then any non-zero value will resolve to `true`, if the value is a string then any of the following values are considered valid: `1, t, T, TRUE, true, True, 0, f, F, FALSE`. + +==== Parameters + +*`default`* <(optional) bool> An optional value to yield if the target cannot be parsed as a boolean. + +==== Examples + + +```coffeescript +root.foo = this.thing.bool() +root.bar = this.thing.bool(true) +``` + +=== `bytes` + +Marshal a value into a byte array. If the value is already a byte array it is unchanged. + +==== Examples + + +```coffeescript +root.first_byte = this.name.bytes().index(0) + +# In: {"name":"foobar bazson"} +# Out: {"first_byte":102} +``` + +=== `not_empty` + +Ensures that the given string, array or object value is not empty, and if so returns it, otherwise an error is returned. + +==== Examples + + +```coffeescript +root.a = this.a.not_empty() + +# In: {"a":"foo"} +# Out: {"a":"foo"} + +# In: {"a":""} +# Out: Error("failed assignment (line 1): field `this.a`: string value is empty") + +# In: {"a":["foo","bar"]} +# Out: {"a":["foo","bar"]} + +# In: {"a":[]} +# Out: Error("failed assignment (line 1): field `this.a`: array value is empty") + +# In: {"a":{"b":"foo","c":"bar"}} +# Out: {"a":{"b":"foo","c":"bar"}} + +# In: {"a":{}} +# Out: Error("failed assignment (line 1): field `this.a`: object value is empty") +``` + +=== `not_null` + +Ensures that the given value is not `null`, and if so returns it, otherwise an error is returned. + +==== Examples + + +```coffeescript +root.a = this.a.not_null() + +# In: {"a":"foobar","b":"barbaz"} +# Out: {"a":"foobar"} + +# In: {"b":"barbaz"} +# Out: Error("failed assignment (line 1): field `this.a`: value is null") +``` + +=== `number` + +Attempt to parse a value into a number. An optional argument can be provided, in which case if the value cannot be parsed into a number the argument will be returned instead. + +==== Parameters + +*`default`* <(optional) float> An optional value to yield if the target cannot be parsed as a number. + +==== Examples + + +```coffeescript +root.foo = this.thing.number() + 10 +root.bar = this.thing.number(5) * 10 +``` + +=== `string` + +Marshal a value into a string. If the value is already a string it is unchanged. + +==== Examples + + +```coffeescript +root.nested_json = this.string() + +# In: {"foo":"bar"} +# Out: {"nested_json":"{\"foo\":\"bar\"}"} +``` + +```coffeescript +root.id = this.id.string() + +# In: {"id":228930314431312345} +# Out: {"id":"228930314431312345"} +``` + +=== `type` + +Returns the type of a value as a string, providing one of the following values: `string`, `bytes`, `number`, `bool`, `timestamp`, `array`, `object` or `null`. + +==== Examples + + +```coffeescript +root.bar_type = this.bar.type() +root.foo_type = this.foo.type() + +# In: {"bar":10,"foo":"is a string"} +# Out: {"bar_type":"number","foo_type":"string"} +``` + +```coffeescript +root.type = this.type() + +# In: "foobar" +# Out: {"type":"string"} + +# In: 666 +# Out: {"type":"number"} + +# In: false +# Out: {"type":"bool"} + +# In: ["foo", "bar"] +# Out: {"type":"array"} + +# In: {"foo": "bar"} +# Out: {"type":"object"} + +# In: null +# Out: {"type":"null"} +``` + +```coffeescript +root.type = content().type() + +# In: foobar +# Out: {"type":"bytes"} +``` + +```coffeescript +root.type = this.ts_parse("2006-01-02").type() + +# In: "2022-06-06" +# Out: {"type":"timestamp"} +``` + +== Object & Array Manipulation + +=== `all` + +Checks each element of an array against a query and returns true if all elements passed. An error occurs if the target is not an array, or if any element results in the provided query returning a non-boolean result. Returns false if the target array is empty. + +==== Parameters + +*`test`* <query expression> A test query to apply to each element. + +==== Examples + + +```coffeescript +root.all_over_21 = this.patrons.all(patron -> patron.age >= 21) + +# In: {"patrons":[{"id":"1","age":18},{"id":"2","age":23}]} +# Out: {"all_over_21":false} + +# In: {"patrons":[{"id":"1","age":45},{"id":"2","age":23}]} +# Out: {"all_over_21":true} +``` + +=== `any` + +Checks the elements of an array against a query and returns true if any element passes. An error occurs if the target is not an array, or if an element results in the provided query returning a non-boolean result. Returns false if the target array is empty. + +==== Parameters + +*`test`* <query expression> A test query to apply to each element. + +==== Examples + + +```coffeescript +root.any_over_21 = this.patrons.any(patron -> patron.age >= 21) + +# In: {"patrons":[{"id":"1","age":18},{"id":"2","age":23}]} +# Out: {"any_over_21":true} + +# In: {"patrons":[{"id":"1","age":10},{"id":"2","age":12}]} +# Out: {"any_over_21":false} +``` + +=== `append` + +Returns an array with new elements appended to the end. + +==== Examples + + +```coffeescript +root.foo = this.foo.append("and", "this") + +# In: {"foo":["bar","baz"]} +# Out: {"foo":["bar","baz","and","this"]} +``` + +=== `assign` + +Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the value in the destination object will be overwritten by that of source object. In order to preserve both values on collision use the <> method. + +==== Parameters + +*`with`* <unknown> A value to merge the target value with. + +==== Examples + + +```coffeescript +root = this.foo.assign(this.bar) + +# In: {"foo":{"first_name":"fooer","likes":"bars"},"bar":{"second_name":"barer","likes":"foos"}} +# Out: {"first_name":"fooer","likes":"foos","second_name":"barer"} +``` + +=== `collapse` + +Collapse an array or object into an object of key/value pairs for each field, where the key is the full path of the structured field in dot path notation. Empty arrays an objects are ignored by default. + +==== Parameters + +*`include_empty`* <bool, default `false`> Whether to include empty objects and arrays in the resulting object. + +==== Examples + + +```coffeescript +root.result = this.collapse() + +# In: {"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]} +# Out: {"result":{"foo.0.bar":"1","foo.2.bar":"2"}} +``` + +An optional boolean parameter can be set to true in order to include empty objects and arrays. + +```coffeescript +root.result = this.collapse(include_empty: true) + +# In: {"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]} +# Out: {"result":{"foo.0.bar":"1","foo.1.bar":{},"foo.2.bar":"2","foo.3.bar":[]}} +``` + +=== `concat` + +Concatenates an array value with one or more argument arrays. + +==== Examples + + +```coffeescript +root.foo = this.foo.concat(this.bar, this.baz) + +# In: {"foo":["a","b"],"bar":["c"],"baz":["d","e","f"]} +# Out: {"foo":["a","b","c","d","e","f"]} +``` + +=== `contains` + +Checks whether an array contains an element matching the argument, or an object contains a value matching the argument, and returns a boolean result. Numerical comparisons are made irrespective of the representation type (float versus integer). + +==== Parameters + +*`value`* <unknown> A value to test against elements of the target. + +==== Examples + + +```coffeescript +root.has_foo = this.thing.contains("foo") + +# In: {"thing":["this","foo","that"]} +# Out: {"has_foo":true} + +# In: {"thing":["this","bar","that"]} +# Out: {"has_foo":false} +``` + +```coffeescript +root.has_bar = this.thing.contains(20) + +# In: {"thing":[10.3,20.0,"huh",3]} +# Out: {"has_bar":true} + +# In: {"thing":[2,3,40,67]} +# Out: {"has_bar":false} +``` + +=== `diff` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information. + +Introduced in version 4.25.0. + + +==== Parameters + +*`other`* <unknown> The value to compare against. + +=== `enumerated` + +Converts an array into a new array of objects, where each object has a field index containing the `index` of the element and a field `value` containing the original value of the element. + +==== Examples + + +```coffeescript +root.foo = this.foo.enumerated() + +# In: {"foo":["bar","baz"]} +# Out: {"foo":[{"index":0,"value":"bar"},{"index":1,"value":"baz"}]} +``` + +=== `explode` + +Explodes an array or object at a xref:configuration:field_paths.adoc[field path]. + +==== Parameters + +*`path`* <string> A xref:configuration:field_paths.adoc[dot path] to a field to explode. + +==== Examples + + +##### On arrays + +Exploding arrays results in an array containing elements matching the original document, where the target field of each element is an element of the exploded array: + +```coffeescript +root = this.explode("value") + +# In: {"id":1,"value":["foo","bar","baz"]} +# Out: [{"id":1,"value":"foo"},{"id":1,"value":"bar"},{"id":1,"value":"baz"}] +``` + +##### On objects + +Exploding objects results in an object where the keys match the target object, and the values match the original document but with the target field replaced by the exploded value: + +```coffeescript +root = this.explode("value") + +# In: {"id":1,"value":{"foo":2,"bar":[3,4],"baz":{"bev":5}}} +# Out: {"bar":{"id":1,"value":[3,4]},"baz":{"id":1,"value":{"bev":5}},"foo":{"id":1,"value":2}} +``` + +=== `filter` + +Executes a mapping query argument for each element of an array or key/value pair of an object. If the query returns `false` the item is removed from the resulting array or object. The item will also be removed if the query returns any non-boolean value. + +==== Parameters + +*`test`* <query expression> A query to apply to each element, if this query resolves to any value other than a boolean `true` the element will be removed from the result. + +==== Examples + + +```coffeescript +root.new_nums = this.nums.filter(num -> num > 10) + +# In: {"nums":[3,11,4,17]} +# Out: {"new_nums":[11,17]} +``` + +##### On objects + +When filtering objects the mapping query argument is provided a context with a field `key` containing the value key, and a field `value` containing the value. + +```coffeescript +root.new_dict = this.dict.filter(item -> item.value.contains("foo")) + +# In: {"dict":{"first":"hello foo","second":"world","third":"this foo is great"}} +# Out: {"new_dict":{"first":"hello foo","third":"this foo is great"}} +``` + +=== `find` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns the index of the first occurrence of a value in an array. `-1` is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer). + +==== Parameters + +*`value`* <unknown> A value to find. + +==== Examples + + +```coffeescript +root.index = this.find("bar") + +# In: ["foo", "bar", "baz"] +# Out: {"index":1} +``` + +```coffeescript +root.index = this.things.find(this.goal) + +# In: {"goal":"bar","things":["foo", "bar", "baz"]} +# Out: {"index":1} +``` + +=== `find_all` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns an array containing the indexes of all occurrences of a value in an array. An empty array is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer). + +==== Parameters + +*`value`* <unknown> A value to find. + +==== Examples + + +```coffeescript +root.index = this.find_all("bar") + +# In: ["foo", "bar", "baz", "bar"] +# Out: {"index":[1,3]} +``` + +```coffeescript +root.indexes = this.things.find_all(this.goal) + +# In: {"goal":"bar","things":["foo", "bar", "baz", "bar", "buz"]} +# Out: {"indexes":[1,3]} +``` + +=== `find_all_by` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns an array containing the indexes of all occurrences of an array where the provided query resolves to a boolean `true`. An empty array is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer). + +==== Parameters + +*`query`* <query expression> A query to execute for each element. + +==== Examples + + +```coffeescript +root.index = this.find_all_by(v -> v != "bar") + +# In: ["foo", "bar", "baz"] +# Out: {"index":[0,2]} +``` + +=== `find_by` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Returns the index of the first occurrence of an array where the provided query resolves to a boolean `true`. `-1` is returned if there are no matches. + +==== Parameters + +*`query`* <query expression> A query to execute for each element. + +==== Examples + + +```coffeescript +root.index = this.find_by(v -> v != "bar") + +# In: ["foo", "bar", "baz"] +# Out: {"index":0} +``` + +=== `flatten` + +Iterates an array and any element that is itself an array is removed and has its elements inserted directly in the resulting array. + +==== Examples + + +```coffeescript +root.result = this.flatten() + +# In: ["foo",["bar","baz"],"buz"] +# Out: {"result":["foo","bar","baz","buz"]} +``` + +=== `fold` + +Takes two arguments: an initial value, and a mapping query. For each element of an array the mapping context is an object with two fields `tally` and `value`, where `tally` contains the current accumulated value and `value` is the value of the current element. The mapping must return the result of adding the value to the tally. + +The first argument is the value that `tally` will have on the first call. + +==== Parameters + +*`initial`* <unknown> The initial value to start the fold with. For example, an empty object `{}`, a zero count `0`, or an empty string `""`. +*`query`* <query expression> A query to apply for each element. The query is provided an object with two fields; `tally` containing the current tally, and `value` containing the value of the current element. The query should result in a new tally to be passed to the next element query. + +==== Examples + + +```coffeescript +root.sum = this.foo.fold(0, item -> item.tally + item.value) + +# In: {"foo":[3,8,11]} +# Out: {"sum":22} +``` + +```coffeescript +root.result = this.foo.fold("", item -> "%v%v".format(item.tally, item.value)) + +# In: {"foo":["hello ", "world"]} +# Out: {"result":"hello world"} +``` + +You can use fold to merge an array of objects together: + +```coffeescript +root.smoothie = this.fruits.fold({}, item -> item.tally.merge(item.value)) + +# In: {"fruits":[{"apple":5},{"banana":3},{"orange":8}]} +# Out: {"smoothie":{"apple":5,"banana":3,"orange":8}} +``` + +=== `get` + +Extract a field value, identified via a xref:configuration:field_paths.adoc[dot path], from an object. + +==== Parameters + +*`path`* <string> A xref:configuration:field_paths.adoc[dot path] identifying a field to obtain. + +==== Examples + + +```coffeescript +root.result = this.foo.get(this.target) + +# In: {"foo":{"bar":"from bar","baz":"from baz"},"target":"bar"} +# Out: {"result":"from bar"} + +# In: {"foo":{"bar":"from bar","baz":"from baz"},"target":"baz"} +# Out: {"result":"from baz"} +``` + +=== `index` + +Extract an element from an array by an index. The index can be negative, and if so the element will be selected from the end counting backwards starting from -1. E.g. an index of -1 returns the last element, an index of -2 returns the element before the last, and so on. + +==== Parameters + +*`index`* <integer> The index to obtain from an array. + +==== Examples + + +```coffeescript +root.last_name = this.names.index(-1) + +# In: {"names":["rachel","stevens"]} +# Out: {"last_name":"stevens"} +``` + +It is also possible to use this method on byte arrays, in which case the selected element will be returned as an integer. + +```coffeescript +root.last_byte = this.name.bytes().index(-1) + +# In: {"name":"foobar bazson"} +# Out: {"last_byte":110} +``` + +=== `join` + +Join an array of strings with an optional delimiter into a single string. + +==== Parameters + +*`delimiter`* <(optional) string> An optional delimiter to add between each string. + +==== Examples + + +```coffeescript +root.joined_words = this.words.join() +root.joined_numbers = this.numbers.map_each(this.string()).join(",") + +# In: {"words":["hello","world"],"numbers":[3,8,11]} +# Out: {"joined_numbers":"3,8,11","joined_words":"helloworld"} +``` + +=== `json_path` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Executes the given JSONPath expression on an object or array and returns the result. The JSONPath expression syntax can be found at https://goessner.net/articles/JsonPath/. For more complex logic, you can use Gval expressions (https://github.com/PaesslerAG/gval). + +==== Parameters + +*`expression`* <string> The JSONPath expression to execute. + +==== Examples + + +```coffeescript +root.all_names = this.json_path("$..name") + +# In: {"name":"alice","foo":{"name":"bob"}} +# Out: {"all_names":["alice","bob"]} + +# In: {"thing":["this","bar",{"name":"alice"}]} +# Out: {"all_names":["alice"]} +``` + +```coffeescript +root.text_objects = this.json_path("$.body[?(@.type=='text')]") + +# In: {"body":[{"type":"image","id":"foo"},{"type":"text","id":"bar"}]} +# Out: {"text_objects":[{"id":"bar","type":"text"}]} +``` + +=== `json_schema` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Checks a https://json-schema.org/[JSON schema] against a value and returns the value if it matches or throws and error if it does not. + +==== Parameters + +*`schema`* <string> The schema to check values against. + +==== Examples + + +```coffeescript +root = this.json_schema("""{ + "type":"object", + "properties":{ + "foo":{ + "type":"string" + } + } +}""") + +# In: {"foo":"bar"} +# Out: {"foo":"bar"} + +# In: {"foo":5} +# Out: Error("failed assignment (line 1): field `this`: foo invalid type. expected: string, given: integer") +``` + +In order to load a schema from a file use the `file` function. + +```coffeescript +root = this.json_schema(file(env("BENTHOS_TEST_BLOBLANG_SCHEMA_FILE"))) +``` + +=== `key_values` + +Returns the key/value pairs of an object as an array, where each element is an object with a `key` field and a `value` field. The order of the resulting array will be random. + +==== Examples + + +```coffeescript +root.foo_key_values = this.foo.key_values().sort_by(pair -> pair.key) + +# In: {"foo":{"bar":1,"baz":2}} +# Out: {"foo_key_values":[{"key":"bar","value":1},{"key":"baz","value":2}]} +``` + +=== `keys` + +Returns the keys of an object as an array. + +==== Examples + + +```coffeescript +root.foo_keys = this.foo.keys() + +# In: {"foo":{"bar":1,"baz":2}} +# Out: {"foo_keys":["bar","baz"]} +``` + +=== `length` + +Returns the length of an array or object (number of keys). + +==== Examples + + +```coffeescript +root.foo_len = this.foo.length() + +# In: {"foo":["first","second"]} +# Out: {"foo_len":2} + +# In: {"foo":{"first":"bar","second":"baz"}} +# Out: {"foo_len":2} +``` + +=== `map_each` + + + +==== Parameters + +*`query`* <query expression> A query that will be used to map each element. + +==== Examples + + +##### On arrays + +Apply a mapping to each element of an array and replace the element with the result. Within the argument mapping the context is the value of the element being mapped. + +```coffeescript +root.new_nums = this.nums.map_each(num -> if num < 10 { + deleted() +} else { + num - 10 +}) + +# In: {"nums":[3,11,4,17]} +# Out: {"new_nums":[1,7]} +``` + +##### On objects + +Apply a mapping to each value of an object and replace the value with the result. Within the argument mapping the context is an object with a field `key` containing the value key, and a field `value`. + +```coffeescript +root.new_dict = this.dict.map_each(item -> item.value.uppercase()) + +# In: {"dict":{"foo":"hello","bar":"world"}} +# Out: {"new_dict":{"bar":"WORLD","foo":"HELLO"}} +``` + +=== `map_each_key` + +Apply a mapping to each key of an object, and replace the key with the result, which must be a string. + +==== Parameters + +*`query`* <query expression> A query that will be used to map each key. + +==== Examples + + +```coffeescript +root.new_dict = this.dict.map_each_key(key -> key.uppercase()) + +# In: {"dict":{"keya":"hello","keyb":"world"}} +# Out: {"new_dict":{"KEYA":"hello","KEYB":"world"}} +``` + +```coffeescript +root = this.map_each_key(key -> if key.contains("kafka") { "_" + key }) + +# In: {"amqp_key":"foo","kafka_key":"bar","kafka_topic":"baz"} +# Out: {"_kafka_key":"bar","_kafka_topic":"baz","amqp_key":"foo"} +``` + +=== `merge` + +Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the result will be an array containing both values, where values that are already arrays will be expanded into the resulting array. In order to simply override destination fields on collision use the <> method. + +==== Parameters + +*`with`* <unknown> A value to merge the target value with. + +==== Examples + + +```coffeescript +root = this.foo.merge(this.bar) + +# In: {"foo":{"first_name":"fooer","likes":"bars"},"bar":{"second_name":"barer","likes":"foos"}} +# Out: {"first_name":"fooer","likes":["bars","foos"],"second_name":"barer"} +``` + +=== `patch` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information. + +Introduced in version 4.25.0. + + +==== Parameters + +*`changelog`* <unknown> The changelog to apply. + +=== `slice` + +Extract a slice from an array by specifying two indices, a low and high bound, which selects a half-open range that includes the first element, but excludes the last one. If the second index is omitted then it defaults to the length of the input sequence. + +==== Parameters + +*`low`* <integer> The low bound, which is the first element of the selection, or if negative selects from the end. +*`high`* <(optional) integer> An optional high bound. + +==== Examples + + +```coffeescript +root.beginning = this.value.slice(0, 2) +root.end = this.value.slice(4) + +# In: {"value":["foo","bar","baz","buz","bev"]} +# Out: {"beginning":["foo","bar"],"end":["bev"]} +``` + +A negative low index can be used, indicating an offset from the end of the sequence. If the low index is greater than the length of the sequence then an empty result is returned. + +```coffeescript +root.last_chunk = this.value.slice(-2) +root.the_rest = this.value.slice(0, -2) + +# In: {"value":["foo","bar","baz","buz","bev"]} +# Out: {"last_chunk":["buz","bev"],"the_rest":["foo","bar","baz"]} +``` + +=== `sort` + +Attempts to sort the values of an array in increasing order. The type of all values must match in order for the ordering to succeed. Supports string and number values. + +==== Parameters + +*`compare`* <(optional) query expression> An optional query that should explicitly compare elements `left` and `right` and provide a boolean result. + +==== Examples + + +```coffeescript +root.sorted = this.foo.sort() + +# In: {"foo":["bbb","ccc","aaa"]} +# Out: {"sorted":["aaa","bbb","ccc"]} +``` + +It's also possible to specify a mapping argument, which is provided an object context with fields `left` and `right`, the mapping must return a boolean indicating whether the `left` value is less than `right`. This allows you to sort arrays containing non-string or non-number values. + +```coffeescript +root.sorted = this.foo.sort(item -> item.left.v < item.right.v) + +# In: {"foo":[{"id":"foo","v":"bbb"},{"id":"bar","v":"ccc"},{"id":"baz","v":"aaa"}]} +# Out: {"sorted":[{"id":"baz","v":"aaa"},{"id":"foo","v":"bbb"},{"id":"bar","v":"ccc"}]} +``` + +=== `sort_by` + +Attempts to sort the elements of an array, in increasing order, by a value emitted by an argument query applied to each element. The type of all values must match in order for the ordering to succeed. Supports string and number values. + +==== Parameters + +*`query`* <query expression> A query to apply to each element that yields a value used for sorting. + +==== Examples + + +```coffeescript +root.sorted = this.foo.sort_by(ele -> ele.id) + +# In: {"foo":[{"id":"bbb","message":"bar"},{"id":"aaa","message":"foo"},{"id":"ccc","message":"baz"}]} +# Out: {"sorted":[{"id":"aaa","message":"foo"},{"id":"bbb","message":"bar"},{"id":"ccc","message":"baz"}]} +``` + +=== `squash` + +Squashes an array of objects into a single object, where key collisions result in the values being merged (following similar rules as the `.merge()` method) + +==== Examples + + +```coffeescript +root.locations = this.locations.map_each(loc -> {loc.state: [loc.name]}).squash() + +# In: {"locations":[{"name":"Seattle","state":"WA"},{"name":"New York","state":"NY"},{"name":"Bellevue","state":"WA"},{"name":"Olympia","state":"WA"}]} +# Out: {"locations":{"NY":["New York"],"WA":["Seattle","Bellevue","Olympia"]}} +``` + +=== `sum` + +Sum the numerical values of an array. + +==== Examples + + +```coffeescript +root.sum = this.foo.sum() + +# In: {"foo":[3,8,4]} +# Out: {"sum":15} +``` + +=== `unique` + +Attempts to remove duplicate values from an array. The array may contain a combination of different value types, but numbers and strings are checked separately (`"5"` is a different element to `5`). + +==== Parameters + +*`emit`* <(optional) query expression> An optional query that can be used in order to yield a value for each element to determine uniqueness. + +==== Examples + + +```coffeescript +root.uniques = this.foo.unique() + +# In: {"foo":["a","b","a","c"]} +# Out: {"uniques":["a","b","c"]} +``` + +=== `values` + +Returns the values of an object as an array. The order of the resulting array will be random. + +==== Examples + + +```coffeescript +root.foo_vals = this.foo.values().sort() + +# In: {"foo":{"bar":1,"baz":2}} +# Out: {"foo_vals":[1,2]} +``` + +=== `with` + +Returns an object where all but one or more xref:configuration:field_paths.adoc[field path] arguments are removed. Each path specifies a specific field to be retained from the input object, allowing for nested fields. + +If a key within a nested path does not exist then it is ignored. + +==== Examples + + +```coffeescript +root = this.with("inner.a","inner.c","d") + +# In: {"inner":{"a":"first","b":"second","c":"third"},"d":"fourth","e":"fifth"} +# Out: {"d":"fourth","inner":{"a":"first","c":"third"}} +``` + +=== `without` + +Returns an object where one or more xref:configuration:field_paths.adoc[field path] arguments are removed. Each path specifies a specific field to be deleted from the input object, allowing for nested fields. + +If a key within a nested path does not exist or is not an object then it is not removed. + +==== Examples + + +```coffeescript +root = this.without("inner.a","inner.c","d") + +# In: {"inner":{"a":"first","b":"second","c":"third"},"d":"fourth","e":"fifth"} +# Out: {"e":"fifth","inner":{"b":"second"}} +``` + +=== `zip` + +Zip an array value with one or more argument arrays. Each array must match in length. + +==== Examples + + +```coffeescript +root.foo = this.foo.zip(this.bar, this.baz) + +# In: {"foo":["a","b","c"],"bar":[1,2,3],"baz":[4,5,6]} +# Out: {"foo":[["a",1,4],["b",2,5],["c",3,6]]} +``` + +== Parsing + +=== `bloblang` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Executes an argument Bloblang mapping on the target. This method can be used in order to execute dynamic mappings. Imports and functions that interact with the environment, such as `file` and `env`, or that access message information directly, such as `content` or `json`, are not enabled for dynamic Bloblang mappings. + +==== Parameters + +*`mapping`* <string> The mapping to execute. + +==== Examples + + +```coffeescript +root.body = this.body.bloblang(this.mapping) + +# In: {"body":{"foo":"hello world"},"mapping":"root.foo = this.foo.uppercase()"} +# Out: {"body":{"foo":"HELLO WORLD"}} + +# In: {"body":{"foo":"hello world 2"},"mapping":"root.foo = this.foo.capitalize()"} +# Out: {"body":{"foo":"Hello World 2"}} +``` + +=== `format_json` + +[CAUTION] +.Beta +==== +This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. +==== +Serializes a target value into a pretty-printed JSON byte array (with 4 space indentation by default). + +==== Parameters + +*`indent`* <string, default `" "`> Indentation string. Each element in a JSON object or array will begin on a new, indented line followed by one or more copies of indent according to the indentation nesting. +*`no_indent`* <bool, default `false`> Disable indentation. + +==== Examples + + +```coffeescript +root = this.doc.format_json() + +# In: {"doc":{"foo":"bar"}} +# Out: { +# "foo": "bar" +# } +``` + +Pass a string to the `indent` parameter in order to customise the indentation. + +```coffeescript +root = this.format_json(" ") + +# In: {"doc":{"foo":"bar"}} +# Out: { +# "doc": { +# "foo": "bar" +# } +# } +``` + +Use the `.string()` method in order to coerce the result into a string. + +```coffeescript +root.doc = this.doc.format_json().string() + +# In: {"doc":{"foo":"bar"}} +# Out: {"doc":"{\n \"foo\": \"bar\"\n}"} +``` + +Set the `no_indent` parameter to true to disable indentation. The result is equivalent to calling `bytes()`. + +```coffeescript +root = this.doc.format_json(no_indent: true) + +# In: {"doc":{"foo":"bar"}} +# Out: {"foo":"bar"} +``` + +=== `format_msgpack` + +Formats data as a https://msgpack.org/[MessagePack] message in bytes format. + +==== Examples + + +```coffeescript +root = this.format_msgpack().encode("hex") + +# In: {"foo":"bar"} +# Out: 81a3666f6fa3626172 +``` + +```coffeescript +root.encoded = this.format_msgpack().encode("base64") + +# In: {"foo":"bar"} +# Out: {"encoded":"gaNmb2+jYmFy"} +``` + +=== `format_xml` + + +Serializes a target value into an XML byte array. + + +==== Parameters + +*`indent`* <string, default `" "`> Indentation string. Each element in an XML object or array will begin on a new, indented line followed by one or more copies of indent according to the indentation nesting. +*`no_indent`* <bool, default `false`> Disable indentation. + +==== Examples + + +Serializes a target value into a pretty-printed XML byte array (with 4 space indentation by default). + +```coffeescript +root = this.format_xml() + +# In: {"foo":{"bar":{"baz":"foo bar baz"}}} +# Out: +# +# foo bar baz +# +# +``` + +Pass a string to the `indent` parameter in order to customise the indentation. + +```coffeescript +root = this.format_xml(" ") + +# In: {"foo":{"bar":{"baz":"foo bar baz"}}} +# Out: +# +# foo bar baz +# +# +``` + +Use the `.string()` method in order to coerce the result into a string. + +```coffeescript +root.doc = this.format_xml("").string() + +# In: {"foo":{"bar":{"baz":"foo bar baz"}}} +# Out: {"doc":"\n\nfoo bar baz\n\n"} +``` + +Set the `no_indent` parameter to true to disable indentation. + +```coffeescript +root = this.format_xml(no_indent: true) + +# In: {"foo":{"bar":{"baz":"foo bar baz"}}} +# Out: foo bar baz +``` + +=== `format_yaml` + +Serializes a target value into a YAML byte array. + +==== Examples + + +```coffeescript +root = this.doc.format_yaml() + +# In: {"doc":{"foo":"bar"}} +# Out: foo: bar +``` + +Use the `.string()` method in order to coerce the result into a string. + +```coffeescript +root.doc = this.doc.format_yaml().string() + +# In: {"doc":{"foo":"bar"}} +# Out: {"doc":"foo: bar\n"} +``` + +=== `parse_csv` + +Attempts to parse a string into an array of objects by following the CSV format described in RFC 4180. + +==== Parameters + +*`parse_header_row`* <bool, default `true`> Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, the output will be an array of row arrays. +*`delimiter`* <string, default `","`> The delimiter to use for splitting values in each record. It must be a single character. +*`lazy_quotes`* <bool, default `false`> If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field. + +==== Examples + + +Parses CSV data with a header row + +```coffeescript +root.orders = this.orders.parse_csv() + +# In: {"orders":"foo,bar\nfoo 1,bar 1\nfoo 2,bar 2"} +# Out: {"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]} +``` + +Parses CSV data without a header row + +```coffeescript +root.orders = this.orders.parse_csv(false) + +# In: {"orders":"foo 1,bar 1\nfoo 2,bar 2"} +# Out: {"orders":[["foo 1","bar 1"],["foo 2","bar 2"]]} +``` + +Parses CSV data delimited by dots + +```coffeescript +root.orders = this.orders.parse_csv(delimiter:".") + +# In: {"orders":"foo.bar\nfoo 1.bar 1\nfoo 2.bar 2"} +# Out: {"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]} +``` + +Parses CSV data containing a quote in an unquoted field + +```coffeescript +root.orders = this.orders.parse_csv(lazy_quotes:true) + +# In: {"orders":"foo,bar\nfoo 1,bar 1\nfoo\" \"2,bar\" \"2"} +# Out: {"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar\" \"2","foo":"foo\" \"2"}]} +``` + +=== `parse_form_url_encoded` + +Attempts to parse a url-encoded query string (from an x-www-form-urlencoded request body) and returns a structured result. + +==== Examples + + +```coffeescript +root.values = this.body.parse_form_url_encoded() + +# In: {"body":"noise=meow&animal=cat&fur=orange&fur=fluffy"} +# Out: {"values":{"animal":"cat","fur":["orange","fluffy"],"noise":"meow"}} +``` + +=== `parse_json` + +Attempts to parse a string as a JSON document and returns the result. + +==== Parameters + +*`use_number`* <(optional) bool> An optional flag that when set makes parsing numbers as json.Number instead of the default float64. + +==== Examples + + +```coffeescript +root.doc = this.doc.parse_json() + +# In: {"doc":"{\"foo\":\"bar\"}"} +# Out: {"doc":{"foo":"bar"}} +``` + +```coffeescript +root.doc = this.doc.parse_json(use_number: true) + +# In: {"doc":"{\"foo\":\"11380878173205700000000000000000000000000000000\"}"} +# Out: {"doc":{"foo":"11380878173205700000000000000000000000000000000"}} +``` + +=== `parse_msgpack` + +Parses a https://msgpack.org/[MessagePack] message into a structured document. + +==== Examples + + +```coffeescript +root = content().decode("hex").parse_msgpack() + +# In: 81a3666f6fa3626172 +# Out: {"foo":"bar"} +``` + +```coffeescript +root = this.encoded.decode("base64").parse_msgpack() + +# In: {"encoded":"gaNmb2+jYmFy"} +# Out: {"foo":"bar"} +``` + +=== `parse_parquet` + +Decodes a https://parquet.apache.org/docs/[Parquet file] into an array of objects, one for each row within the file. + +==== Parameters + +*`byte_array_as_string`* <bool, default `false`> Deprecated: This parameter is no longer used. + +==== Examples + + +```coffeescript +root = content().parse_parquet() +``` + +=== `parse_url` + +Attempts to parse a URL from a string value, returning a structured result that describes the various facets of the URL. The fields returned within the structured result roughly follow https://pkg.go.dev/net/url#URL, and may be expanded in future in order to present more information. + +==== Examples + + +```coffeescript +root.foo_url = this.foo_url.parse_url() + +# In: {"foo_url":"https://www.docs.redpanda.com/redpanda-connect/guides/bloblang/about/"} +# Out: {"foo_url":{"fragment":"","host":"www.docs.redpanda.com","opaque":"","path":"/redpanda-connect/guides/bloblang/about/","raw_fragment":"","raw_path":"","raw_query":"","scheme":"https"}} +``` + +```coffeescript +root.username = this.url.parse_url().user.name | "unknown" + +# In: {"url":"amqp://foo:bar@127.0.0.1:5672/"} +# Out: {"username":"foo"} + +# In: {"url":"redis://localhost:6379"} +# Out: {"username":"unknown"} +``` + +=== `parse_xml` + + +Attempts to parse a string as an XML document and returns a structured result, where elements appear as keys of an object according to the following rules: + +- If an element contains attributes they are parsed by prefixing a hyphen, `-`, to the attribute label. +- If the element is a simple element and has attributes, the element value is given the key `#text`. +- XML comments, directives, and process instructions are ignored. +- When elements are repeated the resulting JSON value is an array. +- If cast is true, try to cast values to numbers and booleans instead of returning strings. + + +==== Parameters + +*`cast`* <(optional) bool, default `false`> whether to try to cast values that are numbers and booleans to the right type. + +==== Examples + + +```coffeescript +root.doc = this.doc.parse_xml() + +# In: {"doc":"This is a titleThis is some content"} +# Out: {"doc":{"root":{"content":"This is some content","title":"This is a title"}}} +``` + +```coffeescript +root.doc = this.doc.parse_xml(cast: false) + +# In: {"doc":"This is a title123True"} +# Out: {"doc":{"root":{"bool":"True","number":{"#text":"123","-id":"99"},"title":"This is a title"}}} +``` + +```coffeescript +root.doc = this.doc.parse_xml(cast: true) + +# In: {"doc":"This is a title123True"} +# Out: {"doc":{"root":{"bool":true,"number":{"#text":123,"-id":99},"title":"This is a title"}}} +``` + +=== `parse_yaml` + +Attempts to parse a string as a single YAML document and returns the result. + +==== Examples + + +```coffeescript +root.doc = this.doc.parse_yaml() + +# In: {"doc":"foo: bar"} +# Out: {"doc":{"foo":"bar"}} +``` + +== Encoding and Encryption + +=== `compress` + +Compresses a string or byte array value according to a specified algorithm. + +==== Parameters + +*`algorithm`* <string> One of `flate`, `gzip`, `pgzip`, `lz4`, `snappy`, `zlib`, `zstd`. +*`level`* <integer, default `-1`> The level of compression to use. May not be applicable to all algorithms. + +==== Examples + + +```coffeescript +let long_content = range(0, 1000).map_each(content()).join(" ") +root.a_len = $long_content.length() +root.b_len = $long_content.compress("gzip").length() + + +# In: hello world this is some content +# Out: {"a_len":32999,"b_len":161} +``` + +```coffeescript +root.compressed = content().compress("lz4").encode("base64") + +# In: hello world I love space +# Out: {"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="} +``` + +=== `decode` + +Decodes an encoded string target according to a chosen scheme and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `string`, or encoded using the method `encode`, otherwise it will be base64 encoded by default. + +Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`. + +==== Parameters + +*`scheme`* <string> The decoding scheme to use. + +==== Examples + + +```coffeescript +root.decoded = this.value.decode("hex").string() + +# In: {"value":"68656c6c6f20776f726c64"} +# Out: {"decoded":"hello world"} +``` + +```coffeescript +root = this.encoded.decode("ascii85") + +# In: {"encoded":"FD,B0+DGm>FDl80Ci\"A>F`)8BEckl6F`M&(+Cno&@/"} +# Out: this is totally unstructured data +``` + +=== `decompress` + +Decompresses a string or byte array value according to a specified algorithm. The result of decompression + +==== Parameters + +*`algorithm`* <string> One of `gzip`, `pgzip`, `zlib`, `bzip2`, `flate`, `snappy`, `lz4`, `zstd`. + +==== Examples + + +```coffeescript +root = this.compressed.decode("base64").decompress("lz4") + +# In: {"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="} +# Out: hello world I love space +``` + +Use the `.string()` method in order to coerce the result into a string, this makes it possible to place the data within a JSON document without automatic base64 encoding. + +```coffeescript +root.result = this.compressed.decode("base64").decompress("lz4").string() + +# In: {"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="} +# Out: {"result":"hello world I love space"} +``` + +=== `decrypt_aes` + +Decrypts an encrypted string or byte array target according to a chosen AES encryption method and returns the result as a byte array. The algorithms require a key and an initialization vector / nonce. Available schemes are: `ctr`, `ofb`, `cbc`. + +==== Parameters + +*`scheme`* <string> The scheme to use for decryption, one of `ctr`, `ofb`, `cbc`. +*`key`* <string> A key to decrypt with. +*`iv`* <string> An initialization vector / nonce. + +==== Examples + + +```coffeescript +let key = "2b7e151628aed2a6abf7158809cf4f3c".decode("hex") +let vector = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff".decode("hex") +root.decrypted = this.value.decode("hex").decrypt_aes("ctr", $key, $vector).string() + +# In: {"value":"84e9b31ff7400bdf80be7254"} +# Out: {"decrypted":"hello world!"} +``` + +=== `encode` + +Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`. + +==== Parameters + +*`scheme`* <string> The encoding scheme to use. + +==== Examples + + +```coffeescript +root.encoded = this.value.encode("hex") + +# In: {"value":"hello world"} +# Out: {"encoded":"68656c6c6f20776f726c64"} +``` + +```coffeescript +root.encoded = content().encode("ascii85") + +# In: this is totally unstructured data +# Out: {"encoded":"FD,B0+DGm>FDl80Ci\"A>F`)8BEckl6F`M&(+Cno&@/"} +``` + +=== `encrypt_aes` + +Encrypts a string or byte array target according to a chosen AES encryption method and returns a string result. The algorithms require a key and an initialization vector / nonce. Available schemes are: `ctr`, `ofb`, `cbc`. + +==== Parameters + +*`scheme`* <string> The scheme to use for encryption, one of `ctr`, `ofb`, `cbc`. +*`key`* <string> A key to encrypt with. +*`iv`* <string> An initialization vector / nonce. + +==== Examples + + +```coffeescript +let key = "2b7e151628aed2a6abf7158809cf4f3c".decode("hex") +let vector = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff".decode("hex") +root.encrypted = this.value.encrypt_aes("ctr", $key, $vector).encode("hex") + +# In: {"value":"hello world!"} +# Out: {"encrypted":"84e9b31ff7400bdf80be7254"} +``` + +=== `hash` + +Hashes a string or byte array according to a chosen algorithm and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method xref:guides:bloblang/methods.adoc#string[`string`], or encoded using the method xref:guides:bloblang/methods.adoc#encode[`encode`], otherwise it will be base64 encoded by default. + +Available algorithms are: `hmac_sha1`, `hmac_sha256`, `hmac_sha512`, `md5`, `sha1`, `sha256`, `sha512`, `xxhash64`, `crc32`. + +The following algorithms require a key, which is specified as a second argument: `hmac_sha1`, `hmac_sha256`, `hmac_sha512`. + +==== Parameters + +*`algorithm`* <string> The hasing algorithm to use. +*`key`* <(optional) string> An optional key to use. +*`polynomial`* <string, default `"IEEE"`> An optional polynomial key to use when selecting the `crc32` algorithm, otherwise ignored. Options are `IEEE` (default), `Castagnoli` and `Koopman` + +==== Examples + + +```coffeescript +root.h1 = this.value.hash("sha1").encode("hex") +root.h2 = this.value.hash("hmac_sha1","static-key").encode("hex") + +# In: {"value":"hello world"} +# Out: {"h1":"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed","h2":"d87e5f068fa08fe90bb95bc7c8344cb809179d76"} +``` + +The `crc32` algorithm supports options for the polynomial. + +```coffeescript +root.h1 = this.value.hash(algorithm: "crc32", polynomial: "Castagnoli").encode("hex") +root.h2 = this.value.hash(algorithm: "crc32", polynomial: "Koopman").encode("hex") + +# In: {"value":"hello world"} +# Out: {"h1":"c99465aa","h2":"df373d3c"} +``` + +== JSON Web Tokens + +=== `parse_jwt_es256` + +Parses a claims object from a JWT string encoded with ES256. This method does not validate JWT claims. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The ES256 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_es256("""-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGtLqIBePHmIhQcf0JLgc+F/4W/oI +dp0Gta53G35VerNDgUUXmp78J2kfh4qLdh0XtmOMI587tCaqjvDAXfs//w== +-----END PUBLIC KEY-----""") + +# In: {"signed":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.GIRajP9JJbpTlqSCdNEz4qpQkRvzX4Q51YnTwVyxLDM9tKjR_a8ggHWn9CWj7KG0x8J56OWtmUxn112SRTZVhQ"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_es384` + +Parses a claims object from a JWT string encoded with ES384. This method does not validate JWT claims. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The ES384 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_es384("""-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERoz74/B6SwmLhs8X7CWhnrWyRrB13AuU +8OYeqy0qHRu9JWNw8NIavqpTmu6XPT4xcFanYjq8FbeuM11eq06C52mNmS4LLwzA +2imlFEgn85bvJoC3bnkuq4mQjwt9VxdH +-----END PUBLIC KEY-----""") + +# In: {"signed":"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.H2HBSlrvQBaov2tdreGonbBexxtQB-xzaPL4-tNQZ6TVh7VH8VBcSwcWHYa1lBAHmdsKOFcB2Wk0SB7QWeGT3ptSgr-_EhDMaZ8bA5spgdpq5DsKfaKHrd7DbbQlmxNq"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_es512` + +Parses a claims object from a JWT string encoded with ES512. This method does not validate JWT claims. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The ES512 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_es512("""-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAkHLdts9P56fFkyhpYQ31M/Stwt3w +vpaxhlfudxnXgTO1IP4RQRgryRxZ19EUzhvWDcG3GQIckoNMY5PelsnCGnIBT2Xh +9NQkjWF5K6xS4upFsbGSAwQ+GIyyk5IPJ2LHgOyMSCVh5gRZXV3CZLzXujx/umC9 +UeYyTt05zRRWuD+p5bY= +-----END PUBLIC KEY-----""") + +# In: {"signed":"eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.ACrpLuU7TKpAnncDCpN9m85nkL55MJ45NFOBl6-nEXmNT1eIxWjiP4pwWVbFH9et_BgN14119jbL_KqEJInPYc9nAXC6dDLq0aBU-dalvNl4-O5YWpP43-Y-TBGAsWnbMTrchILJ4-AEiICe73Ck5yWPleKg9c3LtkEFWfGs7BoPRguZ"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_hs256` + +Parses a claims object from a JWT string encoded with HS256. This method does not validate JWT claims. + +Introduced in version v4.12.0. + + +==== Parameters + +*`signing_secret`* <string> The HS256 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_hs256("""dont-tell-anyone""") + +# In: {"signed":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.YwXOM8v3gHVWcQRRRQc_zDlhmLnM62fwhFYGpiA0J1A"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_hs384` + +Parses a claims object from a JWT string encoded with HS384. This method does not validate JWT claims. + +Introduced in version v4.12.0. + + +==== Parameters + +*`signing_secret`* <string> The HS384 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_hs384("""dont-tell-anyone""") + +# In: {"signed":"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.2Y8rf_ijwN4t8hOGGViON_GrirLkCQVbCOuax6EoZ3nluX0tCGezcJxbctlIfsQ2"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_hs512` + +Parses a claims object from a JWT string encoded with HS512. This method does not validate JWT claims. + +Introduced in version v4.12.0. + + +==== Parameters + +*`signing_secret`* <string> The HS512 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_hs512("""dont-tell-anyone""") + +# In: {"signed":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.utRb0urG6LGGyranZJVo5Dk0Fns1QNcSUYPN0TObQ-YzsGGB8jrxHwM5NAJccjJZzKectEUqmmKCaETZvuX4Fg"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_rs256` + +Parses a claims object from a JWT string encoded with RS256. This method does not validate JWT claims. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The RS256 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_rs256("""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ibN8r68pLMR6gRzg4S +8v8l6Q7yi8qURjkEbcNeM1rkokC7xh0I4JVTwxYSVv/JIW8qJdyspl5NIfuAVi32 +WfKvSAs+NIs+DMsNPYw3yuQals4AX8hith1YDvYpr8SD44jxhz/DR9lYKZFGhXGB ++7NqQ7vpTWp3BceLYocazWJgusZt7CgecIq57ycM5hjM93BvlrUJ8nQ1a46wfL/8 +Cy4P0et70hzZrsjjN41KFhKY0iUwlyU41yEiDHvHDDsTMBxAZosWjSREGfJL6Mfp +XOInTHs/Gg6DZMkbxjQu6L06EdJ+Q/NwglJdAXM7Zo9rNELqRig6DdvG5JesdMsO ++QIDAQAB +-----END PUBLIC KEY-----""") + +# In: {"signed":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.b0lH3jEupZZ4zoaly4Y_GCvu94HH6UKdKY96zfGNsIkPZpQLHIkZ7jMWlLlNOAd8qXlsBGP_i8H2qCKI4zlWJBGyPZgxXDzNRPVrTDfFpn4t4nBcA1WK2-ntXP3ehQxsaHcQU8Z_nsogId7Pme5iJRnoHWEnWtbwz5DLSXL3ZZNnRdrHM9MdI7QSDz9mojKDCaMpGN9sG7Xl-tGdBp1XzXuUOzG8S03mtZ1IgVR1uiBL2N6oohHIAunk8DIAmNWI-zgycTgzUGU7mvPkKH43qO8Ua1-13tCUBKKa8VxcotZ67Mxm1QAvBGoDnTKwWMwghLzs6d6WViXQg6eWlJcpBA"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_rs384` + +Parses a claims object from a JWT string encoded with RS384. This method does not validate JWT claims. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The RS384 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_rs384("""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ibN8r68pLMR6gRzg4S +8v8l6Q7yi8qURjkEbcNeM1rkokC7xh0I4JVTwxYSVv/JIW8qJdyspl5NIfuAVi32 +WfKvSAs+NIs+DMsNPYw3yuQals4AX8hith1YDvYpr8SD44jxhz/DR9lYKZFGhXGB ++7NqQ7vpTWp3BceLYocazWJgusZt7CgecIq57ycM5hjM93BvlrUJ8nQ1a46wfL/8 +Cy4P0et70hzZrsjjN41KFhKY0iUwlyU41yEiDHvHDDsTMBxAZosWjSREGfJL6Mfp +XOInTHs/Gg6DZMkbxjQu6L06EdJ+Q/NwglJdAXM7Zo9rNELqRig6DdvG5JesdMsO ++QIDAQAB +-----END PUBLIC KEY-----""") + +# In: {"signed":"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.orcXYBcjVE5DU7mvq4KKWFfNdXR4nEY_xupzWoETRpYmQZIozlZnM_nHxEk2dySvpXlAzVm7kgOPK2RFtGlOVaNRIa3x-pMMr-bhZTno4L8Hl4sYxOks3bWtjK7wql4uqUbqThSJB12psAXw2-S-I_FMngOPGIn4jDT9b802ottJSvTpXcy0-eKTjrV2PSkRRu-EYJh0CJZW55MNhqlt6kCGhAXfbhNazN3ASX-dmpd_JixyBKphrngr_zRA-FCn_Xf3QQDA-5INopb4Yp5QiJ7UxVqQEKI80X_JvJqz9WE1qiAw8pq5-xTen1t7zTP-HT1NbbD3kltcNa3G8acmNg"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `parse_jwt_rs512` + +Parses a claims object from a JWT string encoded with RS512. This method does not validate JWT claims. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The RS512 secret that was used for signing the token. + +==== Examples + + +```coffeescript +root.claims = this.signed.parse_jwt_rs512("""-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ibN8r68pLMR6gRzg4S +8v8l6Q7yi8qURjkEbcNeM1rkokC7xh0I4JVTwxYSVv/JIW8qJdyspl5NIfuAVi32 +WfKvSAs+NIs+DMsNPYw3yuQals4AX8hith1YDvYpr8SD44jxhz/DR9lYKZFGhXGB ++7NqQ7vpTWp3BceLYocazWJgusZt7CgecIq57ycM5hjM93BvlrUJ8nQ1a46wfL/8 +Cy4P0et70hzZrsjjN41KFhKY0iUwlyU41yEiDHvHDDsTMBxAZosWjSREGfJL6Mfp +XOInTHs/Gg6DZMkbxjQu6L06EdJ+Q/NwglJdAXM7Zo9rNELqRig6DdvG5JesdMsO ++QIDAQAB +-----END PUBLIC KEY-----""") + +# In: {"signed":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.rsMp_X5HMrUqKnZJIxo27aAoscovRA6SSQYR9rq7pifIj0YHXxMyNyOBDGnvVALHKTi25VUGHpfNUW0VVMmae0A4t_ObNU6hVZHguWvetKZZq4FZpW1lgWHCMqgPGwT5_uOqwYCH6r8tJuZT3pqXeL0CY4putb1AN2w6CVp620nh3l8d3XWb4jaifycd_4CEVCqHuWDmohfug4VhmoVKlIXZkYoAQowgHlozATDssBSWdYtv107Wd2AzEoiXPu6e3pflsuXULlyqQnS4ELEKPYThFLafh1NqvZDPddqozcPZ-iODBW-xf3A4DYDdivnMYLrh73AZOGHexxu8ay6nDA"} +# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} +``` + +=== `sign_jwt_es256` + +Hash and sign an object representing JSON Web Token (JWT) claims using ES256. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_es256("""-----BEGIN EC PRIVATE KEY----- +... signature data ... +-----END EC PRIVATE KEY-----""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.-8LrOdkEiv_44ADWW08lpbq41ZmHCel58NMORPq1q4Dyw0zFhqDVLrRoSvCvuyyvgXAFb9IHfR-9MlJ_2ShA9A"} +``` + +=== `sign_jwt_es384` + +Hash and sign an object representing JSON Web Token (JWT) claims using ES384. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_es384("""-----BEGIN EC PRIVATE KEY----- +... signature data ... +-----END EC PRIVATE KEY-----""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.8FmTKH08dl7dyxrNu0rmvhegiIBCy-O9cddGco2e9lpZtgv5mS5qHgPkgBC5eRw1d7SRJsHwHZeehzdqT5Ba7aZJIhz9ds0sn37YQ60L7jT0j2gxCzccrt4kECHnUnLw"} +``` + +=== `sign_jwt_es512` + +Hash and sign an object representing JSON Web Token (JWT) claims using ES512. + +Introduced in version v4.20.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_es512("""-----BEGIN EC PRIVATE KEY----- +... signature data ... +-----END EC PRIVATE KEY-----""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.AQbEWymoRZxDJEJtKSFFG2k2VbDCTYSuBwAZyMqexCspr3If8aERTVGif8HXG3S7TzMBCCzxkcKr3eIU441l3DlpAMNfQbkcOlBqMvNBn-CX481WyKf3K5rFHQ-6wRonz05aIsWAxCDvAozI_9J0OWllxdQ2MBAuTPbPJ38OqXsYkCQs"} +``` + +=== `sign_jwt_hs256` + +Hash and sign an object representing JSON Web Token (JWT) claims using HS256. + +Introduced in version v4.12.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_hs256("""dont-tell-anyone""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.hUl-nngPMY_3h9vveWJUPsCcO5PeL6k9hWLnMYeFbFQ"} +``` + +=== `sign_jwt_hs384` + +Hash and sign an object representing JSON Web Token (JWT) claims using HS384. + +Introduced in version v4.12.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_hs384("""dont-tell-anyone""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.zGYLr83aToon1efUNq-hw7XgT20lPvZb8sYei8x6S6mpHwb433SJdXJXx0Oio8AZ"} +``` + +=== `sign_jwt_hs512` + +Hash and sign an object representing JSON Web Token (JWT) claims using HS512. + +Introduced in version v4.12.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_hs512("""dont-tell-anyone""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.zBNR9o_6EDwXXKkpKLNJhG26j8Dc-mV-YahBwmEdCrmiWt5les8I9rgmNlWIowpq6Yxs4kLNAdFhqoRz3NXT3w"} +``` + +=== `sign_jwt_rs256` + +Hash and sign an object representing JSON Web Token (JWT) claims using RS256. + +Introduced in version v4.18.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_rs256("""-----BEGIN RSA PRIVATE KEY----- +... signature data ... +-----END RSA PRIVATE KEY-----""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.b0lH3jEupZZ4zoaly4Y_GCvu94HH6UKdKY96zfGNsIkPZpQLHIkZ7jMWlLlNOAd8qXlsBGP_i8H2qCKI4zlWJBGyPZgxXDzNRPVrTDfFpn4t4nBcA1WK2-ntXP3ehQxsaHcQU8Z_nsogId7Pme5iJRnoHWEnWtbwz5DLSXL3ZZNnRdrHM9MdI7QSDz9mojKDCaMpGN9sG7Xl-tGdBp1XzXuUOzG8S03mtZ1IgVR1uiBL2N6oohHIAunk8DIAmNWI-zgycTgzUGU7mvPkKH43qO8Ua1-13tCUBKKa8VxcotZ67Mxm1QAvBGoDnTKwWMwghLzs6d6WViXQg6eWlJcpBA"} +``` + +=== `sign_jwt_rs384` + +Hash and sign an object representing JSON Web Token (JWT) claims using RS384. + +Introduced in version v4.18.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_rs384("""-----BEGIN RSA PRIVATE KEY----- +... signature data ... +-----END RSA PRIVATE KEY-----""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.orcXYBcjVE5DU7mvq4KKWFfNdXR4nEY_xupzWoETRpYmQZIozlZnM_nHxEk2dySvpXlAzVm7kgOPK2RFtGlOVaNRIa3x-pMMr-bhZTno4L8Hl4sYxOks3bWtjK7wql4uqUbqThSJB12psAXw2-S-I_FMngOPGIn4jDT9b802ottJSvTpXcy0-eKTjrV2PSkRRu-EYJh0CJZW55MNhqlt6kCGhAXfbhNazN3ASX-dmpd_JixyBKphrngr_zRA-FCn_Xf3QQDA-5INopb4Yp5QiJ7UxVqQEKI80X_JvJqz9WE1qiAw8pq5-xTen1t7zTP-HT1NbbD3kltcNa3G8acmNg"} +``` + +=== `sign_jwt_rs512` + +Hash and sign an object representing JSON Web Token (JWT) claims using RS512. + +Introduced in version v4.18.0. + + +==== Parameters + +*`signing_secret`* <string> The secret to use for signing the token. + +==== Examples + + +```coffeescript +root.signed = this.claims.sign_jwt_rs512("""-----BEGIN RSA PRIVATE KEY----- +... signature data ... +-----END RSA PRIVATE KEY-----""") + +# In: {"claims":{"sub":"user123"}} +# Out: {"signed":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.rsMp_X5HMrUqKnZJIxo27aAoscovRA6SSQYR9rq7pifIj0YHXxMyNyOBDGnvVALHKTi25VUGHpfNUW0VVMmae0A4t_ObNU6hVZHguWvetKZZq4FZpW1lgWHCMqgPGwT5_uOqwYCH6r8tJuZT3pqXeL0CY4putb1AN2w6CVp620nh3l8d3XWb4jaifycd_4CEVCqHuWDmohfug4VhmoVKlIXZkYoAQowgHlozATDssBSWdYtv107Wd2AzEoiXPu6e3pflsuXULlyqQnS4ELEKPYThFLafh1NqvZDPddqozcPZ-iODBW-xf3A4DYDdivnMYLrh73AZOGHexxu8ay6nDA"} +``` + +== GeoIP + +=== `geoip_anonymous_ip` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the anonymous IP associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_asn` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the ASN associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_city` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the city associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_connection_type` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the connection type associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_country` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the country associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_domain` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the domain associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_enterprise` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the enterprise associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +=== `geoip_isp` + +[CAUTION] +.Experimental +==== +This method is experimental and therefore breaking changes could be made to it outside of major version releases. +==== +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the ISP associated with it. + +==== Parameters + +*`path`* <string> A path to an mmdb (maxmind) file. + +== Deprecated + +=== `format_timestamp` + +Attempts to format a timestamp value as a string according to a specified format, or RFC 3339 by default. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. + +The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the <> method. + +==== Parameters + +*`format`* <string, default `"2006-01-02T15:04:05.999999999Z07:00"`> The output format to use. +*`tz`* <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. + +=== `format_timestamp_strftime` + +Attempts to format a timestamp value as a string according to a specified strftime-compatible format. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. + +==== Parameters + +*`format`* <string> The output format to use. +*`tz`* <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used. + +=== `format_timestamp_unix` + +Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +=== `format_timestamp_unix_micro` + +Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +=== `format_timestamp_unix_milli` + +Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +=== `format_timestamp_unix_nano` + +Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats. + +=== `parse_timestamp` + +Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as <>. + +The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the <> method. + +==== Parameters + +*`format`* <string> The format of the target string. + +=== `parse_timestamp_strptime` + +Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into <>. + +==== Parameters + +*`format`* <string> The format of the target string. + diff --git a/internal/api/docs.md b/internal/api/docs.adoc similarity index 61% rename from internal/api/docs.md rename to internal/api/docs.adoc index 25da977f03..9ef81f0af2 100644 --- a/internal/api/docs.md +++ b/internal/api/docs.adoc @@ -1,15 +1,14 @@ ---- -title: HTTP ---- += HTTP - + internal/api/docs.adoc +//// -When Benthos runs it kicks off an HTTP server that provides a few generally useful endpoints and is also where configured components such as the [`http_server` input][inputs.http_server] [and output][outputs.http_server] can register their own endpoints if they don't require their own host/port. +When {page-component-title} runs it kicks off an HTTP server that provides a few generally useful endpoints and is also where configured components such as the xref:components:inputs/http_server.adoc[`http_server` input] xref:components:outputs/http_server.adoc[and output] can register their own endpoints if they don't require their own host/port. The configuration for this server lives under the `http` namespace, with the following default values: @@ -19,73 +18,70 @@ The configuration for this server lives under the `http` namespace, with the fol {{.CommonConfig -}} ``` {{else}} -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - +[tabs] +====== +Common:: ++ +-- ```yaml # Common config fields, showing default values {{.CommonConfig -}} ``` - - +-- +Advanced:: ++ +-- ```yaml # All config fields, showing default values {{.AdvancedConfig -}} ``` - - - +-- +====== {{end -}} The field `enabled` can be set to `false` in order to disable the server. The field `root_path` specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a `root_path` set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`. -## Enabling HTTPS +== Enabling HTTPS -By default Benthos will serve traffic over HTTP. In order to enforce TLS and serve traffic exclusively over HTTPS you must provide a `cert_file` and `key_file` path in your config, which point to a file containing a certificate and a matching private key for the server respectively. +By default {page-component-title} will serve traffic over HTTP. In order to enforce TLS and serve traffic exclusively over HTTPS you must provide a `cert_file` and `key_file` path in your config, which point to a file containing a certificate and a matching private key for the server respectively. If the certificate is signed by a certificate authority, the `cert_file` should be the concatenation of the server's certificate, any intermediates, and the CA's certificate. -## Enabling Basic Authentication +== Enabling basic authentication -By default Benthos does not do any sort of authentication for the service-wide HTTP server. However, it's possible to configure basic authentication with the [`basic_auth`](#basic_auth) field. Passwords configured must be hashed according to the specified algorithm and base64 encoded, for some hashing algorithms you can do this using Benthos itself: +By default {page-component-title} does not do any sort of authentication for the service-wide HTTP server. However, it's possible to configure basic authentication with the <> field. Passwords configured must be hashed according to the specified algorithm and base64 encoded, for some hashing algorithms you can do this using {page-component-title} itself: ```sh echo mynewpassword | benthos blobl 'root = content().hash("sha256").encode("base64")' ``` -## Endpoints +== Endpoints The following endpoints will be generally available when the HTTP server is enabled: - `/version` provides version info. - `/ping` can be used as a liveness probe as it always returns a 200. - `/ready` can be used as a readiness probe as it serves a 200 only when both the input and output are connected, otherwise a 503 is returned. -- `/metrics`, `/stats` both provide metrics when the metrics type is either [`json_api`][metrics.json_api] or [`prometheus`][metrics.prometheus]. +- `/metrics`, `/stats` both provide metrics when the metrics type is either xref:components:metrics/json_api.adoc[`json_api`] or xref:components:metrics/prometheus.adoc[`prometheus`]. - `/endpoints` provides a JSON object containing a list of available endpoints, including those registered by configured components. -## CORS +== CORS In order to serve Cross-Origin Resource Sharing headers, which instruct browsers to allow CORS requests, set the subfield `cors.enabled` to `true`. -### allowed_origins +=== allowed_origins A list of allowed origins to connect from. The literal value `*` can be specified as a wildcard. Note `cors.enabled` must be set to `true` for this list to take effect. -## Debug Endpoints +== Debug endpoints -The field `debug_endpoints` when set to `true` prompts Benthos to register a few extra endpoints that can be useful for debugging performance or behavioral problems: +The field `debug_endpoints` when set to `true` prompts {page-component-title} to register a few extra endpoints that can be useful for debugging performance or behavioral problems: - `/debug/config/json` returns the loaded config as JSON. - `/debug/config/yaml` returns the loaded config as YAML. @@ -98,13 +94,8 @@ The field `debug_endpoints` when set to `true` prompts Benthos to register a few - `/debug/pprof/trace` responds with the execution trace in binary form. Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified. - `/debug/stack` returns a snapshot of the current service stack trace. -## Fields +== Fields The schema of the `http` section is as follows: {{template "field_docs" . -}} - -[inputs.http_server]: /docs/components/inputs/http_server -[outputs.http_server]: /docs/components/outputs/http_server -[metrics.json_api]: /docs/components/metrics/json_api -[metrics.prometheus]: /docs/components/metrics/prometheus diff --git a/internal/api/docs.go b/internal/api/docs.go index 6db7d2b11c..21153ecffc 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -28,7 +28,7 @@ func Spec() docs.FieldSpecs { } } -//go:embed docs.md +//go:embed docs.adoc var httpDocs string type templateContext struct { @@ -78,7 +78,10 @@ http: // some of the caveats in registering endpoints due to their non-deterministic // ordering and lack of explicit path terminators. func EndpointCaveats() string { - return `:::caution Endpoint Caveats + return ` +[CAUTION] +.Endpoint caveats +==== Components within a Benthos config will register their respective endpoints in a non-deterministic order. This means that establishing precedence of endpoints that are registered via multiple ` + "`http_server`" + ` inputs or outputs (either within brokers or from cohabiting streams) is not possible in a predictable way. This ambiguity makes it difficult to ensure that paths which are both a subset of a path registered by a separate component, and end in a slash (` + "`/`" + `) and will therefore match against all extensions of that path, do not prevent the more specific path from matching against requests. @@ -86,5 +89,5 @@ This ambiguity makes it difficult to ensure that paths which are both a subset o It is therefore recommended that you ensure paths of separate components do not collide unless they are explicitly non-competing. For example, if you were to deploy two separate ` + "`http_server`" + ` inputs, one with a path ` + "`/foo/`" + ` and the other with a path ` + "`/foo/bar`" + `, it would not be possible to ensure that the path ` + "`/foo/`" + ` does not swallow requests made to ` + "`/foo/bar`" + `. -:::` +====` } diff --git a/internal/batch/policy/docs.go b/internal/batch/policy/docs.go index 1504661461..01454bbb19 100644 --- a/internal/batch/policy/docs.go +++ b/internal/batch/policy/docs.go @@ -8,7 +8,7 @@ func FieldSpec() docs.FieldSpec { Name: "batching", Type: docs.FieldTypeObject, Description: ` -Allows you to configure a [batching policy](/docs/configuration/batching).`, +Allows you to configure a xref:configuration:batching.adoc[batching policy].`, Examples: []any{ map[string]any{ "count": 0, @@ -41,12 +41,12 @@ Allows you to configure a [batching policy](/docs/configuration/batching).`, ).HasDefault(""), docs.FieldBloblang( "check", - "A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch.", + "A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch.", `this.type == "end_of_transaction"`, ).HasDefault(""), docs.FieldProcessor( "processors", - "A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op.", + "A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op.", []map[string]any{ { "archive": map[string]any{ diff --git a/internal/bloblang/query/functions.go b/internal/bloblang/query/functions.go index 6ab6be6932..7100915bce 100644 --- a/internal/bloblang/query/functions.go +++ b/internal/bloblang/query/functions.go @@ -214,7 +214,7 @@ var _ = registerSimpleFunction( var _ = registerSimpleFunction( NewFunctionSpec( FunctionCategoryMessage, "content", - "Returns the full raw contents of the mapping target message as a byte array. When mapping to a JSON field the value should be encoded using the method [`encode`][methods.encode], or cast to a string directly using the method [`string`][methods.string], otherwise it will be base64 encoded by default.", + "Returns the full raw contents of the mapping target message as a byte array. When mapping to a JSON field the value should be encoded using the method xref:guides:bloblang/methods.adoc#encode[`encode`], or cast to a string directly using the method xref:guides:bloblang/methods.adoc#string[`string`], otherwise it will be base64 encoded by default.", NewExampleSpec("", `root.doc = content().string()`, `{"foo":"bar"}`, @@ -231,7 +231,7 @@ var _ = registerSimpleFunction( var _ = registerSimpleFunction( NewFunctionSpec( FunctionCategoryMessage, "tracing_span", - "Provides the message tracing span [(created via Open Telemetry APIs)](/docs/components/tracers/about) as an object serialised via text map formatting. The returned value will be `null` if the message does not have a span.", + "Provides the message tracing span xref:components:tracers/about.adoc[(created via Open Telemetry APIs)] as an object serialized via text map formatting. The returned value will be `null` if the message does not have a span.", NewExampleSpec("", `root.headers.traceparent = tracing_span().traceparent`, `{"some_stuff":"just can't be explained by science"}`, @@ -314,7 +314,7 @@ func countFunction(args *ParsedParams) (Function, error) { var _ = registerFunction( NewFunctionSpec( FunctionCategoryGeneral, "deleted", - "A function that returns a result indicating that the mapping target should be deleted. Deleting, also known as dropping, messages will result in them being acknowledged as successfully processed to inputs in a Benthos pipeline. For more information about error handling patterns read [here][error_handling].", + "A function that returns a result indicating that the mapping target should be deleted. Deleting, also known as dropping, messages will result in them being acknowledged as successfully processed to inputs in a Benthos pipeline. For more information about error handling patterns read xref:configuration:error_handling.adoc[].", NewExampleSpec("", `root = this root.bar = deleted()`, @@ -338,7 +338,7 @@ root.bar = deleted()`, var _ = registerSimpleFunction( NewFunctionSpec( FunctionCategoryMessage, "error", - "If an error has occurred during the processing of a message this function returns the reported cause of the error as a string, otherwise `null`. For more information about error handling patterns read [here][error_handling].", + "If an error has occurred during the processing of a message this function returns the reported cause of the error as a string, otherwise `null`. For more information about error handling patterns read xref:configuration:error_handling.adoc[].", NewExampleSpec("", `root.doc.error = error()`, ), @@ -355,7 +355,7 @@ var _ = registerSimpleFunction( var _ = registerSimpleFunction( NewFunctionSpec( FunctionCategoryMessage, "errored", - "Returns a boolean value indicating whether an error has occurred during the processing of a message. For more information about error handling patterns read [here][error_handling].", + "Returns a boolean value indicating whether an error has occurred during the processing of a message. For more information about error handling patterns read xref:configuration:error_handling.adoc[].", NewExampleSpec("", `root.doc.status = if errored() { 400 } else { 200 }`, ), @@ -508,7 +508,7 @@ func NewMetaFunction(key string) Function { var _ = registerFunction( NewFunctionSpec( FunctionCategoryMessage, "metadata", - "Returns the value of a metadata key from the input message, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map, in order to query metadata mutations made within a mapping use the [`@` operator](/docs/guides/bloblang/about#metadata). This function supports extracting metadata from other messages of a batch with the `from` method.", + "Returns the value of a metadata key from the input message, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map, in order to query metadata mutations made within a mapping use the xref:guides:bloblang/about.adoc#metadata[`@` operator]. This function supports extracting metadata from other messages of a batch with the `from` method.", NewExampleSpec("", `root.topic = metadata("kafka_topic")`), NewExampleSpec( "The key parameter is optional and if omitted the entire metadata contents are returned as an object.", @@ -555,7 +555,7 @@ var _ = registerFunction( var _ = registerFunction( NewDeprecatedFunctionSpec( "meta", - "Returns the value of a metadata key from the input message as a string, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map. In order to query metadata mutations made within a mapping use the [`root_meta` function](#root_meta). This function supports extracting metadata from other messages of a batch with the `from` method.", + "Returns the value of a metadata key from the input message as a string, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map. In order to query metadata mutations made within a mapping use the <>. This function supports extracting metadata from other messages of a batch with the `from` method.", NewExampleSpec("", `root.topic = meta("kafka_topic")`, `root.topic = meta("nope") | meta("also nope") | "default"`, diff --git a/internal/bloblang/query/methods.go b/internal/bloblang/query/methods.go index df9c922480..1b3d20c947 100644 --- a/internal/bloblang/query/methods.go +++ b/internal/bloblang/query/methods.go @@ -158,7 +158,7 @@ func catchMethod(fn Function, args *ParsedParams) (Function, error) { var _ = registerMethod( NewMethodSpec( "from", - "Modifies a target query such that certain functions are executed from the perspective of another message in the batch. This allows you to mutate events based on the contents of other messages. Functions that support this behaviour are `content`, `json` and `meta`.", + "Modifies a target query such that certain functions are executed from the perspective of another message in the batch. This allows you to mutate events based on the contents of other messages. Functions that support this behavior are `content`, `json` and `meta`.", NewExampleSpec("For example, the following map extracts the contents of the JSON field `foo` specifically from message index `1` of a batch, effectively overriding the field `foo` for all messages of a batch to that of message 1:", `root = this root.foo = json("foo").from(1)`, @@ -200,7 +200,7 @@ func (f *fromMethod) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetP var _ = registerMethod( NewMethodSpec( "from_all", - "Modifies a target query such that certain functions are executed from the perspective of each message in the batch, and returns the set of results as an array. Functions that support this behaviour are `content`, `json` and `meta`.", + "Modifies a target query such that certain functions are executed from the perspective of each message in the batch, and returns the set of results as an array. Functions that support this behavior are `content`, `json` and `meta`.", NewExampleSpec("", `root = this root.foo_summed = json("foo").from_all().sum()`, @@ -229,7 +229,7 @@ func fromAllMethod(target Function, _ *ParsedParams) (Function, error) { var _ = registerMethod( NewMethodSpec( "get", - "Extract a field value, identified via a [dot path][field_paths], from an object.", + "Extract a field value, identified via a xref:configuration:field_paths.adoc[dot path], from an object.", ).InCategory( MethodCategoryObjectAndArray, "", NewExampleSpec("", @@ -239,7 +239,7 @@ var _ = registerMethod( `{"foo":{"bar":"from bar","baz":"from baz"},"target":"baz"}`, `{"result":"from baz"}`, ), - ).Param(ParamString("path", "A [dot path][field_paths] identifying a field to obtain.")), + ).Param(ParamString("path", "A xref:configuration:field_paths.adoc[dot path] identifying a field to obtain.")), getMethodCtor, ) diff --git a/internal/bloblang/query/methods_strings.go b/internal/bloblang/query/methods_strings.go index e0f592fe93..e34bd8c873 100644 --- a/internal/bloblang/query/methods_strings.go +++ b/internal/bloblang/query/methods_strings.go @@ -89,7 +89,7 @@ var _ = registerSimpleMethod( "encode", "", ).InCategory( MethodCategoryEncoding, - "Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` [(RFC 4648 with padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `base64rawurl` [(RFC 4648 without padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `hex`, `ascii85`.", + "Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`.", // NOTE: z85 has been removed from the list until we can support // misaligned data automatically. It'll still be supported for backwards // compatibility, but given it behaves differently to `ascii85` I think @@ -195,7 +195,7 @@ var _ = registerSimpleMethod( "decode", "", ).InCategory( MethodCategoryEncoding, - "Decodes an encoded string target according to a chosen scheme and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method [`string`][methods.string], or encoded using the method [`encode`][methods.encode], otherwise it will be base64 encoded by default.\n\nAvailable schemes are: `base64`, `base64url` [(RFC 4648 with padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `base64rawurl` [(RFC 4648 without padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `hex`, `ascii85`.", + "Decodes an encoded string target according to a chosen scheme and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `string`, or encoded using the method `encode`, otherwise it will be base64 encoded by default.\n\nAvailable schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`.", // NOTE: z85 has been removed from the list until we can support // misaligned data automatically. It'll still be supported for backwards // compatibility, but given it behaves differently to `ascii85` I think @@ -631,7 +631,7 @@ var _ = registerSimpleMethod( "format", "", ).InCategory( MethodCategoryStrings, - "Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go [`fmt` package documentation](https://pkg.go.dev/fmt) for the list of valid format verbs.", + "Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go https://pkg.go.dev/fmt[`fmt` package documentation] for the list of valid format verbs.", NewExampleSpec("", `root.foo = "%s(%v): %v".format(this.name, this.age, this.fingers)`, `{"name":"lance","age":37,"fingers":13}`, @@ -719,7 +719,7 @@ var _ = registerSimpleMethod( ).InCategory( MethodCategoryEncoding, ` -Hashes a string or byte array according to a chosen algorithm and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `+"[`string`][methods.string], or encoded using the method [`encode`][methods.encode]"+`, otherwise it will be base64 encoded by default. +Hashes a string or byte array according to a chosen algorithm and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `+"xref:guides:bloblang/methods.adoc#string[`string`], or encoded using the method xref:guides:bloblang/methods.adoc#encode[`encode`]"+`, otherwise it will be base64 encoded by default. Available algorithms are: `+"`hmac_sha1`, `hmac_sha256`, `hmac_sha512`, `md5`, `sha1`, `sha256`, `sha512`, `xxhash64`, `crc32`"+`. @@ -1247,8 +1247,8 @@ var _ = registerSimpleMethod( MethodCategoryParsing, "", NewExampleSpec("", `root.foo_url = this.foo_url.parse_url()`, - `{"foo_url":"https://www.benthos.dev/docs/guides/bloblang/about"}`, - `{"foo_url":{"fragment":"","host":"www.benthos.dev","opaque":"","path":"/docs/guides/bloblang/about","raw_fragment":"","raw_path":"","raw_query":"","scheme":"https"}}`, + `{"foo_url":"https://www.docs.redpanda.com/redpanda-connect/guides/bloblang/about/"}`, + `{"foo_url":{"fragment":"","host":"www.docs.redpanda.com","opaque":"","path":"/redpanda-connect/guides/bloblang/about/","raw_fragment":"","raw_path":"","raw_query":"","scheme":"https"}}`, ), NewExampleSpec("", `root.username = this.url.parse_url().user.name | "unknown"`, diff --git a/internal/bloblang/query/methods_structured.go b/internal/bloblang/query/methods_structured.go index 7cbe7f5988..5e24b6b5c7 100644 --- a/internal/bloblang/query/methods_structured.go +++ b/internal/bloblang/query/methods_structured.go @@ -276,7 +276,7 @@ var _ = registerSimpleMethod( var _ = registerSimpleMethod( NewMethodSpec( "exists", - "Checks that a field, identified via a [dot path][field_paths], exists in an object.", + "Checks that a field, identified via a xref:configuration:field_paths.adoc[dot path], exists in an object.", NewExampleSpec("", `root.result = this.foo.exists("bar.baz")`, `{"foo":{"bar":{"baz":"yep, I exist"}}}`, @@ -286,7 +286,7 @@ var _ = registerSimpleMethod( `{"foo":{}}`, `{"result":false}`, ), - ).Param(ParamString("path", "A [dot path][field_paths] to a field.")), + ).Param(ParamString("path", "A xref:configuration:field_paths.adoc[dot path] to a field.")), func(args *ParsedParams) (simpleMethod, error) { pathStr, err := args.FieldString("path") if err != nil { @@ -306,7 +306,7 @@ var _ = registerSimpleMethod( "explode", "", ).InCategory( MethodCategoryObjectAndArray, - "Explodes an array or object at a [field path][field_paths].", + "Explodes an array or object at a xref:configuration:field_paths.adoc[field path].", NewExampleSpec(`##### On arrays Exploding arrays results in an array containing elements matching the original document, where the target field of each element is an element of the exploded array:`, @@ -321,7 +321,7 @@ Exploding objects results in an object where the keys match the target object, a `{"id":1,"value":{"foo":2,"bar":[3,4],"baz":{"bev":5}}}`, `{"bar":{"id":1,"value":[3,4]},"baz":{"id":1,"value":{"bev":5}},"foo":{"id":1,"value":2}}`, ), - ).Param(ParamString("path", "A [dot path][field_paths] to a field to explode.")), + ).Param(ParamString("path", "A xref:configuration:field_paths.adoc[dot path] to a field to explode.")), func(args *ParsedParams) (simpleMethod, error) { pathRaw, err := args.FieldString("path") if err != nil { @@ -747,7 +747,7 @@ var _ = registerSimpleMethod( var _ = registerSimpleMethod( NewMethodSpec( "json_schema", - "Checks a [JSON schema](https://json-schema.org/) against a value and returns the value if it matches or throws and error if it does not.", + "Checks a https://json-schema.org/[JSON schema] against a value and returns the value if it matches or throws and error if it does not.", ).InCategory( MethodCategoryObjectAndArray, "", @@ -1045,7 +1045,7 @@ var _ = registerSimpleMethod( var _ = registerMethod( NewMethodSpec( - "merge", "Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the result will be an array containing both values, where values that are already arrays will be expanded into the resulting array. In order to simply override destination fields on collision use the [`assign`](#assign) method.", + "merge", "Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the result will be an array containing both values, where values that are already arrays will be expanded into the resulting array. In order to simply override destination fields on collision use the <> method.", ).InCategory( MethodCategoryObjectAndArray, "", NewExampleSpec(``, @@ -1095,7 +1095,7 @@ func mergeMethod(target Function, args *ParsedParams) (Function, error) { var _ = registerMethod( NewMethodSpec( - "assign", "Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the value in the destination object will be overwritten by that of source object. In order to preserve both values on collision use the [`merge`](#merge) method.", + "assign", "Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the value in the destination object will be overwritten by that of source object. In order to preserve both values on collision use the <> method.", ).InCategory( MethodCategoryObjectAndArray, "", NewExampleSpec(``, @@ -1676,7 +1676,7 @@ var _ = registerSimpleMethod( "without", "", ).InCategory( MethodCategoryObjectAndArray, - `Returns an object where one or more [field path][field_paths] arguments are removed. Each path specifies a specific field to be deleted from the input object, allowing for nested fields. + `Returns an object where one or more xref:configuration:field_paths.adoc[field path] arguments are removed. Each path specifies a specific field to be deleted from the input object, allowing for nested fields. If a key within a nested path does not exist or is not an object then it is not removed.`, NewExampleSpec("", diff --git a/internal/component/errors.go b/internal/component/errors.go index fa5b309a64..327ae3745c 100644 --- a/internal/component/errors.go +++ b/internal/component/errors.go @@ -24,7 +24,7 @@ func (e *errInvalidType) Error() string { } // ErrInvalidType creates an error that describes a component type being -// initialised with an unrecognised implementation. +// initialized with an unrecognised implementation. func ErrInvalidType(typeStr, tried string) error { return &errInvalidType{ typeStr: typeStr, diff --git a/internal/config/test/case.go b/internal/config/test/case.go index 1252cc3b18..f2a3725295 100644 --- a/internal/config/test/case.go +++ b/internal/config/test/case.go @@ -33,7 +33,7 @@ func (c *Case) Line() int { func caseFields() docs.FieldSpecs { return docs.FieldSpecs{ - docs.FieldString(fieldCaseName, "The name of the test, this should be unique and give a rough indication of what behaviour is being tested."), + docs.FieldString(fieldCaseName, "The name of the test, this should be unique and give a rough indication of what behavior is being tested."), docs.FieldString(fieldCaseEnvironment, "An optional map of environment variables to set for the duration of the test.").Map().Optional(), docs.FieldString(fieldCaseTargetProcessors, ` A [JSON Pointer][json-pointer] that identifies the specific processors which should be executed by the test. The target can either be a single processor or an array of processors. Alternatively a resource label can be used to identify a processor. diff --git a/internal/config/test/docs.md b/internal/config/test/docs.adoc similarity index 69% rename from internal/config/test/docs.md rename to internal/config/test/docs.adoc index d2e758a3f7..be870e5f3c 100644 --- a/internal/config/test/docs.md +++ b/internal/config/test/docs.adoc @@ -1,25 +1,20 @@ ---- -title: Unit Testing ---- += Unit Testing +:json-pointer-url: https://tools.ietf.org/html/rfc6901 +:bloblang-url: xref:guides:bloblang/about.adoc +:logger-url: xref:components:logger/about.adoc +:processors-mapping-url: xref:components:processors/mapping.adoc - - -The Benthos service offers a command `benthos test` for running unit tests on sections of a configuration file. This makes it easy to protect your config files from regressions over time. + internal/config/test/docs.adoc +//// -## Contents +The {page-component-title} service offers a command `benthos test` for running unit tests on sections of a configuration file. This makes it easy to protect your config files from regressions over time. -1. [Writing a Test](#writing-a-test) -2. [Output Conditions](#output-conditions) -3. [Running Tests](#running-tests) -4. [Mocking Processors](#mocking-processors) -5. [Config Field Spec](#fields) - -## Writing a Test +== Writing a test Let's imagine we have a configuration file `foo.yaml` containing some processors: @@ -60,7 +55,7 @@ tests: Under `tests` we have a list of any number of unit tests to execute for the config file. Each test is run in complete isolation, including any resources defined by the config file. Tests should be allocated a unique `name` that identifies the feature being tested. -The field `target_processors` is either the label of a processor to test, or a [JSON Pointer][json-pointer] that identifies the position of a processor, or list of processors, within the file which should be executed by the test. For example a value of `foo` would target a processor with the label `foo`, and a value of `/input/processors` would target all processors within the input section of the config. +The field `target_processors` is either the label of a processor to test, or a {json-pointer-url}[JSON Pointer] that identifies the position of a processor, or list of processors, within the file which should be executed by the test. For example a value of `foo` would target a processor with the label `foo`, and a value of `/input/processors` would target all processors within the input section of the config. The field `environment` allows you to define an object of key/value pairs that set environment variables to be evaluated during the parsing of the target config file. These are unique to each test, allowing you to test different environment variable interpolation combinations. @@ -68,21 +63,21 @@ The field `input_batch` lists one or more messages to be fed into the targeted p For the common case where the messages are in JSON format, you can use `json_content` instead of `content` to specify the message structurally rather than verbatim. -The field `output_batches` lists any number of batches of messages which are expected to result from the target processors. Each batch lists any number of messages, each one defining [`conditions`](#output-conditions) to describe the expected contents of the message. +The field `output_batches` lists any number of batches of messages which are expected to result from the target processors. Each batch lists any number of messages, each one defining <> to describe the expected contents of the message. If the number of batches defined does not match the resulting number of batches the test will fail. If the number of messages defined in each batch does not match the number in the resulting batches the test will fail. If any condition of a message fails then the test fails. -### Inline Tests +=== Inline tests Sometimes it's more convenient to define your tests within the config being tested. This is fine, simply add the `tests` field to the end of the config being tested. -### Bloblang Tests +=== Bloblang tests -Sometimes when working with large [Bloblang mappings][bloblang] it's preferred to have the full mapping in a separate file to your Benthos configuration. In this case it's possible to write unit tests that target and execute the mapping directly with the field `target_mapping`, which when specified is interpreted as either an absolute path or a path relative to the test definition file that points to a file containing only a Bloblang mapping. +Sometimes when working with large {bloblang-url}[Bloblang mappings] it's preferred to have the full mapping in a separate file to your {page-component-title} configuration. In this case it's possible to write unit tests that target and execute the mapping directly with the field `target_mapping`, which when specified is interpreted as either an absolute path or a path relative to the test definition file that points to a file containing only a Bloblang mapping. For example, if we were to have a file `cities.blobl` containing a mapping: -```coffee +```coffeescript root.Cities = this.locations. filter(loc -> loc.state == "WA"). map_each(loc -> loc.name). @@ -111,13 +106,13 @@ tests: - json_equals: {"Cities": "Bellevue, Olympia, Seattle"} ``` -And execute this test the same way we execute other Benthos tests (`benthos test ./dir/cities_test.yaml`, `benthos test ./dir/...`, etc). +And execute this test the same way we execute other {page-component-title} tests (`benthos test ./dir/cities_test.yaml`, `benthos test ./dir/...`, etc). -### Fragmented Tests +=== Fragmented tests -Sometimes the number of tests you need to define in order to cover a config file is so vast that it's necessary to split them across multiple test definition files. This is possible but Benthos still requires a way to detect the configuration file being targeted by these fragmented test definition files. In order to do this we must prefix our `target_processors` field with the path of the target relative to the definition file. +Sometimes the number of tests you need to define in order to cover a config file is so vast that it's necessary to split them across multiple test definition files. This is possible but {page-component-title} still requires a way to detect the configuration file being targeted by these fragmented test definition files. In order to do this we must prefix our `target_processors` field with the path of the target relative to the definition file. -The syntax of `target_processors` in this case is a full [JSON Pointer][json-pointer] that should look something like `target.yaml#/pipeline/processors`. For example, if we saved our test definition above in an arbitrary location like `./tests/first.yaml` and wanted to target our original `foo.yaml` config file, we could do that with the following: +The syntax of `target_processors` in this case is a full {json-pointer-url}[JSON Pointer] that should look something like `target.yaml#/pipeline/processors`. For example, if we saved our test definition above in an arbitrary location like `./tests/first.yaml` and wanted to target our original `foo.yaml` config file, we could do that with the following: ```yml tests: @@ -135,13 +130,13 @@ tests: example_key: example metadata value ``` -## Input Definitions +== Input Definitions -### `content` +=== `content` Sets the raw content of the message. -### `json_content` +=== `json_content` ```yml json_content: @@ -151,7 +146,7 @@ json_content: Sets the raw content of the message to a JSON document matching the structure of the value. -### `file_content` +=== `file_content` ```yml file_content: ./foo/bar.txt @@ -159,21 +154,21 @@ file_content: ./foo/bar.txt Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. -### `metadata` +=== `metadata` A map of key/value pairs that sets the metadata values of the message. -## Output Conditions +== Output Conditions -### `bloblang` +=== `bloblang` ```yml bloblang: 'this.age > 10 && @foo.length() > 0' ``` -Executes a [Bloblang expression][bloblang] on a message, if the result is anything other than a boolean equalling `true` the test fails. +Executes a {bloblang-url}[Bloblang expression] on a message, if the result is anything other than a boolean equalling `true` the test fails. -### `content_equals` +=== `content_equals` ```yml content_equals: example content @@ -181,7 +176,7 @@ content_equals: example content Checks the full raw contents of a message against a value. -### `content_matches` +=== `content_matches` ```yml content_matches: "^foo [a-z]+ bar$" @@ -189,7 +184,7 @@ content_matches: "^foo [a-z]+ bar$" Checks whether the full raw contents of a message matches a regular expression (re2). -### `metadata_equals` +=== `metadata_equals` ```yml metadata_equals: @@ -198,7 +193,7 @@ metadata_equals: Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. -### `file_equals` +=== `file_equals` ```yml file_equals: ./foo/bar.txt @@ -206,7 +201,7 @@ file_equals: ./foo/bar.txt Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. -### `file_json_equals` +=== `file_json_equals` ```yml file_json_equals: ./foo/bar.json @@ -214,7 +209,7 @@ file_json_equals: ./foo/bar.json Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. -### `json_equals` +=== `json_equals` ```yml json_equals: { "key": "value" } @@ -229,7 +224,7 @@ json_equals: key: value ``` -### `json_contains` +=== `json_contains` ```yml json_contains: { "key": "value" } @@ -237,16 +232,16 @@ json_contains: { "key": "value" } Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. -## Running Tests +== Running tests Executing tests for a specific config can be done by pointing the subcommand `test` at either the config to be tested or its test definition, e.g. `benthos test ./config.yaml` and `benthos test ./config_benthos_test.yaml` are equivalent. The `test` subcommand also supports wildcard patterns e.g. `benthos test ./foo/*.yaml` will execute all tests within matching files. In order to walk a directory tree and execute all tests found you can use the shortcut `./...`, e.g. `benthos test ./...` will execute all tests found in the current directory, any child directories, and so on. If you want to allow components to write logs at a provided level to stdout when running the tests, you can use -`benthos test --log `. Please consult the [logger docs][logger] for further details. +`benthos test --log `. Please consult the {logger-url}[logger docs] for further details. -## Mocking Processors +== Mocking processors BETA: This feature is currently in a BETA phase, which means breaking changes could be made if a fundamental issue with the feature is found. @@ -263,7 +258,7 @@ pipeline: - mapping: 'root = content().uppercase()' ``` -Rather than create a fake service for the `http` processor to interact with we can define a mock in our test definition that replaces it with a [`mapping` processor][processors.mapping]. Mocks are configured as a map of labels that identify a processor to replace and the config to replace it with: +Rather than create a fake service for the `http` processor to interact with we can define a mock in our test definition that replaces it with a {processors-mapping-url}[`mapping` processor]. Mocks are configured as a map of labels that identify a processor to replace and the config to replace it with: ```yaml tests: @@ -278,13 +273,13 @@ tests: - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" ``` -With the above test definition the `http` processor will be swapped out for `mapping: 'root = content().string() + " this is some mock content"'`. For the purposes of mocking it is recommended that you use a [`mapping` processor][processors.mapping] that simply mutates the message in a way that you would expect the mocked processor to. +With the above test definition the `http` processor will be swapped out for `mapping: 'root = content().string() + " this is some mock content"'`. For the purposes of mocking it is recommended that you use a {processors-mapping-url}[`mapping` processor] that simply mutates the message in a way that you would expect the mocked processor to. -> Note: It's not currently possible to mock components that are imported as separate resource files (using `--resource`/`-r`). It is recommended that you mock these by maintaining separate definitions for test purposes (`-r "./test/*.yaml"`). +NOTE: It's not currently possible to mock components that are imported as separate resource files (using `--resource`/`-r`). It is recommended that you mock these by maintaining separate definitions for test purposes (`-r "./test/*.yaml"`). -### More granular mocking +=== More granular mocking -It is also possible to target specific fields within the test config by [JSON pointers][json-pointer] as an alternative to labels. The following test definition would create the same mock as the previous: +It is also possible to target specific fields within the test config by {json-pointer-url}[JSON pointers] as an alternative to labels. The following test definition would create the same mock as the previous: ```yaml tests: @@ -299,13 +294,8 @@ tests: - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" ``` -## Fields +== Fields The schema of a template file is as follows: {{template "field_docs" . -}} - -[json-pointer]: https://tools.ietf.org/html/rfc6901 -[bloblang]: /docs/guides/bloblang/about -[logger]: /docs/components/logger/about -[processors.mapping]: /docs/components/processors/mapping diff --git a/internal/config/test/docs.go b/internal/config/test/docs.go index 658656d416..4222046e24 100644 --- a/internal/config/test/docs.go +++ b/internal/config/test/docs.go @@ -14,7 +14,7 @@ import ( const fieldTests = "tests" -//go:embed docs.md +//go:embed docs.adoc var testDocs string type testContext struct { diff --git a/internal/docs/benchmark_test.go b/internal/docs/benchmark_test.go index f7c8f01ded..8d3f02cee5 100644 --- a/internal/docs/benchmark_test.go +++ b/internal/docs/benchmark_test.go @@ -23,7 +23,7 @@ func BenchmarkFields(b *testing.B) { Type: docs.TypeMetrics, Status: docs.StatusStable, Summary: ` -Pushes metrics using the [StatsD protocol](https://github.com/statsd/statsd). +Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. Supported tagging formats are 'none', 'datadog' and 'influxdb'.`, Description: ` The underlying client library has recently been updated in order to support @@ -50,7 +50,7 @@ tagging.`, Type: docs.TypeMetrics, Status: docs.StatusStable, Summary: ` -Pushes metrics using the [StatsD protocol](https://github.com/statsd/statsd). +Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. Supported tagging formats are 'none', 'datadog' and 'influxdb'.`, Description: ` The underlying client library has recently been updated in order to support diff --git a/internal/docs/bloblang_markdown.go b/internal/docs/bloblang_markdown.go index 0afc688f9c..b8842ee37f 100644 --- a/internal/docs/bloblang_markdown.go +++ b/internal/docs/bloblang_markdown.go @@ -19,10 +19,10 @@ type functionsContext struct { var bloblangParamsTemplate = `{{define "parameters" -}} {{if gt (len .Definitions) 0}} -#### Parameters +==== Parameters {{range $i, $param := .Definitions -}} -` + "**`{{$param.Name}}`**" + ` <{{if $param.IsOptional}}(optional) {{end}}{{$param.ValueType}}{{if $param.DefaultValue}}, default ` + "`{{$param.PrettyDefault}}`" + `{{end}}> {{$param.Description}} +` + "*`{{$param.Name}}`*" + ` <{{if $param.IsOptional}}(optional) {{end}}{{$param.ValueType}}{{if $param.DefaultValue}}, default ` + "`{{$param.PrettyDefault}}`" + `{{end}}> {{$param.Description}} {{end -}} {{end -}} {{end -}} @@ -34,7 +34,7 @@ var bloblangFunctionsTemplate = bloblangParamsTemplate + `{{define "function_exa {{end -}} -` + "```coffee" + ` +` + "```coffeescript" + ` {{.Mapping}} {{range $i, $result := .Results}} # In: {{index $result 0}} @@ -44,17 +44,21 @@ var bloblangFunctionsTemplate = bloblangParamsTemplate + `{{define "function_exa {{end -}} {{define "function_spec" -}} -### ` + "`{{.Name}}`" + ` +=== ` + "`{{.Name}}`" + ` {{if eq .Status "beta" -}} -:::caution BETA +[NOTE] +.Beta +==== This function is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: +==== {{end -}} {{if eq .Status "experimental" -}} -:::caution EXPERIMENTAL +[CAUTION] +.Experimental +==== This function is experimental and therefore breaking changes could be made to it outside of major version releases. -::: +==== {{end -}} {{.Description}}{{if gt (len .Version) 0}} @@ -62,7 +66,7 @@ Introduced in version {{.Version}}. {{end}} {{template "parameters" .Params -}} {{if gt (len .Examples) 0}} -#### Examples +==== Examples {{range $i, $example := .Examples}} {{template "function_example" $example -}} @@ -71,26 +75,22 @@ Introduced in version {{.Version}}. {{end -}} ---- -title: Bloblang Functions -sidebar_label: Functions -description: A list of Bloblang functions ---- += Bloblang Functions +:description: A list of Bloblang functions - +//// -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; Functions can be placed anywhere and allow you to extract information from your environment, generate values, or access data from the underlying message being mapped: -` + "```coffee" + ` +` + "```coffeescript" + ` root.doc.id = uuid_v4() root.doc.received_at = now() root.doc.host = hostname() @@ -98,24 +98,19 @@ root.doc.host = hostname() Functions support both named and nameless style arguments: -` + "```coffee" + ` +` + "```coffeescript" + ` root.values_one = range(start: 0, stop: this.max, step: 2) root.values_two = range(0, this.max, 2) ` + "```" + ` {{range $i, $cat := .Categories -}} -## {{$cat.Name}} +== {{$cat.Name}} {{range $i, $spec := $cat.Specs -}} {{template "function_spec" $spec}} {{end -}} {{end -}} -[error_handling]: /docs/configuration/error_handling -[field_paths]: /docs/configuration/field_paths -[meta_proc]: /docs/components/processors/metadata -[methods.encode]: /docs/guides/bloblang/methods#encode -[methods.string]: /docs/guides/bloblang/methods#string ` func prefixExamples(s []query.ExampleSpec) { @@ -187,7 +182,7 @@ var bloblangMethodsTemplate = bloblangParamsTemplate + `{{define "method_example {{end -}} -` + "```coffee" + ` +` + "```coffeescript" + ` {{.Mapping}} {{range $i, $result := .Results}} # In: {{index $result 0}} @@ -197,17 +192,21 @@ var bloblangMethodsTemplate = bloblangParamsTemplate + `{{define "method_example {{end -}} {{define "method_spec" -}} -### ` + "`{{.Name}}`" + ` +=== ` + "`{{.Name}}`" + ` {{if eq .Status "beta" -}} -:::caution BETA +[CAUTION] +.Beta +==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: +==== {{end -}} {{if eq .Status "experimental" -}} -:::caution EXPERIMENTAL +[CAUTION] +.Experimental +==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: +==== {{end -}} {{.Description}}{{if gt (len .Version) 0}} @@ -215,7 +214,7 @@ Introduced in version {{.Version}}. {{end}} {{template "parameters" .Params -}} {{if gt (len .Examples) 0}} -#### Examples +==== Examples {{range $i, $example := .Examples}} {{template "method_example" $example -}} @@ -224,27 +223,23 @@ Introduced in version {{.Version}}. {{end -}} ---- -title: Bloblang Methods -sidebar_label: Methods -description: A list of Bloblang methods ---- += Bloblang Methods +:description: A list of Bloblang methods + - +//// -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; Methods provide most of the power in Bloblang as they allow you to augment values and can be added to any expression (including other methods): -` + "```coffee" + ` +` + "```coffeescript" + ` root.doc.id = this.thing.id.string().catch(uuid_v4()) root.doc.reduced_nums = this.thing.nums.map_each(num -> if num < 10 { deleted() @@ -256,13 +251,13 @@ root.has_good_taste = ["pikachu","mewtwo","magmar"].contains(this.user.fav_pokem Methods support both named and nameless style arguments: -` + "```coffee" + ` +` + "```coffeescript" + ` root.foo_one = this.(bar | baz).trim().replace_all(old: "dog", new: "cat") root.foo_two = this.(bar | baz).trim().replace_all("dog", "cat") ` + "```" + ` {{if gt (len .General) 0 -}} -## General +== General {{range $i, $spec := .General -}} {{template "method_spec" $spec}} @@ -270,16 +265,12 @@ root.foo_two = this.(bar | baz).trim().replace_all("dog", "cat") {{end -}} {{range $i, $cat := .Categories -}} -## {{$cat.Name}} +== {{$cat.Name}} {{range $i, $spec := $cat.Specs -}} {{template "method_spec" $spec}} {{end -}} {{end -}} - -[field_paths]: /docs/configuration/field_paths -[methods.encode]: #encode -[methods.string]: #string ` func methodForCat(s query.MethodSpec, cat string) (query.MethodSpec, bool) { diff --git a/internal/docs/component.go b/internal/docs/component.go index 3c1800151e..318d8bbaf6 100644 --- a/internal/docs/component.go +++ b/internal/docs/component.go @@ -85,16 +85,16 @@ type ComponentSpec struct { // Plugin is true for all plugin components. Plugin bool `json:"plugin"` - // Summary of the component (in markdown, must be short). + // Summary of the component (in Asciidoc, must be short). Summary string `json:"summary,omitempty"` - // Description of the component (in markdown). + // Description of the component (in Asciidoc). Description string `json:"description,omitempty"` // Categories that describe the purpose of the component. Categories []string `json:"categories"` - // Footnotes of the component (in markdown). + // Footnotes of the component (in Asciidoc). Footnotes string `json:"footnotes,omitempty"` // Examples demonstrating use cases for the component. diff --git a/internal/docs/component_markdown.go b/internal/docs/component_markdown.go index 64b34d28a2..2a2469cad4 100644 --- a/internal/docs/component_markdown.go +++ b/internal/docs/component_markdown.go @@ -26,44 +26,43 @@ type componentContext struct { Version string } -var componentTemplate = FieldsTemplate(false) + `--- -title: {{.Name}} -slug: {{.Name}} -type: {{.Type}} -status: {{.Status}} +var componentTemplate = FieldsTemplate(false) + ` += {{.Name}} +:type: {{.Type}} +:status: {{.Status}} {{if gt (len .FrontMatterSummary) 0 -}} -description: "{{.FrontMatterSummary}}" +:description: "{{.FrontMatterSummary}}" {{end -}} {{if gt (len .Categories) 0 -}} -categories: {{.Categories}} -{{end -}} ---- +:categories: {{.Categories}} +{{end}} + - +//// + + +component_type_dropdown::[] -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; {{if eq .Status "beta" -}} -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: + {{end -}} {{if eq .Status "experimental" -}} -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: + {{end -}} {{if eq .Status "deprecated" -}} -:::warning DEPRECATED -This component is deprecated and will be removed in the next major version release. Please consider moving onto [alternative components](#alternatives). -::: +[WARNING] +.Deprecated +==== +This component is deprecated and will be removed in the next major version release. Please consider moving onto <>. +==== {{end -}} + {{if gt (len .Summary) 0 -}} {{.Summary}} {{end -}}{{if gt (len .Version) 0}} @@ -75,49 +74,48 @@ Introduced in version {{.Version}}. {{.CommonConfig -}} ` + "```" + ` {{else}} - - - +[tabs] +====== +Common:: ++ +-- ` + "```yml" + ` # Common config fields, showing default values {{.CommonConfig -}} ` + "```" + ` - - +-- +Advanced:: ++ +-- ` + "```yml" + ` # All config fields, showing default values {{.AdvancedConfig -}} ` + "```" + ` - - +-- +====== {{end -}} {{if gt (len .Description) 0}} {{.Description}} {{end}} {{if and (le (len .Fields) 4) (gt (len .Fields) 0) -}} -## Fields +== Fields {{template "field_docs" . -}} {{end -}} {{if gt (len .Examples) 0 -}} -## Examples - - +== Examples +[tabs] +====== {{range $i, $example := .Examples -}} - +{{$example.Title}}:: ++ +-- {{if gt (len $example.Summary) 0 -}} {{$example.Summary}} @@ -125,14 +123,14 @@ Introduced in version {{.Version}}. {{if gt (len $example.Config) 0 -}} ` + "```yaml" + `{{$example.Config}}` + "```" + ` {{end}} - +-- {{end -}} - +====== {{end -}} {{if gt (len .Fields) 4 -}} -## Fields +== Fields {{template "field_docs" . -}} {{end -}} diff --git a/internal/docs/field.go b/internal/docs/field.go index 0b15858c80..dd64c2285f 100644 --- a/internal/docs/field.go +++ b/internal/docs/field.go @@ -83,7 +83,7 @@ type FieldSpec struct { // Kind of the field. Kind FieldKind `json:"kind"` - // Description of the field purpose (in markdown). + // Description of the field purpose (in Asciidoc). Description string `json:"description,omitempty"` // IsAdvanced is true for optional fields that will not be present in most diff --git a/internal/docs/field_template.go b/internal/docs/field_template.go index 0d4a7dcb01..5574fc6bc7 100644 --- a/internal/docs/field_template.go +++ b/internal/docs/field_template.go @@ -30,33 +30,45 @@ func FieldsTemplate(lintableExamples bool) string { if lintableExamples { exampleHint = "yaml" } - // Use trailing whitespace below to render line breaks in Markdown + // Use trailing whitespace below to render line breaks in Asciidoc return `{{define "field_docs" -}} {{range $i, $field := .Fields -}} -### ` + "`{{$field.FullName}}`" + ` +=== ` + "`{{$field.FullName}}`" + ` {{$field.Spec.Description}} {{if $field.Spec.IsSecret -}} -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: + +[WARNING] +.Secret +==== +This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. +==== + {{end -}} {{if $field.Spec.Interpolated -}} -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). +This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. {{end}} -Type: {{if eq $field.Spec.Kind "array"}}list of {{end}}{{if eq $field.Spec.Kind "map"}}map of {{end}}` + "`{{$field.Spec.Type}}`" + ` -{{if gt (len $field.DefaultMarshalled) 0}}Default: ` + "`{{$field.DefaultMarshalled}}`" + ` +*Type*: {{if eq $field.Spec.Kind "array"}}list of {{end}}{{if eq $field.Spec.Kind "map"}}map of {{end}}` + "`{{$field.Spec.Type}}`" + ` + +{{if gt (len $field.DefaultMarshalled) 0}}*Default*: ` + "`{{$field.DefaultMarshalled}}`" + ` {{end -}} -{{if gt (len $field.Spec.Version) 0}}Requires version {{$field.Spec.Version}} or newer +{{if gt (len $field.Spec.Version) 0}}Requires version {{$field.Spec.Version}} or newer {{end -}} {{if gt (len $field.Spec.AnnotatedOptions) 0}} -| Option | Summary | -|---|---| -{{range $j, $option := $field.Spec.AnnotatedOptions -}}` + "| `" + `{{index $option 0}}` + "` |" + ` {{index $option 1}} | +|=== +| Option | Summary + +{{range $j, $option := $field.Spec.AnnotatedOptions -}} +| ` + "`{{index $option 0}}`" + ` +| {{index $option 1}} {{end}} -{{else if gt (len $field.Spec.Options) 0}}Options: {{range $j, $option := $field.Spec.Options -}} -{{if ne $j 0}}, {{end}}` + "`" + `{{$option}}` + "`" + `{{end}}. +|=== +{{else if gt (len $field.Spec.Options) 0}} +Options: +{{range $j, $option := $field.Spec.Options -}} +{{if ne $j 0}}, {{end}}` + "`{{$option}}`" + ` +{{end}}. {{end}} {{if gt (len $field.Spec.Examples) 0 -}} ` + "```" + exampleHint + ` diff --git a/internal/docs/metrics_mapping.go b/internal/docs/metrics_mapping.go index fb3f64d198..14dd7a15b7 100644 --- a/internal/docs/metrics_mapping.go +++ b/internal/docs/metrics_mapping.go @@ -11,6 +11,6 @@ func MetricsMappingFieldSpec(name string) FieldSpec { "output_sent" ].contains(this) { deleted() }`, } - summary := "An optional [Bloblang mapping](/docs/guides/bloblang/about) that allows you to rename or prevent certain metrics paths from being exported. For more information check out the [metrics documentation](/docs/components/metrics/about#metric-mapping). When metric paths are created, renamed and dropped a trace log is written, enabling TRACE level logging is therefore a good way to diagnose path mappings." + summary := "An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that allows you to rename or prevent certain metrics paths from being exported. For more information check out the xref:components:metrics/about.adoc#metric-mapping[metrics documentation]. When metric paths are created, renamed and dropped a trace log is written, enabling TRACE level logging is therefore a good way to diagnose path mappings." return FieldBloblang(name, summary, examples...).HasDefault("") } diff --git a/internal/filepath/glob.go b/internal/filepath/glob.go index 231670399f..3b0a68ee68 100644 --- a/internal/filepath/glob.go +++ b/internal/filepath/glob.go @@ -58,7 +58,7 @@ func GlobsAndSuperPaths(f fs.FS, paths []string, extensions ...string) ([]string } // hasMeta reports whether path contains any of the magic characters -// recognized by Match. +// recognised by Match. // // Taken from path/filepath/match.go. func hasMeta(path string) bool { diff --git a/internal/httpclient/config.go b/internal/httpclient/config.go index 93a22369b1..422a21b99f 100644 --- a/internal/httpclient/config.go +++ b/internal/httpclient/config.go @@ -68,7 +68,7 @@ func ConfigField(defaultVerb string, forOutput bool, extraChildren ...*service.C Description(extractHeadersDesc). Advanced(), service.NewStringField(hcFieldRateLimit). - Description("An optional [rate limit](/docs/components/rate_limits/about) to throttle requests by."). + Description("An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by."). Optional(), service.NewDurationField(hcFieldTimeout). Description("A static timeout to apply to requests."). diff --git a/internal/impl/amqp09/input.go b/internal/impl/amqp09/input.go index 79c4c5a8f1..2a192f8b01 100644 --- a/internal/impl/amqp09/input.go +++ b/internal/impl/amqp09/input.go @@ -25,7 +25,7 @@ func amqp09InputSpec() *service.ConfigSpec { Description(` TLS is automatic when connecting to an `+"`amqps`"+` URL, but custom settings can be enabled in the `+"`tls`"+` section. -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -50,7 +50,7 @@ This input adds the following metadata fields to each message: - All existing message headers, including nested headers prefixed with the key of their respective parent. `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries).`).Fields( +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolations].`).Fields( service.NewURLListField(urlsField). Description("A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs."). Example([]string{"amqp://guest:guest@127.0.0.1:5672/"}). diff --git a/internal/impl/amqp09/output.go b/internal/impl/amqp09/output.go index 927ccf59fd..e2061defe8 100644 --- a/internal/impl/amqp09/output.go +++ b/internal/impl/amqp09/output.go @@ -27,7 +27,7 @@ It's possible for this output type to create the target exchange by setting `+"` TLS is automatic when connecting to an `+"`amqps`"+` URL, but custom settings can be enabled in the `+"`tls`"+` section. -The fields 'key', 'exchange' and 'type' can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries).`). +The fields 'key', 'exchange' and 'type' can be dynamically set using xref:configuration:interpolation.adoc#bloblang-queries[function interpolations].`). Fields( service.NewURLListField(urlsField). Description("A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs."). diff --git a/internal/impl/amqp1/config.go b/internal/impl/amqp1/config.go index 4ac30805a7..6ff2d13c06 100644 --- a/internal/impl/amqp1/config.go +++ b/internal/impl/amqp1/config.go @@ -30,7 +30,7 @@ const ( metaFilterField = "metadata" ) -// ErrSASLMechanismNotSupported is returned if a SASL mechanism was not recognized. +// ErrSASLMechanismNotSupported is returned if a SASL mechanism was not recognised. type ErrSASLMechanismNotSupported string // Error implements the standard error interface. diff --git a/internal/impl/amqp1/input.go b/internal/impl/amqp1/input.go index 98f859606e..482e330ec8 100644 --- a/internal/impl/amqp1/input.go +++ b/internal/impl/amqp1/input.go @@ -17,7 +17,7 @@ import ( "github.com/benthosdev/benthos/v4/public/service" ) -//go:embed input_description.md +//go:embed input_description.adoc var inputDescription string func amqp1InputSpec() *service.ConfigSpec { @@ -421,7 +421,7 @@ func uuidFromLockTokenBytes(bytes []byte) (*amqp.UUID, error) { // Get lock token from the deliveryTag var lockTokenBytes [16]byte copy(lockTokenBytes[:], bytes[:16]) - // translate from .net guid byte serialisation format to amqp rfc standard + // translate from .net guid byte serialization format to amqp rfc standard swapIndex(0, 3, &lockTokenBytes) swapIndex(1, 2, &lockTokenBytes) swapIndex(4, 5, &lockTokenBytes) diff --git a/internal/impl/amqp1/input_description.md b/internal/impl/amqp1/input_description.adoc similarity index 71% rename from internal/impl/amqp1/input_description.md rename to internal/impl/amqp1/input_description.adoc index 66362ce7b6..bbdf59c17d 100644 --- a/internal/impl/amqp1/input_description.md +++ b/internal/impl/amqp1/input_description.adoc @@ -1,19 +1,19 @@ -### Metadata +== Metadata This input adds the following metadata fields to each message: -``` text +```text - amqp_content_type - amqp_content_encoding - amqp_creation_time - All string typed message annotations ``` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. By setting `read_header` to `true`, additional message header properties will be added to each message: -``` text +```text - amqp_durable - amqp_priority - amqp_ttl @@ -21,7 +21,7 @@ By setting `read_header` to `true`, additional message header properties will be - amqp_delivery_count ``` -## Performance +== Performance -This input benefits from receiving multiple messages in flight in parallel for improved performance. +This input benefits from receiving multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages with the field `credit`. diff --git a/internal/impl/amqp1/output.go b/internal/impl/amqp1/output.go index 98faa63e28..7592df99bc 100644 --- a/internal/impl/amqp1/output.go +++ b/internal/impl/amqp1/output.go @@ -19,11 +19,11 @@ func amqp1OutputSpec() *service.ConfigSpec { Categories("Services"). Summary("Sends messages to an AMQP (1.0) server."). Description(` -### Metadata +== Metadata Message metadata is added to each AMQP message as string annotations. In order to control which metadata keys are added use the `+"`metadata`"+` config field. -## Performance +== Performance This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `+"`max_in_flight`"+`.`). Fields( diff --git a/internal/impl/avro/processor.go b/internal/impl/avro/processor.go index c50178da9f..f5f2435582 100644 --- a/internal/impl/avro/processor.go +++ b/internal/impl/avro/processor.go @@ -19,21 +19,21 @@ func avroConfigSpec() *service.ConfigSpec { Categories("Parsing"). Summary(`Performs Avro based operations on messages based on a schema.`). Description(` -WARNING: If you are consuming or generating messages using a schema registry service then it is likely this processor will fail as those services require messages to be prefixed with the identifier of the schema version being used. Instead, try the ` + "[`schema_registry_encode`](/docs/components/processors/schema_registry_encode) and [`schema_registry_decode`](/docs/components/processors/schema_registry_decode)" + ` processors. +WARNING: If you are consuming or generating messages using a schema registry service then it is likely this processor will fail as those services require messages to be prefixed with the identifier of the schema version being used. Instead, try the ` + "xref:components:processors/schema_registry_encode.adoc[`schema_registry_encode`] and xref:components:processors/schema_registry_decode.adoc[`schema_registry_decode`]" + ` processors. -## Operators +== Operators -### ` + "`to_json`" + ` +=== ` + "`to_json`" + ` Converts Avro documents into a JSON structure. This makes it easier to manipulate the contents of the document within Benthos. The encoding field specifies how the source documents are encoded. -### ` + "`from_json`" + ` +=== ` + "`from_json`" + ` Attempts to convert JSON documents into Avro documents according to the specified encoding.`). - Field(service.NewStringEnumField("operator", "to_json", "from_json").Description("The [operator](#operators) to execute")). + Field(service.NewStringEnumField("operator", "to_json", "from_json").Description("The <> to execute")). Field(service.NewStringEnumField("encoding", "textual", "binary", "single").Description("An Avro encoding format to use for conversions to and from a schema.").Default("textual")). Field(service.NewStringField("schema").Description("A full Avro schema to use.").Default("")). Field(service.NewStringField("schema_path"). diff --git a/internal/impl/avro/scanner.go b/internal/impl/avro/scanner.go index 589675a82a..4200dc7fbe 100644 --- a/internal/impl/avro/scanner.go +++ b/internal/impl/avro/scanner.go @@ -19,9 +19,9 @@ func avroScannerSpec() *service.ConfigSpec { Stable(). Summary("Consume a stream of Avro OCF datum."). Description(` -### Avro JSON Format +== Avro JSON format -This scanner yields documents formatted as [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding) when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +This scanner yields documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is ` + "`null`, then it is encoded as a JSON `null`" + `; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -32,11 +32,11 @@ For example, the union schema ` + "`[\"null\",\"string\",\"Foo\"]`, where `Foo`" - the string ` + "`\"a\"` as `{\"string\": \"a\"}`" + `; and - a ` + "`Foo` instance as `{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. -However, it is possible to instead create documents in [standard/raw JSON format](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) by setting the field ` + "[`avro_raw_json`](#avro_raw_json) to `true`" + `. +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field ` + "<> to `true`" + `. `). Fields( service.NewBoolField(sFieldRawJSON). - Description("Whether messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding). If `true` the schema returned from the subject should be decoded as [standard json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) instead of as [avro json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec). There is a [comment in goavro](https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249), the [underlining library used for avro serialization](https://github.com/linkedin/goavro), that explains in more detail the difference between the standard json and avro json."). + Description("Whether messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json."). Advanced(). Default(false), ) diff --git a/internal/impl/awk/processor.go b/internal/impl/awk/processor.go index b7fbde4d86..8850a979e7 100644 --- a/internal/impl/awk/processor.go +++ b/internal/impl/awk/processor.go @@ -24,23 +24,23 @@ func awkSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Categories("Mapping"). - Summary(`Executes an AWK program on messages. This processor is very powerful as it offers a range of [custom functions](#awk-functions) for querying and mutating message contents and metadata.`). + Summary(`Executes an AWK program on messages. This processor is very powerful as it offers a range of <> for querying and mutating message contents and metadata.`). Description(` -Works by feeding message contents as the program input based on a chosen [codec](#codecs) and replaces the contents of each message with the result. If the result is empty (nothing is printed by the program) then the original message contents remain unchanged. +Works by feeding message contents as the program input based on a chosen <> and replaces the contents of each message with the result. If the result is empty (nothing is printed by the program) then the original message contents remain unchanged. -Comes with a wide range of [custom functions](#awk-functions) for accessing message metadata, json fields, printing logs, etc. These functions can be overridden by functions within the program. +Comes with a wide range of <> for accessing message metadata, json fields, printing logs, etc. These functions can be overridden by functions within the program. -Check out the [examples section](#examples) in order to see how this processor can be used. +Check out the <> in order to see how this processor can be used. -This processor uses [GoAWK][goawk], in order to understand the differences in how the program works you can [read more about it here][goawk.differences].`). +This processor uses https://github.com/benhoyt/goawk[GoAWK], in order to understand the differences in how the program works you can read more about it in https://github.com/benhoyt/goawk#differences-from-awk[goawk.differences].`). Footnotes(` -## Codecs +== Codecs The chosen codec determines how the contents of the message are fed into the -program. Codecs only impact the input string and variables initialised for your +program. Codecs only impact the input string and variables initialized for your program, they do not change the range of custom functions available. -### `+"`none`"+` +=== `+"`none`"+` An empty string is fed into the program. Functions can still be used in order to extract and mutate metadata and message contents. @@ -49,23 +49,23 @@ This is useful for when your program only uses functions and doesn't need the full text of the message to be parsed by the program, as it is significantly faster. -### `+"`text`"+` +=== `+"`text`"+` The full contents of the message are fed into the program as a string, allowing -you to reference tokenised segments of the message with variables ($0, $1, etc). +you to reference tokenized segments of the message with variables ($0, $1, etc). Custom functions can still be used with this codec. This is the default codec as it behaves most similar to typical usage of the awk command line tool. -### `+"`json`"+` +=== `+"`json`"+` An empty string is fed into the program, and variables are automatically -initialised before execution of your program by walking the flattened JSON +initialized before execution of your program by walking the flattened JSON structure. Each value is converted into a variable by taking its full path, e.g. the object: -`+"``` json"+` +`+"```json"+` { "foo": { "bar": { @@ -85,21 +85,21 @@ foo_created_at = "2018-12-18T11:57:32" Custom functions can also still be used with this codec. -## AWK Functions +== AWK functions -`+"### `json_get`"+` +`+"=== `json_get`"+` Signature: `+"`json_get(path)`"+` Attempts to find a JSON value in the input message payload by a -[dot separated path](/docs/configuration/field_paths) and returns it as a string. +xref:configuration:field_paths.adoc[dot separated path] and returns it as a string. -`+"### `json_set`"+` +`+"=== `json_set`"+` Signature: `+"`json_set(path, value)`"+` Attempts to set a JSON value in the input message payload identified by a -[dot separated path](/docs/configuration/field_paths), the value argument will be interpreted +xref:configuration:field_paths.adoc[dot separated path], the value argument will be interpreted as a string. In order to set non-string values use one of the following typed varieties: @@ -108,12 +108,12 @@ In order to set non-string values use one of the following typed varieties: `+"- `json_set_float(path, value)`"+` `+"- `json_set_bool(path, value)`"+` -`+"### `json_append`"+` +`+"=== `json_append`"+` Signature: `+"`json_append(path, value)`"+` Attempts to append a value to an array identified by a -[dot separated path](/docs/configuration/field_paths). If the target does not +xref:configuration:field_paths.adoc[dot separated path]. If the target does not exist it will be created. If the target exists but is not already an array then it will be converted into one, with its original contents set to the first element of the array. @@ -125,34 +125,34 @@ non-string values use one of the following typed varieties: `+"- `json_append_float(path, value)`"+` `+"- `json_append_bool(path, value)`"+` -`+"### `json_delete`"+` +`+"=== `json_delete`"+` Signature: `+"`json_delete(path)`"+` Attempts to delete a JSON field from the input message payload identified by a -[dot separated path](/docs/configuration/field_paths). +xref:configuration:field_paths.adoc[dot separated path]. -`+"### `json_length`"+` +`+"=== `json_length`"+` Signature: `+"`json_length(path)`"+` Returns the size of the string or array value of JSON field from the input -message payload identified by a [dot separated path](/docs/configuration/field_paths). +message payload identified by a xref:configuration:field_paths.adoc[dot separated path]. If the target field does not exist, or is not a string or array type, then zero is returned. In order to explicitly check the type of a field use `+"`json_type`"+`. -`+"### `json_type`"+` +`+"=== `json_type`"+` Signature: `+"`json_type(path)`"+` Returns the type of a JSON field from the input message payload identified by a -[dot separated path](/docs/configuration/field_paths). +xref:configuration:field_paths.adoc[dot separated path]. Possible values are: "string", "int", "float", "bool", "undefined", "null", "array", "object". -`+"### `create_json_object`"+` +`+"=== `create_json_object`"+` Signature: `+"`create_json_object(key1, val1, key2, val2, ...)`"+` @@ -164,9 +164,9 @@ resolve to a string regardless of the value type. E.g. the following call: Would result in this string: -`+"`{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\"}`"+` +`+"`\\{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\"}`"+` -`+"### `create_json_array`"+` +`+"=== `create_json_array`"+` Signature: `+"`create_json_array(val1, val2, ...)`"+` @@ -180,33 +180,33 @@ Would result in this string: `+"`[\"1\",\"2\",\"3\"]`"+` -`+"### `metadata_set`"+` +`+"=== `metadata_set`"+` Signature: `+"`metadata_set(key, value)`"+` Set a metadata key for the message to a value. The value will always resolve to a string regardless of the value type. -`+"### `metadata_get`"+` +`+"=== `metadata_get`"+` Signature: `+"`metadata_get(key) string`"+` Get the value of a metadata key from the message. -`+"### `timestamp_unix`"+` +`+"=== `timestamp_unix`"+` Signature: `+"`timestamp_unix() int`"+` Returns the current unix timestamp (the number of seconds since 01-01-1970). -`+"### `timestamp_unix`"+` +`+"=== `timestamp_unix`"+` Signature: `+"`timestamp_unix(date) int`"+` Attempts to parse a date string by detecting its format and returns the equivalent unix timestamp (the number of seconds since 01-01-1970). -`+"### `timestamp_unix`"+` +`+"=== `timestamp_unix`"+` Signature: `+"`timestamp_unix(date, format) int`"+` @@ -216,14 +216,14 @@ unix timestamp (the number of seconds since 01-01-1970). The format is defined by showing how the reference time, defined to be `+"`Mon Jan 2 15:04:05 -0700 MST 2006`"+` would be displayed if it were the value. -`+"### `timestamp_unix_nano`"+` +`+"=== `timestamp_unix_nano`"+` Signature: `+"`timestamp_unix_nano() int`"+` Returns the current unix timestamp in nanoseconds (the number of nanoseconds since 01-01-1970). -`+"### `timestamp_unix_nano`"+` +`+"=== `timestamp_unix_nano`"+` Signature: `+"`timestamp_unix_nano(date) int`"+` @@ -231,7 +231,7 @@ Attempts to parse a date string by detecting its format and returns the equivalent unix timestamp in nanoseconds (the number of nanoseconds since 01-01-1970). -`+"### `timestamp_unix_nano`"+` +`+"=== `timestamp_unix_nano`"+` Signature: `+"`timestamp_unix_nano(date, format) int`"+` @@ -241,7 +241,7 @@ unix timestamp in nanoseconds (the number of nanoseconds since 01-01-1970). The format is defined by showing how the reference time, defined to be `+"`Mon Jan 2 15:04:05 -0700 MST 2006`"+` would be displayed if it were the value. -`+"### `timestamp_format`"+` +`+"=== `timestamp_format`"+` Signature: `+"`timestamp_format(unix, format) string`"+` @@ -252,7 +252,7 @@ were the value. The format is optional, and if omitted RFC3339 (`+"`2006-01-02T15:04:05Z07:00`"+`) will be used. -`+"### `timestamp_format_nano`"+` +`+"=== `timestamp_format_nano`"+` Signature: `+"`timestamp_format_nano(unixNano, format) string`"+` @@ -263,31 +263,29 @@ displayed if it were the value. The format is optional, and if omitted RFC3339 (`+"`2006-01-02T15:04:05Z07:00`"+`) will be used. -`+"### `print_log`"+` +`+"=== `print_log`"+` Signature: `+"`print_log(message, level)`"+` Prints a Benthos log message at a particular log level. The log level is optional, and if omitted the level `+"`INFO`"+` will be used. -`+"### `base64_encode`"+` +`+"=== `base64_encode`"+` Signature: `+"`base64_encode(data)`"+` Encodes the input data to a base64 string. -`+"### `base64_decode`"+` +`+"=== `base64_decode`"+` Signature: `+"`base64_decode(data)`"+` Attempts to base64-decode the input data and returns the decoded string if successful. It will emit an error otherwise. -[goawk]: https://github.com/benhoyt/goawk -[goawk.differences]: https://github.com/benhoyt/goawk#differences-from-awk `). Field(service.NewStringEnumField("codec", "none", "text", "json"). - Description("A [codec](#codecs) defines how messages should be inserted into the AWK program as variables. The codec does not change which [custom Benthos functions](#awk-functions) are available. The `text` codec is the closest to a typical AWK use case.")). + Description("A <> defines how messages should be inserted into the AWK program as variables. The codec does not change which <> are available. The `text` codec is the closest to a typical AWK use case.")). Field(service.NewStringField("program"). Description("An AWK program to execute")). Example("JSON Mapping and Arithmetic", ` diff --git a/internal/impl/aws/config/config.go b/internal/impl/aws/config/config.go index 9f63a668ca..c07c1ad48f 100644 --- a/internal/impl/aws/config/config.go +++ b/internal/impl/aws/config/config.go @@ -29,7 +29,7 @@ func SessionFields() []*service.ConfigField { Description("The token for the credentials being used, required when using short term credentials."). Default("").Advanced(), service.NewBoolField("from_ec2_role"). - Description("Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html)."). + Description("Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]."). Default(false).Version("4.2.0"), service.NewStringField("role"). Description("A role ARN to assume."). @@ -38,6 +38,6 @@ func SessionFields() []*service.ConfigField { Description("An external ID to provide when assuming a role."). Default("").Advanced()). Advanced(). - Description("Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws)."), + Description("Optional manual configuration of AWS credentials to use. More information can be found in xref:guides:cloud/aws.adoc[]."), } } diff --git a/internal/impl/aws/input_kinesis.go b/internal/impl/aws/input_kinesis.go index 805262b2ce..8e7613b3eb 100644 --- a/internal/impl/aws/input_kinesis.go +++ b/internal/impl/aws/input_kinesis.go @@ -82,21 +82,21 @@ func kinesisInputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary("Receive messages from one or more Kinesis streams."). Description(` -Consumes messages from one or more Kinesis streams either by automatically balancing shards across other instances of this input, or by consuming shards listed explicitly. The latest message sequence consumed by this input is stored within a [DynamoDB table](#table-schema), which allows it to resume at the correct sequence of the shard during restarts. This table is also used for coordination across distributed inputs when shard balancing. +Consumes messages from one or more Kinesis streams either by automatically balancing shards across other instances of this input, or by consuming shards listed explicitly. The latest message sequence consumed by this input is stored within a <>, which allows it to resume at the correct sequence of the shard during restarts. This table is also used for coordination across distributed inputs when shard balancing. Benthos will not store a consumed sequence unless it is acknowledged at the output level, which ensures at-least-once delivery guarantees. -### Ordering +== Ordering By default messages of a shard can be processed in parallel, up to a limit determined by the field `+"`checkpoint_limit`"+`. However, if strict ordered processing is required then this value must be set to 1 in order to process shard messages in lock-step. When doing so it is recommended that you perform batching at this component for performance as it will not be possible to batch lock-stepped messages at the output level. -### Table Schema +== Table schema It's possible to configure Benthos to create the DynamoDB table required for coordination if it does not already exist. However, if you wish to create this yourself (recommended) then create a table with a string HASH key `+"`StreamID`"+` and a string RANGE key `+"`ShardID`"+`. -### Batching +== Batching -Use the `+"`batching`"+` fields to configure an optional [batching policy](/docs/configuration/batching#batch-policy). Each stream shard will be batched separately in order to ensure that acknowledgements aren't contaminated. +Use the `+"`batching`"+` fields to configure an optional xref:configuration:batching.adoc#batch-policy[batching policy]. Each stream shard will be batched separately in order to ensure that acknowledgements aren't contaminated. `).Fields( service.NewStringListField(kiFieldStreams). Description("One or more Kinesis data streams to consume from. Streams can either be specified by their name or full ARN. Shards of a stream are automatically balanced across consumers by coordinating through the provided DynamoDB table. Multiple comma separated streams can be listed in a single element. Shards are automatically distributed across consumers of a stream by coordinating through the provided DynamoDB table. Alternatively, it's possible to specify an explicit shard to consume from with a colon after the stream name, e.g. `foo:0` would consume the shard `0` of the stream `foo`."). diff --git a/internal/impl/aws/input_s3.go b/internal/impl/aws/input_s3.go index ee0826ab4d..4b919789ee 100644 --- a/internal/impl/aws/input_s3.go +++ b/internal/impl/aws/input_s3.go @@ -119,25 +119,25 @@ func s3InputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Downloads objects within an Amazon S3 bucket, optionally filtered by a prefix, either by walking the items in the bucket or by streaming upload notifications in realtime.`). Description(` -## Streaming Objects on Upload with SQS +== Stream objects on upload with SQS A common pattern for consuming S3 objects is to emit upload notification events from the bucket either directly to an SQS queue, or to an SNS topic that is consumed by an SQS queue, and then have your consumer listen for events which prompt it to download the newly uploaded objects. More information about this pattern and how to set it up can be found at: https://docs.aws.amazon.com/AmazonS3/latest/dev/ways-to-add-notification-config-to-bucket.html. -Benthos is able to follow this pattern when you configure an `+"`sqs.url`"+`, where it consumes events from SQS and only downloads object keys received within those events. In order for this to work Benthos needs to know where within the event the key and bucket names can be found, specified as [dot paths](/docs/configuration/field_paths) with the fields `+"`sqs.key_path` and `sqs.bucket_path`"+`. The default values for these fields should already be correct when following the guide above. +Benthos is able to follow this pattern when you configure an `+"`sqs.url`"+`, where it consumes events from SQS and only downloads object keys received within those events. In order for this to work Benthos needs to know where within the event the key and bucket names can be found, specified as xref:configuration:field_paths.adoc[dot paths] with the fields `+"`sqs.key_path` and `sqs.bucket_path`"+`. The default values for these fields should already be correct when following the guide above. If your notification events are being routed to SQS via an SNS topic then the events will be enveloped by SNS, in which case you also need to specify the field `+"`sqs.envelope_path`"+`, which in the case of SNS to SQS will usually be `+"`Message`"+`. When using SQS please make sure you have sensible values for `+"`sqs.max_messages`"+` and also the visibility timeout of the queue itself. When Benthos consumes an S3 object the SQS message that triggered it is not deleted until the S3 object has been sent onwards. This ensures at-least-once crash resiliency, but also means that if the S3 object takes longer to process than the visibility timeout of your queue then the same objects might be processed multiple times. -## Downloading Large Files +== Download large files -When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a `+"[`codec`](#codec)"+` can be specified that determines how to break the input into smaller individual messages. +When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a `+"<>"+` can be specified that determines how to break the input into smaller individual messages. -## Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. -## Metadata +== Metadata This input adds the following metadata fields to each message: @@ -152,7 +152,7 @@ This input adds the following metadata fields to each message: - All user defined metadata `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). Note that user defined metadata is case insensitive within AWS, and it is likely that the keys will be received in a capitalized form, if you wish to make them consistent you can map all metadata keys to lower or uppercase using a Bloblang mapping such as `+"`meta = meta().map_each_key(key -> key.lowercase())`"+`.`). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. Note that user defined metadata is case insensitive within AWS, and it is likely that the keys will be received in a capitalized form, if you wish to make them consistent you can map all metadata keys to lower or uppercase using a Bloblang mapping such as `+"`meta = meta().map_each_key(key -> key.lowercase())`"+`.`). Fields( service.NewStringField(s3iFieldBucket). Description("The bucket to consume from. If the field `sqs.url` is specified this field is optional."). @@ -183,13 +183,13 @@ You can access these metadata fields using [function interpolation](/docs/config Default(""). Advanced(), service.NewStringField(s3iSQSFieldKeyPath). - Description("A [dot path](/docs/configuration/field_paths) whereby object keys are found in SQS messages."). + Description("A xref:configuration:field_paths.adoc[dot path] whereby object keys are found in SQS messages."). Default("Records.*.s3.object.key"), service.NewStringField(s3iSQSFieldBucketPath). - Description("A [dot path](/docs/configuration/field_paths) whereby the bucket name can be found in SQS messages."). + Description("A xref:configuration:field_paths.adoc[dot path] whereby the bucket name can be found in SQS messages."). Default("Records.*.s3.bucket.name"), service.NewStringField(s3iSQSFieldEnvelopePath). - Description("A [dot path](/docs/configuration/field_paths) of a field to extract an enveloped JSON payload for further extracting the key and bucket from SQS messages. This is specifically useful when subscribing an SQS queue to an SNS topic that receives bucket events."). + Description("A xref:configuration:field_paths.adoc[dot path] of a field to extract an enveloped JSON payload for further extracting the key and bucket from SQS messages. This is specifically useful when subscribing an SQS queue to an SNS topic that receives bucket events."). Default(""). Example("Message"), service.NewStringField(s3iSQSFieldDelayPeriod). diff --git a/internal/impl/aws/input_sqs.go b/internal/impl/aws/input_sqs.go index 8ad2f5d0c6..910c2a7c5c 100644 --- a/internal/impl/aws/input_sqs.go +++ b/internal/impl/aws/input_sqs.go @@ -61,14 +61,14 @@ func sqsInputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Consume messages from an AWS SQS URL.`). Description(` -### Credentials +== Credentials By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, -allowing you to transfer data across accounts. You can find out more -[in this document](/docs/guides/cloud/aws). +allowing you to transfer data across accounts. You can find out more in +xref:guides:cloud/aws.adoc[]. -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -80,7 +80,7 @@ This input adds the following metadata fields to each message: `+"```"+` You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields( service.NewURLField(sqsiFieldURL). Description("The SQS URL to consume from."), diff --git a/internal/impl/aws/metrics_cloudwatch.go b/internal/impl/aws/metrics_cloudwatch.go index a0ff20ea1d..0e9b518293 100644 --- a/internal/impl/aws/metrics_cloudwatch.go +++ b/internal/impl/aws/metrics_cloudwatch.go @@ -42,11 +42,11 @@ func cwMetricsSpec() *service.ConfigSpec { Version("3.36.0"). Summary(`Send metrics to AWS CloudWatch using the PutMetricData endpoint.`). Description(` -### Timing Metrics +== Timing metrics The smallest timing unit that CloudWatch supports is microseconds, therefore timing metrics are automatically downgraded to microseconds (by dividing delta values by 1000). This conversion will also apply to custom timing metrics produced with a `+"`metric`"+` processor. -### Billing +== Billing AWS bills per metric series exported, it is therefore STRONGLY recommended that you reduce the metrics that are exposed with a `+"`mapping`"+` like this: diff --git a/internal/impl/aws/output_dynamodb.go b/internal/impl/aws/output_dynamodb.go index 5c5474bce9..d2525643f9 100644 --- a/internal/impl/aws/output_dynamodb.go +++ b/internal/impl/aws/output_dynamodb.go @@ -74,7 +74,7 @@ func ddboOutputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Inserts items into a DynamoDB table.`). Description(` -The field `+"`string_columns`"+` is a map of column names to string values, where the values are [function interpolated](/docs/configuration/interpolation#bloblang-queries) per message of a batch. This allows you to populate string columns of an item by extracting fields within the document payload or metadata like follows: +The field `+"`string_columns`"+` is a map of column names to string values, where the values are xref:configuration:interpolation.adoc#bloblang-queries[function interpolated] per message of a batch. This allows you to populate string columns of an item by extracting fields within the document payload or metadata like follows: `+"```yml"+` string_columns: @@ -84,7 +84,7 @@ string_columns: full_content: ${!content()} `+"```"+` -The field `+"`json_map_columns`"+` is a map of column names to json paths, where the [dot path](/docs/configuration/field_paths) is extracted from each document and converted into a map value. Both an empty path and the path `+"`.`"+` are interpreted as the root of the document. This allows you to populate map columns of an item like follows: +The field `+"`json_map_columns`"+` is a map of column names to json paths, where the xref:configuration:field_paths.adoc[dot path] is extracted from each document and converted into a map value. Both an empty path and the path `+"`.`"+` are interpreted as the root of the document. This allows you to populate map columns of an item like follows: `+"```yml"+` json_map_columns: @@ -101,15 +101,15 @@ json_map_columns: In which case the top level document fields will be written at the root of the item, potentially overwriting previously defined column values. If a path is not found within a document the column will not be populated. -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. -## Performance +== Performance This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `+"`max_in_flight`"+`. -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. `). Fields( service.NewStringField(ddboFieldTable). @@ -124,7 +124,7 @@ This output benefits from sending messages as a batch for improved performance. "full_content": "${!content()}", }), service.NewStringMapField(ddboFieldJSONMapColumns). - Description("A map of column keys to [field paths](/docs/configuration/field_paths) pointing to value data within messages."). + Description("A map of column keys to xref:configuration:field_paths.adoc[field paths] pointing to value data within messages."). Default(map[string]any{}). Example(map[string]any{ "user": "path.to.user", diff --git a/internal/impl/aws/output_kinesis.go b/internal/impl/aws/output_kinesis.go index ec7c173cdd..f2e0c6edf1 100644 --- a/internal/impl/aws/output_kinesis.go +++ b/internal/impl/aws/output_kinesis.go @@ -61,11 +61,11 @@ func koOutputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Sends messages to a Kinesis stream.`). Description(` -Both the `+"`partition_key`"+`(required) and `+"`hash_key`"+` (optional) fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages the interpolations are performed per message part. +Both the `+"`partition_key`"+`(required) and `+"`hash_key`"+` (optional) fields can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages the interpolations are performed per message part. -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws).`+service.OutputPerformanceDocs(true, true)). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[].`+service.OutputPerformanceDocs(true, true)). Fields( service.NewStringField(koFieldStream). Description("The stream to publish messages to. Streams can either be specified by their name or full ARN."). diff --git a/internal/impl/aws/output_kinesis_firehose.go b/internal/impl/aws/output_kinesis_firehose.go index 9e131ff96b..0c4fe15404 100644 --- a/internal/impl/aws/output_kinesis_firehose.go +++ b/internal/impl/aws/output_kinesis_firehose.go @@ -48,15 +48,15 @@ func kfoOutputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Sends messages to a Kinesis Firehose delivery stream.`). Description(` -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. -## Performance +== Performance This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `+"`max_in_flight`"+`. -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc]. `). Fields( service.NewStringField(kfoFieldStream). diff --git a/internal/impl/aws/output_s3.go b/internal/impl/aws/output_s3.go index 190bd3e404..64f85bafd6 100644 --- a/internal/impl/aws/output_s3.go +++ b/internal/impl/aws/output_s3.go @@ -137,15 +137,15 @@ func s3oOutputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Sends message parts as objects to an Amazon S3 bucket. Each object is uploaded with the path specified with the `+"`path`"+` field.`). Description(` -In order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. +In order to have a different path for each object you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries], which are calculated per message of a batch. -### Metadata +== Metadata -Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the [metadata docs](/docs/configuration/metadata). +Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the xref:configuration:metadata.adoc[metadata docs]. -### Tags +== Tags -The tags field allows you to specify key/value pairs to attach to objects as tags, where the values support [interpolation functions](/docs/configuration/interpolation#bloblang-queries): +The tags field allows you to specify key/value pairs to attach to objects as tags, where the values support xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]: `+"```yaml"+` output: @@ -157,13 +157,13 @@ output: Timestamp: ${!meta("Timestamp")} `+"```"+` -### Credentials +=== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[]. -### Batching +== Batching -It's common to want to upload messages to S3 as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an `+"[`archive`](/docs/components/processors/archive)"+` and/or `+"[`compress`](/docs/components/processors/compress)"+` processor. +It's common to want to upload messages to S3 as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an `+"xref:components:processors/archive.adoc[`archive`]"+` and/or `+"xref:components:processors/compress.adoc[`compress`]"+` processor. For example, if we wished to upload messages as a .tar.gz archive of documents we could achieve that with the following config: diff --git a/internal/impl/aws/output_sns.go b/internal/impl/aws/output_sns.go index fb330ba0b6..85a3e93fc3 100644 --- a/internal/impl/aws/output_sns.go +++ b/internal/impl/aws/output_sns.go @@ -69,9 +69,9 @@ func snsoOutputSpec() *service.ConfigSpec { Categories("Services", "AWS"). Summary(`Sends messages to an AWS SNS topic.`). Description(` -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws).`+service.OutputPerformanceDocs(true, false)). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[].`+service.OutputPerformanceDocs(true, false)). Fields( service.NewStringField(snsoFieldTopicARN). Description("The topic to publish to."), diff --git a/internal/impl/aws/output_sqs.go b/internal/impl/aws/output_sqs.go index 2e2ac4ca1a..46d72d7d3d 100644 --- a/internal/impl/aws/output_sqs.go +++ b/internal/impl/aws/output_sqs.go @@ -85,11 +85,11 @@ func sqsoOutputSpec() *service.ConfigSpec { Description(` Metadata values are sent along with the payload as attributes with the data type String. If the number of metadata values in a message exceeds the message attribute limit (10) then the top ten keys ordered alphabetically will be selected. -The fields `+"`message_group_id`, `message_deduplication_id` and `delay_seconds`"+` can be set dynamically using [function interpolations](/docs/configuration/interpolation#bloblang-queries), which are resolved individually for each message of a batch. +The fields `+"`message_group_id`, `message_deduplication_id` and `delay_seconds`"+` can be set dynamically using xref:configuration:interpolation.adoc#bloblang-queries[function interpolations], which are resolved individually for each message of a batch. -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws).`+service.OutputPerformanceDocs(true, true)). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[].`+service.OutputPerformanceDocs(true, true)). Fields( service.NewStringField(sqsoFieldURL).Description("The URL of the target SQS queue."), service.NewInterpolatedStringField(sqsoFieldMessageGroupID). diff --git a/internal/impl/aws/processor_dynamodb_partiql.go b/internal/impl/aws/processor_dynamodb_partiql.go index e9ba4360b2..be17e597e1 100644 --- a/internal/impl/aws/processor_dynamodb_partiql.go +++ b/internal/impl/aws/processor_dynamodb_partiql.go @@ -22,7 +22,7 @@ func init() { Field(service.NewStringField("query").Description("A PartiQL query to execute for each message.")). Field(service.NewBoolField("unsafe_dynamic_query").Description("Whether to enable dynamic queries that support interpolation functions.").Advanced().Default(false)). Field(service.NewBloblangField("args_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) that, for each message, creates a list of arguments to use with the query.").Default("")). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that, for each message, creates a list of arguments to use with the query.").Default("")). Example( "Insert", `The following example inserts rows into the table footable with the columns foo, bar and baz populated with values extracted from messages:`, diff --git a/internal/impl/aws/processor_lambda.go b/internal/impl/aws/processor_lambda.go index b4b16cc3b3..d1e75b5560 100644 --- a/internal/impl/aws/processor_lambda.go +++ b/internal/impl/aws/processor_lambda.go @@ -18,15 +18,15 @@ func init() { conf := service.NewConfigSpec(). Stable(). Summary("Invokes an AWS lambda for each message. The contents of the message is the payload of the request, and the result of the invocation will become the new contents of the message."). - Description(`The `+"`rate_limit`"+` field can be used to specify a rate limit [resource](/docs/components/rate_limits/about) to cap the rate of requests across parallel components service wide. + Description(`The `+"`rate_limit`"+` field can be used to specify a rate limit xref:components:rate_limits/about.adoc[resource] to cap the rate of requests across parallel components service wide. -In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the `+"[`branch` processor](/docs/components/processors/branch)"+`. +In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the `+"xref:components:processors/branch.adoc[`branch` processor]"+`. -### Error Handling +== Error handling -When Benthos is unable to connect to the AWS endpoint or is otherwise unable to invoke the target lambda function it will retry the request according to the configured number of retries. Once these attempts have been exhausted the failed message will continue through the pipeline with it's contents unchanged, but flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). +When Benthos is unable to connect to the AWS endpoint or is otherwise unable to invoke the target lambda function it will retry the request according to the configured number of retries. Once these attempts have been exhausted the failed message will continue through the pipeline with it's contents unchanged, but flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. -However, if the invocation of the function is successful but the function itself throws an error, then the message will have it's contents updated with a JSON payload describing the reason for the failure, and a metadata field `+"`lambda_function_error`"+` will be added to the message allowing you to detect and handle function errors with a `+"[`branch`](/docs/components/processors/branch)"+`: +However, if the invocation of the function is successful but the function itself throws an error, then the message will have it's contents updated with a JSON payload describing the reason for the failure, and a metadata field `+"`lambda_function_error`"+` will be added to the message allowing you to detect and handle function errors with a `+"xref:components:processors/branch.adoc[`branch`]"+`: `+"```yaml"+` pipeline: @@ -52,15 +52,15 @@ output: resource: somewhere_else `+"```"+` -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws).`). +By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more in xref:guides:cloud/aws.adoc[].`). Categories("Integration"). Version("3.36.0"). Example( "Branched Invoke", ` -This example uses a `+"[`branch` processor](/docs/components/processors/branch/)"+` to map a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged.`, +This example uses a `+"xref:components:processors/branch.adoc[`branch` processor]"+` to map a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged.`, ` pipeline: processors: @@ -77,7 +77,7 @@ pipeline: Field(service.NewStringField("function"). Description("The function to invoke.")). Field(service.NewStringField("rate_limit"). - Description("An optional [`rate_limit`](/docs/components/rate_limits/about) to throttle invocations by."). + Description("An optional xref:components:rate_limits/about.adoc[`rate_limit`] to throttle invocations by."). Default(""). Advanced()) diff --git a/internal/impl/azure/cosmosdb/docs.go b/internal/impl/azure/cosmosdb/docs.go index 64732c4ad8..1444bb85fd 100644 --- a/internal/impl/azure/cosmosdb/docs.go +++ b/internal/impl/azure/cosmosdb/docs.go @@ -74,19 +74,19 @@ type CRUDConfig struct { // CredentialsDocs credentials docs var CredentialsDocs = ` -## Credentials +== Credentials You can use one of the following authentication mechanisms: - Set the ` + "`endpoint`" + ` field and the ` + "`account_key`" + ` field -- Set only the ` + "`endpoint`" + ` field to use [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) +- Set only the ` + "`endpoint`" + ` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] - Set the ` + "`connection_string`" + ` field ` // MetadataDocs metadata docs var MetadataDocs = ` -## Metadata +== Metadata This component adds the following metadata fields to each message: ` + "```" + ` @@ -94,33 +94,33 @@ This component adds the following metadata fields to each message: - request_charge ` + "```" + ` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. ` // BatchingDocs batching docs var BatchingDocs = ` -## Batching +== Batching -CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (details [here](https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits)). +CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here]). ` // EmulatorDocs emulator docs var EmulatorDocs = ` -## CosmosDB Emulator +== CosmosDB emulator -If you wish to run the CosmosDB emulator that is referenced in the documentation [here](https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator), the following Docker command should do the trick: +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: -` + "```shell" + ` +` + "```bash" + ` > docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator ` + "```" + ` Note: ` + "`AZURE_COSMOS_EMULATOR_PARTITION_COUNT`" + ` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. -Additionally, instead of installing the container self-signed certificate which is exposed via ` + "`https://localhost:8081/_explorer/emulator.pem`" + `, you can run [mitmproxy](https://mitmproxy.org/) like so: +Additionally, instead of installing the container self-signed certificate which is exposed via ` + "`https://localhost:8081/_explorer/emulator.pem`" + `, you can run https://mitmproxy.org/[mitmproxy] like so: -` + "```shell" + ` +` + "```bash" + ` > mitmproxy -k --mode "reverse:https://localhost:8081" ` + "```" + ` @@ -182,7 +182,7 @@ func PartitionKeysField(isInputField bool) *service.ConfigField { // TODO: Add examples for hierarchical / empty Partition Keys this when the following issues are addressed: // - https://github.com/Azure/azure-sdk-for-go/issues/18578 // - https://github.com/Azure/azure-sdk-for-go/issues/21063 - field := service.NewBloblangField(FieldPartitionKeys).Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided.").Example(`root = "blobfish"`).Example(`root = 41`).Example(`root = true`).Example(`root = null`) + field := service.NewBloblangField(FieldPartitionKeys).Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided.").Example(`root = "blobfish"`).Example(`root = 41`).Example(`root = true`).Example(`root = null`) // Add dynamic examples if !isInputField { @@ -215,7 +215,7 @@ func CRUDFields(hasReadOperation bool) []*service.ConfigField { string(patchOperationSet): "Set patch operation.", }).Description("Operation.").Default(string(patchOperationAdd)), service.NewStringField(fieldPatchPath).Description("Path.").Example("/foo/bar/baz"), - service.NewBloblangField(fieldPatchValue).Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a value of any type that is supported by CosmosDB.").Example(`root = "blobfish"`).Example(`root = 41`).Example(`root = true`).Example(`root = json("blobfish").depth`).Example(`root = [1, 2, 3]`).Optional(), + service.NewBloblangField(fieldPatchValue).Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to a value of any type that is supported by CosmosDB.").Example(`root = "blobfish"`).Example(`root = 41`).Example(`root = true`).Example(`root = json("blobfish").depth`).Example(`root = [1, 2, 3]`).Optional(), }...).Description("Patch operations to be performed when `" + fieldOperation + ": " + string(OperationPatch) + "` .").Optional().Advanced(), service.NewInterpolatedStringField(fieldPatchCondition).Description("Patch operation condition.").Optional().Advanced().Example(`from c where not is_defined(c.blobfish)`), service.NewBoolField(fieldAutoID).Description("Automatically set the item `id` field to a random UUID v4. If the `id` field is already set, then it will not be overwritten. Setting this to `false` can improve performance, since the messages will not have to be parsed.").Default(true).Advanced(), diff --git a/internal/impl/azure/input_blob_storage.go b/internal/impl/azure/input_blob_storage.go index 3b7103ed57..5b0f28be20 100644 --- a/internal/impl/azure/input_blob_storage.go +++ b/internal/impl/azure/input_blob_storage.go @@ -75,22 +75,22 @@ Supports multiple authentication methods but only one of the following is requir - `+"`storage_connection_string`"+` - `+"`storage_account` and `storage_access_key`"+` - `+"`storage_account` and `storage_sas_token`"+` -- `+"`storage_account` to access via [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential)"+` +- `+"`storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential]"+` If multiple are set then the `+"`storage_connection_string`"+` is given priority. If the `+"`storage_connection_string`"+` does not contain the `+"`AccountName`"+` parameter, please specify it in the `+"`storage_account`"+` field. -## Downloading Large Files +== Download large files -When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a `+"[`scanner`](#scanner)"+` can be specified that determines how to break the input into smaller individual messages. +When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a `+"<>"+` can be specified that determines how to break the input into smaller individual messages. -## Streaming New Files +== Stream new files -By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as [an Event Grid source](https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage) and then use this as a `+"[`targets_input`](#targetsinput)"+`, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. +By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage[an Event Grid source] and then use this as a `+"<>"+`, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. -## Metadata +== Metadata This input adds the following metadata fields to each message: @@ -104,7 +104,7 @@ This input adds the following metadata fields to each message: - All user defined metadata `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields( service.NewStringField(bsiFieldContainer). Description("The name of the container from which to download blobs."), @@ -119,7 +119,7 @@ You can access these metadata fields using [function interpolation](/docs/config Advanced(). Default(false), service.NewInputField(bsiFieldTargetsInput). - Description("EXPERIMENTAL: An optional source of download targets, configured as a [regular Benthos input](/docs/components/inputs/about). Each message yielded by this input should be a single structured object containing a field `name`, which represents the blob to be downloaded."). + Description("EXPERIMENTAL: An optional source of download targets, configured as a xref:components:inputs/about.adoc[regular Benthos input]. Each message yielded by this input should be a single structured object containing a field `name`, which represents the blob to be downloaded."). Optional(). Version("4.27.0"). Example(map[string]any{ diff --git a/internal/impl/azure/input_cosmosdb.go b/internal/impl/azure/input_cosmosdb.go index 4a40dd1444..ae9a1309fb 100644 --- a/internal/impl/azure/input_cosmosdb.go +++ b/internal/impl/azure/input_cosmosdb.go @@ -26,18 +26,18 @@ func cosmosDBInputSpec() *service.ConfigSpec { // Beta(). Categories("Azure"). Version("v4.25.0"). - Summary(`Executes a SQL query against [Azure CosmosDB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) and creates a batch of messages from each page of items.`). + Summary(`Executes a SQL query against https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB] and creates a batch of messages from each page of items.`). Description(` -## Cross-partition Queries +== Cross-partition queries -Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey value(s) must be known in advance and specified in the config. See details [here](https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989). +Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey values must be known in advance and specified in the config. https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989[See details]. `+cosmosdb.CredentialsDocs+cosmosdb.MetadataDocs). Footnotes(cosmosdb.EmulatorDocs). Fields(cosmosdb.ContainerClientConfigFields()...). Field(cosmosdb.PartitionKeysField(true)). Field(service.NewStringField(cdbiFieldQuery).Description("The query to execute").Example(`SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp`)). Field(service.NewBloblangField(cdbiFieldArgsMapping). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) that, for each message, creates a list of arguments to use with the query.").Optional().Example(`root = [ + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that, for each message, creates a list of arguments to use with the query.").Optional().Example(`root = [ { "Name": "@name", "Value": "benthos" }, ]`)). Field(service.NewIntField(cdbiFieldBatchCount). diff --git a/internal/impl/azure/input_table_storage.go b/internal/impl/azure/input_table_storage.go index f33d40e2c2..233909f796 100644 --- a/internal/impl/azure/input_table_storage.go +++ b/internal/impl/azure/input_table_storage.go @@ -56,13 +56,13 @@ func tsiSpec() *service.ConfigSpec { Summary(`Queries an Azure Storage Account Table, optionally with multiple filters.`). Description(` Queries an Azure Storage Account Table, optionally with multiple filters. -## Metadata +== Metadata This input adds the following metadata fields to each message: `+"```"+` - table_storage_name - row_num `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields( service.NewStringField(tsiFieldTableName). Description("The table to read messages from."). diff --git a/internal/impl/azure/output_blob_storage.go b/internal/impl/azure/output_blob_storage.go index 03a623ee17..abece7ee06 100644 --- a/internal/impl/azure/output_blob_storage.go +++ b/internal/impl/azure/output_blob_storage.go @@ -65,14 +65,14 @@ func bsoSpec() *service.ConfigSpec { Summary(`Sends message parts as objects to an Azure Blob Storage Account container. Each object is uploaded with the filename specified with the `+"`container`"+` field.`). Description(` In order to have a different path for each object you should use function -interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are +interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here], which are calculated per message of a batch. Supports multiple authentication methods but only one of the following is required: - `+"`storage_connection_string`"+` - `+"`storage_account` and `storage_access_key`"+` - `+"`storage_account` and `storage_sas_token`"+` -- `+"`storage_account` to access via [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential)"+` +- `+"`storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential]"+` If multiple are set then the `+"`storage_connection_string`"+` is given priority. @@ -89,7 +89,7 @@ If the `+"`storage_connection_string`"+` does not contain the `+"`AccountName`"+ Example(`${!json("doc.namespace")}/${!json("doc.id")}.json`). Default(`${!count("files")}-${!timestamp_unix_nano()}.txt`), service.NewInterpolatedStringEnumField(bsoFieldBlobType, "BLOCK", "APPEND"). - Description("Block and Append blobs are comprised of blocks, and each blob can support up to 50,000 blocks. The default value is `+\"`BLOCK`\"+`.`"). + Description("Block and Append blobs are comprized of blocks, and each blob can support up to 50,000 blocks. The default value is `+\"`BLOCK`\"+`.`"). Advanced(). Default("BLOCK"), service.NewInterpolatedStringEnumField(bsoFieldPublicAccessLevel, "PRIVATE", "BLOB", "CONTAINER"). diff --git a/internal/impl/azure/output_cosmosdb.go b/internal/impl/azure/output_cosmosdb.go index 817b0dcd72..f5dfee2a6e 100644 --- a/internal/impl/azure/output_cosmosdb.go +++ b/internal/impl/azure/output_cosmosdb.go @@ -20,9 +20,9 @@ func cosmosDBOutputConfig() *service.ConfigSpec { // Stable(). TODO Categories("Azure"). Version("v4.25.0"). - Summary("Creates or updates messages as JSON documents in [Azure CosmosDB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction)."). + Summary("Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]."). Description(` -When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. More details can be found [here](https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents). +When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. The `+"`partition_keys`"+` field must resolve to the same value(s) across the entire message batch. `+cosmosdb.CredentialsDocs+cosmosdb.BatchingDocs+service.OutputPerformanceDocs(true, true)). diff --git a/internal/impl/azure/output_queue_storage.go b/internal/impl/azure/output_queue_storage.go index 86961e4ba6..a531b21df1 100644 --- a/internal/impl/azure/output_queue_storage.go +++ b/internal/impl/azure/output_queue_storage.go @@ -46,7 +46,7 @@ func qsoSpec() *service.ConfigSpec { Description(` Only one authentication method is required, `+"`storage_connection_string`"+` or `+"`storage_account` and `storage_access_key`"+`. If both are set then the `+"`storage_connection_string`"+` is given priority. -In order to set the `+"`queue_name`"+` you can use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch.`+service.OutputPerformanceDocs(true, true)). +In order to set the `+"`queue_name`"+` you can use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here], which are calculated per message of a batch.`+service.OutputPerformanceDocs(true, true)). Fields( service.NewInterpolatedStringField(qsoFieldQueueName). Description("The name of the target Queue Storage queue."), diff --git a/internal/impl/azure/output_table_storage.go b/internal/impl/azure/output_table_storage.go index cf8b9546d7..03161018e8 100644 --- a/internal/impl/azure/output_table_storage.go +++ b/internal/impl/azure/output_table_storage.go @@ -73,7 +73,7 @@ func tsoSpec() *service.ConfigSpec { Description(` Only one authentication method is required, `+"`storage_connection_string`"+` or `+"`storage_account` and `storage_access_key`"+`. If both are set then the `+"`storage_connection_string`"+` is given priority. -In order to set the `+"`table_name`"+`, `+"`partition_key`"+` and `+"`row_key`"+` you can use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. +In order to set the `+"`table_name`"+`, `+"`partition_key`"+` and `+"`row_key`"+` you can use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here], which are calculated per message of a batch. If the `+"`properties`"+` are not set in the config, all the `+"`json`"+` fields are marshalled and stored in the table, which will be created if it does not exist. diff --git a/internal/impl/azure/processor_cosmosdb.go b/internal/impl/azure/processor_cosmosdb.go index 4178497501..fab9c2fff5 100644 --- a/internal/impl/azure/processor_cosmosdb.go +++ b/internal/impl/azure/processor_cosmosdb.go @@ -19,9 +19,9 @@ func cosmosDBProcessorConfig() *service.ConfigSpec { // Stable(). TODO Categories("Azure"). Version("v4.25.0"). - Summary("Creates or updates messages as JSON documents in [Azure CosmosDB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction)."). + Summary("Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]."). Description(` -When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. More details can be found [here](https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents). +When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. The `+"`partition_keys`"+` field must resolve to the same value(s) across the entire message batch. `+cosmosdb.CredentialsDocs+cosmosdb.MetadataDocs+cosmosdb.BatchingDocs). diff --git a/internal/impl/cassandra/output.go b/internal/impl/cassandra/output.go index 13a2d16445..6353e2062f 100644 --- a/internal/impl/cassandra/output.go +++ b/internal/impl/cassandra/output.go @@ -71,7 +71,7 @@ output: service.NewStringField(coFieldQuery). Description("A query to execute for each message."), service.NewBloblangField(coFieldArgsMapping). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) that can be used to provide arguments to Cassandra queries. The result of the query must be an array containing a matching number of elements to the query arguments."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that can be used to provide arguments to Cassandra queries. The result of the query must be an array containing a matching number of elements to the query arguments."). Version("3.55.0"). Optional(), service.NewStringEnumField(coFieldConsistency, diff --git a/internal/impl/changelog/bloblang.go b/internal/impl/changelog/bloblang.go index 8f6aeba78c..ac45e03c75 100644 --- a/internal/impl/changelog/bloblang.go +++ b/internal/impl/changelog/bloblang.go @@ -12,7 +12,7 @@ func init() { diffSpec := bloblang.NewPluginSpec(). Beta(). Category("Object & Array Manipulation"). - Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its [docs](https://pkg.go.dev/github.com/r3labs/diff/v3) for more information.`). + Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information.`). Version("4.25.0"). Param(bloblang.NewAnyParam("other").Description("The value to compare against.")) @@ -45,7 +45,7 @@ func init() { patchSpec := bloblang.NewPluginSpec(). Beta(). Category("Object & Array Manipulation"). - Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its [docs](https://pkg.go.dev/github.com/r3labs/diff/v3) for more information.`). + Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information.`). Version("4.25.0"). Param(bloblang.NewAnyParam("changelog").Description("The changelog to apply.")) diff --git a/internal/impl/cockroachdb/input_changefeed.go b/internal/impl/cockroachdb/input_changefeed.go index b31dcb2b2f..dfa609ea0a 100644 --- a/internal/impl/cockroachdb/input_changefeed.go +++ b/internal/impl/cockroachdb/input_changefeed.go @@ -30,8 +30,8 @@ var sampleString = `{ func crdbChangefeedInputConfig() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Integration"). - Summary(fmt.Sprintf("Listens to a [CockroachDB Core Changefeed](https://www.cockroachlabs.com/docs/stable/changefeed-examples) and creates a message for each row received. Each message is a json object looking like: \n```json\n%s\n```", sampleString)). - Description("This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered.\n\nNote: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to [the official CockroachDB documentation.](https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core)"). + Summary(fmt.Sprintf("Listens to a https://www.cockroachlabs.com/docs/stable/changefeed-examples[CockroachDB Core Changefeed] and creates a message for each row received. Each message is a json object looking like: \n```json\n%s\n```", sampleString)). + Description("This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered.\n\nNote: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core[the official CockroachDB documentation]."). Fields( service.NewStringField("dsn"). Description(`A Data Source Name to identify the target database.`). @@ -41,7 +41,7 @@ func crdbChangefeedInputConfig() *service.ConfigSpec { Description("CSV of tables to be included in the changefeed"). Example([]string{"table1", "table2"}), service.NewStringField("cursor_cache"). - Description("A [cache resource](https://www.benthos.dev/docs/components/caches/about) to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table."). + Description("A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache resource] to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table."). Optional(), service.NewStringListField("options"). Description("A list of options to be included in the changefeed (WITH X, Y...).\n**NOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case.**"). diff --git a/internal/impl/confluent/processor_schema_registry_decode.go b/internal/impl/confluent/processor_schema_registry_decode.go index 02200c305a..5987aad617 100644 --- a/internal/impl/confluent/processor_schema_registry_decode.go +++ b/internal/impl/confluent/processor_schema_registry_decode.go @@ -23,13 +23,13 @@ func schemaRegistryDecoderConfig() *service.ConfigSpec { Categories("Parsing", "Integration"). Summary("Automatically decodes and validates messages with schemas from a Confluent Schema Registry service."). Description(` -Decodes messages automatically from a schema stored within a [Confluent Schema Registry service](https://docs.confluent.io/platform/current/schema-registry/index.html) by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). +Decodes messages automatically from a schema stored within a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. -### Avro JSON Format +== Avro JSON format -This processor creates documents formatted as [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding) when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +This processor creates documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is ` + "`null`, then it is encoded as a JSON `null`" + `; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -37,16 +37,17 @@ This processor creates documents formatted as [Avro JSON](https://avro.apache.or For example, the union schema ` + "`[\"null\",\"string\",\"Foo\"]`, where `Foo`" + ` is a record name, would encode: - ` + "`null` as `null`" + `; -- the string ` + "`\"a\"` as `{\"string\": \"a\"}`" + `; and -- a ` + "`Foo` instance as `{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. +- the string ` + "`\"a\"` as `\\{\"string\": \"a\"}`" + `; and +- a ` + "`Foo` instance as `\\{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. -However, it is possible to instead create documents in [standard/raw JSON format](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) by setting the field ` + "[`avro_raw_json`](#avro_raw_json) to `true`" + `. -### Protobuf Format +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field ` + "<> to `true`" + `. + +== Protobuf format This processor decodes protobuf messages to JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json `). Field(service.NewBoolField("avro_raw_json"). - Description("Whether Avro messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding). If `true` the schema returned from the subject should be decoded as [standard json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) instead of as [avro json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec). There is a [comment in goavro](https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249), the [underlining library used for avro serialization](https://github.com/linkedin/goavro), that explains in more detail the difference between the standard json and avro json."). + Description("Whether Avro messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json."). Advanced().Default(false)). Field(service.NewURLField("url").Description("The base URL of the schema registry service.")) diff --git a/internal/impl/confluent/processor_schema_registry_encode.go b/internal/impl/confluent/processor_schema_registry_encode.go index cabbfb5076..46fe3f3e0b 100644 --- a/internal/impl/confluent/processor_schema_registry_encode.go +++ b/internal/impl/confluent/processor_schema_registry_encode.go @@ -24,15 +24,15 @@ func schemaRegistryEncoderConfig() *service.ConfigSpec { Categories("Parsing", "Integration"). Summary("Automatically encodes and validates messages with schemas from a Confluent Schema Registry service."). Description(` -Encodes messages automatically from schemas obtains from a [Confluent Schema Registry service](https://docs.confluent.io/platform/current/schema-registry/index.html) by polling the service for the latest schema version for target subjects. +Encodes messages automatically from schemas obtains from a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by polling the service for the latest schema version for target subjects. -If a message fails to encode under the schema then it will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). +If a message fails to encode under the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. -### Avro JSON Format +== Avro JSON format -By default this processor expects documents formatted as [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding) when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +By default this processor expects documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is ` + "`null`, then it is encoded as a JSON `null`" + `; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -40,24 +40,24 @@ By default this processor expects documents formatted as [Avro JSON](https://avr For example, the union schema ` + "`[\"null\",\"string\",\"Foo\"]`, where `Foo`" + ` is a record name, would encode: - ` + "`null` as `null`" + `; -- the string ` + "`\"a\"` as `{\"string\": \"a\"}`" + `; and -- a ` + "`Foo` instance as `{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. +- the string ` + "`\"a\"` as `\\{\"string\": \"a\"}`" + `; and +- a ` + "`Foo` instance as `\\{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. -However, it is possible to instead consume documents in [standard/raw JSON format](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) by setting the field ` + "[`avro_raw_json`](#avro_raw_json) to `true`" + `. +However, it is possible to instead consume documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field ` + "<> to `true`" + `. -#### Known Issues +=== Known issues -Important! There is an outstanding issue in the [avro serializing library](https://github.com/linkedin/goavro) that benthos uses which means it [doesn't encode logical types correctly](https://github.com/linkedin/goavro/issues/252). It's still possible to encode logical types that are in-line with the spec if ` + "`avro_raw_json` is set to true" + `, though now of course non-logical types will not be in-line with the spec. +Important! There is an outstanding issue in the https://github.com/linkedin/goavro[avro serializing library] that benthos uses which means it https://github.com/linkedin/goavro/issues/252[doesn't encode logical types correctly]. It's still possible to encode logical types that are in-line with the spec if ` + "`avro_raw_json` is set to true" + `, though now of course non-logical types will not be in-line with the spec. -### Protobuf Format +== Protobuf format This processor encodes protobuf messages either from any format parsed within Benthos (encoded as JSON by default), or from raw JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json -#### Multiple Message Support +=== Multiple message support When a target subject presents a protobuf schema that contains multiple messages it becomes ambiguous which message definition a given input data should be encoded against. In such scenarios Benthos will attempt to encode the data against each of them and select the first to successfully match against the data, this process currently *ignores all nested message definitions*. In order to speed up this exhaustive search the last known successful message will be attempted first for each subsequent input. -We will be considering alternative approaches in future so please [get in touch](/community) with thoughts and feedback. +We will be considering alternative approaches in future so please https://redpanda.com/slack[get in touch] with thoughts and feedback. `). Field(service.NewURLField("url").Description("The base URL of the schema registry service.")). Field(service.NewInterpolatedStringField("subject").Description("The schema subject to derive schemas from."). @@ -69,7 +69,7 @@ We will be considering alternative approaches in future so please [get in touch] Example("60s"). Example("1h")). Field(service.NewBoolField("avro_raw_json"). - Description("Whether messages encoded in Avro format should be parsed as normal JSON (\"json that meets the expectations of regular internet json\") rather than [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding). If `true` the schema returned from the subject should be parsed as [standard json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) instead of as [avro json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec). There is a [comment in goavro](https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249), the [underlining library used for avro serialization](https://github.com/linkedin/goavro), that explains in more detail the difference between standard json and avro json."). + Description("Whether messages encoded in Avro format should be parsed as normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be parsed as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between standard json and avro json."). Advanced().Default(false).Version("3.59.0")) for _, f := range service.NewHTTPRequestAuthSignerFields() { diff --git a/internal/impl/couchbase/client/docs.go b/internal/impl/couchbase/client/docs.go index 49b0a359d8..5eea1d14b8 100644 --- a/internal/impl/couchbase/client/docs.go +++ b/internal/impl/couchbase/client/docs.go @@ -18,7 +18,7 @@ func NewConfigSpec() *service.ConfigSpec { string(TranscoderRawJSON): `RawJSONTranscoder implements passthrough behavior of JSON data. This transcoder does not apply any serialization. It will forward data across the network without incurring unnecessary parsing costs. This will apply the following behavior to the value: binary ([]byte) -> JSON bytes, JSON expectedFlags. string -> JSON bytes, JSON expectedFlags. default -> error.`, string(TranscoderRawString): `RawStringTranscoder implements passthrough behavior of raw string data. This transcoder does not apply any serialization. This will apply the following behavior to the value: string -> string bytes, string expectedFlags. default -> error.`, string(TranscoderJSON): `JSONTranscoder implements the default transcoding behavior and applies JSON transcoding to all values. This will apply the following behavior to the value: binary ([]byte) -> error. default -> JSON value, JSON Flags.`, - string(TranscoderLegacy): `LegacyTranscoder implements the behaviour for a backward-compatible transcoder. This transcoder implements behaviour matching that of gocb v1.This will apply the following behavior to the value: binary ([]byte) -> binary bytes, Binary expectedFlags. string -> string bytes, String expectedFlags. default -> JSON value, JSON expectedFlags.`, + string(TranscoderLegacy): `LegacyTranscoder implements the behavior for a backward-compatible transcoder. This transcoder implements behavior matching that of gocb v1.This will apply the following behavior to the value: binary ([]byte) -> binary bytes, Binary expectedFlags. string -> string bytes, String expectedFlags. default -> JSON value, JSON expectedFlags.`, }).Description("Couchbase transcoder to use.").Default(string(TranscoderLegacy)).Advanced()). Field(service.NewDurationField("timeout").Description("Operation timeout.").Advanced().Default("15s")) } diff --git a/internal/impl/dgraph/cache_ristretto.go b/internal/impl/dgraph/cache_ristretto.go index 635f8993c2..36388f1c20 100644 --- a/internal/impl/dgraph/cache_ristretto.go +++ b/internal/impl/dgraph/cache_ristretto.go @@ -20,7 +20,7 @@ func ristrettoCacheConfig() *service.ConfigSpec { spec := service.NewConfigSpec(). Stable(). - Summary(`Stores key/value pairs in a map held in the memory-bound [Ristretto cache](https://github.com/dgraph-io/ristretto).`). + Summary(`Stores key/value pairs in a map held in the memory-bound https://github.com/dgraph-io/ristretto[Ristretto cache].`). Description(`This cache is more efficient and appropriate for high-volume use cases than the standard memory cache. However, the add command is non-atomic, and therefore this cache is not suitable for deduplication.`). Field(service.NewDurationField("default_ttl"). Description("A default TTL to set for items, calculated from the moment the item is cached. Set to an empty string or zero duration to disable TTLs."). diff --git a/internal/impl/discord/output.go b/internal/impl/discord/output.go index f59fadea51..43085b053f 100644 --- a/internal/impl/discord/output.go +++ b/internal/impl/discord/output.go @@ -15,9 +15,9 @@ func outputConfig() *service.ConfigSpec { Categories("Services", "Social"). Summary("Writes messages to a Discord channel."). Description(` -This output POSTs messages to the `+"`/channels/{channel_id}/messages`"+` Discord API endpoint authenticated as a bot using token based authentication. +This output POSTs messages to the `+"`/channels/\\{channel_id}/messages`"+` Discord API endpoint authenticated as a bot using token based authentication. -If the format of a message is a JSON object matching the [Discord API message type](https://discord.com/developers/docs/resources/channel#message-object) then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. +If the format of a message is a JSON object matching the https://discord.com/developers/docs/resources/channel#message-object[Discord API message type] then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. `). Fields( service.NewStringField("channel_id"). diff --git a/internal/impl/elasticsearch/output.go b/internal/impl/elasticsearch/output.go index 2508fb49bf..1cdc434499 100644 --- a/internal/impl/elasticsearch/output.go +++ b/internal/impl/elasticsearch/output.go @@ -188,9 +188,9 @@ func OutputSpec() *service.ConfigSpec { Categories("Services"). Summary(`Publishes messages into an Elasticsearch index. If the index does not exist then it is created with a dynamic mapping.`). Description(` -Both the `+"`id` and `index`"+` fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part. +Both the `+"`id` and `index`"+` fields can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. When sending batched messages these interpolations are performed per message part. -### AWS +== AWS It's possible to enable AWS connectivity with this output using the `+"`aws`"+` fields. However, you may need to set `+"`sniff` and `healthcheck`"+` to false for connections to succeed.`+service.OutputPerformanceDocs(true, true)). Fields( diff --git a/internal/impl/gcp/input_bigquery_select.go b/internal/impl/gcp/input_bigquery_select.go index 8876541946..bcd9c316ec 100644 --- a/internal/impl/gcp/input_bigquery_select.go +++ b/internal/impl/gcp/input_bigquery_select.go @@ -82,7 +82,7 @@ func newBigQuerySelectInputConfig() *service.ConfigSpec { Version("3.63.0"). Categories("Services", "GCP"). Summary("Executes a `SELECT` query against BigQuery and creates a message for each row received."). - Description(`Once the rows from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute).`). + Description(`Once the rows from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute).`). Field(service.NewStringField("project").Description("GCP project where the query job will execute.")). Field(service.NewStringField("table").Description("Fully-qualified BigQuery table name to query.").Example("bigquery-public-data.samples.shakespeare")). Field(service.NewStringListField("columns").Description("A list of columns to query.")). @@ -96,7 +96,7 @@ func newBigQuerySelectInputConfig() *service.ConfigSpec { Field(service.NewStringMapField("job_labels").Description("A list of labels to add to the query job.").Default(map[string]any{})). Field(service.NewStringField("priority").Description("The priority with which to schedule the query.").Default("")). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). Example(`root = [ "article", now().ts_format("2006-01-02") ]`). Optional()). Field(service.NewStringField("prefix"). diff --git a/internal/impl/gcp/input_cloud_storage.go b/internal/impl/gcp/input_cloud_storage.go index b558eb2ae0..0abde7c71d 100644 --- a/internal/impl/gcp/input_cloud_storage.go +++ b/internal/impl/gcp/input_cloud_storage.go @@ -52,7 +52,7 @@ func csiSpec() *service.ConfigSpec { Categories("Services", "GCP"). Summary(`Downloads objects within a Google Cloud Storage bucket, optionally filtered by a prefix.`). Description(` -## Metadata +== Metadata This input adds the following metadata fields to each message: @@ -66,11 +66,11 @@ This input adds the following metadata fields to each message: - All user defined metadata `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. -### Credentials +=== Credentials -By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more [in this document](/docs/guides/cloud/gcp).`). +By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more in xref:guides:cloud/gcp.adoc[].`). Fields( service.NewStringField(csiFieldBucket). Description("The name of the bucket from which to download objects."), diff --git a/internal/impl/gcp/input_pubsub.go b/internal/impl/gcp/input_pubsub.go index 65013967ae..8b901fb2af 100644 --- a/internal/impl/gcp/input_pubsub.go +++ b/internal/impl/gcp/input_pubsub.go @@ -73,19 +73,19 @@ func pbiSpec() *service.ConfigSpec { Categories("Services", "GCP"). Summary(`Consumes messages from a GCP Cloud Pub/Sub subscription.`). Description(` -For information on how to set up credentials check out [this guide](https://cloud.google.com/docs/authentication/production). +For information on how to set up credentials see https://cloud.google.com/docs/authentication/production[this guide]. -### Metadata +== Metadata This input adds the following metadata fields to each message: -`+"``` text"+` +`+"```text"+` - gcp_pubsub_publish_time_unix - The time at which the message was published to the topic. - gcp_pubsub_delivery_attempt - When dead lettering is enabled, this is set to the number of times PubSub has attempted to deliver a message. - All message attributes `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. `). Fields( service.NewStringField(pbiFieldProjectID). @@ -93,7 +93,7 @@ You can access these metadata fields using [function interpolation](/docs/config service.NewStringField(pbiFieldSubscriptionID). Description("The target subscription ID."), service.NewStringField(pbiFieldEndpoint). - Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values check out [this document.](https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints)"). + Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]."). Example("us-central1-pubsub.googleapis.com:443"). Example("us-west3-pubsub.googleapis.com:443"). Default(""), diff --git a/internal/impl/gcp/output_bigquery.go b/internal/impl/gcp/output_bigquery.go index feaf1e7150..dc64b01415 100644 --- a/internal/impl/gcp/output_bigquery.go +++ b/internal/impl/gcp/output_bigquery.go @@ -119,15 +119,15 @@ func gcpBigQueryConfig() *service.ConfigSpec { Version("3.55.0"). Summary(`Sends messages as new rows to a Google Cloud BigQuery table.`). Description(` -## Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more [in this document](/docs/guides/cloud/gcp). +By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more in xref:guides:cloud/gcp.adoc[]. -## Format +== Format This output currently supports only CSV and NEWLINE_DELIMITED_JSON formats. Learn more about how to use GCP BigQuery with them here: -- ` + "[`NEWLINE_DELIMITED_JSON`](https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json)" + ` -- ` + "[`CSV`](https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv)" + ` +- ` + "https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json[`NEWLINE_DELIMITED_JSON`]" + ` +- ` + "https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv[`CSV`]" + ` Each message may contain multiple elements separated by newlines. For example a single message containing: @@ -150,7 +150,7 @@ And: The same is true for the CSV format. -### CSV +=== CSV For the CSV format when the field ` + "`csv.header`" + ` is specified a header row will be inserted as the first line of each message batch. If this field is not provided then the first message of each message batch must include a header line.` + service.OutputPerformanceDocs(true, true)). Field(service.NewStringField("project").Description("The project ID of the dataset to insert data to. If not set, it will be inferred from the credentials or read from the GOOGLE_CLOUD_PROJECT environment variable.").Default("")). diff --git a/internal/impl/gcp/output_cloud_storage.go b/internal/impl/gcp/output_cloud_storage.go index 0548a3832b..571ffbccc4 100644 --- a/internal/impl/gcp/output_cloud_storage.go +++ b/internal/impl/gcp/output_cloud_storage.go @@ -82,19 +82,19 @@ func csoSpec() *service.ConfigSpec { Categories("Services", "GCP"). Summary(`Sends message parts as objects to a Google Cloud Storage bucket. Each object is uploaded with the path specified with the `+"`path`"+` field.`). Description(` -In order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. +In order to have a different path for each object you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries], which are calculated per message of a batch. -### Metadata +== Metadata -Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the [metadata docs](/docs/configuration/metadata). +Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the xref:configuration:metadata.adoc[metadata docs]. -### Credentials +== Credentials -By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more [in this document](/docs/guides/cloud/gcp). +By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more in xref:guides:cloud/gcp.adoc[]. -### Batching +== Batching -It's common to want to upload messages to Google Cloud Storage as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an `+"[`archive`](/docs/components/processors/archive)"+` and/or `+"[`compress`](/docs/components/processors/compress)"+` processor. +It's common to want to upload messages to Google Cloud Storage as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an `+"xref:components:processors/archive.adoc[`archive`]"+` and/or `+"xref:components:processors/compress.adoc[`compress`]"+` processor. For example, if we wished to upload messages as a .tar.gz archive of documents we could achieve that with the following config: diff --git a/internal/impl/gcp/output_pubsub.go b/internal/impl/gcp/output_pubsub.go index 490b6d78a3..409a8a73fd 100644 --- a/internal/impl/gcp/output_pubsub.go +++ b/internal/impl/gcp/output_pubsub.go @@ -18,11 +18,11 @@ func newPubSubOutputConfig() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Categories("Services", "GCP"). - Summary("Sends messages to a GCP Cloud Pub/Sub topic. [Metadata](/docs/configuration/metadata) from messages are sent as attributes."). + Summary("Sends messages to a GCP Cloud Pub/Sub topic. xref:configuration:metadata.adoc[Metadata] from messages are sent as attributes."). Description(` -For information on how to set up credentials check out [this guide](https://cloud.google.com/docs/authentication/production). +For information on how to set up credentials, see https://cloud.google.com/docs/authentication/production[this guide]. -### Troubleshooting +== Troubleshooting If you're consistently seeing `+"`Failed to send message to gcp_pubsub: context deadline exceeded`"+` error logs without any further information it is possible that you are encountering https://github.com/benthosdev/benthos/issues/1042, which occurs when metadata values contain characters that are not valid utf-8. This can frequently occur when consuming from Kafka as the key metadata field may be populated with an arbitrary binary value, but this issue is not exclusive to Kafka. @@ -49,7 +49,7 @@ pipeline: Default(""). Example("us-central1-pubsub.googleapis.com:443"). Example("us-west3-pubsub.googleapis.com:443"). - Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values check out [this document.](https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints)"), + Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]."), service.NewInterpolatedStringField("ordering_key"). Optional(). Description("The ordering key to use for publishing messages."). @@ -167,7 +167,7 @@ func newPubSubOutput(conf *service.ParsedConfig) (*pubsubOutput, error) { case "signal_error": flowControl.LimitExceededBehavior = pubsub.FlowControlSignalError default: - return nil, fmt.Errorf("unrecognized flow control setting: %s", limitBehavior) + return nil, fmt.Errorf("unrecognised flow control setting: %s", limitBehavior) } settings.FlowControlSettings = flowControl diff --git a/internal/impl/gcp/processor_bigquery_select.go b/internal/impl/gcp/processor_bigquery_select.go index c91dd4205f..b52f7977b4 100644 --- a/internal/impl/gcp/processor_bigquery_select.go +++ b/internal/impl/gcp/processor_bigquery_select.go @@ -87,7 +87,7 @@ func newBigQuerySelectProcessorConfig() *service.ConfigSpec { ). Field(service.NewStringMapField("job_labels").Description("A list of labels to add to the query job.").Default(map[string]any{})). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). Example(`root = [ "article", now().ts_format("2006-01-02") ]`). Optional()). Field(service.NewStringField("prefix"). diff --git a/internal/impl/gcp/tracer_cloudtrace.go b/internal/impl/gcp/tracer_cloudtrace.go index a74632d359..ff3d35ad99 100644 --- a/internal/impl/gcp/tracer_cloudtrace.go +++ b/internal/impl/gcp/tracer_cloudtrace.go @@ -24,7 +24,7 @@ const ( func cloudTraceSpec() *service.ConfigSpec { return service.NewConfigSpec(). Version("4.2.0"). - Summary(`Send tracing events to a [Google Cloud Trace](https://cloud.google.com/trace).`). + Summary(`Send tracing events to a https://cloud.google.com/trace[Google Cloud Trace].`). Fields( service.NewStringField(ctFieldProject). Description("The google project with Cloud Trace API enabled. If this is omitted then the Google Cloud SDK will attempt auto-detect it from the environment."), diff --git a/internal/impl/hdfs/input.go b/internal/impl/hdfs/input.go index cbab795773..5ab40630f0 100644 --- a/internal/impl/hdfs/input.go +++ b/internal/impl/hdfs/input.go @@ -21,17 +21,17 @@ func inputSpec() *service.ConfigSpec { Categories("Services"). Summary(`Reads files from a HDFS directory, where each discrete file will be consumed as a single message payload.`). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: -`+"``` text"+` +`+"```text"+` - hdfs_name - hdfs_path `+"```"+` You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields( service.NewStringListField(iFieldHosts). Description("A list of target host addresses to connect to."). diff --git a/internal/impl/hdfs/output.go b/internal/impl/hdfs/output.go index 938cfef18f..23a6c69787 100644 --- a/internal/impl/hdfs/output.go +++ b/internal/impl/hdfs/output.go @@ -24,7 +24,7 @@ func outputSpec() *service.ConfigSpec { Stable(). Categories("Services"). Summary(`Sends message parts as files to a HDFS directory.`). - Description(`Each file is written with the path specified with the 'path' field, in order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries).`+service.OutputPerformanceDocs(true, false)). + Description(`Each file is written with the path specified with the 'path' field, in order to have a different path for each object you should use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here].`+service.OutputPerformanceDocs(true, false)). Fields( service.NewStringListField(oFieldHosts). Description("A list of target host addresses to connect to."). diff --git a/internal/impl/io/bloblang.go b/internal/impl/io/bloblang.go index 8068c5c93e..468a9148bb 100644 --- a/internal/impl/io/bloblang.go +++ b/internal/impl/io/bloblang.go @@ -44,7 +44,7 @@ func init() { Example("", `root.thing.key = env("key").or("default value")`). Example("", `root.thing.key = env(this.thing.key_name)`). Example( - "When the name parameter is static this function will only resolve once and yield the same result for each invocation as an optimisation, this means that updates to env vars during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the variable lookup to be performed for each execution of the mapping.", + "When the name parameter is static this function will only resolve once and yield the same result for each invocation as an optimization, this means that updates to env vars during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the variable lookup to be performed for each execution of the mapping.", `root.thing.key = env(name: "key", no_cache: true)`, ), func(args *bloblang.ParsedParams) (bloblang.Function, error) { @@ -87,7 +87,7 @@ func init() { return !noCache }). Category(query.FunctionCategoryEnvironment). - Description("Reads a file and returns its contents. Relative paths are resolved from the directory of the process executing the mapping. In order to read files relative to the mapping file use the newer [`file_rel` function](#file_rel)"). + Description("Reads a file and returns its contents. Relative paths are resolved from the directory of the process executing the mapping. In order to read files relative to the mapping file use the newer <>"). Param(bloblang.NewStringParam("path"). Description("The path of the target file.")). Param(bloblang.NewBoolParam("no_cache"). @@ -98,7 +98,7 @@ func init() { `{"doc":{"foo":"bar"}}`, }). Example( - "When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimisation, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping.", + "When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimization, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping.", `root.doc = file(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json()`, [2]string{`{}`, `{"doc":{"foo":"bar"}}`}, ), @@ -151,7 +151,7 @@ func init() { `{"doc":{"foo":"bar"}}`, }). Example( - "When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimisation, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping.", + "When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimization, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping.", `root.doc = file_rel(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json()`, [2]string{`{}`, `{"doc":{"foo":"bar"}}`}, ), diff --git a/internal/impl/io/input_csv.go b/internal/impl/io/input_csv.go index a1c9a5a828..27dbbe8fc1 100644 --- a/internal/impl/io/input_csv.go +++ b/internal/impl/io/input_csv.go @@ -29,7 +29,7 @@ func csviFieldSpec() *service.ConfigSpec { Categories("Local"). Summary("Reads one or more CSV files as structured records following the format described in RFC 4180."). Description(` -This input offers more control over CSV parsing than the `+"[`file` input](/docs/components/inputs/file)"+`. +This input offers more control over CSV parsing than the `+"xref:components:inputs/file.adoc[`file` input]"+`. When parsing with a header row each line of the file will be consumed as a structured object, where the key names are determined from the header now. For example, the following CSV file: @@ -53,7 +53,7 @@ If, however, the field `+"`parse_header_row` is set to `false`"+` then arrays ar ["second foo","second bar","second baz"] `+"```"+` -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -64,13 +64,13 @@ This input adds the following metadata fields to each message: - mod_time (RFC3339) `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. Note: The `+"`header`"+` field is only set when `+"`parse_header_row`"+` is `+"`true`"+`. -### Output CSV column order +=== Output CSV column order -When [creating CSV](/docs/guides/bloblang/advanced#creating-csv) from Benthos messages, the columns must be sorted lexicographically to make the output deterministic. Alternatively, when using the `+"`csv`"+` input, one can leverage the `+"`header`"+` metadata field to retrieve the column order: +When xref:guides:bloblang/advanced.adoc#creating-csv[creating CSV] from Benthos messages, the columns must be sorted lexicographically to make the output deterministic. Alternatively, when using the `+"`csv`"+` input, one can leverage the `+"`header`"+` metadata field to retrieve the column order: `+"```yaml"+` input: @@ -101,7 +101,7 @@ output: path: ./output/${! @path.filepath_split().index(-1) } `+"```"+` `). - Footnotes(`This input is particularly useful when consuming CSV from files too large to parse entirely within memory. However, in cases where CSV is consumed from other input types it's also possible to parse them using the `+"[Bloblang `parse_csv` method](/docs/guides/bloblang/methods#parse_csv)"+`.`). + Footnotes(`This input is particularly useful when consuming CSV from files too large to parse entirely within memory. However, in cases where CSV is consumed from other input types it's also possible to parse them using the `+"xref:guides:bloblang/methods.adoc#parse_csv[Bloblang `parse_csv` method]"+`.`). Fields( service.NewStringListField(csviFieldPaths). Description("A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star)."). diff --git a/internal/impl/io/input_dynamic.go b/internal/impl/io/input_dynamic.go index d89a23b6c7..51c197d490 100644 --- a/internal/impl/io/input_dynamic.go +++ b/internal/impl/io/input_dynamic.go @@ -26,25 +26,25 @@ func dynInputSpec() *service.ConfigSpec { Categories("Utility"). Summary(`A special broker type where the inputs are identified by unique labels and can be created, changed and removed during runtime via a REST HTTP interface.`). Footnotes(` -## Endpoints +== Endpoints -### GET `+"`/inputs`"+` +=== GET `+"`/inputs`"+` Returns a JSON object detailing all dynamic inputs, providing information such as their current uptime and configuration. -### GET `+"`/inputs/{id}`"+` +=== GET `+"`/inputs/\\{id}`"+` Returns the configuration of an input. -### POST `+"`/inputs/{id}`"+` +=== POST `+"`/inputs/\\{id}`"+` Creates or updates an input with a configuration provided in the request body (in YAML or JSON format). -### DELETE `+"`/inputs/{id}`"+` +=== DELETE `+"`/inputs/\\{id}`"+` Stops and removes an input. -### GET `+"`/inputs/{id}/uptime`"+` +=== GET `+"`/inputs/\\{id}/uptime`"+` Returns the uptime of an input as a duration string (of the form "72h3m0.5s"), or "stopped" in the case where the input has gracefully terminated.`). Fields( diff --git a/internal/impl/io/input_file.go b/internal/impl/io/input_file.go index 3e3efc1947..99c202bace 100644 --- a/internal/impl/io/input_file.go +++ b/internal/impl/io/input_file.go @@ -24,7 +24,7 @@ func fileInputSpec() *service.ConfigSpec { Categories("Local"). Summary(`Consumes data from files on disk, emitting messages according to a chosen codec.`). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -35,7 +35,7 @@ This input adds the following metadata fields to each message: `+"```"+` You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Example( "Read a Bunch of CSVs", "If we wished to consume a directory of CSV files as structured documents we can use a glob pattern and the `csv` scanner:", diff --git a/internal/impl/io/input_http_client.go b/internal/impl/io/input_http_client.go index 2cd221447f..4939bca891 100644 --- a/internal/impl/io/input_http_client.go +++ b/internal/impl/io/input_http_client.go @@ -29,15 +29,15 @@ func httpClientInputSpec() *service.ConfigSpec { Categories("Network"). Summary("Connects to a server and continuously performs requests for a single message."). Description(` -The URL and header values of this type can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). +The URL and header values of this type can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. -### Streaming +== Streaming If you enable streaming then Benthos will consume the body of the response as a continuous stream of data, breaking messages out following a chosen scanner. This allows you to consume APIs that provide long lived streamed data feeds (such as Twitter). -### Pagination +== Pagination -This input supports interpolation functions in the `+"`url` and `headers`"+` fields where data from the previous successfully consumed message (if there was one) can be referenced. This can be used in order to support basic levels of pagination. However, in cases where pagination depends on logic it is recommended that you use an `+"[`http` processor](/docs/components/processors/http) instead, often combined with a [`generate` input](/docs/components/inputs/generate)"+` in order to schedule the processor.`). +This input supports interpolation functions in the `+"`url` and `headers`"+` fields where data from the previous successfully consumed message (if there was one) can be referenced. This can be used in order to support basic levels of pagination. However, in cases where pagination depends on logic it is recommended that you use an `+"xref:components:processors/http.adoc[`http` processor] instead, often combined with a xref:components:inputs/generate.adoc[`generate` input]"+` in order to schedule the processor.`). Example( "Basic Pagination", "Interpolation functions within the `url` and `headers` fields can be used to reference the previously consumed message, which allows simple pagination.", diff --git a/internal/impl/io/input_http_server.go b/internal/impl/io/input_http_server.go index da82e94e65..2639a80680 100644 --- a/internal/impl/io/input_http_server.go +++ b/internal/impl/io/input_http_server.go @@ -162,27 +162,27 @@ func hsiSpec() *service.ConfigSpec { Categories("Network"). Summary(`Receive messages POSTed over HTTP(S). HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified.`). Description(` -If the `+"`address`"+` config field is left blank the [service-wide HTTP server](/docs/components/http/about) will be used. +If the `+"`address`"+` config field is left blank the xref:components:http/about.adoc[service-wide HTTP server] will be used. -The field `+"`rate_limit`"+` allows you to specify an optional `+"[`rate_limit` resource](/docs/components/rate_limits/about)"+`, which will be applied to each HTTP request made and each websocket payload received. +The field `+"`rate_limit`"+` allows you to specify an optional `+"xref:components:rate_limits/about.adoc[`rate_limit` resource]"+`, which will be applied to each HTTP request made and each websocket payload received. When the rate limit is breached HTTP requests will have a 429 response returned with a Retry-After header. Websocket payloads will be dropped and an optional response payload will be sent as per `+"`ws_rate_limit_message`"+`. -### Responses +== Responses -It's possible to return a response for each message received using [synchronous responses](/docs/guides/sync_responses). When doing so you can customise headers with the `+"`sync_response` field `headers`"+`, which can also use [function interpolation](/docs/configuration/interpolation#bloblang-queries) in the value based on the response message contents. +It's possible to return a response for each message received using xref:guides:sync_responses.adoc[synchronous responses]. When doing so you can customize headers with the `+"`sync_response` field `headers`"+`, which can also use xref:configuration:interpolation.adoc#bloblang-queries[function interpolation] in the value based on the response message contents. -### Endpoints +== Endpoints -The following fields specify endpoints that are registered for sending messages, and support path parameters of the form `+"`/{foo}`"+`, which are added to ingested messages as metadata. A path ending in `+"`/`"+` will match against all extensions of that path: +The following fields specify endpoints that are registered for sending messages, and support path parameters of the form `+"`/\\{foo}`"+`, which are added to ingested messages as metadata. A path ending in `+"`/`"+` will match against all extensions of that path: -#### `+"`path` (defaults to `/post`)"+` +=== `+"`path` (defaults to `/post`)"+` This endpoint expects POST requests where the entire request body is consumed as a single message. -If the request contains a multipart `+"`content-type`"+` header as per [rfc1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. +If the request contains a multipart `+"`content-type`"+` header as per https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[rfc1341] then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. -#### `+"`ws_path` (defaults to `/post/ws`)"+` +=== `+"`ws_path` (defaults to `/post/ws`)"+` Creates a websocket connection, where payloads received on the socket are passed through the pipeline as a batch of one message. @@ -192,11 +192,11 @@ You may specify an optional `+"`ws_welcome_message`"+`, which is a static payloa It's also possible to specify a `+"`ws_rate_limit_message`"+`, which is a static payload to be sent to clients that have triggered the servers rate limit. -### Metadata +== Metadata This input adds the following metadata fields to each message: -`+"``` text"+` +`+"```text"+` - http_server_user_agent - http_server_request_path - http_server_verb @@ -208,13 +208,13 @@ This input adds the following metadata fields to each message: `+"```"+` If HTTPS is enabled, the following fields are added as well: -`+"``` text"+` +`+"```text"+` - http_server_tls_version - http_server_tls_subject - http_server_tls_cipher_suite `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields( service.NewStringField(hsiFieldAddress). Description("An alternative address to host from. If left empty the service wide address is used."). @@ -241,7 +241,7 @@ You can access these metadata fields using [function interpolation](/docs/config Description("Timeout for requests. If a consumed messages takes longer than this to be delivered the connection is closed, but the message may still be delivered."). Default("5s"), service.NewStringField(hsiFieldRateLimit). - Description("An optional [rate limit](/docs/components/rate_limits/about) to throttle requests by."). + Description("An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by."). Default(""), service.NewStringField(hsiFieldCertFile). Description("Enable TLS by specifying a certificate and key file. Only valid with a custom `address`."). @@ -265,7 +265,7 @@ You can access these metadata fields using [function interpolation](/docs/config service.NewMetadataFilterField(hsiFieldResponseExtractMetadata). Description("Specify criteria for which metadata values are added to the response as headers."), ). - Description("Customise messages returned via [synchronous responses](/docs/guides/sync_responses)."). + Description("Customize messages returned via xref:guides:sync_responses.adoc[synchronous responses]."). Advanced(), ). Example( diff --git a/internal/impl/io/input_socket_server.go b/internal/impl/io/input_socket_server.go index 8e1247f3f3..23aa877bd9 100644 --- a/internal/impl/io/input_socket_server.go +++ b/internal/impl/io/input_socket_server.go @@ -37,7 +37,7 @@ const ( func socketServerInputSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). - Summary(`Creates a server that receives a stream of messages over a tcp, udp or unix socket.`). + Summary(`Creates a server that receives a stream of messages over a TCP, UDP or Unix socket.`). Categories("Network"). Fields( service.NewStringEnumField(issFieldNetwork, "unix", "tcp", "udp", "tls"). @@ -46,7 +46,7 @@ func socketServerInputSpec() *service.ConfigSpec { Description("The address to listen from."). Examples("/tmp/benthos.sock", "0.0.0.0:6000"), service.NewStringField(issFieldAddressCache). - Description("An optional [`cache`](/docs/components/caches/about) within which this input should write it's bound address once known. The key of the cache item containing the address will be the label of the component suffixed with `_address` (e.g. `foo_address`), or `socket_server_address` when a label has not been provided. This is useful in situations where the address is dynamically allocated by the server (`127.0.0.1:0`) and you want to store the allocated address somewhere for reference by other systems and components."). + Description("An optional xref:components:caches/about.adoc[`cache`] within which this input should write it's bound address once known. The key of the cache item containing the address will be the label of the component suffixed with `_address` (e.g. `foo_address`), or `socket_server_address` when a label has not been provided. This is useful in situations where the address is dynamically allocated by the server (`127.0.0.1:0`) and you want to store the allocated address somewhere for reference by other systems and components."). Optional(). Version("4.25.0"), service.NewObjectField(issFieldTLS, diff --git a/internal/impl/io/output_dynamic.go b/internal/impl/io/output_dynamic.go index 3033df48db..ff1e874733 100644 --- a/internal/impl/io/output_dynamic.go +++ b/internal/impl/io/output_dynamic.go @@ -28,25 +28,25 @@ func dynOutputSpec() *service.ConfigSpec { Summary(`A special broker type where the outputs are identified by unique labels and can be created, changed and removed during runtime via a REST API.`). Description(`The broker pattern used is always `+"`fan_out`"+`, meaning each message will be delivered to each dynamic output.`). Footnotes(` -## Endpoints +== Endpoints -### GET `+"`/outputs`"+` +=== GET `+"`/outputs`"+` Returns a JSON object detailing all dynamic outputs, providing information such as their current uptime and configuration. -### GET `+"`/outputs/{id}`"+` +=== GET `+"`/outputs/\\{id}`"+` Returns the configuration of an output. -### POST `+"`/outputs/{id}`"+` +=== POST `+"`/outputs/\\{id}`"+` Creates or updates an output with a configuration provided in the request body (in YAML or JSON format). -### DELETE `+"`/outputs/{id}`"+` +=== DELETE `+"`/outputs/\\{id}`"+` Stops and removes an output. -### GET `+"`/outputs/{id}/uptime`"+` +=== GET `+"`/outputs/\\{id}/uptime`"+` Returns the uptime of an output as a duration string (of the form "72h3m0.5s").`). Fields( diff --git a/internal/impl/io/output_file.go b/internal/impl/io/output_file.go index 8fd1d1134a..7da1dde0ae 100644 --- a/internal/impl/io/output_file.go +++ b/internal/impl/io/output_file.go @@ -24,7 +24,7 @@ func fileOutputSpec() *service.ConfigSpec { Stable(). Categories("Local"). Summary(`Writes messages to files on disk based on a chosen codec.`). - Description(`Messages can be written to different files by using [interpolation functions](/docs/configuration/interpolation#bloblang-queries) in the path field. However, only one file is ever open at a given time, and therefore when the path changes the previously open file is closed.`). + Description(`Messages can be written to different files by using xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the path field. However, only one file is ever open at a given time, and therefore when the path changes the previously open file is closed.`). Fields( service.NewInterpolatedStringField(fileOutputFieldPath). Description("The file to write to, if the file does not yet exist it will be created."). diff --git a/internal/impl/io/output_http_client.go b/internal/impl/io/output_http_client.go index 18c8e22be0..f70f93e677 100644 --- a/internal/impl/io/output_http_client.go +++ b/internal/impl/io/output_http_client.go @@ -13,21 +13,21 @@ func httpClientOutputSpec() *service.ConfigSpec { Categories("Network"). Summary("Sends messages to an HTTP server."). Description(` -When the number of retries expires the output will reject the message, the behaviour after this will depend on the pipeline but usually this simply means the send is attempted again until successful whilst applying back pressure. +When the number of retries expires the output will reject the message, the behavior after this will depend on the pipeline but usually this simply means the send is attempted again until successful whilst applying back pressure. -The URL and header values of this type can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). +The URL and header values of this type can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. -The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). This behaviour can be disabled by setting the field ` + "[`batch_as_multipart`](#batch_as_multipart) to `false`" + `. +The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be disabled by setting the field ` + "<> to `false`" + `. -### Propagating Responses +== Propagate responses -It's possible to propagate the response from each HTTP request back to the input source by setting ` + "`propagate_response` to `true`" + `. Only inputs that support [synchronous responses](/docs/guides/sync_responses) are able to make use of these propagated responses.` + service.OutputPerformanceDocs(true, true)). +It's possible to propagate the response from each HTTP request back to the input source by setting ` + "`propagate_response` to `true`" + `. Only inputs that support xref:guides:sync_responses.adoc[synchronous responses] are able to make use of these propagated responses.` + service.OutputPerformanceDocs(true, true)). Field(httpclient.ConfigField("POST", true, service.NewBoolField("batch_as_multipart"). - Description("Send message batches as a single request using [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). If disabled messages in batches will be sent as individual requests."). + Description("Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. If disabled messages in batches will be sent as individual requests."). Advanced().Default(false), service.NewBoolField("propagate_response"). - Description("Whether responses from the server should be [propagated back](/docs/guides/sync_responses) to the input."). + Description("Whether responses from the server should be xref:guides:sync_responses.adoc[propagated back] to the input."). Advanced().Default(false), service.NewIntField("max_in_flight"). Description("The maximum number of parallel message batches to have in flight at any given time."). @@ -46,7 +46,7 @@ It's possible to propagate the response from each HTTP request back to the input Description("The body of the individual message part."). Example(`${! this.data.part1 }`). Default(""), - ).Description("EXPERIMENTAL: Create explicit multipart HTTP requests by specifying an array of parts to add to the request, each part specified consists of content headers and a data field that can be populated dynamically. If this field is populated it will override the default request creation behaviour."). + ).Description("EXPERIMENTAL: Create explicit multipart HTTP requests by specifying an array of parts to add to the request, each part specified consists of content headers and a data field that can be populated dynamically. If this field is populated it will override the default request creation behavior."). Advanced().Version("3.63.0").Default([]any{}), )) } diff --git a/internal/impl/io/output_http_server.go b/internal/impl/io/output_http_server.go index 9a1425c7d6..7e2d82f81c 100644 --- a/internal/impl/io/output_http_server.go +++ b/internal/impl/io/output_http_server.go @@ -106,11 +106,11 @@ func hsoSpec() *service.ConfigSpec { Stable(). Categories("Network"). Summary(`Sets up an HTTP server that will send messages over HTTP(S) GET requests. HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified.`). - Description(`Sets up an HTTP server that will send messages over HTTP(S) GET requests. If the `+"`address`"+` config field is left blank the [service-wide HTTP server](/docs/components/http/about) will be used. + Description(`Sets up an HTTP server that will send messages over HTTP(S) GET requests. If the `+"`address`"+` config field is left blank the xref:components:http/about.adoc[service-wide HTTP server] will be used. Three endpoints will be registered at the paths specified by the fields `+"`path`, `stream_path` and `ws_path`"+`. Which allow you to consume a single message batch, a continuous stream of line delimited messages, or a websocket of messages for each request respectively. -When messages are batched the `+"`path`"+` endpoint encodes the batch according to [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). This behaviour can be overridden by [archiving your batches](/docs/configuration/batching#post-batch-processing). +When messages are batched the `+"`path`"+` endpoint encodes the batch according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be overridden by xref:configuration:batching.adoc#post-batch-processing[archiving your batches]. Please note, messages are considered delivered as soon as the data is written to the client. There is no concept of at least once delivery on this output. diff --git a/internal/impl/io/processor_command.go b/internal/impl/io/processor_command.go index 8f66b0e3ac..47e7cfa0f4 100644 --- a/internal/impl/io/processor_command.go +++ b/internal/impl/io/processor_command.go @@ -24,13 +24,13 @@ func commandProcSpec() *service.ConfigSpec { Description(` The specified command is executed for each message processed, with the raw bytes of the message being fed into the stdin of the command process, and the resulting message having its contents replaced with the stdout of it. -## Performance +== Performance -Since this processor executes a new process for each message performance will likely be an issue for high throughput streams. If this is the case then consider using the [`+"`subprocess` processor"+`](/docs/components/processors/subprocess) instead as it keeps the underlying process alive long term and uses codecs to insert and extract inputs and outputs to it via stdin/stdout. +Since this processor executes a new process for each message performance will likely be an issue for high throughput streams. If this is the case then consider using the xref:components:processors/subprocess.adoc[`+"`subprocess` processor"+`] instead as it keeps the underlying process alive long term and uses codecs to insert and extract inputs and outputs to it via stdin/stdout. -## Error Handling +== Error handling -If a non-zero error code is returned by the command then an error containing the entirety of stderr (or a generic message if nothing is written) is set on the message. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about these patterns [here](/docs/configuration/error_handling). +If a non-zero error code is returned by the command then an error containing the entirety of stderr (or a generic message if nothing is written) is set on the message. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about xref:configuration:error_handling.adoc[these patterns]. If the command is successful but stderr is written to then a metadata field `+"`command_stderr`"+` is populated with its contents. `). @@ -39,13 +39,13 @@ If the command is successful but stderr is written to then a metadata field `+"` Description("The name of the command to execute."). Examples("bash", "go", "${! @command }"), service.NewBloblangField(cpArgsField). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) that, when specified, should resolve into an array of arguments to pass to the command. Command arguments are expressed this way in order to support dynamic behaviour."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that, when specified, should resolve into an array of arguments to pass to the command. Command arguments are expressed this way in order to support dynamic behavior."). Optional(). Examples(`[ "-c", this.script_path ]`), ). Example( "Cron Scheduled Command", - `This example uses a [`+"`generate`"+` input](/docs/components/inputs/generate) to trigger a command on a cron schedule:`, + `This example uses a xref:components:inputs/generate.adoc[`+"`generate`"+` input] to trigger a command on a cron schedule:`, ` input: generate: diff --git a/internal/impl/io/processor_http.go b/internal/impl/io/processor_http.go index 4352d7c436..a471a76c96 100644 --- a/internal/impl/io/processor_http.go +++ b/internal/impl/io/processor_http.go @@ -15,13 +15,13 @@ func httpProcSpec() *service.ConfigSpec { Categories("Integration"). Summary("Performs an HTTP request using a message batch as the request body, and replaces the original message parts with the body of the response."). Description(` -The `+"`rate_limit`"+` field can be used to specify a rate limit [resource](/docs/components/rate_limits/about) to cap the rate of requests across all parallel components service wide. +The `+"`rate_limit`"+` field can be used to specify a rate limit xref:components:rate_limits/about.adoc[resource] to cap the rate of requests across all parallel components service wide. -The URL and header values of this type can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). +The URL and header values of this type can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. -In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the `+"[`branch` processor](/docs/components/processors/branch)"+`. +In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the `+"xref:components:processors/branch.adoc[`branch` processor]"+`. -## Response Codes +== Response codes Benthos considers any response code between 200 and 299 inclusive to indicate a successful response, you can add more success status codes with the field `+"`successful_on`"+`. @@ -29,18 +29,18 @@ When a request returns a response code within the `+"`backoff_on`"+` field it wi When a request returns a response code within the `+"`drop_on`"+` field it will not be reattempted and is immediately considered a failed request. -## Adding Metadata +== Add metadata If the request returns an error response code this processor sets a metadata field `+"`http_status_code`"+` on the resulting message. Use the field `+"`extract_headers`"+` to specify rules for which other headers should be copied into the resulting message from the response. -## Error Handling +== Error handling -When all retry attempts for a message are exhausted the processor cancels the attempt. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about these patterns [here](/docs/configuration/error_handling).`). +When all retry attempts for a message are exhausted the processor cancels the attempt. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about xref:configuration:error_handling.adoc[these patterns].`). Example( "Branched Request", - `This example uses a `+"[`branch` processor](/docs/components/processors/branch/)"+` to strip the request message into an empty body, grab an HTTP payload, and place the result back into the original message at the path `+"`repo.status`"+`:`, + `This example uses a `+"xref:components:processors/branch.adoc[`branch` processor]"+` to strip the request message into an empty body, grab an HTTP payload, and place the result back into the original message at the path `+"`repo.status`"+`:`, ` pipeline: processors: @@ -56,7 +56,7 @@ pipeline: `, ). Field(httpclient.ConfigField("POST", false, - service.NewBoolField("batch_as_multipart").Description("Send message batches as a single request using [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html).").Advanced().Default(false), + service.NewBoolField("batch_as_multipart").Description("Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341].").Advanced().Default(false), service.NewBoolField("parallel").Description("When processing batched messages, whether to send messages of the batch in parallel, otherwise they are sent serially.").Default(false)), ) } diff --git a/internal/impl/io/processor_subprocess.go b/internal/impl/io/processor_subprocess.go index 293a98ce90..fea4c08cd4 100644 --- a/internal/impl/io/processor_subprocess.go +++ b/internal/impl/io/processor_subprocess.go @@ -47,23 +47,24 @@ func subProcSpec() *service.ConfigSpec { Stable(). Summary("Executes a command as a subprocess and, for each message, will pipe its contents to the stdin stream of the process followed by a newline."). Description(` -:::info -This processor keeps the subprocess alive and requires very specific behaviour from the command executed. If you wish to simply execute a command for each message take a look at the [`+"`command`"+` processor](/docs/components/processors/command) instead. -::: +[NOTE] +==== +This processor keeps the subprocess alive and requires very specific behavior from the command executed. If you wish to simply execute a command for each message take a look at the xref:components:processors/command.adoc[`+"`command`"+` processor] instead. +==== -The subprocess must then either return a line over stdout or stderr. If a response is returned over stdout then its contents will replace the message. If a response is instead returned from stderr it will be logged and the message will continue unchanged and will be [marked as failed](/docs/configuration/error_handling). +The subprocess must then either return a line over stdout or stderr. If a response is returned over stdout then its contents will replace the message. If a response is instead returned from stderr it will be logged and the message will continue unchanged and will be xref:configuration:error_handling.adoc[marked as failed]. -Rather than separating data by a newline it's possible to specify alternative `+"[`codec_send`](#codec_send) and [`codec_recv`](#codec_recv)"+` values, which allow binary messages to be encoded for logical separation. +Rather than separating data by a newline it's possible to specify alternative `+"<> and <>"+` values, which allow binary messages to be encoded for logical separation. The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. The field `+"`max_buffer`"+` defines the maximum response size able to be read from the subprocess. This value should be set significantly above the real expected maximum response size. -## Subprocess requirements +== Subprocess requirements It is required that subprocesses flush their stdout and stderr pipes for each line. Benthos will attempt to keep the process alive for as long as the pipeline is running. If the process exits early it will be restarted. -## Messages containing line breaks +== Messages containing line breaks If a message contains line breaks each line of the message is piped to the subprocess and flushed, and a response is expected from the subprocess before another line is fed in.`). Fields( @@ -201,7 +202,7 @@ func (e *subprocessProc) getSendSubprocessorFunc(codec string) (func(part *messa return nil }, nil } - return nil, fmt.Errorf("unrecognized codec_send value: %v", codec) + return nil, fmt.Errorf("unrecognised codec_send value: %v", codec) } type subprocWrapper struct { diff --git a/internal/impl/jaeger/tracer_jaeger.go b/internal/impl/jaeger/tracer_jaeger.go index 92e33a9ee1..01dd3a79d3 100644 --- a/internal/impl/jaeger/tracer_jaeger.go +++ b/internal/impl/jaeger/tracer_jaeger.go @@ -39,7 +39,7 @@ type jaegerConfig struct { func jaegerConfigSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). - Summary("Send tracing events to a [Jaeger](https://www.jaegertracing.io/) agent or collector."). + Summary("Send tracing events to a https://www.jaegertracing.io/[Jaeger] agent or collector."). Fields( service.NewStringField(jtFieldAgentAddress). Description("The address of a Jaeger agent to send tracing events to."). diff --git a/internal/impl/javascript/processor.go b/internal/impl/javascript/processor.go index 96bd2f007c..c7c9778ce1 100644 --- a/internal/impl/javascript/processor.go +++ b/internal/impl/javascript/processor.go @@ -44,19 +44,19 @@ func javascriptProcessorConfig() *service.ConfigSpec { Version("4.14.0"). Summary("Executes a provided JavaScript code block or file for each message."). Description(` -The [execution engine](https://github.com/dop251/goja) behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. +The https://github.com/dop251/goja[execution engine] behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. -Imports via `+"`require`"+` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be [found here](https://github.com/dop251/goja#known-incompatibilities-and-caveats). +Imports via `+"`require`"+` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be found on https://github.com/dop251/goja#known-incompatibilities-and-caveats[GitHub]. -This processor is implemented using the [github.com/dop251/goja](https://github.com/dop251/goja) library.`). +This processor is implemented using the https://github.com/dop251/goja[github.com/dop251/goja] library.`). Footnotes(` -## Runtime +== Runtime -In order to optimise code execution JS runtimes are created on demand (in order to support parallel execution) and are reused across invocations. Therefore, it is important to understand that global state created by your programs will outlive individual invocations. In order for your programs to avoid failing after the first invocation ensure that you do not define variables at the global scope. +In order to optimize code execution JS runtimes are created on demand (in order to support parallel execution) and are reused across invocations. Therefore, it is important to understand that global state created by your programs will outlive individual invocations. In order for your programs to avoid failing after the first invocation ensure that you do not define variables at the global scope. -Although technically possible, it is recommended that you do not rely on the global state for maintaining state across invocations as the pooling nature of the runtimes will prevent deterministic behaviour. We aim to support deterministic strategies for mutating global state in the future. +Although technically possible, it is recommended that you do not rely on the global state for maintaining state across invocations as the pooling nature of the runtimes will prevent deterministic behavior. We aim to support deterministic strategies for mutating global state in the future. -## Functions +== Functions `+description.String()+` `). Field(service.NewInterpolatedStringField(codeField). diff --git a/internal/impl/kafka/input_kafka_franz.go b/internal/impl/kafka/input_kafka_franz.go index 628574cd73..902bf22903 100644 --- a/internal/impl/kafka/input_kafka_franz.go +++ b/internal/impl/kafka/input_kafka_franz.go @@ -24,17 +24,17 @@ func franzKafkaInputConfig() *service.ConfigSpec { Beta(). Categories("Services"). Version("3.61.0"). - Summary(`A Kafka input using the [Franz Kafka client library](https://github.com/twmb/franz-go).`). + Summary(`A Kafka input using the https://github.com/twmb/franz-go[Franz Kafka client library].`). Description(` When a consumer group is specified this input consumes one or more topics where partitions will automatically balance across any other connected clients with the same consumer group. When a consumer group is not specified topics can either be consumed in their entirety or with explicit partitions. This input often out-performs the traditional ` + "`kafka`" + ` input as well as providing more useful logs and error messages. -### Metadata +== Metadata This input adds the following metadata fields to each message: -` + "``` text" + ` +` + "```text" + ` - kafka_key - kafka_topic - kafka_partition @@ -93,7 +93,7 @@ Finally, it's also possible to specify an explicit offset to consume from by add Field(saslField()). Field(service.NewBoolField("multi_header").Description("Decode headers into lists to allow handling of multiple values with the same key").Default(false).Advanced()). Field(service.NewBatchPolicyField("batching"). - Description("Allows you to configure a [batching policy](/docs/configuration/batching) that applies to individual topic partitions in order to batch messages together before flushing them for processing. Batching can be beneficial for performance as well as useful for windowed processing, and doing so this way preserves the ordering of topic partitions."). + Description("Allows you to configure a xref:configuration:batching.adoc[batching policy] that applies to individual topic partitions in order to batch messages together before flushing them for processing. Batching can be beneficial for performance as well as useful for windowed processing, and doing so this way preserves the ordering of topic partitions."). Advanced()). LintRule(` let has_topic_partitions = this.topics.any(t -> t.contains(":")) diff --git a/internal/impl/kafka/input_sarama_kafka.go b/internal/impl/kafka/input_sarama_kafka.go index 5361dd99e6..b997c4018e 100644 --- a/internal/impl/kafka/input_sarama_kafka.go +++ b/internal/impl/kafka/input_sarama_kafka.go @@ -43,17 +43,17 @@ func iskConfigSpec() *service.ConfigSpec { Description(` Offsets are managed within Kafka under the specified consumer group, and partitions for each topic are automatically balanced across members of the consumer group. -The Kafka input allows parallel processing of messages from different topic partitions, and messages of the same topic partition are processed with a maximum parallelism determined by the field `+"[`checkpoint_limit`](#checkpoint_limit)"+`. +The Kafka input allows parallel processing of messages from different topic partitions, and messages of the same topic partition are processed with a maximum parallelism determined by the field `+"<>"+`. -In order to enforce ordered processing of partition messages set the `+"[`checkpoint_limit`](#checkpoint_limit) to `1`"+` and this will force partitions to be processed in lock-step, where a message will only be processed once the prior message is delivered. +In order to enforce ordered processing of partition messages set the `+"> to `1`"+` and this will force partitions to be processed in lock-step, where a message will only be processed once the prior message is delivered. -Batching messages before processing can be enabled using the `+"[`batching`](#batching)"+` field, and this batching is performed per-partition such that messages of a batch will always originate from the same partition. This batching mechanism is capable of creating batches of greater size than the `+"[`checkpoint_limit`](#checkpoint_limit)"+`, in which case the next batch will only be created upon delivery of the current one. +Batching messages before processing can be enabled using the `+"<>"+` field, and this batching is performed per-partition such that messages of a batch will always originate from the same partition. This batching mechanism is capable of creating batches of greater size than the `+"<>"+`, in which case the next batch will only be created upon delivery of the current one. -### Metadata +== Metadata This input adds the following metadata fields to each message: -`+"``` text"+` +`+"```text"+` - kafka_key - kafka_topic - kafka_partition @@ -66,19 +66,19 @@ This input adds the following metadata fields to each message: The field `+"`kafka_lag`"+` is the calculated difference between the high water mark offset of the partition at the time of ingestion and the current message offset. -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. -### Ordering +== Ordering By default messages of a topic partition can be processed in parallel, up to a limit determined by the field `+"`checkpoint_limit`"+`. However, if strict ordered processing is required then this value must be set to 1 in order to process shard messages in lock-step. When doing so it is recommended that you perform batching at this component for performance as it will not be possible to batch lock-stepped messages at the output level. -### Troubleshooting +== Troubleshooting -If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer `+"[`kafka_franz` input](/docs/components/inputs/kafka_franz)"+`. +If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer `+"xref:components:inputs/kafka_franz.adoc[`kafka_franz` input]"+`. - I'm seeing logs that report `+"`Failed to connect to kafka: kafka: client has run out of available brokers to talk to (Is your cluster reachable?)`"+`, but the brokers are definitely reachable. -Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have [enabled TLS](#tlsenabled) if applicable.`). +Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have <> if applicable.`). Fields( service.NewStringListField(iskFieldAddresses). Description("A list of broker addresses to connect to. If an item of the list contains commas it will be expanded into multiple addresses."). diff --git a/internal/impl/kafka/output_kafka_franz.go b/internal/impl/kafka/output_kafka_franz.go index 0a0ccbaac4..da168f6eaa 100644 --- a/internal/impl/kafka/output_kafka_franz.go +++ b/internal/impl/kafka/output_kafka_franz.go @@ -21,7 +21,7 @@ func franzKafkaOutputConfig() *service.ConfigSpec { Beta(). Categories("Services"). Version("3.61.0"). - Summary("A Kafka output using the [Franz Kafka client library](https://github.com/twmb/franz-go)."). + Summary("A Kafka output using the https://github.com/twmb/franz-go[Franz Kafka client library]."). Description(` Writes a batch of messages to Kafka brokers and waits for acknowledgement before propagating it back to the input. diff --git a/internal/impl/kafka/output_sarama_kafka.go b/internal/impl/kafka/output_sarama_kafka.go index 7cb67a9b1c..6ae80b759f 100644 --- a/internal/impl/kafka/output_sarama_kafka.go +++ b/internal/impl/kafka/output_sarama_kafka.go @@ -54,25 +54,25 @@ func OSKConfigSpec() *service.ConfigSpec { Description(` The config field `+"`ack_replicas`"+` determines whether we wait for acknowledgement from all replicas or just a single broker. -Both the `+"`key` and `topic`"+` fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). +Both the `+"`key` and `topic`"+` fields can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. -[Metadata](/docs/configuration/metadata) will be added to each message sent as headers (version 0.11+), but can be restricted using the field `+"[`metadata`](#metadata)"+`. +xref:configuration:metadata.adoc[Metadata] will be added to each message sent as headers (version 0.11+), but can be restricted using the field `+"<>"+`. -### Strict Ordering and Retries +== Strict ordering and retries When strict ordering is required for messages written to topic partitions it is important to ensure that both the field `+"`max_in_flight` is set to `1` and that the field `retry_as_batch` is set to `true`"+`. You must also ensure that failed batches are never rerouted back to the same output. This can be done by setting the field `+"`max_retries` to `0` and `backoff.max_elapsed_time`"+` to empty, which will apply back pressure indefinitely until the batch is sent successfully. -However, this also means that manual intervention will eventually be required in cases where the batch cannot be sent due to configuration problems such as an incorrect `+"`max_msg_bytes`"+` estimate. A less strict but automated alternative would be to route failed batches to a dead letter queue using a `+"[`fallback` broker](/docs/components/outputs/fallback)"+`, but this would allow subsequent batches to be delivered in the meantime whilst those failed batches are dealt with. +However, this also means that manual intervention will eventually be required in cases where the batch cannot be sent due to configuration problems such as an incorrect `+"`max_msg_bytes`"+` estimate. A less strict but automated alternative would be to route failed batches to a dead letter queue using a `+"xref:components:outputs/fallback.adoc[`fallback` broker]"+`, but this would allow subsequent batches to be delivered in the meantime whilst those failed batches are dealt with. -### Troubleshooting +== Troubleshooting -If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer `+"[`kafka_franz` output](/docs/components/outputs/kafka_franz)"+`. +If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer `+"xref:components:outputs/kafka_franz.adoc[`kafka_franz` output]"+`. - I'm seeing logs that report `+"`Failed to connect to kafka: kafka: client has run out of available brokers to talk to (Is your cluster reachable?)`"+`, but the brokers are definitely reachable. -Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have [enabled TLS](#tlsenabled) if applicable.`+service.OutputPerformanceDocs(true, true)). +Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have <> if applicable.`+service.OutputPerformanceDocs(true, true)). Fields( service.NewStringListField(oskFieldAddresses). Description("A list of broker addresses to connect to. If an item of the list contains commas it will be expanded into multiple addresses."). diff --git a/internal/impl/kafka/sasl.go b/internal/impl/kafka/sasl.go index 394c927adc..88b78bdfb7 100644 --- a/internal/impl/kafka/sasl.go +++ b/internal/impl/kafka/sasl.go @@ -205,7 +205,7 @@ func SaramaSASLField() *service.ConfigField { service.NewStringAnnotatedEnumField(saramaFieldSASLMechanism, map[string]string{ "none": "Default, no SASL authentication.", - "PLAIN": "Plain text authentication. NOTE: When using plain text auth it is extremely likely that you'll also need to [enable TLS](#tlsenabled).", + "PLAIN": "Plain text authentication. NOTE: When using plain text auth it is extremely likely that you'll also need to <>.", "OAUTHBEARER": "OAuth Bearer based authentication.", "SCRAM-SHA-256": "Authentication using the SCRAM-SHA-256 mechanism.", "SCRAM-SHA-512": "Authentication using the SCRAM-SHA-512 mechanism.", @@ -225,7 +225,7 @@ func SaramaSASLField() *service.ConfigField { Description("A static OAUTHBEARER access token"). Default(""), service.NewStringField(saramaFieldSASLTokenCache). - Description("Instead of using a static `access_token` allows you to query a [`cache`](/docs/components/caches/about) resource to fetch OAUTHBEARER tokens from"). + Description("Instead of using a static `access_token` allows you to query a xref:components:caches/about.adoc[`cache`] resource to fetch OAUTHBEARER tokens from"). Default(""), service.NewStringField(saramaFieldSASLTokenKey). Description("Required when using a `token_cache`, the key to query the cache with for tokens."). diff --git a/internal/impl/lang/bloblang.go b/internal/impl/lang/bloblang.go index 2798c76014..ac959de5f1 100644 --- a/internal/impl/lang/bloblang.go +++ b/internal/impl/lang/bloblang.go @@ -24,7 +24,7 @@ func init() { slugSpec := bloblang.NewPluginSpec(). Beta(). Category("String Manipulation"). - Description(`Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its [docs](https://pkg.go.dev/github.com/gosimple/slug) for more information.`). + Description(`Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its https://pkg.go.dev/github.com/gosimple/slug[docs] for more information.`). Version("4.2.0"). Example("Creates a slug from an English string", `root.slug = this.value.slug()`, @@ -57,13 +57,13 @@ func init() { fakerSpec := bloblang.NewPluginSpec(). Beta(). Category("Fake Data Generation"). - Description("Takes in a string that maps to a [faker](https://github.com/go-faker/faker) function and returns the result from that faker function. "+ + Description("Takes in a string that maps to a https://github.com/go-faker/faker[faker] function and returns the result from that faker function. "+ "Returns an error if the given string doesn't match a supported faker function. Supported functions: `latitude`, `longitude`, `unix_time`, "+ "`date`, `time_string`, `month_name`, `year_string`, `day_of_week`, `day_of_month`, `timestamp`, `century`, `timezone`, `time_period`, "+ "`email`, `mac_address`, `domain_name`, `url`, `username`, `ipv4`, `ipv6`, `password`, `jwt`, `word`, `sentence`, `paragraph`, "+ "`cc_type`, `cc_number`, `currency`, `amount_with_currency`, `title_male`, `title_female`, `first_name`, `first_name_male`, "+ "`first_name_female`, `last_name`, `name`, `gender`, `chinese_first_name`, `chinese_last_name`, `chinese_name`, `phone_number`, "+ - "`toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the [faker](https://github.com/go-faker/faker) docs "+ + "`toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the https://github.com/go-faker/faker[faker] docs "+ "for details on these functions."). Param(bloblang.NewStringParam("function").Description("The name of the function to use to generate the value.").Default("")). Example("Use `time_string` to generate a time in the format `00:00:00`:", @@ -72,7 +72,7 @@ func init() { `root.email = fake("email")`). Example("Use `jwt` to generate a JWT token:", `root.jwt = fake("jwt")`). - Example("Use `uuid_hyphenated` to generate a hypenated UUID:", + Example("Use `uuid_hyphenated` to generate a hyphenated UUID:", `root.uuid = fake("uuid_hyphenated")`) if err := bloblang.RegisterFunctionV2( diff --git a/internal/impl/maxmind/bloblang_geoip.go b/internal/impl/maxmind/bloblang_geoip.go index 350b3f79cd..673471afa8 100644 --- a/internal/impl/maxmind/bloblang_geoip.go +++ b/internal/impl/maxmind/bloblang_geoip.go @@ -16,7 +16,7 @@ func registerMaxmindMethodSpec(name, entity string, fn func(*geoip2.Reader, net. bloblang.NewPluginSpec(). Experimental(). Category("GeoIP"). - Description(fmt.Sprintf("Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the %v associated with it.", entity)). + Description(fmt.Sprintf("Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the %v associated with it.", entity)). Param(bloblang.NewStringParam("path").Description("A path to an mmdb (maxmind) file.")), func(args *bloblang.ParsedParams) (bloblang.Method, error) { path, err := args.GetString("path") diff --git a/internal/impl/mongodb/common.go b/internal/impl/mongodb/common.go index a685611b3c..69c614404f 100644 --- a/internal/impl/mongodb/common.go +++ b/internal/impl/mongodb/common.go @@ -296,18 +296,18 @@ const ( func writeMapsFields() []*service.ConfigField { return []*service.ConfigField{ service.NewBloblangField(commonFieldDocumentMap). - Description("A bloblang map representing a document to store within MongoDB, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). The document map is required for the operations " + + Description("A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The document map is required for the operations " + "insert-one, replace-one and update-one."). Examples(mapExamples()...). Default(""), service.NewBloblangField(commonFieldFilterMap). - Description("A bloblang map representing a filter for a MongoDB command, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). The filter map is required for all operations except " + + Description("A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The filter map is required for all operations except " + "insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should " + "have the fields required to locate the document to delete."). Examples(mapExamples()...). Default(""), service.NewBloblangField(commonFieldHintMap). - Description("A bloblang map representing the hint for the MongoDB command, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). This map is optional and is used with all operations " + + Description("A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. This map is optional and is used with all operations " + "except insert-one. It is used to improve performance of finding the documents in the mongodb."). Examples(mapExamples()...). Default(""), diff --git a/internal/impl/mongodb/input.go b/internal/impl/mongodb/input.go index 720b5d8d1c..32a49ec454 100644 --- a/internal/impl/mongodb/input.go +++ b/internal/impl/mongodb/input.go @@ -24,7 +24,7 @@ func mongoConfigSpec() *service.ConfigSpec { Version("3.64.0"). Categories("Services"). Summary("Executes a query and creates a message for each document received."). - Description(`Once the documents from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute).`). + Description(`Once the documents from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute).`). Fields(clientFields()...). Field(service.NewStringField("collection").Description("The collection to select from.")). Field(service.NewStringEnumField("operation", FindInputOperation, AggregateInputOperation). diff --git a/internal/impl/mqtt/input.go b/internal/impl/mqtt/input.go index b5bce1eb67..9ef4dc1b11 100644 --- a/internal/impl/mqtt/input.go +++ b/internal/impl/mqtt/input.go @@ -22,11 +22,11 @@ func inputConfigSpec() *service.ConfigSpec { Categories("Services"). Summary("Subscribe to topics on MQTT brokers."). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: -`+"``` text"+` +`+"```text"+` - mqtt_duplicate - mqtt_qos - mqtt_retained @@ -34,7 +34,7 @@ This input adds the following metadata fields to each message: - mqtt_message_id `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields(ClientFields()...). Fields( service.NewStringListField(miFieldTopics). diff --git a/internal/impl/mqtt/output.go b/internal/impl/mqtt/output.go index af37ba7323..96ccddde00 100644 --- a/internal/impl/mqtt/output.go +++ b/internal/impl/mqtt/output.go @@ -26,7 +26,7 @@ func outputConfigSpec() *service.ConfigSpec { Categories("Services"). Summary("Pushes messages to an MQTT broker."). Description(` -The `+"`topic`"+` field can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part.`+service.OutputPerformanceDocs(true, false)). +The `+"`topic`"+` field can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages these interpolations are performed per message part.`+service.OutputPerformanceDocs(true, false)). Fields(ClientFields()...). Fields( service.NewInterpolatedStringField(moFieldTopic). diff --git a/internal/impl/msgpack/bloblang.go b/internal/impl/msgpack/bloblang.go index d0b7f95298..55891f1262 100644 --- a/internal/impl/msgpack/bloblang.go +++ b/internal/impl/msgpack/bloblang.go @@ -12,7 +12,7 @@ func init() { msgpackParseSpec := bloblang.NewPluginSpec(). Category("Parsing"). - Description("Parses a [MessagePack](https://msgpack.org/) message into a structured document."). + Description("Parses a https://msgpack.org/[MessagePack] message into a structured document."). Example("", `root = content().decode("hex").parse_msgpack()`, [2]string{ @@ -47,7 +47,7 @@ func init() { msgpackFormatSpec := bloblang.NewPluginSpec(). Category("Parsing"). - Description("Formats data as a [MessagePack](https://msgpack.org/) message in bytes format."). + Description("Formats data as a https://msgpack.org/[MessagePack] message in bytes format."). Example("", `root = this.format_msgpack().encode("hex")`, [2]string{ diff --git a/internal/impl/msgpack/processor.go b/internal/impl/msgpack/processor.go index ef4518d2d8..52ed350d9a 100644 --- a/internal/impl/msgpack/processor.go +++ b/internal/impl/msgpack/processor.go @@ -13,7 +13,7 @@ func processorConfig() *service.ConfigSpec { return service.NewConfigSpec(). Beta(). Categories("Parsing"). - Summary("Converts messages to or from the [MessagePack](https://msgpack.org/) format."). + Summary("Converts messages to or from the https://msgpack.org/[MessagePack] format."). Field(service.NewStringAnnotatedEnumField("operator", map[string]string{ "to_json": "Convert MessagePack messages to JSON format", "from_json": "Convert JSON messages to MessagePack format", diff --git a/internal/impl/nats/auth.go b/internal/impl/nats/auth.go index fba2235684..af87233e06 100644 --- a/internal/impl/nats/auth.go +++ b/internal/impl/nats/auth.go @@ -16,35 +16,37 @@ import ( ) func authDescription() string { - return `### Authentication + return ` -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). +== Authentication -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). +There are several components within Benthos which uses NATS services. You will find that each of these components +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. -#### NKey file +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. + +=== NKey file The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the ` + "`nkey_file`" + ` field. -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). +https://docs.nats.io/developing-with-nats/security/nkey[More details]. -#### User Credentials +=== User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server which is configured to use this authentication scheme. The ` + "`user_credentials_file`" + ` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. Alternatively, the ` + "`user_jwt`" + ` field can contain a plain text JWT and the ` + "`user_nkey_seed`" + `can contain the plain text NKey Seed. -More details [here](https://docs.nats.io/developing-with-nats/security/creds).` +https://docs.nats.io/developing-with-nats/security/creds[More details].` } func authFieldSpec() *service.ConfigField { @@ -90,7 +92,7 @@ func authConfToOptions(auth authConfig, fs *service.FS) []nats.Option { // Previously we used nats.UserCredentials to authenticate. In order to // support a custom FS implementation in our NATS components, we needed to - // switch to the nats.UserJWT option, while still preserving the behavior + // switch to the nats.UserJWT option, while still preserving the behaviour // of the nats.UserCredentials option, which includes things like path // expansing, home directory support and wiping credentials held in memory if auth.UserCredentialsFile != "" { diff --git a/internal/impl/nats/docs.go b/internal/impl/nats/docs.go index 1e428ade73..6567407777 100644 --- a/internal/impl/nats/docs.go +++ b/internal/impl/nats/docs.go @@ -13,7 +13,7 @@ const ( ) func connectionNameDescription() string { - return `### Connection Name + return `== Connection name When monitoring and managing a production NATS system, it is often useful to know which connection a message was send/received from. This can be achieved by diff --git a/internal/impl/nats/input.go b/internal/impl/nats/input.go index cb2213678b..2a52beea46 100644 --- a/internal/impl/nats/input.go +++ b/internal/impl/nats/input.go @@ -17,17 +17,17 @@ func natsInputConfig() *service.ConfigSpec { Categories("Services"). Summary(`Subscribe to a NATS subject.`). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: -` + "``` text" + ` +` + "```text" + ` - nats_subject - nats_reply_subject - All message headers (when supported by the connection) ` + "```" + ` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. ` + connectionNameDescription() + authDescription()). Fields(connectionHeadFields()...). diff --git a/internal/impl/nats/input_jetstream.go b/internal/impl/nats/input_jetstream.go index c39d798b97..fef9a6eea3 100644 --- a/internal/impl/nats/input_jetstream.go +++ b/internal/impl/nats/input_jetstream.go @@ -22,11 +22,11 @@ func natsJetStreamInputConfig() *service.ConfigSpec { Version("3.46.0"). Summary("Reads messages from NATS JetStream subjects."). Description(` -### Consuming Mirrored Streams +== Consume mirrored streams In the case where a stream being consumed is mirrored from a different JetStream domain the stream cannot be resolved from the subject name alone, and so the stream name as well as the subject (if applicable) must both be specified. -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -41,7 +41,7 @@ This input adds the following metadata fields to each message: ` + "```" + ` You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. ` + connectionNameDescription() + authDescription()). Fields(connectionHeadFields()...). diff --git a/internal/impl/nats/input_kv.go b/internal/impl/nats/input_kv.go index 330dc5f2ad..bce8322c91 100644 --- a/internal/impl/nats/input_kv.go +++ b/internal/impl/nats/input_kv.go @@ -26,7 +26,7 @@ func natsKVInputConfig() *service.ConfigSpec { Version("4.12.0"). Summary("Watches for updates in a NATS key-value bucket."). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: diff --git a/internal/impl/nats/input_stream.go b/internal/impl/nats/input_stream.go index 747d41e813..7dcc626066 100644 --- a/internal/impl/nats/input_stream.go +++ b/internal/impl/nats/input_stream.go @@ -82,24 +82,26 @@ func siSpec() *service.ConfigSpec { Categories("Services"). Summary(`Subscribe to a NATS Stream subject. Joining a queue is optional and allows multiple clients of a subject to consume using queue semantics.`). Description(` -:::caution Deprecation Notice -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use [JetStream](https://docs.nats.io/nats-concepts/jetstream). -::: +[CAUTION] +.Deprecation notice +==== +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +==== Tracking and persisting offsets through a durable name is also optional and works with or without a queue. If a durable name is not provided then subjects are consumed from the most recently published message. When a consumer closes its connection it unsubscribes, when all consumers of a durable queue do this the offsets are deleted. In order to avoid this you can stop the consumers from unsubscribing by setting the field `+"`unsubscribe_on_close` to `false`"+`. -### Metadata +== Metadata This input adds the following metadata fields to each message: -`+"``` text"+` +`+"```text"+` - nats_stream_subject - nats_stream_sequence `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. `+authDescription()). Fields(connectionHeadFields()...). diff --git a/internal/impl/nats/output.go b/internal/impl/nats/output.go index ee4ae8e415..5ed38896e2 100644 --- a/internal/impl/nats/output.go +++ b/internal/impl/nats/output.go @@ -16,7 +16,7 @@ func natsOutputConfig() *service.ConfigSpec { Stable(). Categories("Services"). Summary("Publish to an NATS subject."). - Description(`This output will interpolate functions within the subject field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries). + Description(`This output will interpolate functions within the subject field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here]. ` + connectionNameDescription() + authDescription()). Fields(connectionHeadFields()...). diff --git a/internal/impl/nats/output_kv.go b/internal/impl/nats/output_kv.go index 8a61451d54..422eadb8c2 100644 --- a/internal/impl/nats/output_kv.go +++ b/internal/impl/nats/output_kv.go @@ -24,7 +24,7 @@ func natsKVOutputConfig() *service.ConfigSpec { Summary("Put messages in a NATS key-value bucket."). Description(` The field ` + "`key`" + ` supports -[interpolation functions](/docs/configuration/interpolation#bloblang-queries), allowing +xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing you to create a unique key for each message. ` + connectionNameDescription() + authDescription()). diff --git a/internal/impl/nats/output_stream.go b/internal/impl/nats/output_stream.go index fafe97af81..f11237c641 100644 --- a/internal/impl/nats/output_stream.go +++ b/internal/impl/nats/output_stream.go @@ -53,9 +53,11 @@ func soSpec() *service.ConfigSpec { Categories("Services"). Summary(`Publish to a NATS Stream subject.`). Description(` -:::caution Deprecation Notice -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use [JetStream](https://docs.nats.io/nats-concepts/jetstream). -::: +[CAUTION] +.Deprecation notice +==== +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +==== `+authDescription()+service.OutputPerformanceDocs(true, false)). Fields(connectionHeadFields()...). diff --git a/internal/impl/nats/processor_kv.go b/internal/impl/nats/processor_kv.go index 380e9d5749..af8ac3f901 100644 --- a/internal/impl/nats/processor_kv.go +++ b/internal/impl/nats/processor_kv.go @@ -55,15 +55,15 @@ func natsKVProcessorConfig() *service.ConfigSpec { Version("4.12.0"). Summary("Perform operations on a NATS key-value bucket."). Description(` -### KV Operations +== KV operations -The NATS KV processor supports a multitude of KV operations via the [operation](#operation) field. Along with ` + "`get`" + `, ` + "`put`" + `, and ` + "`delete`" + `, this processor supports atomic operations like ` + "`update`" + ` and ` + "`create`" + `, as well as utility operations like ` + "`purge`" + `, ` + "`history`" + `, and ` + "`keys`" + `. +The NATS KV processor supports a multitude of KV operations via the <> field. Along with ` + "`get`" + `, ` + "`put`" + `, and ` + "`delete`" + `, this processor supports atomic operations like ` + "`update`" + ` and ` + "`create`" + `, as well as utility operations like ` + "`purge`" + `, ` + "`history`" + `, and ` + "`keys`" + `. -### Metadata +== Metadata This processor adds the following metadata fields to each message, depending on the chosen ` + "`operation`" + `: -#### get, get_revision +=== get, get_revision ` + "``` text" + ` - nats_kv_key - nats_kv_bucket @@ -73,7 +73,7 @@ This processor adds the following metadata fields to each message, depending on - nats_kv_created ` + "```" + ` -#### create, update, delete, purge +=== create, update, delete, purge ` + "``` text" + ` - nats_kv_key - nats_kv_bucket @@ -81,7 +81,7 @@ This processor adds the following metadata fields to each message, depending on - nats_kv_operation ` + "```" + ` -#### keys +=== keys ` + "``` text" + ` - nats_kv_bucket ` + "```" + ` @@ -91,7 +91,7 @@ This processor adds the following metadata fields to each message, depending on service.NewStringAnnotatedEnumField(kvpFieldOperation, kvpOperations). Description("The operation to perform on the KV bucket."), service.NewInterpolatedStringField(kvpFieldKey). - Description("The key for each message. Supports [wildcards](https://docs.nats.io/nats-concepts/subjects#wildcards) for the `history` and `keys` operations."). + Description("The key for each message. Supports https://docs.nats.io/nats-concepts/subjects#wildcards[wildcards] for the `history` and `keys` operations."). Example("foo"). Example("foo.bar.baz"). Example("foo.*"). diff --git a/internal/impl/nats/processor_request_reply.go b/internal/impl/nats/processor_request_reply.go index 41cdbf651b..d3d938cfba 100644 --- a/internal/impl/nats/processor_request_reply.go +++ b/internal/impl/nats/processor_request_reply.go @@ -17,7 +17,7 @@ func natsRequestReplyConfig() *service.ConfigSpec { Version("4.27.0"). Summary("Sends a message to a NATS subject and expects a reply, from a NATS subscriber acting as a responder, back."). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -31,7 +31,7 @@ This input adds the following metadata fields to each message: - nats_timestamp_unix_nano ` + "```" + ` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. ` + connectionNameDescription() + authDescription()). Fields(connectionHeadFields()...). diff --git a/internal/impl/nsq/input.go b/internal/impl/nsq/input.go index dd43d7df61..ea5927042b 100644 --- a/internal/impl/nsq/input.go +++ b/internal/impl/nsq/input.go @@ -31,7 +31,7 @@ func inputConfigSpec() *service.ConfigSpec { Categories("Services"). Summary(`Subscribe to an NSQ instance topic and channel.`). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -42,7 +42,7 @@ This input adds the following metadata fields to each message: - nsq_timestamp `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. `). Fields( service.NewStringListField(niFieldNSQDAddrs). diff --git a/internal/impl/nsq/output.go b/internal/impl/nsq/output.go index b30509dec4..dd01d1b2df 100644 --- a/internal/impl/nsq/output.go +++ b/internal/impl/nsq/output.go @@ -25,7 +25,7 @@ func outputConfigSpec() *service.ConfigSpec { Stable(). Categories("Services"). Summary(`Publish to an NSQ topic.`). - Description(`The `+"`topic`"+` field can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part.`+service.OutputPerformanceDocs(true, false)). + Description(`The `+"`topic`"+` field can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages these interpolations are performed per message part.`+service.OutputPerformanceDocs(true, false)). Fields( service.NewStringField(noFieldNSQDAddr). Description("The address of the target NSQD server."), diff --git a/internal/impl/opensearch/output.go b/internal/impl/opensearch/output.go index 203321297a..d86d7e7005 100644 --- a/internal/impl/opensearch/output.go +++ b/internal/impl/opensearch/output.go @@ -136,7 +136,7 @@ func OutputSpec() *service.ConfigSpec { Categories("Services"). Summary(`Publishes messages into an Elasticsearch index. If the index does not exist then it is created with a dynamic mapping.`). Description(` -Both the `+"`id` and `index`"+` fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part.`+service.OutputPerformanceDocs(true, true)). +Both the `+"`id` and `index`"+` fields can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. When sending batched messages these interpolations are performed per message part.`+service.OutputPerformanceDocs(true, true)). Fields( service.NewStringListField(esoFieldURLs). Description("A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs."). @@ -176,7 +176,7 @@ Both the `+"`id` and `index`"+` fields can be dynamically set using function int service.NewBatchPolicyField(esoFieldBatching), AWSField(), ). - Example("Updating Documents", "When [updating documents](https://opensearch.org/docs/latest/api-reference/document-apis/update-document/) the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors.", ` + Example("Updating Documents", "When https://opensearch.org/docs/latest/api-reference/document-apis/update-document/[updating documents] the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors.", ` output: processors: - mapping: | diff --git a/internal/impl/otlp/tracer_otlp.go b/internal/impl/otlp/tracer_otlp.go index 4114ba964a..85dab447e0 100644 --- a/internal/impl/otlp/tracer_otlp.go +++ b/internal/impl/otlp/tracer_otlp.go @@ -20,7 +20,7 @@ import ( func oltpSpec() *service.ConfigSpec { return service.NewConfigSpec(). - Summary("Send tracing events to an [Open Telemetry collector](https://opentelemetry.io/docs/collector/)."). + Summary("Send tracing events to an https://opentelemetry.io/docs/collector/[Open Telemetry collector]."). Field(service.NewObjectListField("http", service.NewStringField("address"). Description("The endpoint of a collector to send tracing events to."). diff --git a/internal/impl/parquet/bloblang.go b/internal/impl/parquet/bloblang.go index a47ed9bcfe..e8889573e0 100644 --- a/internal/impl/parquet/bloblang.go +++ b/internal/impl/parquet/bloblang.go @@ -14,7 +14,7 @@ func init() { parquetParseSpec := bloblang.NewPluginSpec(). Category("Parsing"). - Description("Decodes a [Parquet file](https://parquet.apache.org/docs/) into an array of objects, one for each row within the file."). + Description("Decodes a https://parquet.apache.org/docs/[Parquet file] into an array of objects, one for each row within the file."). Param(bloblang.NewBoolParam("byte_array_as_string"). Description("Deprecated: This parameter is no longer used.").Default(false)). Example("", `root = content().parse_parquet()`) diff --git a/internal/impl/parquet/input_parquet.go b/internal/impl/parquet/input_parquet.go index 3b1b308e32..1fbc44c67e 100644 --- a/internal/impl/parquet/input_parquet.go +++ b/internal/impl/parquet/input_parquet.go @@ -18,7 +18,7 @@ func parquetInputConfig() *service.ConfigSpec { return service.NewConfigSpec(). // Stable(). TODO Categories("Local"). - Summary("Reads and decodes [Parquet files](https://parquet.apache.org/docs/) into a stream of structured messages."). + Summary("Reads and decodes https://parquet.apache.org/docs/[Parquet files] into a stream of structured messages."). Field(service.NewStringListField("paths"). Description("A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star)."). Example("/tmp/foo.parquet"). @@ -30,7 +30,7 @@ func parquetInputConfig() *service.ConfigSpec { Advanced()). Field(service.NewAutoRetryNacksToggleField()). Description(` -This input uses [https://github.com/parquet-go/parquet-go](https://github.com/parquet-go/parquet-go), which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This input uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. By default any BYTE_ARRAY or FIXED_LEN_BYTE_ARRAY value will be extracted as a byte slice (` + "`[]byte`" + `) unless the logical type is UTF8, in which case they are extracted as a string (` + "`string`" + `). diff --git a/internal/impl/parquet/processor.go b/internal/impl/parquet/processor.go index afe1da4402..b8fc5ac810 100644 --- a/internal/impl/parquet/processor.go +++ b/internal/impl/parquet/processor.go @@ -18,19 +18,19 @@ func parquetProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). Deprecated(). Categories("Parsing"). - Summary("Converts batches of documents to or from [Parquet files](https://parquet.apache.org/docs/)."). + Summary("Converts batches of documents to or from https://parquet.apache.org/docs/[Parquet files]."). Description(` -### Alternatives +== Alternatives -This processor is now deprecated, it's recommended that you use the new ` + "[`parquet_decode`](/docs/components/processors/parquet_decode) and [`parquet_encode`](/docs/components/processors/parquet_encode)" + ` processors as they provide a number of advantages, the most important of which is better error messages for when schemas are mismatched or files could not be consumed. +This processor is now deprecated, it's recommended that you use the new ` + "xref:components:processors/parquet_decode.adoc[`parquet_decode`] and xref:components:processors/parquet_encode.adoc[`parquet_encode`]" + ` processors as they provide a number of advantages, the most important of which is better error messages for when schemas are mismatched or files could not be consumed. -### Troubleshooting +== Troubleshooting -This processor is experimental and the error messages that it provides are often vague and unhelpful. An error message of the form ` + "`interface {} is nil, not `" + ` implies that a field of the given type was expected but not found in the processed message when writing parquet files. +This processor is experimental and the error messages that it provides are often vague and unhelpful. An error message of the form ` + "`interface \\{} is nil, not `" + ` implies that a field of the given type was expected but not found in the processed message when writing parquet files. Unfortunately the name of the field will sometimes be missing from the error, in which case it's worth double checking the schema you provided to make sure that there are no typos in the field names, and if that doesn't reveal the issue it can help to mark fields as OPTIONAL in the schema and gradually change them back to REQUIRED until the error returns. -### Defining the Schema +== Define the schema The schema must be specified as a JSON string, containing an object that describes the fields expected at the root of each document. Each field can itself have more fields defined, allowing for nested structures: diff --git a/internal/impl/parquet/processor_decode.go b/internal/impl/parquet/processor_decode.go index e1e780a758..9ad92344af 100644 --- a/internal/impl/parquet/processor_decode.go +++ b/internal/impl/parquet/processor_decode.go @@ -16,12 +16,12 @@ func parquetDecodeProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). // Stable(). TODO Categories("Parsing"). - Summary("Decodes [Parquet files](https://parquet.apache.org/docs/) into a batch of structured messages."). + Summary("Decodes https://parquet.apache.org/docs/[Parquet files] into a batch of structured messages."). Field(service.NewBoolField("byte_array_as_string"). - Description("Whether to extract BYTE_ARRAY and FIXED_LEN_BYTE_ARRAY values as strings rather than byte slices in all cases. Values with a logical type of UTF8 will automatically be extracted as strings irrespective of this field. Enabling this field makes serialising the data as JSON more intuitive as `[]byte` values are serialised as base64 encoded strings by default."). + Description("Whether to extract BYTE_ARRAY and FIXED_LEN_BYTE_ARRAY values as strings rather than byte slices in all cases. Values with a logical type of UTF8 will automatically be extracted as strings irrespective of this field. Enabling this field makes serializing the data as JSON more intuitive as `[]byte` values are serialized as base64 encoded strings by default."). Default(false).Deprecated()). Description(` -This processor uses [https://github.com/parquet-go/parquet-go](https://github.com/parquet-go/parquet-go), which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases.`). +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases.`). Version("4.4.0"). Example("Reading Parquet Files from AWS S3", "In this example we consume files from AWS S3 as they're written by listening onto an SQS queue for upload events. We make sure to use the `to_the_end` scanner which means files are read into memory in full, which then allows us to use a `parquet_decode` processor to expand each file into a batch of messages. Finally, we write the data out to local files as newline delimited JSON.", diff --git a/internal/impl/parquet/processor_encode.go b/internal/impl/parquet/processor_encode.go index 50c66e22da..c126e30dcd 100644 --- a/internal/impl/parquet/processor_encode.go +++ b/internal/impl/parquet/processor_encode.go @@ -15,7 +15,7 @@ func parquetEncodeProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). // Stable(). TODO Categories("Parsing"). - Summary("Encodes [Parquet files](https://parquet.apache.org/docs/) from a batch of structured messages."). + Summary("Encodes https://parquet.apache.org/docs/[Parquet files] from a batch of structured messages."). Field(parquetSchemaConfig()). Field(service.NewStringEnumField("default_compression", "uncompressed", "snappy", "gzip", "brotli", "zstd", "lz4raw", @@ -30,7 +30,7 @@ func parquetEncodeProcessorConfig() *service.ConfigSpec { Advanced(). Version("4.11.0")). Description(` -This processor uses [https://github.com/parquet-go/parquet-go](https://github.com/parquet-go/parquet-go), which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. `). Version("4.4.0"). // TODO: Add an example that demonstrates error handling diff --git a/internal/impl/prometheus/metrics_prometheus.go b/internal/impl/prometheus/metrics_prometheus.go index 7ff5a2d871..8d2fc15b3a 100644 --- a/internal/impl/prometheus/metrics_prometheus.go +++ b/internal/impl/prometheus/metrics_prometheus.go @@ -39,9 +39,9 @@ func ConfigSpec() *service.ConfigSpec { Stable(). Summary("Host endpoints (`/metrics` and `/stats`) for Prometheus scraping."). Footnotes(` -## Push Gateway +== Push gateway -The field `+"`push_url`"+` is optional and when set will trigger a push of metrics to a [Prometheus Push Gateway](https://prometheus.io/docs/instrumenting/pushing/) once Benthos shuts down. It is also possible to specify a `+"`push_interval`"+` which results in periodic pushes. +The field `+"`push_url`"+` is optional and when set will trigger a push of metrics to a https://prometheus.io/docs/instrumenting/pushing/[Prometheus Push Gateway] once Benthos shuts down. It is also possible to specify a `+"`push_interval`"+` which results in periodic pushes. The Push Gateway is useful for when Benthos instances are short lived. Do not include the "/metrics/jobs/..." path in the push URL. @@ -87,7 +87,7 @@ If the Push Gateway requires HTTP Basic Authentication it can be configured with Advanced(). Default(false), service.NewURLField(pmFieldPushURL). - Description("An optional [Push Gateway URL](#push-gateway) to push metrics to."). + Description("An optional <> to push metrics to."). Advanced(). Optional(), service.NewDurationField(pmFieldPushInterval). diff --git a/internal/impl/protobuf/processor_protobuf.go b/internal/impl/protobuf/processor_protobuf.go index 2b22631371..ff74734b5e 100644 --- a/internal/impl/protobuf/processor_protobuf.go +++ b/internal/impl/protobuf/processor_protobuf.go @@ -32,22 +32,22 @@ func protobufProcessorSpec() *service.ConfigSpec { Summary(` Performs conversions to or from a protobuf message. This processor uses reflection, meaning conversions can be made directly from the target .proto files. `).Description(` -The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: [https://developers.google.com/protocol-buffers/docs/proto3#json](https://developers.google.com/protocol-buffers/docs/proto3#json) +The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json[https://developers.google.com/protocol-buffers/docs/proto3#json] -Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at [https://github.com/benthosdev/benthos-plugin-example](https://github.com/benthosdev/benthos-plugin-example) +Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at https://github.com/benthosdev/benthos-plugin-example[https://github.com/benthosdev/benthos-plugin-example] -## Operators +== Operators -### `+"`to_json`"+` +=== `+"`to_json`"+` Converts protobuf messages into a generic JSON structure. This makes it easier to manipulate the contents of the document within Benthos. -### `+"`from_json`"+` +=== `+"`from_json`"+` Attempts to create a target protobuf message from a generic JSON structure. `).Fields( service.NewStringEnumField(fieldOperator, "to_json", "from_json"). - Description("The [operator](#operators) to execute"), + Description("The <> to execute"), service.NewStringField(fieldMessage). Description("The fully qualified name of the protobuf message to convert to/from."), service.NewBoolField(fieldDiscardUnknown). diff --git a/internal/impl/pulsar/input.go b/internal/impl/pulsar/input.go index 0b55bb112a..7bf72086da 100644 --- a/internal/impl/pulsar/input.go +++ b/internal/impl/pulsar/input.go @@ -35,7 +35,7 @@ func inputConfigSpec() *service.ConfigSpec { Categories("Services"). Summary("Reads messages from an Apache Pulsar server."). Description(` -### Metadata +== Metadata This input adds the following metadata fields to each message: @@ -52,7 +52,7 @@ This input adds the following metadata fields to each message: ` + "```" + ` You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). +xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. `). Field(service.NewURLField("url"). Description("A URL to connect to."). @@ -68,7 +68,7 @@ You can access these metadata fields using Field(service.NewStringField("subscription_name"). Description("Specify the subscription name for this consumer.")). Field(service.NewStringEnumField("subscription_type", "shared", "key_shared", "failover", "exclusive"). - Description("Specify the subscription type for this consumer.\n\n> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See [Pulsar documentation](https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement) and [this Github issue](https://github.com/apache/pulsar/issues/12208) for more details."). + Description("Specify the subscription type for this consumer.\n\n> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement[Pulsar documentation] and https://github.com/apache/pulsar/issues/12208[this Github issue] for more details."). Default(defaultSubscriptionType)). Field(service.NewObjectField("tls", service.NewStringField("root_cas_file"). diff --git a/internal/impl/pure/bloblang_general.go b/internal/impl/pure/bloblang_general.go index f49afc2ac7..dd26bfae36 100644 --- a/internal/impl/pure/bloblang_general.go +++ b/internal/impl/pure/bloblang_general.go @@ -76,7 +76,7 @@ root.woof_id = null.apply("foos") }, ). Example( - "The `set` parameter can also be utilised to peek at the counter without mutating it by returning `null`.", + "The `set` parameter can also be utilized to peek at the counter without mutating it by returning `null`.", `root.things = counter(set: if this.id == null { null })`, [2]string{`{"id":"a"}`, `{"things":1}`}, [2]string{`{"id":"b"}`, `{"things":2}`}, diff --git a/internal/impl/pure/bloblang_numbers.go b/internal/impl/pure/bloblang_numbers.go index a7a1f363af..d2192188ae 100644 --- a/internal/impl/pure/bloblang_numbers.go +++ b/internal/impl/pure/bloblang_numbers.go @@ -32,7 +32,7 @@ root.d = this.d.$NAME().catch(0) Description(replacer.Replace(` Converts a numerical type into a $LONGNAME, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a $LONGNAME. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use `+"[`.round()`](#round)"+` on the value. Please refer to the [`+"`strconv.ParseInt`"+` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats.`)). +If the value is a string then an attempt will be made to parse it as a $LONGNAME. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`+"`strconv.ParseInt`"+` documentation] for details regarding the supported formats.`)). Example("", exampleOneBody, exampleOneIO). Example("", replacer.Replace(` root = this.$NAME() @@ -109,7 +109,7 @@ func init() { Description(` Converts a numerical type into a 64-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the [`+"`strconv.ParseFloat`"+` documentation](https://pkg.go.dev/strconv#ParseFloat) for details regarding the supported formats.`). +If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`+"`strconv.ParseFloat`"+` documentation] for details regarding the supported formats.`). Example("", ` root.out = this.in.float64() `, @@ -129,7 +129,7 @@ root.out = this.in.float64() Description(` Converts a numerical type into a 32-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the [`+"`strconv.ParseFloat`"+` documentation](https://pkg.go.dev/strconv#ParseFloat) for details regarding the supported formats.`). +If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`+"`strconv.ParseFloat`"+` documentation] for details regarding the supported formats.`). Example("", ` root.out = this.in.float32() `, diff --git a/internal/impl/pure/bloblang_objects.go b/internal/impl/pure/bloblang_objects.go index 57acd4e8ae..4c63600310 100644 --- a/internal/impl/pure/bloblang_objects.go +++ b/internal/impl/pure/bloblang_objects.go @@ -40,7 +40,7 @@ func init() { bloblang.NewPluginSpec(). Category(query.MethodCategoryObjectAndArray). Variadic(). - Description(`Returns an object where all but one or more [field path][field_paths] arguments are removed. Each path specifies a specific field to be retained from the input object, allowing for nested fields. + Description(`Returns an object where all but one or more xref:configuration:field_paths.adoc[field path] arguments are removed. Each path specifies a specific field to be retained from the input object, allowing for nested fields. If a key within a nested path does not exist then it is ignored.`). Example("", `root = this.with("inner.a","inner.c","d")`, diff --git a/internal/impl/pure/bloblang_time.go b/internal/impl/pure/bloblang_time.go index 327dc454e8..8ffd58fec3 100644 --- a/internal/impl/pure/bloblang_time.go +++ b/internal/impl/pure/bloblang_time.go @@ -26,7 +26,7 @@ func init() { Beta(). Static(). Category(query.MethodCategoryTime). - Description(`Returns the result of rounding a timestamp to the nearest multiple of the argument duration (nanoseconds). The rounding behavior for halfway values is to round up. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The `+"[`ts_parse`](#ts_parse)"+` method can be used in order to parse different timestamp formats.`). + Description(`Returns the result of rounding a timestamp to the nearest multiple of the argument duration (nanoseconds). The rounding behavior for halfway values is to round up. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The `+"<>"+` method can be used in order to parse different timestamp formats.`). Param(bloblang.NewInt64Param("duration").Description("A duration measured in nanoseconds to round by.")). Version("4.2.0"). Example("Use the method `parse_duration` to convert a duration string into an integer argument.", @@ -55,7 +55,7 @@ func init() { Beta(). Static(). Category(query.MethodCategoryTime). - Description(`Returns the result of converting a timestamp to a specified timezone. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The `+"[`ts_parse`](#ts_parse)"+` method can be used in order to parse different timestamp formats.`). + Description(`Returns the result of converting a timestamp to a specified timezone. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The `+"<>"+` method can be used in order to parse different timestamp formats.`). Param(bloblang.NewStringParam("tz").Description(`The timezone to change to. If set to "UTC" then the timezone will be UTC. If set to "Local" then the local timezone will be used. Otherwise, the argument is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York".`)). Version("4.3.0"). Example("", @@ -214,9 +214,9 @@ func init() { Category(query.MethodCategoryTime). Beta(). Static(). - Description(`Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as ` + "[`ts_format`](#ts_format)" + `. + Description(`Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as ` + "<>" + `. -The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the ` + "[`ts_strptime`](#ts_strptime)" + ` method.`). +The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the ` + "<>" + ` method.`). Param(bloblang.NewStringParam("format").Description("The format of the target string.")) parseTSSpecDep := asDeprecated(parseTSSpec) @@ -261,14 +261,14 @@ The input format is defined by showing how the reference time, defined to be Mon Category(query.MethodCategoryTime). Beta(). Static(). - Description("Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into [`ts_format`](#ts_format)."). + Description("Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into <>."). Param(bloblang.NewStringParam("format").Description("The format of the target string.")) parseTSStrptimeSpecDep := asDeprecated(parseTSStrptimeSpec) parseTSStrptimeSpec = parseTSStrptimeSpec. Example( - "The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behaviour of the specifier. Please refer to [man 3 strptime](https://linux.die.net/man/3/strptime) for the list of format specifiers.", + "The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strptime[man 3 strptime] for the list of format specifiers.", `root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d")`, [2]string{ `{"doc":{"timestamp":"2020-Aug-14"}}`, @@ -276,7 +276,7 @@ The input format is defined by showing how the reference time, defined to be Mon }, ). Example( - "As an extension provided by the underlying formatting library, [itchyny/timefmt-go](https://github.com/itchyny/timefmt-go), the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported.", + "As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported.", `root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d %H:%M:%S.%f")`, [2]string{ `{"doc":{"timestamp":"2020-Aug-14 11:50:26.371000"}}`, @@ -319,7 +319,7 @@ The input format is defined by showing how the reference time, defined to be Mon Static(). Description(`Attempts to format a timestamp value as a string according to a specified format, or RFC 3339 by default. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. -The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the ` + "[`ts_strftime`](#ts_strftime)" + ` method.`). +The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the ` + "<>" + ` method.`). Param(bloblang.NewStringParam("format").Description("The output format to use.").Default(time.RFC3339Nano)). Param(bloblang.NewStringParam("tz").Description("An optional timezone to use, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used.").Optional()) @@ -405,7 +405,7 @@ The output format is defined by showing how the reference time, defined to be Mo formatTSStrftimeSpec = formatTSStrftimeSpec. Example( - "The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behaviour of the specifier. Please refer to [man 3 strftime](https://linux.die.net/man/3/strftime) for the list of format specifiers.", + "The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strftime[man 3 strftime] for the list of format specifiers.", `root.something_at = (this.created_at + 300).ts_strftime("%Y-%b-%d %H:%M:%S")`, // `{"created_at":1597405526}`, // `{"something_at":"2020-Aug-14 11:50:26"}`, @@ -423,7 +423,7 @@ The output format is defined by showing how the reference time, defined to be Mo }, ). Example( - "As an extension provided by the underlying formatting library, [itchyny/timefmt-go](https://github.com/itchyny/timefmt-go), the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported.", + "As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported.", `root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S.%f", "UTC")`, [2]string{ `{"created_at":1597405526}`, @@ -470,7 +470,7 @@ The output format is defined by showing how the reference time, defined to be Mo Category(query.MethodCategoryTime). Beta(). Static(). - Description("Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats.") + Description("Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") formatTSUnixSpecDep := asDeprecated(formatTSUnixSpec) @@ -501,7 +501,7 @@ The output format is defined by showing how the reference time, defined to be Mo Category(query.MethodCategoryTime). Beta(). Static(). - Description("Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats.") + Description("Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") formatTSUnixMilliSpecDep := asDeprecated(formatTSUnixMilliSpec) @@ -532,7 +532,7 @@ The output format is defined by showing how the reference time, defined to be Mo Category(query.MethodCategoryTime). Beta(). Static(). - Description("Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats.") + Description("Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") formatTSUnixMicroSpecDep := asDeprecated(formatTSUnixMicroSpec) @@ -563,7 +563,7 @@ The output format is defined by showing how the reference time, defined to be Mo Category(query.MethodCategoryTime). Beta(). Static(). - Description("Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats.") + Description("Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") formatTSUnixNanoSpecDep := asDeprecated(formatTSUnixNanoSpec) @@ -594,7 +594,7 @@ The output format is defined by showing how the reference time, defined to be Mo Beta(). Static(). Category(query.MethodCategoryTime). - Description(`Returns the difference in nanoseconds between the target timestamp (t1) and the timestamp provided as a parameter (t2). The `+"[`ts_parse`](#ts_parse)"+` method can be used in order to parse different timestamp formats.`). + Description(`Returns the difference in nanoseconds between the target timestamp (t1) and the timestamp provided as a parameter (t2). The `+"<>"+` method can be used in order to parse different timestamp formats.`). Param(bloblang.NewTimestampParam("t2").Description("The second timestamp to be subtracted from the method target.")). Version("4.23.0"). Example("Use the `.abs()` method in order to calculate an absolute duration between two timestamps.", diff --git a/internal/impl/pure/buffer_memory.go b/internal/impl/pure/buffer_memory.go index 8aef1fcec8..ee69686c89 100644 --- a/internal/impl/pure/buffer_memory.go +++ b/internal/impl/pure/buffer_memory.go @@ -38,13 +38,13 @@ This buffer is appropriate when consuming messages from inputs that do not grace This buffer has a configurable limit, where consumption will be stopped with back pressure upstream if the total size of messages in the buffer reaches this amount. Since this calculation is only an estimate, and the real size of messages in RAM is always higher, it is recommended to set the limit significantly below the amount of RAM available. -## Delivery Guarantees +== Delivery guarantees This buffer intentionally weakens the delivery guarantees of the pipeline and therefore should never be used in places where data loss is unacceptable. -## Batching +== Batching -It is possible to batch up messages sent from this buffer using a [batch policy](/docs/configuration/batching#batch-policy).`). +It is possible to batch up messages sent from this buffer using a xref:configuration:batching.adoc#batch-policy[batch policy].`). Field(service.NewIntField("limit"). Description(`The maximum buffer size (in bytes) to allow before applying backpressure upstream.`). Default(524288000)). diff --git a/internal/impl/pure/buffer_system_window.go b/internal/impl/pure/buffer_system_window.go index 963b2374f4..f9e1504257 100644 --- a/internal/impl/pure/buffer_system_window.go +++ b/internal/impl/pure/buffer_system_window.go @@ -20,25 +20,25 @@ func tumblingWindowBufferConfig() *service.ConfigSpec { Categories("Windowing"). Summary("Chops a stream of messages into tumbling or sliding windows of fixed temporal size, following the system clock."). Description(` -A window is a grouping of messages that fit within a discrete measure of time following the system clock. Messages are allocated to a window either by the processing time (the time at which they're ingested) or by the event time, and this is controlled via the `+"[`timestamp_mapping` field](#timestamp_mapping)"+`. +A window is a grouping of messages that fit within a discrete measure of time following the system clock. Messages are allocated to a window either by the processing time (the time at which they're ingested) or by the event time, and this is controlled via the `+"<>"+`. In tumbling mode (default) the beginning of a window immediately follows the end of a prior window. When the buffer is initialized the first window to be created and populated is aligned against the zeroth minute of the zeroth hour of the day by default, and may therefore be open for a shorter period than the specified size. -A window is flushed only once the system clock surpasses its scheduled end. If an `+"[`allowed_lateness`](#allowed_lateness)"+` is specified then the window will not be flushed until the scheduled end plus that length of time. +A window is flushed only once the system clock surpasses its scheduled end. If an `+"<>"+` is specified then the window will not be flushed until the scheduled end plus that length of time. When a message is added to a window it has a metadata field `+"`window_end_timestamp`"+` added to it containing the timestamp of the end of the window as an RFC3339 string. -## Sliding Windows +== Sliding windows -Sliding windows begin from an offset of the prior windows' beginning rather than its end, and therefore messages may belong to multiple windows. In order to produce sliding windows specify a `+"[`slide` duration](#slide)"+`. +Sliding windows begin from an offset of the prior windows' beginning rather than its end, and therefore messages may belong to multiple windows. In order to produce sliding windows specify a `+"<>"+`. -## Back Pressure +== Back pressure If back pressure is applied to this buffer either due to output services being unavailable or resources being saturated, windows older than the current and last according to the system clock will be dropped in order to prevent unbounded resource usage. This means you should ensure that under the worst case scenario you have enough system memory to store two windows' worth of data at a given time (plus extra for redundancy and other services). If messages could potentially arrive with event timestamps in the future (according to the system clock) then you should also factor in these extra messages in memory usage estimates. -## Delivery Guarantees +== Delivery guarantees This buffer honours the transaction model within Benthos in order to ensure that messages are not acknowledged until they are either intentionally dropped or successfully delivered to outputs. However, since messages belonging to an expired window are intentionally dropped there are circumstances where not all messages entering the system will be delivered. @@ -48,7 +48,7 @@ During graceful termination if the current window is partially populated with me `). Field(service.NewBloblangField("timestamp_mapping"). Description(` -A [Bloblang mapping](/docs/guides/bloblang/about) applied to each message during ingestion that provides the timestamp to use for allocating it a window. By default the function `+"`now()`"+` is used in order to generate a fresh timestamp at the time of ingestion (the processing time), whereas this mapping can instead extract a timestamp from the message itself (the event time). +A xref:guides:bloblang/about.adoc[Bloblang mapping] applied to each message during ingestion that provides the timestamp to use for allocating it a window. By default the function `+"`now()`"+` is used in order to generate a fresh timestamp at the time of ingestion (the processing time), whereas this mapping can instead extract a timestamp from the message itself (the event time). The timestamp value assigned to `+"`root`"+` must either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in ISO 8601 format. If the mapping fails or provides an invalid result the message will be dropped (with logging to describe the problem). `). @@ -80,7 +80,7 @@ The timestamp value assigned to `+"`root`"+` must either be a numerical unix tim } `+"```"+` -We can use a window buffer in order to create periodic messages summarising the traffic for a period of time of this form: +We can use a window buffer in order to create periodic messages summarizing the traffic for a period of time of this form: `+"```json"+` { diff --git a/internal/impl/pure/cache_lru.go b/internal/impl/pure/cache_lru.go index 9eb53ec66f..ca1cb7d041 100644 --- a/internal/impl/pure/cache_lru.go +++ b/internal/impl/pure/cache_lru.go @@ -42,7 +42,7 @@ func lruCacheConfig() *service.ConfigSpec { Summary(`Stores key/value pairs in a lru in-memory cache. This cache is therefore reset every time the service restarts.`). Description(`This provides the lru package which implements a fixed-size thread safe LRU cache. -It uses the package ` + "[`lru`](https://github.com/hashicorp/golang-lru/v2)" + ` +It uses the package ` + "https://github.com/hashicorp/golang-lru/v2[`lru`]" + ` The field ` + lruCacheFieldInitValuesLabel + ` can be used to pre-populate the memory cache with any number of key/value pairs: diff --git a/internal/impl/pure/cache_ttlru.go b/internal/impl/pure/cache_ttlru.go index 134df00b24..3ed6b87d91 100644 --- a/internal/impl/pure/cache_ttlru.go +++ b/internal/impl/pure/cache_ttlru.go @@ -35,7 +35,7 @@ func ttlruCacheConfig() *service.ConfigSpec { This TTL is reset on both modification and access of the value. As a result, if the cache is full, and no items have expired, when adding a new item, the item with the soonest expiration will be evicted. -It uses the package ` + "[`expirable`](https://github.com/hashicorp/golang-lru/v2/expirable)" + ` +It uses the package ` + "https://github.com/hashicorp/golang-lru/v2/expirable[`expirable`]" + ` The field ` + ttlruCacheFieldInitValuesLabel + ` can be used to pre-populate the memory cache with any number of key/value pairs: diff --git a/internal/impl/pure/input_broker.go b/internal/impl/pure/input_broker.go index 128eb94636..de19282505 100644 --- a/internal/impl/pure/input_broker.go +++ b/internal/impl/pure/input_broker.go @@ -56,13 +56,13 @@ input: If the number of copies is greater than zero the list will be copied that number of times. For example, if your inputs were of type foo and bar, with 'copies' set to '2', you would end up with two 'foo' inputs and two 'bar' inputs. -### Batching +== Batching -It's possible to configure a [batch policy](/docs/configuration/batching#batch-policy) with a broker using the `+"`batching`"+` fields. When doing this the feeds from all child inputs are combined. Some inputs do not support broker based batching and specify this in their documentation. +It's possible to configure a xref:configuration:batching.adoc#batch-policy[batch policy] with a broker using the `+"`batching`"+` fields. When doing this the feeds from all child inputs are combined. Some inputs do not support broker based batching and specify this in their documentation. -### Processors +== Processors -It is possible to configure [processors](/docs/components/processors/about) at the broker level, where they will be applied to _all_ child inputs, as well as on the individual child inputs. If you have processors at both the broker level _and_ on child inputs then the broker processors will be applied _after_ the child nodes processors.`). +It is possible to configure xref:components:processors/about.adoc[processors] at the broker level, where they will be applied to _all_ child inputs, as well as on the individual child inputs. If you have processors at both the broker level _and_ on child inputs then the broker processors will be applied _after_ the child nodes processors.`). Fields( service.NewIntField(ibFieldCopies). Description("Whatever is specified within `inputs` will be created this many times."). diff --git a/internal/impl/pure/input_generate.go b/internal/impl/pure/input_generate.go index 895a8c7e96..2e39ac0580 100644 --- a/internal/impl/pure/input_generate.go +++ b/internal/impl/pure/input_generate.go @@ -30,10 +30,10 @@ func genInputSpec() *service.ConfigSpec { Stable(). Categories("Utility"). Version("3.40.0"). - Summary("Generates messages at a given interval using a [Bloblang](/docs/guides/bloblang/about) mapping executed without a context. This allows you to generate messages for testing your pipeline configs."). + Summary("Generates messages at a given interval using a xref:guides:bloblang/about.adoc[Bloblang] mapping executed without a context. This allows you to generate messages for testing your pipeline configs."). Fields( service.NewBloblangField(giFieldMapping). - Description("A [bloblang](/docs/guides/bloblang/about) mapping to use for generating messages."). + Description("A xref:guides:bloblang/about.adoc[Bloblang] mapping to use for generating messages."). Examples( `root = "hello world"`, `root = {"test":"message","id":uuid_v4()}`, diff --git a/internal/impl/pure/input_inproc.go b/internal/impl/pure/input_inproc.go index 17fa7843c2..60b993c116 100644 --- a/internal/impl/pure/input_inproc.go +++ b/internal/impl/pure/input_inproc.go @@ -19,7 +19,7 @@ func inprocInputSpec() *service.ConfigSpec { Stable(). Categories("Utility"). Description(` -Directly connect to an output within a Benthos process by referencing it by a chosen ID. This allows you to hook up isolated streams whilst running Benthos in ` + "[streams mode](/docs/guides/streams_mode/about)" + `, it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. +Directly connect to an output within a Benthos process by referencing it by a chosen ID. This allows you to hook up isolated streams whilst running Benthos in ` + "xref:guides:streams_mode/about.adoc[streams mode]" + `, it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs.`). Field(service.NewStringField("").Default("")) diff --git a/internal/impl/pure/input_read_until.go b/internal/impl/pure/input_read_until.go index 0ae9d491ff..e524540bb8 100644 --- a/internal/impl/pure/input_read_until.go +++ b/internal/impl/pure/input_read_until.go @@ -30,7 +30,7 @@ func readUntilInputSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Categories("Utility"). - Summary("Reads messages from a child input until a consumed message passes a [Bloblang query](/docs/guides/bloblang/about/), at which point the input closes. It is also possible to configure a timeout after which the input is closed if no new messages arrive in that period."). + Summary("Reads messages from a child input until a consumed message passes a xref:guides:bloblang/about.adoc[Bloblang query], at which point the input closes. It is also possible to configure a timeout after which the input is closed if no new messages arrive in that period."). Description(` Messages are read continuously while the query check returns false, when the query returns true the message that triggered the check is sent out and the input is closed. Use this to define inputs where the stream should end once a certain message appears. @@ -38,12 +38,12 @@ If the idle timeout is configured, the input will be closed if no new messages a Sometimes inputs close themselves. For example, when the `+"`file`"+` input type reaches the end of a file it will shut down. By default this type will also shut down. If you wish for the input type to be restarted every time it shuts down until the query check is met then set `+"`restart_input` to `true`."+` -### Metadata +== Metadata A metadata key `+"`benthos_read_until` containing the value `final`"+` is added to the first part of the message that triggers the input to stop.`). Example( "Consume N Messages", - "A common reason to use this input is to consume only N messages from an input and then stop. This can easily be done with the [`count` function](/docs/guides/bloblang/functions/#count):", + "A common reason to use this input is to consume only N messages from an input and then stop. This can easily be done with the xref:guides:bloblang/functions.adoc#count[`count` function]:", ` # Only read 100 messages, and then exit. input: @@ -74,7 +74,7 @@ input: service.NewInputField(ruiFieldInput). Description("The child input to consume from."), service.NewBloblangField(ruiFieldCheck). - Description("A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether the input should now be closed."). + Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether the input should now be closed."). Examples( `this.type == "foo"`, `count("messages") >= 100`, diff --git a/internal/impl/pure/input_resource.go b/internal/impl/pure/input_resource.go index db718ffe1a..063308d450 100644 --- a/internal/impl/pure/input_resource.go +++ b/internal/impl/pure/input_resource.go @@ -22,7 +22,7 @@ func resourceInputSpec() *service.ConfigSpec { Categories("Utility"). Summary(`Resource is an input type that channels messages from a resource input, identified by its name.`). Description(`Resources allow you to tidy up deeply nested configs. For example, the config: - + ` + "```yaml" + ` input: broker: @@ -56,11 +56,11 @@ input_resources: gcp_pubsub: project: bar subscription: baz - ` + "```" + ` +` + "```" + ` Resources also allow you to reference a single input in multiple places, such as multiple streams mode configs, or multiple entries in a broker input. However, when a resource is referenced more than once the messages it produces are distributed across those references, so each message will only be directed to a single reference, not all of them. -You can find out more about resources [in this document.](/docs/configuration/resources)`). +You can find out more about resources in xref:configuration:resources.adoc[].`). Field(service.NewStringField("").Default("")) } diff --git a/internal/impl/pure/input_sequence.go b/internal/impl/pure/input_sequence.go index e51006ea74..e757ab116e 100644 --- a/internal/impl/pure/input_sequence.go +++ b/internal/impl/pure/input_sequence.go @@ -40,7 +40,7 @@ func sequenceInputSpec() *service.ConfigSpec { Description("The type of join to perform. A `full-outer` ensures that all identifiers seen in any of the input sequences are sent, and is performed by consuming all input sequences before flushing the joined results. An `outer` join consumes all input sequences but only writes data joined from the last input in the sequence, similar to a left or right outer join. With an `outer` join if an identifier appears multiple times within the final sequence input it will be flushed each time it appears. `full-outter` and `outter` have been deprecated in favour of `full-outer` and `outer`."). Default("none"), service.NewStringField(siFieldShardedJoinIDPath). - Description("A [dot path](/docs/configuration/field_paths) that points to a common field within messages of each fragmented data set and can be used to join them. Messages that are not structured or are missing this field will be dropped. This field must be set in order to enable joins."). + Description("A xref:configuration:field_paths.adoc[dot path] that points to a common field within messages of each fragmented data set and can be used to join them. Messages that are not structured or are missing this field will be dropped. This field must be set in order to enable joins."). Default(""), service.NewIntField(siFieldShardedJoinIterations). Description("The total number of iterations (shards), increasing this number will increase the overall time taken to process the data, but reduces the memory used in the process. The real memory usage required is significantly higher than the real size of the data and therefore the number of iterations should be at least an order of magnitude higher than the available memory divided by the overall size of the dataset."). @@ -401,7 +401,7 @@ func shardedConfigFromParsed(conf *service.ParsedConfig) (*messageJoiner, error) case "outer", "outter": flushOnLast = true default: - return nil, fmt.Errorf("join type '%v' was not recognized", typeStr) + return nil, fmt.Errorf("join type '%v' was not recognised", typeStr) } idPath, _ := conf.FieldString(siFieldShardedJoinIDPath) diff --git a/internal/impl/pure/output_broker.go b/internal/impl/pure/output_broker.go index 2343923a2e..f0bbaa8c12 100644 --- a/internal/impl/pure/output_broker.go +++ b/internal/impl/pure/output_broker.go @@ -22,9 +22,9 @@ func brokerOutputSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Categories("Utility"). - Summary(`Allows you to route messages to multiple child outputs using a range of brokering [patterns](#patterns).`). + Summary(`Allows you to route messages to multiple child outputs using a range of brokering <>.`). Description(` -[Processors](/docs/components/processors/about) can be listed to apply across individual outputs or all outputs: +xref:components:processors/about.adoc[Processors] can be listed to apply across individual outputs or all outputs: `+"```yaml"+` output: @@ -42,37 +42,37 @@ output: - resource: general_processor `+"```"+``). Footnotes(` -## Patterns +== Patterns The broker pattern determines the way in which messages are allocated and can be chosen from the following: -### `+"`fan_out`"+` +=== `+"`fan_out`"+` With the fan out pattern all outputs will be sent every message that passes through Benthos in parallel. If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. -Sometimes it is useful to disable the back pressure or retries of certain fan out outputs and instead drop messages that have failed or were blocked. In this case you can wrap outputs with a `+"[`drop_on` output](/docs/components/outputs/drop_on)"+`. +Sometimes it is useful to disable the back pressure or retries of certain fan out outputs and instead drop messages that have failed or were blocked. In this case you can wrap outputs with a `+"xref:components:outputs/drop_on.adoc[`drop_on` output]"+`. -### `+"`fan_out_fail_fast`"+` +=== `+"`fan_out_fail_fast`"+` The same as the `+"`fan_out`"+` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. -### `+"`fan_out_sequential`"+` +=== `+"`fan_out_sequential`"+` Similar to the fan out pattern except outputs are written to sequentially, meaning an output is only written to once the preceding output has confirmed receipt of the same message. If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. -### `+"`fan_out_sequential_fail_fast`"+` +=== `+"`fan_out_sequential_fail_fast`"+` The same as the `+"`fan_out_sequential`"+` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. -### `+"`round_robin`"+` +=== `+"`round_robin`"+` With the round robin pattern each message will be assigned a single output following their order. If an output applies back pressure it will block all subsequent messages. If an output fails to send a message then the message will be re-attempted with the next input, and so on. -### `+"`greedy`"+` +=== `+"`greedy`"+` The greedy pattern results in higher output throughput at the cost of potentially disproportionate message allocations to those outputs. Each message is sent to a single output, which is determined by allowing outputs to claim messages as soon as they are able to process them. This results in certain faster outputs potentially processing more messages at the cost of slower outputs.`). Fields( diff --git a/internal/impl/pure/output_cache.go b/internal/impl/pure/output_cache.go index 5c8822e76d..e18fe8c0bf 100644 --- a/internal/impl/pure/output_cache.go +++ b/internal/impl/pure/output_cache.go @@ -25,8 +25,8 @@ func CacheOutputSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Categories("Services"). - Summary(`Stores each message in a [cache](/docs/components/caches/about).`). - Description(`Caches are configured as [resources](/docs/components/caches/about), where there's a wide variety to choose from. + Summary(`Stores each message in a xref:components:caches/about.adoc[cache].`). + Description(`Caches are configured as xref:components:caches/about.adoc[resources], where there's a wide variety to choose from. The `+"`target`"+` field must reference a configured cache resource label like follows: @@ -44,7 +44,7 @@ cache_resources: default_ttl: 60s `+"```"+` -In order to create a unique `+"`key`"+` value per item you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries).`+service.OutputPerformanceDocs(true, false)). +In order to create a unique `+"`key`"+` value per item you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries].`+service.OutputPerformanceDocs(true, false)). Fields( service.NewStringField(coFieldTarget). Description("The target cache to store messages in."), diff --git a/internal/impl/pure/output_fallback.go b/internal/impl/pure/output_fallback.go index 1a76dd07b9..f1900828d8 100644 --- a/internal/impl/pure/output_fallback.go +++ b/internal/impl/pure/output_fallback.go @@ -41,11 +41,11 @@ output: path: /usr/local/benthos/everything_failed.jsonl `+"```"+` -### Metadata +== Metadata When a given output fails the message routed to the following output will have a metadata value named `+"`fallback_error`"+` containing a string error message outlining the cause of the failure. The content of this string will depend on the particular output and can be used to enrich the message or provide information used to broker the data to an appropriate output using something like a `+"`switch`"+` output. -### Batching +== Batching When an output within a fallback sequence uses batching, like so: diff --git a/internal/impl/pure/output_inproc.go b/internal/impl/pure/output_inproc.go index 0dfec0d3e3..8124c6fa66 100644 --- a/internal/impl/pure/output_inproc.go +++ b/internal/impl/pure/output_inproc.go @@ -20,7 +20,7 @@ func init() { Stable(). Categories("Utility"). Description(` -Sends data directly to Benthos inputs by connecting to a unique ID. This allows you to hook up isolated streams whilst running Benthos in `+"[streams mode](/docs/guides/streams_mode/about)"+`, it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. +Sends data directly to Benthos inputs by connecting to a unique ID. This allows you to hook up isolated streams whilst running Benthos in `+"xref:guides:streams_mode/about.adoc[streams mode]"+`, it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs.`). Field(service.NewStringField("").Default("")), diff --git a/internal/impl/pure/output_reject.go b/internal/impl/pure/output_reject.go index 323a9edcd8..5316de94a9 100644 --- a/internal/impl/pure/output_reject.go +++ b/internal/impl/pure/output_reject.go @@ -23,7 +23,7 @@ func init() { Description(` The routing of messages after this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch. -If you're still scratching your head as to when this output could be useful check out [the examples below](#examples).`). +To learn when this output could be useful, see [the <>.`). Example( "Rejecting Failed Messages", ` diff --git a/internal/impl/pure/output_reject_errored.go b/internal/impl/pure/output_reject_errored.go index a5f3156713..8c34a8c3f1 100644 --- a/internal/impl/pure/output_reject_errored.go +++ b/internal/impl/pure/output_reject_errored.go @@ -20,7 +20,7 @@ func init() { "reject_errored", service.NewConfigSpec(). Stable(). Categories("Utility"). - Summary(`Rejects messages that have failed their processing steps, resulting in nack behaviour at the input level, otherwise sends them to a child output.`). + Summary(`Rejects messages that have failed their processing steps, resulting in nack behavior at the input level, otherwise sends them to a child output.`). Description(` The routing of messages rejected by this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch.`). Example( @@ -47,7 +47,7 @@ output: Example( "DLQing Failed Messages", ` -Another use case for this output is to send failed messages straight into a dead-letter queue. We use it within a [fallback output](/docs/components/outputs/fallback) that allows us to specify where these failed messages should go to next.`, +Another use case for this output is to send failed messages straight into a dead-letter queue. You use it within a xref:components:outputs/fallback.adoc[fallback output] that allows you to specify where these failed messages should go to next.`, ` pipeline: processors: diff --git a/internal/impl/pure/output_resource.go b/internal/impl/pure/output_resource.go index c566fa07ec..7bcc494e5e 100644 --- a/internal/impl/pure/output_resource.go +++ b/internal/impl/pure/output_resource.go @@ -57,9 +57,9 @@ output_resources: gcp_pubsub: project: bar topic: baz - `+"```"+` +`+"```"+` -You can find out more about resources [in this document.](/docs/configuration/resources)`). +You can find out more about resources in xref:configuration:resources.adoc[]`). Field(service.NewStringField("").Default("")), func(conf *service.ParsedConfig, res *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { var resName string diff --git a/internal/impl/pure/output_retry.go b/internal/impl/pure/output_retry.go index 123ec91b78..1faaa77d9c 100644 --- a/internal/impl/pure/output_retry.go +++ b/internal/impl/pure/output_retry.go @@ -31,9 +31,9 @@ func retryOutputSpec() *service.ConfigSpec { Description(` All messages in Benthos are always retried on an output error, but this would usually involve propagating the error back to the source of the message, whereby it would be reprocessed before reaching the output layer once again. -This output type is useful whenever we wish to avoid reprocessing a message on the event of a failed send. We might, for example, have a dedupe processor that we want to avoid reapplying to the same message more than once in the pipeline. +This output type is useful whenever we wish to avoid reprocessing a message on the event of a failed send. We might, for example, have a deduplication processor that we want to avoid reapplying to the same message more than once in the pipeline. -Rather than retrying the same output you may wish to retry the send using a different output target (a dead letter queue). In which case you should instead use the ` + "[`fallback`](/docs/components/outputs/fallback)" + ` output type.`). +Rather than retrying the same output you may wish to retry the send using a different output target (a dead letter queue). In which case you should instead use the ` + "xref:components:outputs/fallback.adoc[`fallback`]" + ` output type.`). Fields(retries.CommonRetryBackOffFields(0, "500ms", "3s", "0s")...). Fields( service.NewOutputField(roFieldOutput). diff --git a/internal/impl/pure/output_switch.go b/internal/impl/pure/output_switch.go index 4bbda24087..caa4e58425 100644 --- a/internal/impl/pure/output_switch.go +++ b/internal/impl/pure/output_switch.go @@ -36,7 +36,7 @@ func switchOutputSpec() *service.ConfigSpec { Categories("Utility"). Stable(). Summary(`The switch output type allows you to route messages to different outputs based on their contents.`). - Description(`Messages that do not pass the check of a single output case are effectively dropped. In order to prevent this outcome set the field `+"[`strict_mode`](#strict_mode) to `true`"+`, in which case messages that do not pass at least one case are considered failed and will be nacked and/or reprocessed depending on your input.`). + Description(`Messages that do not pass the check of a single output case are effectively dropped. In order to prevent this outcome set the field `+"<> to `true`"+`, in which case messages that do not pass at least one case are considered failed and will be nacked and/or reprocessed depending on your input.`). Example( "Basic Multiplexing", ` @@ -111,14 +111,14 @@ If a message can be routed to >1 outputs it is usually best to set this to true Default(false), service.NewObjectListField(soFieldCases, service.NewBloblangField(soFieldCasesCheck). - Description("A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should be routed to the case output. If left empty the case always passes."). + Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should be routed to the case output. If left empty the case always passes."). Examples( `this.type == "foo"`, `this.contents.urls.contains("https://benthos.dev/")`, ). Default(""), service.NewOutputField(soFieldCasesOutput). - Description("An [output](/docs/components/outputs/about/) for messages that pass the check to be routed to."), + Description("An xref:components:outputs/about.adoc[output] for messages that pass the check to be routed to."), service.NewBoolField(soFieldCasesContinue). Description("Indicates whether, if this case passes for a message, the next case should also be tested."). Default(false). diff --git a/internal/impl/pure/output_sync_response.go b/internal/impl/pure/output_sync_response.go index ee9668e9ce..2e2b1a83e1 100644 --- a/internal/impl/pure/output_sync_response.go +++ b/internal/impl/pure/output_sync_response.go @@ -17,7 +17,7 @@ func init() { Stable(). Summary(`Returns the final message payload back to the input origin of the message, where it is dealt with according to that specific input type.`). Description(` -For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this output even when combining input types that might not have support for sync responses. An example of an input able to utilise this is the `+"`http_server`"+`. +For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this output even when combining input types that might not have support for sync responses. An example of an input able to utilize this is the `+"`http_server`"+`. It is safe to combine this output with others using broker types. For example, with the `+"`http_server`"+` input we could send the payload to a Kafka topic and also send a modified payload back with: @@ -39,7 +39,7 @@ output: Using the above example and posting the message 'hello world' to the endpoint `+"`/post`"+` Benthos would send it unchanged to the topic `+"`foo_topic`"+` and also respond with 'HELLO WORLD'. -For more information please read [Synchronous Responses](/docs/guides/sync_responses).`). +For more information please read xref:guides:sync_responses.adoc[synchronous responses].`). Field(service.NewObjectField("").Default(map[string]any{})), func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { var s output.Streamed diff --git a/internal/impl/pure/processor_archive.go b/internal/impl/pure/processor_archive.go index b497227c72..8390e74157 100644 --- a/internal/impl/pure/processor_archive.go +++ b/internal/impl/pure/processor_archive.go @@ -20,16 +20,16 @@ func archiveProcConfig() *service.ConfigSpec { Categories("Parsing", "Utility"). Summary("Archives all the messages of a batch into a single message according to the selected archive format."). Description(` -Some archive formats (such as tar, zip) treat each archive item (message part) as a file with a path. Since message parts only contain raw data a unique path must be generated for each part. This can be done by using function interpolations on the 'path' field as described [here](/docs/configuration/interpolation#bloblang-queries). For types that aren't file based (such as binary) the file field is ignored. +Some archive formats (such as tar, zip) treat each archive item (message part) as a file with a path. Since message parts only contain raw data a unique path must be generated for each part. This can be done by using function interpolations on the 'path' field as described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. For types that aren't file based (such as binary) the file field is ignored. The resulting archived message adopts the metadata of the _first_ message part of the batch. -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching).`). +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). Field(service.NewStringAnnotatedEnumField("format", map[string]string{ `concatenate`: `Join the raw contents of each message into a single binary message.`, `tar`: `Archive messages to a unix standard tape archive.`, `zip`: `Archive messages to a zip file.`, - `binary`: `Archive messages to a [binary blob format](https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96).`, + `binary`: `Archive messages to a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format].`, `lines`: `Join the raw contents of each message and insert a line break between each one.`, `json_array`: `Attempt to parse each message as a JSON document and append the result to an array, which becomes the contents of the resulting message.`, }).Description("The archiving format to apply.")). diff --git a/internal/impl/pure/processor_bloblang.go b/internal/impl/pure/processor_bloblang.go index 544c2a8ca0..e522519116 100644 --- a/internal/impl/pure/processor_bloblang.go +++ b/internal/impl/pure/processor_bloblang.go @@ -18,22 +18,22 @@ func init() { err := service.RegisterBatchProcessor("bloblang", service.NewConfigSpec(). Stable(). Categories("Mapping", "Parsing"). - Summary("Executes a [Bloblang](/docs/guides/bloblang/about) mapping on messages."). + Summary("Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping on messages."). Description(` -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information [check out the docs](/docs/guides/bloblang/about). +Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information see xref:guides:bloblang/about.adoc[]. If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `+"`from \"\"`"+`, where the path must be absolute, or relative from the location that Benthos is executed from. -## Component Rename +== Component rename -This processor was recently renamed to the `+"[`mapping` processor](/docs/components/processors/mapping)"+` in order to make the purpose of the processor more prominent. It is still valid to use the existing `+"`bloblang`"+` name but eventually it will be deprecated and replaced by the new name in example configs.`). +This processor was recently renamed to the `+"xref:components:processors/mapping.adoc[`mapping` processor]"+` in order to make the purpose of the processor more prominent. It is still valid to use the existing `+"`bloblang`"+` name but eventually it will be deprecated and replaced by the new name in example configs.`). Footnotes(` -## Error Handling +== Error handling Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use -[standard processor error handling patterns](/docs/configuration/error_handling). +xref:configuration:error_handling.adoc[standard processor error handling patterns]. -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behaviour, which you can read about [in this section](/docs/guides/bloblang/about#error-handling).`). +However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behavior, which you can read about in xref:guides:bloblang/about#error-handling.adoc[Error handling].`). Example("Mapping", ` Given JSON documents containing an array of fans: diff --git a/internal/impl/pure/processor_branch.go b/internal/impl/pure/processor_branch.go index 2f6c9671a3..78b60d3de7 100644 --- a/internal/impl/pure/processor_branch.go +++ b/internal/impl/pure/processor_branch.go @@ -31,19 +31,19 @@ func branchProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Composition"). Stable(). - Summary(`The `+"`branch`"+` processor allows you to create a new request message via a [Bloblang mapping](/docs/guides/bloblang/about), execute a list of processors on the request messages, and, finally, map the result back into the source message using another mapping.`). + Summary(`The `+"`branch`"+` processor allows you to create a new request message via a xref:guides:bloblang/about.adoc[Bloblang mapping], execute a list of processors on the request messages, and, finally, map the result back into the source message using another mapping.`). Description(` This is useful for preserving the original message contents when using processors that would otherwise replace the entire contents. -### Metadata +== Metadata -Metadata fields that are added to messages during branch processing will not be automatically copied into the resulting message. In order to do this you should explicitly declare in your `+"`result_map`"+` either a wholesale copy with `+"`meta = metadata()`"+`, or selective copies with `+"`meta foo = metadata(\"bar\")`"+` and so on. It is also possible to reference the metadata of the origin message in the `+"`result_map`"+` using the [`+"`@`"+` operator](/docs/guides/bloblang/about#metadata). +Metadata fields that are added to messages during branch processing will not be automatically copied into the resulting message. In order to do this you should explicitly declare in your `+"`result_map`"+` either a wholesale copy with `+"`meta = metadata()`"+`, or selective copies with `+"`meta foo = metadata(\"bar\")`"+` and so on. It is also possible to reference the metadata of the origin message in the `+"`result_map`"+` using the xref:guides:bloblang/about.adoc#metadata[`+"`@`"+` operator]. -### Error Handling +== Error handling -If the `+"`request_map`"+` fails the child processors will not be executed. If the child processors themselves result in an (uncaught) error then the `+"`result_map`"+` will not be executed. If the `+"`result_map`"+` fails the message will remain unchanged. Under any of these conditions standard [error handling methods](/docs/configuration/error_handling) can be used in order to filter, DLQ or recover the failed messages. +If the `+"`request_map`"+` fails the child processors will not be executed. If the child processors themselves result in an (uncaught) error then the `+"`result_map`"+` will not be executed. If the `+"`result_map`"+` fails the message will remain unchanged. Under any of these conditions standard xref:configuration:error_handling.adoc[error handling methods] can be used in order to filter, DLQ or recover the failed messages. -### Conditional Branching +== Conditional branching If the root of your request map is set to `+"`deleted()`"+` then the branch processors are skipped for the given message, this allows you to conditionally branch messages.`). Example("HTTP Request", ` @@ -117,7 +117,7 @@ pipeline: func branchSpecFields() []*service.ConfigField { return []*service.ConfigField{ service.NewBloblangField(branchProcFieldReqMap). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata)."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata)."). Examples(`root = { "id": this.doc.id, "content": this.doc.body.text @@ -131,7 +131,7 @@ func branchSpecFields() []*service.ConfigField { service.NewProcessorListField(branchProcFieldProcs). Description("A list of processors to apply to mapped requests. When processing message batches the resulting batch must match the size and ordering of the input batch, therefore filtering, grouping should not be performed within these processors."), service.NewBloblangField(branchProcFieldResMap). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata)."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata)."). Examples(`meta foo_code = metadata("code") root.foo_result = this`, `meta = metadata() diff --git a/internal/impl/pure/processor_cache.go b/internal/impl/pure/processor_cache.go index 609836840b..f9f0b9395c 100644 --- a/internal/impl/pure/processor_cache.go +++ b/internal/impl/pure/processor_cache.go @@ -28,37 +28,37 @@ func cacheProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Integration"). Stable(). - Summary("Performs operations against a [cache resource](/docs/components/caches/about) for each message, allowing you to store or retrieve data within message payloads."). + Summary("Performs operations against a xref:components:caches/about.adoc[cache resource] for each message, allowing you to store or retrieve data within message payloads."). Description(` -For use cases where you wish to cache the result of processors consider using the `+"[`cached` processor](/docs/components/processors/cached)"+` instead. +For use cases where you wish to cache the result of processors consider using the `+"xref:components:processors/cached.adoc[`cached` processor]"+` instead. -This processor will interpolate functions within the `+"`key` and `value`"+` fields individually for each message. This allows you to specify dynamic keys and values based on the contents of the message payloads and metadata. You can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries).`). +This processor will interpolate functions within the `+"`key` and `value`"+` fields individually for each message. This allows you to specify dynamic keys and values based on the contents of the message payloads and metadata. You can find a list of functions in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries].`). Footnotes(` -## Operators +== Operators -### `+"`set`"+` +=== `+"`set`"+` Set a key in the cache to a value. If the key already exists the contents are overridden. -### `+"`add`"+` +=== `+"`add`"+` Set a key in the cache to a value. If the key already exists the action fails with a 'key already exists' error, which can be detected with -[processor error handling](/docs/configuration/error_handling). +xref:configuration:error_handling.adoc[processor error handling]. -### `+"`get`"+` +=== `+"`get`"+` Retrieve the contents of a cached key and replace the original message payload with the result. If the key does not exist the action fails with an error, which -can be detected with [processor error handling](/docs/configuration/error_handling). +can be detected with xref:configuration:error_handling.adoc[processor error handling]. -### `+"`delete`"+` +=== `+"`delete`"+` -Delete a key and its contents from the cache. If the key does not exist the +Delete a key and its contents from the cache. If the key does not exist the action is a no-op and will not fail with an error.`). Example("Deduplication", ` -Deduplication can be done using the add operator with a key extracted from the message payload, since it fails when a key already exists we can remove the duplicates using a [`+"`mapping` processor"+`](/docs/components/processors/mapping):`, +Deduplication can be done using the add operator with a key extracted from the message payload, since it fails when a key already exists we can remove the duplicates using a xref:components:processors/mapping.adoc[`+"`mapping` processor"+`]:`, ` pipeline: processors: @@ -75,7 +75,7 @@ cache_resources: url: tcp://TODO:6379 `). Example("Deduplication Batch-Wide", ` -Sometimes it's necessary to deduplicate a batch of messages (AKA a window) by a single identifying value. This can be done by introducing a `+"[`branch` processor](/docs/components/processors/branch)"+`, which executes the cache only once on behalf of the batch, in this case with a value make from a field extracted from the first and last messages of the batch:`, +Sometimes it's necessary to deduplicate a batch of messages (also known as a window) by a single identifying value. This can be done by introducing a `+"xref:components:processors/branch.adoc[`branch` processor]"+`, which executes the cache only once on behalf of the batch, in this case with a value make from a field extracted from the first and last messages of the batch:`, ` pipeline: processors: @@ -98,7 +98,7 @@ pipeline: } `). Example("Hydration", ` -It's possible to enrich payloads with content previously stored in a cache by using the [`+"`branch`"+`](/docs/components/processors/branch) processor:`, +It's possible to enrich payloads with content previously stored in a cache by using the xref:components:processors/branch.adoc[`+"`branch`"+`] processor:`, ` pipeline: processors: @@ -121,9 +121,9 @@ cache_resources: `). Fields( service.NewStringField(cachePFieldResource). - Description("The [`cache` resource](/docs/components/caches/about) to target with this processor."), + Description("The xref:components:caches/about.adoc[`cache` resource] to target with this processor."), service.NewStringEnumField(cachePFieldOperator, "set", "add", "get", "delete"). - Description("The [operation](#operators) to perform with the cache."), + Description("The <> to perform with the cache."), service.NewInterpolatedStringField(cachePFieldKey). Description("A key to use with the cache."), service.NewInterpolatedStringField(cachePFieldValue). diff --git a/internal/impl/pure/processor_cached.go b/internal/impl/pure/processor_cached.go index a7ff37a615..d5970f8e81 100644 --- a/internal/impl/pure/processor_cached.go +++ b/internal/impl/pure/processor_cached.go @@ -210,12 +210,12 @@ func (proc *cachedProcessor) Process(ctx context.Context, msg *service.Message) return collapsedBatch, nil } - // Any errors in creating a serialised batch or caching are non-fatal and + // Any errors in creating a serialized batch or caching are non-fatal and // should be logged but otherwise regarded as insignificant to the flowing // messages. result, err := cachedProcSerialiseBatch(collapsedBatch) if err != nil { - proc.manager.Logger().Errorf("failed to serialise resulting batch for caching: %w", err) + proc.manager.Logger().Errorf("failed to serialize resulting batch for caching: %w", err) return collapsedBatch, nil } @@ -355,7 +355,7 @@ func cachedProcV1DeserialiseBatch(msg *service.Message, data []byte) (resBatch s func cachedProcResultToBatch(msg *service.Message, cachedResult []byte) (service.MessageBatch, error) { verID, remaining, err := cachedProcExtractUint32(cachedResult) if err != nil { - return nil, fmt.Errorf("failed to extract serialisation format version: %w", err) + return nil, fmt.Errorf("failed to extract serialization format version: %w", err) } if verID == 1 { return cachedProcV1DeserialiseBatch(msg, remaining) diff --git a/internal/impl/pure/processor_catch.go b/internal/impl/pure/processor_catch.go index 07080d1d5d..026a5fd769 100644 --- a/internal/impl/pure/processor_catch.go +++ b/internal/impl/pure/processor_catch.go @@ -15,7 +15,7 @@ func init() { Categories("Composition"). Summary("Applies a list of child processors _only_ when a previous processing step has failed."). Description(` -Behaves similarly to the `+"[`for_each`](/docs/components/processors/for_each)"+` processor, where a list of child processors are applied to individual messages of a batch. However, processors are only applied to messages that failed a processing step prior to the catch. +Behaves similarly to the `+"xref:components:processors/for_each.adoc[`for_each`]"+` processor, where a list of child processors are applied to individual messages of a batch. However, processors are only applied to messages that failed a processing step prior to the catch. For example, with the following config: @@ -32,9 +32,9 @@ If the processor `+"`foo`"+` fails for a particular message, that message will b When messages leave the catch block their fail flags are cleared. This processor is useful for when it's possible to recover failed messages, or when special actions (such as logging/metrics) are required before dropping them. -More information about error handling can be found [here](/docs/configuration/error_handling).`). +More information about error handling can be found in xref:configuration:error_handling.adoc[].`). LintRule(`if this.or([]).any(pconf -> pconf.type.or("") == "try" || pconf.try.type() == "array" ) { - "'catch' block contains a 'try' block which will never execute due to errors only being cleared at the end of the 'catch', for more information about nesting 'try' within 'catch' read: https://www.benthos.dev/docs/components/processors/try#nesting-within-a-catch-block" + "'catch' block contains a 'try' block which will never execute due to errors only being cleared at the end of the 'catch', for more information about nesting 'try' within 'catch' read: https://www.docs.redpanda.com/redpanda-connect/components/processors/try#nesting-within-a-catch-block" }`). Field(service.NewProcessorListField("").Default([]any{})), func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { diff --git a/internal/impl/pure/processor_dedupe.go b/internal/impl/pure/processor_dedupe.go index 7506103922..c868513b48 100644 --- a/internal/impl/pure/processor_dedupe.go +++ b/internal/impl/pure/processor_dedupe.go @@ -28,19 +28,19 @@ func dedupeProcSpec() *service.ConfigSpec { Stable(). Summary(`Deduplicates messages by storing a key value in a cache using the `+"`add`"+` operator. If the key already exists within the cache it is dropped.`). Description(` -Caches must be configured as resources, for more information check out the [cache documentation here](/docs/components/caches/about). +Caches must be configured as resources, for more information check out the xref:components:caches/about.adoc[cache documentation]. -When using this processor with an output target that might fail you should always wrap the output within an indefinite `+"[`retry`](/docs/components/outputs/retry)"+` block. This ensures that during outages your messages aren't reprocessed after failures, which would result in messages being dropped. +When using this processor with an output target that might fail you should always wrap the output within an indefinite `+"xref:components:outputs/retry.adoc[`retry`]"+` block. This ensures that during outages your messages aren't reprocessed after failures, which would result in messages being dropped. -## Batch Deduplication +== Batch deduplication -This processor enacts on individual messages only, in order to perform a deduplication on behalf of a batch (or window) of messages instead use the `+"[`cache` processor](/docs/components/processors/cache#examples)"+`. +This processor enacts on individual messages only, in order to perform a deduplication on behalf of a batch (or window) of messages instead use the `+"xref:components:processors/cache.adoc#examples[`cache` processor]"+`. -## Delivery Guarantees +== Delivery guarantees Performing deduplication on a stream using a distributed cache voids any at-least-once guarantees that it previously had. This is because the cache will preserve message signatures even if the message fails to leave the Benthos pipeline, which would cause message loss in the event of an outage at the output sink followed by a restart of the Benthos instance (or a server crash, etc). -This problem can be mitigated by using an in-memory cache and distributing messages to horizontally scaled Benthos pipelines partitioned by the deduplication key. However, in situations where at-least-once delivery guarantees are important it is worth avoiding deduplication in favour of implement idempotent behaviour at the edge of your stream pipelines.`). +This problem can be mitigated by using an in-memory cache and distributing messages to horizontally scaled Benthos pipelines partitioned by the deduplication key. However, in situations where at-least-once delivery guarantees are important it is worth avoiding deduplication in favour of implement idempotent behavior at the edge of your stream pipelines.`). Example( "Deduplicate based on Kafka key", "The following configuration demonstrates a pipeline that deduplicates messages based on the Kafka key.", @@ -59,7 +59,7 @@ cache_resources: ). Fields( service.NewStringField(dedupFieldCache). - Description("The [`cache` resource](/docs/components/caches/about) to target with this processor."), + Description("The xref:components:caches/about.adoc[`cache` resource] to target with this processor."), service.NewInterpolatedStringField(dedupFieldKey). Description("An interpolated string yielding the key to deduplicate by for each message."). Examples(`${! meta("kafka_key") }`, `${! content().hash("xxhash64") }`), diff --git a/internal/impl/pure/processor_for_each.go b/internal/impl/pure/processor_for_each.go index 5a649f83bc..a704b0d268 100644 --- a/internal/impl/pure/processor_for_each.go +++ b/internal/impl/pure/processor_for_each.go @@ -16,7 +16,7 @@ func init() { Categories("Composition"). Summary("A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message."). Description(` -This is useful for forcing batch wide processors such as `+"[`dedupe`](/docs/components/processors/dedupe)"+` or interpolations such as the `+"`value`"+` field of the `+"`metadata`"+` processor to execute on individual message parts of a batch instead. +This is useful for forcing batch wide processors such as `+"xref:components:processors/dedupe.adoc[`dedupe`]"+` or interpolations such as the `+"`value`"+` field of the `+"`metadata`"+` processor to execute on individual message parts of a batch instead. Please note that most processors already process per message of a batch, and this processor is not needed in those cases.`). Field(service.NewProcessorListField("").Default([]any{})), diff --git a/internal/impl/pure/processor_grok.go b/internal/impl/pure/processor_grok.go index 4595a6102f..4c64f2c90b 100644 --- a/internal/impl/pure/processor_grok.go +++ b/internal/impl/pure/processor_grok.go @@ -34,15 +34,15 @@ func grokProcSpec() *service.ConfigSpec { Stable(). Summary("Parses messages into a structured format by attempting to apply a list of Grok expressions, the first expression to result in at least one value replaces the original message with a JSON object containing the values."). Description(` -Type hints within patterns are respected, therefore with the pattern `+"`%{WORD:first},%{INT:second:int}`"+` and a payload of `+"`foo,1`"+` the resulting payload would be `+"`{\"first\":\"foo\",\"second\":1}`"+`. +Type hints within patterns are respected, therefore with the pattern `+"`%\\{WORD:first},%{INT:second:int}`"+` and a payload of `+"`foo,1`"+` the resulting payload would be `+"`\\{\"first\":\"foo\",\"second\":1}`"+`. -### Performance +== Performance -This processor currently uses the [Go RE2](https://golang.org/s/re2syntax) regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information see [https://swtch.com/~rsc/regexp/regexp1.html](https://swtch.com/~rsc/regexp/regexp1.html).`). +This processor currently uses the https://golang.org/s/re2syntax[Go RE2] regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information, see https://swtch.com/~rsc/regexp/regexp1.html.`). Footnotes(` -## Default Patterns +== Default patterns -A summary of the default patterns on offer can be [found here](https://github.com/Jeffail/grok/blob/master/patterns.go#L5).`). +For summary of the default patterns on offer, see https://github.com/Jeffail/grok/blob/master/patterns.go#L5.`). Example("VPC Flow Logs", ` Grok can be used to parse unstructured logs such as VPC flow logs that look like this: @@ -81,7 +81,7 @@ pipeline: Advanced(). Default(true), service.NewBoolField(gpFieldUseDefaults). - Description("Whether to use a [default set of patterns](#default-patterns)."). + Description("Whether to use a <>."). Advanced(). Default(true), service.NewBoolField(gpFieldRemoveEmpty). diff --git a/internal/impl/pure/processor_group_by.go b/internal/impl/pure/processor_group_by.go index 96e3196685..704aa631d8 100644 --- a/internal/impl/pure/processor_group_by.go +++ b/internal/impl/pure/processor_group_by.go @@ -23,14 +23,14 @@ func groupByProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Composition"). Stable(). - Summary(`Splits a [batch of messages](/docs/configuration/batching) into N batches, where each resulting batch contains a group of messages determined by a [Bloblang query](/docs/guides/bloblang/about).`). + Summary(`Splits a xref:configuration:batching.adoc[batch of messages] into N batches, where each resulting batch contains a group of messages determined by a xref:guides:bloblang/about.adoc[Bloblang query].`). Description(` Once the groups are established a list of processors are applied to their respective grouped batch, which can be used to label the batch as per their grouping. Messages that do not pass the check of any specified group are placed in their own group. -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching).`). +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). Example( "Grouped Processing", - "Imagine we have a batch of messages that we wish to split into a group of foos and everything else, which should be sent to different output destinations based on those groupings. We also need to send the foos as a tar gzip archive. For this purpose we can use the `group_by` processor with a [`switch`](/docs/components/outputs/switch) output:", + "Imagine we have a batch of messages that we wish to split into a group of foos and everything else, which should be sent to different output destinations based on those groupings. We also need to send the foos as a tar gzip archive. For this purpose we can use the `group_by` processor with a xref:components:outputs/switch.adoc[`switch`] output:", ` pipeline: processors: @@ -59,14 +59,14 @@ output: ). Field(service.NewObjectListField("", service.NewBloblangField(gbpFieldCheck). - Description("A [Bloblang query](/docs/guides/bloblang/about) that should return a boolean value indicating whether a message belongs to a given group."). + Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message belongs to a given group."). Examples( `this.type == "foo"`, `this.contents.urls.contains("https://benthos.dev/")`, `true`, ), service.NewProcessorListField(gbpFieldProcessors). - Description("A list of [processors](/docs/components/processors/about) to execute on the newly formed group."). + Description("A list of xref:components:processors/about.adoc[processors] to execute on the newly formed group."). Default([]any{}), )) } diff --git a/internal/impl/pure/processor_group_by_value.go b/internal/impl/pure/processor_group_by_value.go index ab38c77589..6041bdae62 100644 --- a/internal/impl/pure/processor_group_by_value.go +++ b/internal/impl/pure/processor_group_by_value.go @@ -22,13 +22,13 @@ func init() { "group_by_value", service.NewConfigSpec(). Categories("Composition"). Stable(). - Summary(`Splits a batch of messages into N batches, where each resulting batch contains a group of messages determined by a [function interpolated string](/docs/configuration/interpolation#bloblang-queries) evaluated per message.`). + Summary(`Splits a batch of messages into N batches, where each resulting batch contains a group of messages determined by a xref:configuration:interpolation.adoc#bloblang-queries[function interpolated string] evaluated per message.`). Description(` This allows you to group messages using arbitrary fields within their content or metadata, process them individually, and send them to unique locations as per their group. -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching).`). +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). Footnotes(` -## Examples +== Examples If we were consuming Kafka messages and needed to group them by their key, archive the groups, and send them to S3 with the key as part of the path we could achieve that with the following: diff --git a/internal/impl/pure/processor_insert_part.go b/internal/impl/pure/processor_insert_part.go index e5eecfffba..61b9d6bc23 100644 --- a/internal/impl/pure/processor_insert_part.go +++ b/internal/impl/pure/processor_insert_part.go @@ -28,7 +28,7 @@ The index can be negative, and if so the message will be inserted from the end c The new message will have metadata copied from the first pre-existing message of the batch. -This processor will interpolate functions within the 'content' field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries).`). +This processor will interpolate functions within the 'content' field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here].`). Fields( service.NewIntField(ippFieldIndex). Description("The index within the batch to insert the message at."). diff --git a/internal/impl/pure/processor_jmespath.go b/internal/impl/pure/processor_jmespath.go index 9e72f14c10..5e54916a2e 100644 --- a/internal/impl/pure/processor_jmespath.go +++ b/internal/impl/pure/processor_jmespath.go @@ -23,11 +23,13 @@ func jmpProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Mapping"). Stable(). - Summary("Executes a [JMESPath query](http://jmespath.org/) on JSON documents and replaces the message with the resulting document."). + Summary("Executes a http://jmespath.org/[JMESPath query] on JSON documents and replaces the message with the resulting document."). Description(` -:::note Try out Bloblang -For better performance and improved capabilities try out native Benthos mapping with the [`+"`mapping`"+` processor](/docs/components/processors/mapping). -::: +[TIP] +.Try out Bloblang +==== +For better performance and improved capabilities try native Benthos mapping with the xref:components:processors/mapping.adoc[`+"`mapping`"+` processor]. +==== `). Example("Mapping", ` When receiving JSON documents of the form: diff --git a/internal/impl/pure/processor_jq.go b/internal/impl/pure/processor_jq.go index 89425407ce..6467676422 100644 --- a/internal/impl/pure/processor_jq.go +++ b/internal/impl/pure/processor_jq.go @@ -28,27 +28,25 @@ func jqProcSpec() *service.ConfigSpec { Stable(). Summary("Transforms and filters messages using jq queries."). Description(` -:::note Try out Bloblang -For better performance and improved capabilities try out native Benthos mapping with the [`+"`mapping`"+` processor](/docs/components/processors/mapping). -::: +[TIP] +.Try out Bloblang +==== +For better performance and improved capabilities try out native Benthos mapping with the xref:components:processors/mapping.adoc[`+"`mapping`"+` processor]. +==== The provided query is executed on each message, targeting either the contents as a structured JSON value or as a raw string using the field `+"`raw`"+`, and the message is replaced with the query result. Message metadata is also accessible within the query from the variable `+"`$metadata`"+`. -This processor uses the [gojq library][gojq], and therefore does not require jq to be installed as a dependency. However, this also means there are some differences in how these queries are executed versus the jq cli which you can [read about here][gojq-difference]. +This processor uses the https://github.com/itchyny/gojq[gojq library], and therefore does not require jq to be installed as a dependency. However, this also means there are some https://github.com/itchyny/gojq#difference-to-jq[differences in how these queries are executed] versus the jq cli. If the query does not emit any value then the message is filtered, if the query returns multiple values then the resulting message will be an array containing all values. -The full query syntax is described in [jq's documentation][jq-docs]. +The full query syntax is described in https://stedolan.github.io/jq/manual/[jq's documentation]. -## Error Handling +== Error handling -Queries can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling).`). - Footnotes(` -[gojq]: https://github.com/itchyny/gojq -[gojq-difference]: https://github.com/itchyny/gojq#difference-to-jq -[jq-docs]: https://stedolan.github.io/jq/manual/`). +Queries can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns].`). Example("Mapping", ` When receiving JSON documents of the form: diff --git a/internal/impl/pure/processor_jsonschema.go b/internal/impl/pure/processor_jsonschema.go index 7bf4a920c8..336db92a77 100644 --- a/internal/impl/pure/processor_jsonschema.go +++ b/internal/impl/pure/processor_jsonschema.go @@ -26,10 +26,10 @@ func jschemaProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Mapping"). Stable(). - Summary(`Checks messages against a provided JSONSchema definition but does not change the payload under any circumstances. If a message does not match the schema it can be caught using error handling methods outlined [here](/docs/configuration/error_handling).`). - Description(`Please refer to the [JSON Schema website](https://json-schema.org/) for information and tutorials regarding the syntax of the schema.`). + Summary(`Checks messages against a provided JSONSchema definition but does not change the payload under any circumstances. If a message does not match the schema it can be caught using xref:configuration:error_handling.adoc[error handling methods].`). + Description(`Please refer to the https://json-schema.org/[JSON Schema website] for information and tutorials regarding the syntax of the schema.`). Footnotes(` -## Examples +== Examples With the following JSONSchema document: diff --git a/internal/impl/pure/processor_log.go b/internal/impl/pure/processor_log.go index 732e3a47e1..59600cb144 100644 --- a/internal/impl/pure/processor_log.go +++ b/internal/impl/pure/processor_log.go @@ -27,13 +27,13 @@ func logProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Utility"). Stable(). - Summary(`Prints a log event for each message. Messages always remain unchanged. The log message can be set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries) which allows you to log the contents and metadata of messages.`). + Summary(`Prints a log event for each message. Messages always remain unchanged. The log message can be set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries] which allows you to log the contents and metadata of messages.`). Description(` The `+"`level`"+` field determines the log level of the printed events and can be any of the following values: TRACE, DEBUG, INFO, WARN, ERROR. -### Structured Fields +== Structured fields -It's also possible add custom fields to logs when the format is set to a structured form such as `+"`json` or `logfmt`"+` with the config field `+"[`fields_mapping`](#fields_mapping)"+`: +It's also possible add custom fields to logs when the format is set to a structured form such as `+"`json` or `logfmt`"+` with the config field `+"<>"+`: `+"```yaml"+` pipeline: @@ -54,7 +54,7 @@ pipeline: LintRule(``). Default("INFO"), service.NewBloblangField(logPFieldFieldsMapping). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) that can be used to specify extra fields to add to the log. If log fields are also added with the `fields` field then those values will override matching keys from this mapping."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that can be used to specify extra fields to add to the log. If log fields are also added with the `fields` field then those values will override matching keys from this mapping."). Examples( `root.reason = "cus I wana" root.id = this.id diff --git a/internal/impl/pure/processor_mapping.go b/internal/impl/pure/processor_mapping.go index 640b01887d..7baaae7453 100644 --- a/internal/impl/pure/processor_mapping.go +++ b/internal/impl/pure/processor_mapping.go @@ -19,19 +19,19 @@ func init() { Version("4.5.0"). Categories("Mapping", "Parsing"). Field(service.NewBloblangField("")). - Summary("Executes a [Bloblang](/docs/guides/bloblang/about) mapping on messages, creating a new document that replaces (or filters) the original message."). + Summary("Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping on messages, creating a new document that replaces (or filters) the original message."). Description(` -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information [check out the docs](/docs/guides/bloblang/about). +Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information, see xref:guides:bloblang/about.adoc[]. If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `+"`from \"\"`"+`, where the path must be absolute, or relative from the location that Benthos is executed from. -Note: This processor is equivalent to the [bloblang](/docs/components/processors/bloblang#component-rename) one. The latter will be deprecated in a future release. +Note: This processor is equivalent to the xref:components:processors/bloblang.adoc#component-rename[Bloblang] one. The latter will be deprecated in a future release. -## Input Document Immutability +== Input document immutability Mapping operates by creating an entirely new object during assignments, this has the advantage of treating the original referenced document as immutable and therefore queryable at any stage of your mapping. For example, with the following mapping: -`+"```coffee"+` +`+"```coffeescript"+` root.id = this.id root.invitees = this.invitees.filter(i -> i.mood >= 0.5) root.rejected = this.invitees.filter(i -> i.mood < 0.5) @@ -39,13 +39,13 @@ root.rejected = this.invitees.filter(i -> i.mood < 0.5) Notice that we mutate the value of `+"`invitees`"+` in the resulting document by filtering out objects with a lower mood. However, even after doing so we're still able to reference the unchanged original contents of this value from the input document in order to populate a second field. Within this mapping we also have the flexibility to reference the mutable mapped document by using the keyword `+"`root` (i.e. `root.invitees`)"+` on the right-hand side instead. -Mapping documents is advantageous in situations where the result is a document with a dramatically different shape to the input document, since we are effectively rebuilding the document in its entirety and might as well keep a reference to the unchanged input document throughout. However, in situations where we are only performing minor alterations to the input document, the rest of which is unchanged, it might be more efficient to use the `+"[`mutation` processor](/docs/components/processors/mutation)"+` instead. +Mapping documents is advantageous in situations where the result is a document with a dramatically different shape to the input document, since we are effectively rebuilding the document in its entirety and might as well keep a reference to the unchanged input document throughout. However, in situations where we are only performing minor alterations to the input document, the rest of which is unchanged, it might be more efficient to use the `+"xref:components:processors/mutation.adoc[`mutation` processor]"+` instead. -## Error Handling +== Error handling -Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). +Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behaviour, which you can read about [in this section](/docs/guides/bloblang/about#error-handling). +However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired xref:guides:bloblang/about.adoc#error-handling[fallback behavior]. `). Example("Mapping", ` Given JSON documents containing an array of fans: diff --git a/internal/impl/pure/processor_metric.go b/internal/impl/pure/processor_metric.go index 5956067c82..39cca2238d 100644 --- a/internal/impl/pure/processor_metric.go +++ b/internal/impl/pure/processor_metric.go @@ -31,18 +31,18 @@ func metProcSpec() *service.ConfigSpec { Stable(). Summary("Emit custom metrics by extracting values from messages."). Description(` -This processor works by evaluating an [interpolated field `+"`value`"+`](/docs/configuration/interpolation#bloblang-queries) for each message and updating a emitted metric according to the [type](#types). +This processor works by evaluating an xref:configuration:interpolation.adoc#bloblang-queries[interpolated field `+"`value`"+`] for each message and updating a emitted metric according to the <>. -Custom metrics such as these are emitted along with Benthos internal metrics, where you can customize where metrics are sent, which metric names are emitted and rename them as/when appropriate. For more information check out the [metrics docs here](/docs/components/metrics/about).`). +Custom metrics such as these are emitted along with Benthos internal metrics, where you can customize where metrics are sent, which metric names are emitted and rename them as/when appropriate. For more information see the xref:components:metrics/about.adoc[metrics docs].`). Footnotes(` -## Types +== Types -### `+"`counter`"+` +=== `+"`counter`"+` Increments a counter by exactly 1, the contents of `+"`value`"+` are ignored by this type. -### `+"`counter_by`"+` +=== `+"`counter_by`"+` If the contents of `+"`value`"+` can be parsed as a positive integer value then the counter is incremented by this value. @@ -59,7 +59,7 @@ pipeline: value: ${!json("field.some.value")} `+"```"+` -### `+"`gauge`"+` +=== `+"`gauge`"+` If the contents of `+"`value`"+` can be parsed as a positive integer value then the gauge is set to this value. @@ -76,7 +76,7 @@ pipeline: value: ${!json("field.some.value")} `+"```"+` -### `+"`timing`"+` +=== `+"`timing`"+` Equivalent to `+"`gauge`"+` where instead the metric is a timing. It is recommended that timing values are recorded in nanoseconds in order to be consistent with standard Benthos timing metrics, as in some cases these values are automatically converted into other units such as when exporting timings as histograms with Prometheus metrics.`). Example( @@ -124,7 +124,7 @@ metrics: ). Fields( service.NewStringEnumField(metProcFieldType, "counter", "counter_by", "gauge", "timing"). - Description("The metric [type](#types) to create."), + Description("The metric <> to create."), service.NewStringField(metProcFieldName). Description("The name of the metric to create, this must be unique across all Benthos components otherwise it will overwrite those other metrics."), service.NewInterpolatedStringMapField(metProcFieldLabels). diff --git a/internal/impl/pure/processor_mutation.go b/internal/impl/pure/processor_mutation.go index 386d1b113b..50d638a4a9 100644 --- a/internal/impl/pure/processor_mutation.go +++ b/internal/impl/pure/processor_mutation.go @@ -19,37 +19,37 @@ func init() { Version("4.5.0"). Categories("Mapping", "Parsing"). Field(service.NewBloblangField("")). - Summary("Executes a [Bloblang](/docs/guides/bloblang/about) mapping and directly transforms the contents of messages, mutating (or deleting) them."). + Summary("Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping and directly transforms the contents of messages, mutating (or deleting) them."). Description(` -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information [check out the docs](/docs/guides/bloblang/about). +Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information, see xref:guides:bloblang/about.adoc[]. If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `+"`from \"\"`"+`, where the path must be absolute, or relative from the location that Benthos is executed from. -## Input Document Mutability +== Input document mutability A mutation is a mapping that transforms input documents directly, this has the advantage of reducing the need to copy the data fed into the mapping. However, this also means that the referenced document is mutable and therefore changes throughout the mapping. For example, with the following Bloblang: -`+"```coffee"+` +`+"```coffeescript"+` root.rejected = this.invitees.filter(i -> i.mood < 0.5) root.invitees = this.invitees.filter(i -> i.mood >= 0.5) `+"```"+` Notice that we create a field `+"`rejected`"+` by copying the array field `+"`invitees`"+` and filtering out objects with a high mood. We then overwrite the field `+"`invitees`"+` by filtering out objects with a low mood, resulting in two array fields that are each a subset of the original. If we were to reverse the ordering of these assignments like so: -`+"```coffee"+` +`+"```coffeescript"+` root.invitees = this.invitees.filter(i -> i.mood >= 0.5) root.rejected = this.invitees.filter(i -> i.mood < 0.5) `+"```"+` Then the new field `+"`rejected`"+` would be empty as we have already mutated `+"`invitees`"+` to exclude the objects that it would be populated by. We can solve this problem either by carefully ordering our assignments or by capturing the original array using a variable (`+"`let invitees = this.invitees`"+`). -Mutations are advantageous over a standard mapping in situations where the result is a document with mostly the same shape as the input document, since we can avoid unnecessarily copying data from the referenced input document. However, in situations where we are creating an entirely new document shape it can be more convenient to use the traditional `+"[`mapping` processor](/docs/components/processors/mapping)"+` instead. +Mutations are advantageous over a standard mapping in situations where the result is a document with mostly the same shape as the input document, since we can avoid unnecessarily copying data from the referenced input document. However, in situations where we are creating an entirely new document shape it can be more convenient to use the traditional `+"xref:components:processors/mapping.adoc[`mapping` processor]"+` instead. -## Error Handling +== Error handling -Bloblang mappings can fail, in which case the error is logged and the message is flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). +Bloblang mappings can fail, in which case the error is logged and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behaviour, which you can read about [in this section](/docs/guides/bloblang/about#error-handling). +However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired xref:guides:bloblang/about.adoc#error-handling[fallback behavior]. `). Example("Mapping", ` Given JSON documents containing an array of fans: diff --git a/internal/impl/pure/processor_parallel.go b/internal/impl/pure/processor_parallel.go index d1264a0b59..dfd26eb696 100644 --- a/internal/impl/pure/processor_parallel.go +++ b/internal/impl/pure/processor_parallel.go @@ -20,11 +20,11 @@ func init() { "parallel", service.NewConfigSpec(). Categories("Composition"). Stable(). - Summary(`A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message (similar to the `+"[`for_each`](/docs/components/processors/for_each)"+` processor), but where each message is processed in parallel.`). + Summary(`A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message (similar to the `+"xref:components:processors/for_each.adoc[`for_each`]"+` processor), but where each message is processed in parallel.`). Description(` The field `+"`cap`"+`, if greater than zero, caps the maximum number of parallel processing threads. -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching).`). +The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching in xref:configuration:batching.adoc[].`). Fields( service.NewIntField(parProcFieldCap). Description("The maximum number of messages to have processing at a given time."). diff --git a/internal/impl/pure/processor_parse_log.go b/internal/impl/pure/processor_parse_log.go index 6a53f53dbb..92f34fd809 100644 --- a/internal/impl/pure/processor_parse_log.go +++ b/internal/impl/pure/processor_parse_log.go @@ -31,17 +31,17 @@ func parseLogSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Parsing"). Stable(). - Summary(`Parses common log [formats](#formats) into [structured data](#codecs). This is easier and often much faster than `+"[`grok`](/docs/components/processors/grok)"+`.`). + Summary(`Parses common log <> into <>. This is easier and often much faster than `+"xref:components:processors/grok.adoc[`grok`]"+`.`). Footnotes(` -## Codecs +== Codecs Currently the only supported structured data codec is `+"`json`"+`. -## Formats +== Formats -### `+"`syslog_rfc5424`"+` +=== `+"`syslog_rfc5424`"+` -Attempts to parse a log following the [Syslog rfc5424](https://tools.ietf.org/html/rfc5424) spec. The resulting structured document may contain any of the following fields: +Attempts to parse a log following the https://tools.ietf.org/html/rfc5424[Syslog rfc5424] spec. The resulting structured document may contain any of the following fields: - `+"`message`"+` (string) - `+"`timestamp`"+` (string, RFC3339) @@ -55,9 +55,9 @@ Attempts to parse a log following the [Syslog rfc5424](https://tools.ietf.org/ht - `+"`msgid`"+` (string) - `+"`structureddata`"+` (object) -### `+"`syslog_rfc3164`"+` +=== `+"`syslog_rfc3164`"+` -Attempts to parse a log following the [Syslog rfc3164](https://tools.ietf.org/html/rfc3164) spec. The resulting structured document may contain any of the following fields: +Attempts to parse a log following the https://tools.ietf.org/html/rfc3164[Syslog rfc3164] spec. The resulting structured document may contain any of the following fields: - `+"`message`"+` (string) - `+"`timestamp`"+` (string, RFC3339) @@ -71,7 +71,7 @@ Attempts to parse a log following the [Syslog rfc3164](https://tools.ietf.org/ht `). Fields( service.NewStringEnumField(plpFieldFormat, "syslog_rfc5424", "syslog_rfc3164"). - Description("A common log [format](#formats) to parse."), + Description("A common log <> to parse."), service.NewBoolField(plpFieldBestEffort). Description("Still returns partially parsed messages even if an error occurs."). Advanced(). @@ -85,7 +85,7 @@ Attempts to parse a log following the [Syslog rfc3164](https://tools.ietf.org/ht Advanced(). Default("current"), service.NewStringField(plpFieldWithTimezone). - Description("Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) format."). + Description("Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the https://golang.org/pkg/time/#LoadLocation[time.LoadLocation] format."). Advanced(). Default("UTC"), service.NewStringField(plpFieldCodec).Deprecated(), diff --git a/internal/impl/pure/processor_processors.go b/internal/impl/pure/processor_processors.go index fcd8c6ae67..322a6cdc20 100644 --- a/internal/impl/pure/processor_processors.go +++ b/internal/impl/pure/processor_processors.go @@ -16,7 +16,7 @@ func processorsProcSpec() *service.ConfigSpec { Categories("Composition"). Stable(). Summary(`A processor grouping several sub-processors.`). - Description("This processor is useful in situations where you want to collect several processors under a single resource identifier, whether it is for making your configuration easier to read and navigate, or for improving the testability of your configuration. The behaviour of child processors will match exactly the behaviour they would have under any other processors block."). + Description("This processor is useful in situations where you want to collect several processors under a single resource identifier, whether it is for making your configuration easier to read and navigate, or for improving the testability of your configuration. The behavior of child processors will match exactly the behavior they would have under any other processors block."). Example( "Grouped Processing", "Imagine we have a collection of processors who cover a specific functionality. We could use this processor to group them together and make it easier to read and mock during testing by giving the whole block a label:", diff --git a/internal/impl/pure/processor_rate_limit.go b/internal/impl/pure/processor_rate_limit.go index 5b40b48874..3b55f20193 100644 --- a/internal/impl/pure/processor_rate_limit.go +++ b/internal/impl/pure/processor_rate_limit.go @@ -23,9 +23,9 @@ func rlimitProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Utility"). Stable(). - Summary(`Throttles the throughput of a pipeline according to a specified ` + "[`rate_limit`](/docs/components/rate_limits/about)" + ` resource. Rate limits are shared across components and therefore apply globally to all processing pipelines.`). + Summary(`Throttles the throughput of a pipeline according to a specified ` + "xref:components:rate_limits/about.adoc[`rate_limit`]" + ` resource. Rate limits are shared across components and therefore apply globally to all processing pipelines.`). Field(service.NewStringField(rlimitFieldResource). - Description("The target [`rate_limit` resource](/docs/components/rate_limits/about).")) + Description("The target xref:components:rate_limits/about.adoc[`rate_limit` resource].")) } func init() { diff --git a/internal/impl/pure/processor_resource.go b/internal/impl/pure/processor_resource.go index bd1354ca93..95cd82a4ff 100644 --- a/internal/impl/pure/processor_resource.go +++ b/internal/impl/pure/processor_resource.go @@ -44,7 +44,7 @@ processor_resources: root.user.age = this.user.age.number() `+"```"+` -You can find out more about resources [in this document.](/docs/configuration/resources)`). +You can find out more about resources in xref:configuration:resources.adoc[]`). Field(service.NewStringField("").Default("")), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { name, err := conf.FieldString() diff --git a/internal/impl/pure/processor_retry.go b/internal/impl/pure/processor_retry.go index 9916153c25..2d79a79f4f 100644 --- a/internal/impl/pure/processor_retry.go +++ b/internal/impl/pure/processor_retry.go @@ -32,20 +32,22 @@ Executes child processors and if a resulting message is errored then, after a sp It is important to note that any mutations performed on the message during these child processors will be discarded for the next retry, and therefore it is safe to assume that each execution of the child processors will always be performed on the data as it was when it first reached the retry processor. -By default the retry backoff has a specified `+"[`max_elapsed_time`](#backoffmax_elapsed_time)"+`, if this time period is reached during retries and an error still occurs these errored messages will proceed through to the next processor after the retry (or your outputs). Normal [error handling patterns](/docs/configuration/error_handling) can be used on these messages. +By default the retry backoff has a specified `+"<>"+`, if this time period is reached during retries and an error still occurs these errored messages will proceed through to the next processor after the retry (or your outputs). Normal xref:configuration:error_handling.adoc[error handling patterns] can be used on these messages. In order to avoid permanent loops any error associated with messages as they first enter a retry processor will be cleared. -:::caution Batching -If you wish to wrap a batch-aware series of processors then take a look at the [batching section](#batching) below. -::: +[CAUTION] +.Batching +==== +If you wish to wrap a batch-aware series of processors then take a look at the <>. +==== `). Footnotes(` -## Batching +== Batching -When messages are batched the child processors of a `+"retry"+` are executed for each individual message in isolation, performed serially by default but in parallel when the field `+"[`parallel`](#parallel) is set to `true`"+`. This is an intentional limitation of the retry processor and is done in order to ensure that errors are correctly associated with a given input message. Otherwise, the archiving, expansion, grouping, filtering and so on of the child processors could obfuscate this relationship. +When messages are batched the child processors of a `+"retry"+` are executed for each individual message in isolation, performed serially by default but in parallel when the field `+"<> is set to `true`"+`. This is an intentional limitation of the retry processor and is done in order to ensure that errors are correctly associated with a given input message. Otherwise, the archiving, expansion, grouping, filtering and so on of the child processors could obfuscate this relationship. -If the target behaviour of your retried processors is "batch aware", in that you wish to perform some processing across the entire batch of messages and repeat it in the event of errors, you can use an `+"[`archive` processor](/docs/components/processors/archive)"+` to collapse the batch into an individual message. Then, within these child processors either perform your batch aware processing on the archive, or use an `+"[`unarchive` processor](/docs/components/processors/unarchive)"+` in order to expand the single message back out into a batch. +If the target behavior of your retried processors is "batch aware", in that you wish to perform some processing across the entire batch of messages and repeat it in the event of errors, you can use an `+"xref:components:processors/archive.adoc[`archive` processor]"+` to collapse the batch into an individual message. Then, within these child processors either perform your batch aware processing on the archive, or use an `+"xref:components:processors/unarchive.adoc[`unarchive` processor]"+` in order to expand the single message back out into a batch. For example, if the retry processor were being used to wrap an HTTP request where the payload data is a batch archived into a JSON array it should look something like this: @@ -93,7 +95,7 @@ output: Fields( service.NewBackOffField(rpFieldBackoff, true, nil), service.NewProcessorListField(rpFieldProcessors). - Description("A list of [processors](/docs/components/processors/about/) to execute on each message."), + Description("A list of xref:components:processors/about.adoc[processors] to execute on each message."), service.NewBoolField(rpFieldParallel). Description("When processing batches of messages these batches are ignored and the processors apply to each message sequentially. However, when this field is set to `true` each message will be processed in parallel. Caution should be made to ensure that batch sizes do not surpass a point where this would cause resource (CPU, memory, API limits) contention."). Default(false), diff --git a/internal/impl/pure/processor_select_parts.go b/internal/impl/pure/processor_select_parts.go index 02047995f9..0e232bed70 100644 --- a/internal/impl/pure/processor_select_parts.go +++ b/internal/impl/pure/processor_select_parts.go @@ -26,7 +26,7 @@ If none of the selected parts exist in the input batch (resulting in an empty ou Message indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. E.g. if index = -1 then the selected part will be the last part of the message, if index = -2 then the part before the last element with be selected, and so on. -This processor is only applicable to [batched messages](/docs/configuration/batching).`). +This processor is only applicable to xref:configuration:batching.adoc[batched messages].`). Field(service.NewIntListField(spFieldParts). Description(`An array of message indexes of a batch. Indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1.`). Default([]any{})), diff --git a/internal/impl/pure/processor_sleep.go b/internal/impl/pure/processor_sleep.go index 87522c1531..f89edeb8bc 100644 --- a/internal/impl/pure/processor_sleep.go +++ b/internal/impl/pure/processor_sleep.go @@ -24,7 +24,7 @@ func init() { err := service.RegisterBatchProcessor("sleep", service.NewConfigSpec(). Categories("Utility"). Stable(). - Summary(`Sleep for a period of time specified as a duration string for each message. This processor will interpolate functions within the `+"`duration`"+` field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries).`). + Summary(`Sleep for a period of time specified as a duration string for each message. This processor will interpolate functions within the `+"`duration`"+` field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here].`). Field(service.NewInterpolatedStringField(spFieldDuration). Description("The duration of time to sleep for each execution.")), func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { diff --git a/internal/impl/pure/processor_split.go b/internal/impl/pure/processor_split.go index 0f6242d99e..e013beae3d 100644 --- a/internal/impl/pure/processor_split.go +++ b/internal/impl/pure/processor_split.go @@ -22,7 +22,7 @@ func init() { Stable(). Summary(`Breaks message batches (synonymous with multiple part messages) into smaller batches. The size of the resulting batches are determined either by a discrete size or, if the field `+"`byte_size`"+` is non-zero, then by total size in bytes (which ever limit is reached first).`). Description(` -This processor is for breaking batches down into smaller ones. In order to break a single message out into multiple messages use the `+"[`unarchive` processor](/docs/components/processors/unarchive)"+`. +This processor is for breaking batches down into smaller ones. In order to break a single message out into multiple messages use the `+"xref:components:processors/unarchive.adoc[`unarchive` processor]"+`. If there is a remainder of messages after splitting a batch the remainder is also sent as a single batch. For example, if your target size was 10, and the processor received a batch of 95 message parts, the result would be 9 batches of 10 messages followed by a batch of 5 messages.`). Fields( diff --git a/internal/impl/pure/processor_switch.go b/internal/impl/pure/processor_switch.go index 75503a9437..b0bcdec203 100644 --- a/internal/impl/pure/processor_switch.go +++ b/internal/impl/pure/processor_switch.go @@ -26,13 +26,13 @@ func switchProcSpec() *service.ConfigSpec { Categories("Composition"). Stable(). Summary(`Conditionally processes messages based on their contents.`). - Description(`For each switch case a [Bloblang query](/docs/guides/bloblang/about) is checked and, if the result is true (or the check is empty) the child processors are executed on the message.`). + Description(`For each switch case a xref:guides:bloblang/about.adoc[Bloblang query] is checked and, if the result is true (or the check is empty) the child processors are executed on the message.`). Footnotes(` -## Batching +== Batching -When a switch processor executes on a [batch of messages](/docs/configuration/batching) they are checked individually and can be matched independently against cases. During processing the messages matched against a case are processed as a batch, although the ordering of messages during case processing cannot be guaranteed to match the order as received. +When a switch processor executes on a xref:configuration:batching.adoc[batch of messages] they are checked individually and can be matched independently against cases. During processing the messages matched against a case are processed as a batch, although the ordering of messages during case processing cannot be guaranteed to match the order as received. -At the end of switch processing the resulting batch will follow the same ordering as the batch was received. If any child processors have split or otherwise grouped messages this grouping will be lost as the result of a switch is always a single batch. In order to perform conditional grouping and/or splitting use the [`+"`group_by`"+` processor](/docs/components/processors/group_by).`). +At the end of switch processing the resulting batch will follow the same ordering as the batch was received. If any child processors have split or otherwise grouped messages this grouping will be lost as the result of a switch is always a single batch. In order to perform conditional grouping and/or splitting use the xref:components:processors/group_by.adoc[`+"`group_by`"+` processor].`). Example("I Hate George", ` We have a system where we're counting a metric for all messages that pass through our system. However, occasionally we get messages from George where he's rambling about dumb stuff we don't care about. @@ -57,14 +57,14 @@ pipeline: ). Field(service.NewObjectListField("", service.NewBloblangField(spFieldCheck). - Description("A [Bloblang query](/docs/guides/bloblang/about) that should return a boolean value indicating whether a message should have the processors of this case executed on it. If left empty the case always passes. If the check mapping throws an error the message will be flagged [as having failed](/docs/configuration/error_handling) and will not be tested against any other cases."). + Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should have the processors of this case executed on it. If left empty the case always passes. If the check mapping throws an error the message will be flagged xref:configuration:error_handling.adoc[as having failed] and will not be tested against any other cases."). Examples( `this.type == "foo"`, `this.contents.urls.contains("https://benthos.dev/")`, ). Default(""), service.NewProcessorListField(spFieldProcessors). - Description("A list of [processors](/docs/components/processors/about/) to execute on a message."). + Description("A list of xref:components:processors/about.adoc[processors] to execute on a message."). Default([]any{}), service.NewBoolField(spFieldFallthrough). Description("Indicates whether, if this case passes for a message, the next case should also be executed."). diff --git a/internal/impl/pure/processor_sync_response.go b/internal/impl/pure/processor_sync_response.go index 0afb392413..f501c38a3a 100644 --- a/internal/impl/pure/processor_sync_response.go +++ b/internal/impl/pure/processor_sync_response.go @@ -16,9 +16,9 @@ func init() { Stable(). Summary("Adds the payload in its current state as a synchronous response to the input source, where it is dealt with according to that specific input type."). Description(` -For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this processor even when combining input types that might not have support for sync responses. An example of an input able to utilise this is the `+"`http_server`"+`. +For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this processor even when combining input types that might not have support for sync responses. An example of an input able to utilize this is the `+"`http_server`"+`. -For more information please read [Synchronous Responses](/docs/guides/sync_responses).`). +For more information please read xref:guides:sync_responses.adoc[synchronous responses].`). Field(service.NewObjectField("").Default(map[string]any{})), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { p := &syncResponseProc{log: interop.UnwrapManagement(mgr).Logger()} diff --git a/internal/impl/pure/processor_try.go b/internal/impl/pure/processor_try.go index 2792263369..95ae8b043b 100644 --- a/internal/impl/pure/processor_try.go +++ b/internal/impl/pure/processor_try.go @@ -17,7 +17,7 @@ func init() { Categories("Composition"). Summary("Executes a list of child processors on messages only if no prior processors have failed (or the errors have been cleared)."). Description(` -This processor behaves similarly to the `+"[`for_each`](/docs/components/processors/for_each)"+` processor, where a list of child processors are applied to individual messages of a batch. However, if a message has failed any prior processor (before or during the try block) then that message will skip all following processors. +This processor behaves similarly to the `+"xref:components:processors/for_each.adoc[`for_each`]"+` processor, where a list of child processors are applied to individual messages of a batch. However, if a message has failed any prior processor (before or during the try block) then that message will skip all following processors. For example, with the following config: @@ -33,13 +33,13 @@ pipeline: If the processor `+"`bar`"+` fails for a particular message, that message will skip the processors `+"`baz` and `buz`"+`. Similarly, if `+"`bar`"+` succeeds but `+"`baz`"+` does not then `+"`buz`"+` will be skipped. If the processor `+"`foo`"+` fails for a message then none of `+"`bar`, `baz` or `buz`"+` are executed on that message. -This processor is useful for when child processors depend on the successful output of previous processors. This processor can be followed with a `+"[catch](/docs/components/processors/catch)"+` processor for defining child processors to be applied only to failed messages. +This processor is useful for when child processors depend on the successful output of previous processors. This processor can be followed with a `+"xref:components:processors/catch.adoc[catch]"+` processor for defining child processors to be applied only to failed messages. -More information about error handing can be found [here](/docs/configuration/error_handling). +More information about error handing can be found in xref:configuration:error_handling.adoc[]. -### Nesting within a catch block +== Nest within a catch block -In some cases it might be useful to nest a try block within a catch block, since the `+"[`catch` processor](/docs/components/processors/catch)"+` only clears errors _after_ executing its child processors this means a nested try processor will not execute unless the errors are explicitly cleared beforehand. +In some cases it might be useful to nest a try block within a catch block, since the `+"xref:components:processors/catch.adoc[`catch` processor]"+` only clears errors _after_ executing its child processors this means a nested try processor will not execute unless the errors are explicitly cleared beforehand. This can be done by inserting an empty catch block before the try block like as follows: diff --git a/internal/impl/pure/processor_unarchive.go b/internal/impl/pure/processor_unarchive.go index f2b1d433e2..d1fb4143cb 100644 --- a/internal/impl/pure/processor_unarchive.go +++ b/internal/impl/pure/processor_unarchive.go @@ -20,18 +20,18 @@ func unarchiveProcConfig() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Categories("Parsing", "Utility"). - Summary("Unarchives messages according to the selected archive format into multiple messages within a [batch](/docs/configuration/batching)."). + Summary("Unarchives messages according to the selected archive format into multiple messages within a xref:configuration:batching.adoc[batch]."). Description(` -When a message is unarchived the new messages replace the original message in the batch. Messages that are selected but fail to unarchive (invalid format) will remain unchanged in the message batch but will be flagged as having failed, allowing you to [error handle them](/docs/configuration/error_handling). +When a message is unarchived the new messages replace the original message in the batch. Messages that are selected but fail to unarchive (invalid format) will remain unchanged in the message batch but will be flagged as having failed, allowing you to xref:configuration:error_handling.adoc[error handle them]. -## Metadata +== Metadata The metadata found on the messages handled by this processor will be copied into the resulting messages. For the unarchive formats that contain file information (tar, zip), a metadata field is also added to each message called ` + "`archive_filename`" + ` with the extracted filename. `). Field(service.NewStringAnnotatedEnumField("format", map[string]string{ `tar`: `Extract messages from a unix standard tape archive.`, `zip`: `Extract messages from a zip file.`, - `binary`: `Extract messages from a [binary blob format](https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96).`, + `binary`: `Extract messages from a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format].`, `lines`: `Extract the lines of a message each into their own message.`, `json_documents`: `Attempt to parse a message as a stream of concatenated JSON documents. Each parsed document is expanded into a new message.`, `json_array`: `Attempt to parse a message as a JSON array, and extract each element into its own message.`, diff --git a/internal/impl/pure/processor_while.go b/internal/impl/pure/processor_while.go index 59aee845ed..0949ebe033 100644 --- a/internal/impl/pure/processor_while.go +++ b/internal/impl/pure/processor_while.go @@ -29,7 +29,7 @@ func whileProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Composition"). Stable(). - Summary("A processor that checks a [Bloblang query](/docs/guides/bloblang/about/) against each batch of messages and executes child processors on them for as long as the query resolves to true."). + Summary("A processor that checks a xref:guides:bloblang/about.adoc[Bloblang query] against each batch of messages and executes child processors on them for as long as the query resolves to true."). Description(` The field `+"`at_least_once`"+`, if true, ensures that the child processors are always executed at least one time (like a do .. while loop.) @@ -37,7 +37,7 @@ The field `+"`max_loops`"+`, if greater than zero, caps the number of loops for If following a loop execution the number of messages in a batch is reduced to zero the loop is exited regardless of the condition result. If following a loop execution there are more than 1 message batches the query is checked against the first batch only. -The conditions of this processor are applied across entire message batches. You can find out more about batching [in this doc](/docs/configuration/batching).`). +The conditions of this processor are applied across entire message batches. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). Fields( service.NewBoolField(wpFieldAtLeastOnce). @@ -48,7 +48,7 @@ The conditions of this processor are applied across entire message batches. You Advanced(). Default(0), service.NewBloblangField(wpFieldCheck). - Description("A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether the while loop should execute again."). + Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether the while loop should execute again."). Examples(`errored()`, `this.urls.unprocessed.length() > 0`). Default(""), service.NewProcessorListField(wpFieldProcessors). diff --git a/internal/impl/pure/processor_workflow.go b/internal/impl/pure/processor_workflow.go index e2dd7b255a..6c7c4fa764 100644 --- a/internal/impl/pure/processor_workflow.go +++ b/internal/impl/pure/processor_workflow.go @@ -30,21 +30,21 @@ func workflowProcSpec() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Composition"). Stable(). - Summary(`Executes a topology of `+"[`branch` processors][processors.branch]"+`, performing them in parallel where possible.`). + Summary(`Executes a topology of `+"xref:components:processors/branch.adoc[`branch` processors]"+`, performing them in parallel where possible.`). Description(` -## Why Use a Workflow +== Why use a workflow -### Performance +=== Performance -Most of the time the best way to compose processors is also the simplest, just configure them in series. This is because processors are often CPU bound, low-latency, and you can gain vertical scaling by increasing the number of processor pipeline threads, allowing Benthos to process [multiple messages in parallel][configuration.pipelines]. +Most of the time the best way to compose processors is also the simplest, just configure them in series. This is because processors are often CPU bound, low-latency, and you can gain vertical scaling by increasing the number of processor pipeline threads, allowing Benthos to process xref:configuration:processing_pipelines.adoc[multiple messages in parallel]. -However, some processors such as `+"[`http`][processors.http], [`aws_lambda`][processors.aws_lambda] or [`cache`][processors.cache]"+` interact with external services and therefore spend most of their time waiting for a response. These processors tend to be high-latency and low CPU activity, which causes messages to process slowly. +However, some processors such as `+"xref:components:processors/http.adoc[`http`], xref:components:processors/aws_lambda.adoc[`aws_lambda`] or xref:components:processors/cache.adoc[`cache`]"+` interact with external services and therefore spend most of their time waiting for a response. These processors tend to be high-latency and low CPU activity, which causes messages to process slowly. When a processing pipeline contains multiple network processors that aren't dependent on each other we can benefit from performing these processors in parallel for each individual message, reducing the overall message processing latency. -### Simplifying Processor Topology +=== Simplifying processor topology -A workflow is often expressed as a [DAG][dag_wiki] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. +A workflow is often expressed as a https://en.wikipedia.org/wiki/Directed_acyclic_graph[DAG] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. For example, if we had processing stages A, B, C and D, where stage A could result in either stage B or C being next, always followed by D, it might look something like this: @@ -54,7 +54,7 @@ A --| |--> D \--> C --/ `+"```"+` -This flow would be easy to express in a standard Benthos config, we could simply use a `+"[`switch` processor][processors.switch]"+` to route to either B or C depending on a condition on the result of A. However, this method of flow control quickly becomes unfeasible as the DAG gets more complicated, imagine expressing this flow using switch processors: +This flow would be easy to express in a standard Benthos config, we could simply use a `+"xref:components:processors/switch.adoc[`switch` processor]"+` to route to either B or C depending on a condition on the result of A. However, this method of flow control quickly becomes unfeasible as the DAG gets more complicated, imagine expressing this flow using switch processors: `+"```text"+` /--> B -------------|--> D @@ -66,7 +66,7 @@ A --| /--> E --| And imagine doing so knowing that the diagram is subject to change over time. Yikes! Instead, with a workflow we can either trust it to automatically resolve the DAG or express it manually as simply as `+"`order: [ [ A ], [ B, C ], [ E ], [ D, F ] ]`"+`, and the conditional logic for determining if a stage is executed is defined as part of the branch itself.`). Footnotes(` -## Structured Metadata +== Structured metadata When the field `+"`meta_path`"+` is non-empty the workflow processor creates an object describing which workflows were successful, skipped or failed for each message and stores the object within the message at the end. @@ -90,38 +90,28 @@ The previous meta object will also be preserved in the field `+"`.pre If a field `+"`.apply`"+` exists in the meta object for a message and is an array then it will be used as an explicit list of stages to apply, all other stages will be skipped. -## Resources +== Resources -It's common to configure processors (and other components) [as resources][configuration.resources] in order to keep the pipeline configuration cleaner. With the workflow processor you can include branch processors configured as resources within your workflow either by specifying them by name in the field `+"`order`"+`, if Benthos doesn't find a branch within the workflow configuration of that name it'll refer to the resources. +It's common to configure processors (and other components) xref:configuration:resources.adoc[as resources] in order to keep the pipeline configuration cleaner. With the workflow processor you can include branch processors configured as resources within your workflow either by specifying them by name in the field `+"`order`"+`, if Benthos doesn't find a branch within the workflow configuration of that name it'll refer to the resources. Alternatively, if you do not wish to have an explicit ordering, you can add resource names to the field `+"`branch_resources`"+` and they will be included in the workflow with automatic DAG resolution along with any branches configured in the `+"`branches`"+` field. -### Resource Error Conditions +=== Resource error conditions There are two error conditions that could potentially occur when resources included in your workflow are mutated, and if you are planning to mutate resources in your workflow it is important that you understand them. The first error case is that a resource in the workflow is removed and not replaced, when this happens the workflow will still be executed but the individual branch will fail. This should only happen if you explicitly delete a branch resource, as any mutation operation will create the new resource before removing the old one. -The second error case is when automatic DAG resolution is being used and a resource in the workflow is changed in a way that breaks the DAG (circular dependencies, etc). When this happens it is impossible to execute the workflow and therefore the processor will fail, which is possible to capture and handle using [standard error handling patterns][configuration.error-handling]. +The second error case is when automatic DAG resolution is being used and a resource in the workflow is changed in a way that breaks the DAG (circular dependencies, etc). When this happens it is impossible to execute the workflow and therefore the processor will fail, which is possible to capture and handle using xref:configuration:error_handling.adoc[standard error handling patterns]. -## Error Handling +== Error handling -The recommended approach to handle failures within a workflow is to query against the [structured metadata](#structured-metadata) it provides, as it provides granular information about exactly which branches failed and which ones succeeded and therefore aren't necessary to perform again. +The recommended approach to handle failures within a workflow is to query against the <> it provides, as it provides granular information about exactly which branches failed and which ones succeeded and therefore aren't necessary to perform again. -For example, if our meta object is stored at the path `+"`meta.workflow`"+` and we wanted to check whether a message has failed for any branch we can do that using a [Bloblang query][guides.bloblang] like `+"`this.meta.workflow.failed.length() | 0 > 0`"+`, or to check whether a specific branch failed we can use `+"`this.exists(\"meta.workflow.failed.foo\")`"+`. +For example, if our meta object is stored at the path `+"`meta.workflow`"+` and we wanted to check whether a message has failed for any branch we can do that using a xref:guides:bloblang/about.adoc[Bloblang query] like `+"`this.meta.workflow.failed.length() | 0 > 0`"+`, or to check whether a specific branch failed we can use `+"`this.exists(\"meta.workflow.failed.foo\")`"+`. -However, if structured metadata is disabled by setting the field `+"`meta_path`"+` to empty then the workflow processor instead adds a general error flag to messages when any executed branch fails. In this case it's possible to handle failures using [standard error handling patterns][configuration.error-handling]. +However, if structured metadata is disabled by setting the field `+"`meta_path`"+` to empty then the workflow processor instead adds a general error flag to messages when any executed branch fails. In this case it's possible to handle failures using xref:configuration:error_handling.adoc[standard error handling patterns]. -[dag_wiki]: https://en.wikipedia.org/wiki/Directed_acyclic_graph -[processors.switch]: /docs/components/processors/switch -[processors.http]: /docs/components/processors/http -[processors.aws_lambda]: /docs/components/processors/aws_lambda -[processors.cache]: /docs/components/processors/cache -[processors.branch]: /docs/components/processors/branch -[guides.bloblang]: /docs/guides/bloblang/about -[configuration.pipelines]: /docs/configuration/processing_pipelines -[configuration.error-handling]: /docs/configuration/error_handling -[configuration.resources]: /docs/configuration/resources `). Example("Automatic Ordering", ` When the field `+"`order`"+` is omitted a best attempt is made to determine a dependency tree between branches based on their request and result mappings. In the following example the branches foo and bar will be executed first in parallel, and afterwards the branch baz will be executed.`, ` @@ -192,7 +182,7 @@ pipeline: result_map: 'root.tmp.result = this' `). Example("Resources", ` -The `+"`order`"+` field can be used in order to refer to [branch processor resources](#resources), this can sometimes make your pipeline configuration cleaner, as well as allowing you to reuse branch configurations in order places. It's also possible to mix and match branches configured within the workflow and configured as resources.`, ` +The `+"`order`"+` field can be used in order to refer to <>, this can sometimes make your pipeline configuration cleaner, as well as allowing you to reuse branch configurations in order places. It's also possible to mix and match branches configured within the workflow and configured as resources.`, ` pipeline: processors: - workflow: @@ -228,22 +218,22 @@ processor_resources: `). Fields( service.NewStringField(wflowProcFieldMetaPath). - Description("A [dot path](/docs/configuration/field_paths) indicating where to store and reference [structured metadata](#structured-metadata) about the workflow execution."). + Description("A xref:configuration:field_paths.adoc[dot path] indicating where to store and reference <> about the workflow execution."). Default("meta.workflow"), service.NewStringListOfListsField(wflowProcFieldOrder). - Description("An explicit declaration of branch ordered tiers, which describes the order in which parallel tiers of branches should be executed. Branches should be identified by the name as they are configured in the field `branches`. It's also possible to specify branch processors configured [as a resource](#resources)."). + Description("An explicit declaration of branch ordered tiers, which describes the order in which parallel tiers of branches should be executed. Branches should be identified by the name as they are configured in the field `branches`. It's also possible to specify branch processors configured <>."). Examples( []any{[]any{"foo", "bar"}, []any{"baz"}}, []any{[]any{"foo"}, []any{"bar"}, []any{"baz"}}, ). Default([]any{}), service.NewStringListField(wflowProcFieldBranchResources). - Description("An optional list of [`branch` processor](/docs/components/processors/branch) names that are configured as [resources](#resources). These resources will be included in the workflow with any branches configured inline within the [`branches`](#branches) field. The order and parallelism in which branches are executed is automatically resolved based on the mappings of each branch. When using resources with an explicit order it is not necessary to list resources in this field."). + Description("An optional list of xref:components:processors/branch.adoc[`branch` processor] names that are configured as <>. These resources will be included in the workflow with any branches configured inline within the <> field. The order and parallelism in which branches are executed is automatically resolved based on the mappings of each branch. When using resources with an explicit order it is not necessary to list resources in this field."). Version("3.38.0"). Advanced(). Default([]any{}), service.NewObjectMapField(wflowProcFieldBranches, branchSpecFields()...). - Description("An object of named [`branch` processors](/docs/components/processors/branch) that make up the workflow. The order and parallelism in which branches are executed can either be made explicit with the field `order`, or if omitted an attempt is made to automatically resolve an ordering based on the mappings of each branch."). + Description("An object of named xref:components:processors/branch.adoc[`branch` processors] that make up the workflow. The order and parallelism in which branches are executed can either be made explicit with the field `order`, or if omitted an attempt is made to automatically resolve an ordering based on the mappings of each branch."). Default(map[string]any{}), ) } diff --git a/internal/impl/pure/scanner_csv.go b/internal/impl/pure/scanner_csv.go index 113adc8bf7..7fb8945537 100644 --- a/internal/impl/pure/scanner_csv.go +++ b/internal/impl/pure/scanner_csv.go @@ -21,7 +21,7 @@ func csvScannerSpec() *service.ConfigSpec { Stable(). Summary("Consume comma-separated values row by row, including support for custom delimiters."). Description(` -### Metadata +== Metadata This scanner adds the following metadata to each message: diff --git a/internal/impl/pure/scanner_tar.go b/internal/impl/pure/scanner_tar.go index 443110f5b6..e23a1f8547 100644 --- a/internal/impl/pure/scanner_tar.go +++ b/internal/impl/pure/scanner_tar.go @@ -14,7 +14,7 @@ func tarScannerSpec() *service.ConfigSpec { Stable(). Summary("Consume a tar archive file by file."). Description(` -### Metadata +== Metadata This scanner adds the following metadata to each message: diff --git a/internal/impl/pure/scanner_to_the_end.go b/internal/impl/pure/scanner_to_the_end.go index 294820d796..fce00f4b5e 100644 --- a/internal/impl/pure/scanner_to_the_end.go +++ b/internal/impl/pure/scanner_to_the_end.go @@ -12,9 +12,10 @@ func toTheEndScannerSpec() *service.ConfigSpec { Stable(). Summary("Read the input stream all the way until the end and deliver it as a single message."). Description(` -:::caution +[CAUTION] +==== Some sources of data may not have a logical end, therefore caution should be made to exclusively use this scanner when the end of an input stream is clearly defined (and well within memory). -::: +==== `). Field(service.NewObjectField("").Default(map[string]any{})) } diff --git a/internal/impl/redis/output_hash.go b/internal/impl/redis/output_hash.go index 175f54e9dc..f471ae2bee 100644 --- a/internal/impl/redis/output_hash.go +++ b/internal/impl/redis/output_hash.go @@ -23,7 +23,7 @@ func redisHashOutputConfig() *service.ConfigSpec { Stable(). Summary(`Sets Redis hash objects using the HMSET command.`). Description(` -The field `+"`key`"+` supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries), allowing you to create a unique key for each message. +The field `+"`key`"+` supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing you to create a unique key for each message. The field `+"`fields`"+` allows you to specify an explicit map of field names to interpolated values, also evaluated per message of a batch: diff --git a/internal/impl/redis/output_list.go b/internal/impl/redis/output_list.go index 445ab07bde..c1f26dde73 100644 --- a/internal/impl/redis/output_list.go +++ b/internal/impl/redis/output_list.go @@ -26,7 +26,7 @@ func redisListOutputConfig() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). Summary(`Pushes messages onto the end of a Redis list (which is created if it doesn't already exist) using the RPUSH command.`). - Description(`The field `+"`key`"+` supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries), allowing you to create a unique key for each message.`+service.OutputPerformanceDocs(true, true)). + Description(`The field `+"`key`"+` supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing you to create a unique key for each message.`+service.OutputPerformanceDocs(true, true)). Categories("Services"). Fields(clientFields()...). Fields( diff --git a/internal/impl/redis/output_pubsub.go b/internal/impl/redis/output_pubsub.go index 4d896dddf0..0c6efbdc45 100644 --- a/internal/impl/redis/output_pubsub.go +++ b/internal/impl/redis/output_pubsub.go @@ -20,7 +20,7 @@ func redisPubSubOutputConfig() *service.ConfigSpec { Stable(). Summary(`Publishes messages through the Redis PubSub model. It is not possible to guarantee that messages have been received.`). Description(` -This output will interpolate functions within the channel field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries).`+service.OutputPerformanceDocs(true, true)). +This output will interpolate functions within the channel field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here].`+service.OutputPerformanceDocs(true, true)). Categories("Services"). Fields(clientFields()...). Fields( diff --git a/internal/impl/redis/processor.go b/internal/impl/redis/processor.go index 2ae6aa22f7..7aa9c486a6 100644 --- a/internal/impl/redis/processor.go +++ b/internal/impl/redis/processor.go @@ -17,8 +17,8 @@ import ( func redisProcConfig() *service.ConfigSpec { spec := service.NewConfigSpec(). Stable(). - Summary(`Performs actions against Redis that aren't possible using a ` + "[`cache`](/docs/components/processors/cache)" + ` processor. Actions are -performed for each message and the message contents are replaced with the result. In order to merge the result into the original message compose this processor within a ` + "[`branch` processor](/docs/components/processors/branch)" + `.`). + Summary(`Performs actions against Redis that aren't possible using a ` + "xef:components:processors/cache.adoc[`cache`]" + ` processor. Actions are +performed for each message and the message contents are replaced with the result. In order to merge the result into the original message compose this processor within a ` + "xref:components:processors/branch.adoc[`branch` processor]" + `.`). Categories("Integration") for _, f := range clientFields() { @@ -34,7 +34,7 @@ performed for each message and the message contents are replaced with the result Example(`${! meta("command") }`). Optional()). Field(service.NewBloblangField("args_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis command."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis command."). Version("4.3.0"). Optional(). Example("root = [ this.key ]"). @@ -65,7 +65,7 @@ performed for each message and the message contents are replaced with the result this.exists("args_mapping") && this.exists("operator") => [ "field args_mapping is invalid with an operator set" ], }`). Example("Querying Cardinality", - `If given payloads containing a metadata field `+"`set_key`"+` it's possible to query and store the cardinality of the set for each message using a `+"[`branch` processor](/docs/components/processors/branch)"+` in order to augment rather than replace the message contents:`, + `If given payloads containing a metadata field `+"`set_key`"+` it's possible to query and store the cardinality of the set for each message using a `+"xref:components:processors/branch.adoc[`branch` processor]"+` in order to augment rather than replace the message contents:`, ` pipeline: processors: diff --git a/internal/impl/redis/script_processor.go b/internal/impl/redis/script_processor.go index bf9091ba9d..19d8b33cf9 100644 --- a/internal/impl/redis/script_processor.go +++ b/internal/impl/redis/script_processor.go @@ -15,10 +15,10 @@ func redisScriptProcConfig() *service.ConfigSpec { spec := service.NewConfigSpec(). Beta(). Version("4.11.0"). - Summary(`Performs actions against Redis using [LUA scripts](https://redis.io/docs/manual/programmability/eval-intro/).`). + Summary(`Performs actions against Redis using https://redis.io/docs/manual/programmability/eval-intro/[LUA scripts].`). Description(`Actions are performed for each message and the message contents are replaced with the result. -In order to merge the result into the original message compose this processor within a ` + "[`branch` processor](/docs/components/processors/branch)" + `.`). +In order to merge the result into the original message compose this processor within a ` + "xref:components:processors/branch.adoc[`branch` processor]" + `.`). Categories("Integration") for _, f := range clientFields() { @@ -30,11 +30,11 @@ In order to merge the result into the original message compose this processor wi Description("A script to use for the target operator. It has precedence over the 'command' field."). Example("return redis.call('set', KEYS[1], ARGV[1])")). Field(service.NewBloblangField("args_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis script."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis script."). Example("root = [ this.key ]"). Example(`root = [ meta("kafka_key"), "hardcoded_value" ]`)). Field(service.NewBloblangField("keys_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of keys matching in size to the number of arguments required for the specified Redis script."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of keys matching in size to the number of arguments required for the specified Redis script."). Example("root = [ this.key ]"). Example(`root = [ meta("kafka_key"), this.count ]`)). Field(service.NewIntField("retries"). diff --git a/internal/impl/sentry/processor_capture.go b/internal/impl/sentry/processor_capture.go index ddc488732a..bb02adc364 100644 --- a/internal/impl/sentry/processor_capture.go +++ b/internal/impl/sentry/processor_capture.go @@ -20,7 +20,7 @@ const ( func newCaptureProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). Version("4.16.0"). - Summary("Captures log events from messages and submits them to [Sentry](https://sentry.io/)."). + Summary("Captures log events from messages and submits them to https://sentry.io/[Sentry]."). Fields( service.NewStringField("dsn"). Default(""). @@ -316,7 +316,7 @@ func mapLevel(raw string) (sentry.Level, error) { case "FATAL": return sentry.LevelFatal, nil default: - return sentry.Level(""), fmt.Errorf("unrecognized sentry level: %s", raw) + return sentry.Level(""), fmt.Errorf("unrecognised sentry level: %s", raw) } } diff --git a/internal/impl/sftp/input.go b/internal/impl/sftp/input.go index aafee27e9f..7c0ee39361 100644 --- a/internal/impl/sftp/input.go +++ b/internal/impl/sftp/input.go @@ -34,7 +34,7 @@ func sftpInputSpec() *service.ConfigSpec { Version("3.39.0"). Summary(`Consumes files from an SFTP server.`). Description(` -## Metadata +== Metadata This input adds the following metadata fields to each message: @@ -42,7 +42,7 @@ This input adds the following metadata fields to each message: - sftp_path `+"```"+` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries).`). +You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). Fields( service.NewStringField(siFieldAddress). Description("The address of the server to connect to."), @@ -71,7 +71,7 @@ You can access these metadata fields using [function interpolation](/docs/config Default("1s"). Examples("100ms", "1s"), service.NewStringField(siFieldWatcherCache). - Description("A [cache resource](/docs/components/caches/about) for storing the paths of files already consumed."). + Description("A xref:components:caches/about.adoc[cache resource] for storing the paths of files already consumed."). Default(""), ).Description("An experimental mode whereby the input will periodically scan the target paths for new files and consume them, when all files are consumed the input will continue polling for new files."). Version("3.42.0"), diff --git a/internal/impl/sftp/output.go b/internal/impl/sftp/output.go index 804875151f..c23727e731 100644 --- a/internal/impl/sftp/output.go +++ b/internal/impl/sftp/output.go @@ -26,7 +26,7 @@ func sftpOutputSpec() *service.ConfigSpec { Categories("Network"). Version("3.39.0"). Summary(`Writes files to an SFTP server.`). - Description(`In order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries).`+service.OutputPerformanceDocs(true, false)). + Description(`In order to have a different path for each object you should use function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here].`+service.OutputPerformanceDocs(true, false)). Fields( service.NewStringField(soFieldAddress). Description("The address of the server to connect to."), diff --git a/internal/impl/snowflake/output_snowflake_put.go b/internal/impl/snowflake/output_snowflake_put.go index 529b289ec9..29c7b363e8 100644 --- a/internal/impl/snowflake/output_snowflake_put.go +++ b/internal/impl/snowflake/output_snowflake_put.go @@ -59,31 +59,31 @@ func snowflakePutOutputConfig() *service.ConfigSpec { Summary("Sends messages to Snowflake stages and, optionally, calls Snowpipe to load this data into one or more tables."). Description(` In order to use a different stage and / or Snowpipe for each message, you can use function interpolations as described -[here](/docs/configuration/interpolation#bloblang-queries). When using batching, messages are grouped by the calculated +xref:configuration:interpolation.adoc#bloblang-queries[here]. When using batching, messages are grouped by the calculated stage and Snowpipe and are streamed to individual files in their corresponding stage and, optionally, a Snowpipe `+"`insertFiles`"+` REST API call will be made for each individual file. -### Credentials +== Credentials Two authentication mechanisms are supported: - User/password - Key Pair Authentication -#### User/password +=== User/password This is a basic authentication mechanism which allows you to PUT data into a stage. However, it is not compatible with Snowpipe. -#### Key Pair Authentication +=== Key pair authentication This authentication mechanism allows Snowpipe functionality, but it does require configuring an SSH Private Key -beforehand. Please consult the [documentation](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication) +beforehand. Please consult the https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[documentation] for details on how to set it up and assign the Public Key to your user. -Note that the Snowflake documentation [used to suggest](https://twitter.com/felipehoffa/status/1560811785606684672) +Note that the Snowflake documentation https://twitter.com/felipehoffa/status/1560811785606684672[used to suggest] using this command: -`+"```shell"+` +`+"```bash"+` openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 `+"```"+` @@ -92,28 +92,28 @@ to generate an encrypted SSH private key. However, in this case, it uses an encr support it and, if you wish to use password-protected keys directly, you must use PKCS#5 v2.0 to encrypt them by using the following command (as the current Snowflake docs suggest): -`+"```shell"+` +`+"```bash"+` openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 des3 -inform PEM -out rsa_key.p8 `+"```"+` If you have an existing key encrypted with PKCS#5 v1.5, you can re-encrypt it with PKCS#5 v2.0 using this command: -`+"```shell"+` +`+"```bash"+` openssl pkcs8 -in rsa_key_original.p8 -topk8 -v2 des3 -out rsa_key.p8 `+"```"+` -Please consult [this](https://linux.die.net/man/1/pkcs8) pkcs8 command documentation for details on PKCS#5 algorithms. +Please consult the https://linux.die.net/man/1/pkcs8[pkcs8 command documentation] for details on PKCS#5 algorithms. -### Batching +== Batching It's common to want to upload messages to Snowflake as batched archives. The easiest way to do this is to batch your messages at the output level and join the batch of messages with an -`+"[`archive`](/docs/components/processors/archive)"+` and/or `+"[`compress`](/docs/components/processors/compress)"+` +`+"xref:components:processors/archive.adoc[`archive`]"+` and/or `+"xref:components:processors/compress.adoc[`compress`]"+` processor. -For the optimal batch size, please consult the Snowflake [documentation](https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html). +For the optimal batch size, please consult the Snowflake https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html[documentation]. -### Snowpipe +== Snowpipe Given a table called `+"`BENTHOS_TBL`"+` with one column of type `+"`variant`"+`: @@ -129,71 +129,70 @@ CREATE OR REPLACE PIPE BENTHOS_DB.PUBLIC.BENTHOS_PIPE AUTO_INGEST = FALSE AS COP you can configure Benthos to use the implicit table stage `+"`@%BENTHOS_TBL`"+` as the `+"`stage`"+` and `+"`BENTHOS_PIPE`"+` as the `+"`snowpipe`"+`. In this case, you must set `+"`compression`"+` to `+"`AUTO`"+` and, if -using message batching, you'll need to configure an [`+"`archive`"+`](/docs/components/processors/archive) processor +using message batching, you'll need to configure an xref:components:processors/archive.adoc[`+"`archive`"+`] processor with the `+"`concatenate`"+` format. Since the `+"`compression`"+` is set to `+"`AUTO`"+`, the -[gosnowflake](https://github.com/snowflakedb/gosnowflake) client library will compress the messages automatically so you -don't need to add a `+"[`compress`](/docs/components/processors/compress)"+` processor for message batches. +https://github.com/snowflakedb/gosnowflake[gosnowflake] client library will compress the messages automatically so you +don't need to add a `+"xref:components:processors/compress.adoc[`compress`]"+` processor for message batches. If you add `+"`STRIP_OUTER_ARRAY = TRUE`"+` in your Snowpipe `+"`FILE_FORMAT`"+` definition, then you must use `+"`json_array`"+` instead of `+"`concatenate`"+` as the archive processor format. -Note: Only Snowpipes with `+"`FILE_FORMAT`"+` `+"`TYPE`"+` `+"`JSON`"+` are currently supported. +NOTE: Only Snowpipes with `+"`FILE_FORMAT`"+` `+"`TYPE`"+` `+"`JSON`"+` are currently supported. -### Snowpipe Troubleshooting +== Snowpipe troubleshooting -Snowpipe [provides](https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html) the `+"`insertReport`"+` +Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html[provides] the `+"`insertReport`"+` and `+"`loadHistoryScan`"+` REST API endpoints which can be used to get information about recent Snowpipe calls. In order to query them, you'll first need to generate a valid JWT token for your Snowflake account. There are two methods for doing so: -- Using the `+"`snowsql`"+` [utility](https://docs.snowflake.com/en/user-guide/snowsql.html): +- Using the `+"`snowsql`"+` https://docs.snowflake.com/en/user-guide/snowsql.html[utility]: -`+"```shell"+` +`+"```bash"+` snowsql --private-key-path rsa_key.p8 --generate-jwt -a -u `+"```"+` -- Using the Python `+"`sql-api-generate-jwt`"+` [utility](https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python): +- Using the Python `+"`sql-api-generate-jwt`"+` https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python[utility]: -`+"```shell"+` +`+"```bash"+` python3 sql-api-generate-jwt.py --private_key_file_path=rsa_key.p8 --account= --user= `+"```"+` Once you successfully generate a JWT token and store it into the `+"`JWT_TOKEN`"+` environment variable, then you can, for example, query the `+"`insertReport`"+` endpoint using `+"`curl`"+`: -`+"```shell"+` +`+"```bash"+` curl -H "Authorization: Bearer ${JWT_TOKEN}" "https://.snowflakecomputing.com/v1/data/pipes/../insertReport" `+"```"+` If you need to pass in a valid `+"`requestId`"+` to any of these Snowpipe REST API endpoints, you can set a -[uuid_v4()](https://www.benthos.dev/docs/guides/bloblang/functions#uuid_v4) string in a metadata field called -`+"`request_id`"+`, log it via the [`+"`log`"+`](https://www.benthos.dev/docs/components/processors/log) processor and -then configure `+"`request_id: ${ @request_id }`"+` ). Alternatively, you can enable debug logging as described -[here](/docs/components/logger/about) and Benthos will print the Request IDs that it sends to Snowpipe. +xref:guides:bloblang/functions.adoc#uuid_v4[uuid_v4()] string in a metadata field called +`+"`request_id`"+`, log it via the xref:components:processors/log.adoc[`+"`log`"+`] processor and +then configure `+"`request_id: ${ @request_id }`"+` ). Alternatively, you can xref:components:logger/about.adoc[enable debug logging] + and Benthos will print the Request IDs that it sends to Snowpipe. -### General Troubleshooting +== General troubleshooting -The underlying [`+"`gosnowflake`"+` driver](https://github.com/snowflakedb/gosnowflake) requires write access to -the default directory to use for temporary files. Please consult the [`+"`os.TempDir`"+`](https://pkg.go.dev/os#TempDir) +The underlying https://github.com/snowflakedb/gosnowflake[`+"`gosnowflake`"+` driver] requires write access to +the default directory to use for temporary files. Please consult the https://pkg.go.dev/os#TempDir[`+"`os.TempDir`"+`] docs for details on how to change this directory via environment variables. -A silent failure can occur due to [this issue](https://github.com/snowflakedb/gosnowflake/issues/701), where the -underlying [`+"`gosnowflake`"+` driver](https://github.com/snowflakedb/gosnowflake) doesn't return an error and doesn't -log a failure if it can't figure out the current username. One way to trigger this behaviour is by running Benthos in a +A silent failure can occur due to https://github.com/snowflakedb/gosnowflake/issues/701[this issue], where the +underlying https://github.com/snowflakedb/gosnowflake[`+"`gosnowflake`"+` driver] doesn't return an error and doesn't +log a failure if it can't figure out the current username. One way to trigger this behavior is by running Benthos in a Docker container with a non-existent user ID (such as `+"`--user 1000:1000`"+`). `+service.OutputPerformanceDocs(true, true)). - Field(service.NewStringField("account").Description(`Account name, which is the same as the Account Identifier -as described [here](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used). -However, when using an [Account Locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier), + Field(service.NewStringField("account").Description(`Account name, which is the same as the https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used[Account Identifier]. +However, when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator], the Account Identifier is formatted as `+"`..`"+` and this field needs to be populated using the `+"``"+` part. `)). Field(service.NewStringField("region").Description(`Optional region field which needs to be populated when using -an [Account Locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier) +an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] and it must be set to the `+"``"+` part of the Account Identifier (`+"`..`"+`). `).Example("us-west-2").Optional()). Field(service.NewStringField("cloud").Description(`Optional cloud platform field which needs to be populated -when using an [Account Locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier) +when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] and it must be set to the `+"``"+` part of the Account Identifier (`+"`..`"+`). `).Example("aws").Example("gcp").Example("azure").Optional()). @@ -206,7 +205,7 @@ and it must be set to the `+"``"+` part of the Account Identifier Field(service.NewStringField("warehouse").Description("Warehouse.")). Field(service.NewStringField("schema").Description("Schema.")). Field(service.NewInterpolatedStringField("stage").Description(`Stage name. Use either one of the -[supported](https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html) stage types.`)). + https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html[supported] stage types.`)). Field(service.NewInterpolatedStringField("path").Description("Stage path.").Default("")). Field(service.NewInterpolatedStringField("file_name").Description("Stage file name. Will be equal to the Request ID if not set or empty.").Optional().Default("").Version("v4.12.0")). Field(service.NewInterpolatedStringField("file_extension").Description("Stage file extension. Will be derived from the configured `compression` if not set or empty.").Optional().Default("").Example("csv").Example("parquet").Version("v4.12.0")). @@ -228,7 +227,7 @@ and it must be set to the `+"``"+` part of the Account Identifier this.exists("password") && this.password != "" && this.exists("private_key_file") && this.private_key_file != "" => [ "both `+"`password`"+` and `+"`private_key_file`"+` can't be set simultaneously" ], this.exists("snowpipe") && this.snowpipe != "" && (!this.exists("private_key_file") || this.private_key_file == "") => [ "`+"`private_key_file`"+` is required when setting `+"`snowpipe`"+`" ], }`). - Example("Kafka / realtime brokers", "Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the [Kafka Connector scheme](https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table) and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition.", ` + Example("Kafka / realtime brokers", "Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table[Kafka Connector scheme] and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition.", ` input: kafka: addresses: diff --git a/internal/impl/sql/buffer_sqlite.go b/internal/impl/sql/buffer_sqlite.go index 4f047d8234..36494ea1d8 100644 --- a/internal/impl/sql/buffer_sqlite.go +++ b/internal/impl/sql/buffer_sqlite.go @@ -26,11 +26,11 @@ func SQLiteBufferConfig() *service.ConfigSpec { Description(` Stored messages are then consumed as a stream from the database and deleted only once they are successfully sent at the output level. If the service is restarted Benthos will make a best attempt to finish delivering messages that are already read from the database, and when it starts again it will consume from the oldest message that has not yet been delivered. -## Delivery Guarantees +== Delivery guarantees Messages are not acknowledged at the input level until they have been added to the SQLite database, and they are not removed from the SQLite database until they have been successfully delivered. This means at-least-once delivery guarantees are preserved in cases where the service is shut down unexpectedly. However, since this process relies on interaction with the disk (wherever the SQLite DB is stored) these delivery guarantees are not resilient to disk corruption or loss. -## Batching +== Batching Messages that are logically batched at the point where they are added to the buffer will continue to be associated with that batch when they are consumed. This buffer is also more efficient when storing messages within batches, and therefore it is recommended to use batching at the input level in high-throughput use cases even if they are not required for processing. `). @@ -42,7 +42,7 @@ Messages that are logically batched at the point where they are added to the buf Field(service.NewProcessorListField("post_processors"). Description("An optional list of processors to apply to messages after they are consumed from the buffer. These processors are useful for undoing any compression, archiving, etc that may have been done by your `pre_processors`."). Optional()). - Example("Batching for optimisation", "Batching at the input level greatly increases the throughput of this buffer. If logical batches aren't needed for processing add a [`split` processor](/docs/components/processors/split) to the `post_processors`.", ` + Example("Batching for optimization", "Batching at the input level greatly increases the throughput of this buffer. If logical batches aren't needed for processing add a xref:components:processors/split.adoc[`split` processor] to the `post_processors`.", ` input: batched: child: diff --git a/internal/impl/sql/cache_sql.go b/internal/impl/sql/cache_sql.go index 79955d4388..5fce128532 100644 --- a/internal/impl/sql/cache_sql.go +++ b/internal/impl/sql/cache_sql.go @@ -30,21 +30,21 @@ Each cache key/value pair will exist as a row within the specified table. Curren Cache operations are translated into SQL statements as follows: -### Get +== Get All ` + "`get`" + ` operations are performed with a traditional ` + "`select`" + ` statement. -### Delete +== Delete All ` + "`delete`" + ` operations are performed with a traditional ` + "`delete`" + ` statement. -### Set +== Set The ` + "`set`" + ` operation is performed with a traditional ` + "`insert`" + ` statement. This will behave as an ` + "`add`" + ` operation by default, and so ideally needs to be adapted in order to provide updates instead of failing on collision s. Since different SQL engines implement upserts differently it is necessary to specify a ` + "`set_suffix`" + ` that modifies an ` + "`insert`" + ` statement in order to perform updates on conflict. -### Add +== Add The ` + "`add`" + ` operation is performed with a traditional ` + "`insert`" + ` statement. `). diff --git a/internal/impl/sql/conn_fields.go b/internal/impl/sql/conn_fields.go index 063416d106..a24b1c0a3d 100644 --- a/internal/impl/sql/conn_fields.go +++ b/internal/impl/sql/conn_fields.go @@ -13,32 +13,51 @@ import ( ) var driverField = service.NewStringEnumField("driver", "mysql", "postgres", "clickhouse", "mssql", "sqlite", "oracle", "snowflake", "trino", "gocosmos"). - Description("A database [driver](#drivers) to use.") + Description("A database <> to use.") var dsnField = service.NewStringField("dsn"). Description(`A Data Source Name to identify the target database. -#### Drivers +==== Drivers The following is a list of supported drivers, their placeholder style, and their respective DSN formats: -| Driver | Data Source Name Format | -|---|---| -` + "| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) |" + ` -` + "| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` |" + ` -` + "| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` |" + ` -` + "| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` |" + ` -` + "| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` |" + ` -` + "| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` |" + ` -` + "| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` |" + ` -` + "| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) |" + ` -` + "| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) |" + ` +|=== +| Driver | Data Source Name Format + +` + "| `clickhouse` " + ` +` + "| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] " + ` + +` + "| `mysql` " + ` +` + "| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` " + ` + +` + "| `postgres` " + ` +` + "| `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` " + ` + +` + "| `mssql` " + ` +` + "| `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` " + ` + +` + "| `sqlite` " + ` +` + "| `file:/path/to/filename.db[?param&=value1&...]` " + ` + +` + "| `oracle` " + ` +` + "| `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` " + ` + +` + "| `snowflake` " + ` +` + "| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` " + ` + +` + "| `trino` " + ` +` + "| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] " + ` + +` + "| `gocosmos` " + ` +` + "| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] " + ` +|=== Please note that the ` + "`postgres`" + ` driver enforces SSL by default, you can override this with the parameter ` + "`sslmode=disable`" + ` if required. -The ` + "`snowflake`" + ` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: ` + "`@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`" + `, where the value for the ` + "`privateKey`" + ` parameter can be constructed from an unencrypted RSA private key file ` + "`rsa_key.p8`" + ` using ` + "`openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0`" + ` (you can use ` + "`gbasenc`" + ` insted of ` + "`basenc`" + ` on OSX if you install ` + "`coreutils`" + ` via Homebrew). If you have a password-encrypted private key, you can decrypt it using ` + "`openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`" + `. Also, make sure fields such as the username are URL-encoded. +The ` + "`snowflake`" + ` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: ` + "`@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`" + `, where the value for the ` + "`privateKey`" + ` parameter can be constructed from an unencrypted RSA private key file ` + "`rsa_key.p8`" + ` using ` + "`openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0`" + ` (you can use ` + "`gbasenc`" + ` insted of ` + "`basenc`" + ` on OSX if you install ` + "`coreutils`" + ` via Homebrew). If you have a password-encrypted private key, you can decrypt it using ` + "`openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`" + `. Also, make sure fields such as the username are URL-encoded. -The ` + "[`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos)" + ` driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details.`). +The ` + "https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`]" + ` driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details.`). Example("clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60"). Example("foouser:foopassword@tcp(localhost:3306)/foodb"). Example("postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable"). diff --git a/internal/impl/sql/input_sql_raw.go b/internal/impl/sql/input_sql_raw.go index 306b176eab..29edabd2be 100644 --- a/internal/impl/sql/input_sql_raw.go +++ b/internal/impl/sql/input_sql_raw.go @@ -17,13 +17,13 @@ func sqlRawInputConfig() *service.ConfigSpec { Beta(). Categories("Services"). Summary("Executes a select query and creates a message for each row received."). - Description(`Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute).`). + Description(`Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute).`). Field(driverField). Field(dsnField). Field(rawQueryField(). Example("SELECT * FROM footable WHERE user_id = $1;")). Field(service.NewBloblangField("args_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of columns specified."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of columns specified."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`). Optional()). diff --git a/internal/impl/sql/input_sql_select.go b/internal/impl/sql/input_sql_select.go index 3b3617ef18..40bc9058c8 100644 --- a/internal/impl/sql/input_sql_select.go +++ b/internal/impl/sql/input_sql_select.go @@ -19,7 +19,7 @@ func sqlSelectInputConfig() *service.ConfigSpec { Beta(). Categories("Services"). Summary("Executes a select query and creates a message for each row received."). - Description(`Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute).`). + Description(`Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a xref:components:inputs/sequence.adoc[sequence] to execute).`). Field(driverField). Field(dsnField). Field(service.NewStringField("table"). @@ -35,7 +35,7 @@ func sqlSelectInputConfig() *service.ConfigSpec { Example("user_id = ?"). Optional()). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). Example(`root = [ "article", now().ts_format("2006-01-02") ]`). Optional()). Field(service.NewStringField("prefix"). diff --git a/internal/impl/sql/output_sql_deprecated.go b/internal/impl/sql/output_sql_deprecated.go index 9f8156843e..1636ef8576 100644 --- a/internal/impl/sql/output_sql_deprecated.go +++ b/internal/impl/sql/output_sql_deprecated.go @@ -11,15 +11,15 @@ func sqlDeprecatedOutputConfig() *service.ConfigSpec { Categories("Services"). Summary("Executes an arbitrary SQL query for each message."). Description(` -## Alternatives +== Alternatives -For basic inserts use the ` + "[`sql_insert`](/docs/components/outputs/sql)" + ` output. For more complex queries use the ` + "[`sql_raw`](/docs/components/outputs/sql_raw)" + ` output.`). +For basic inserts use the ` + "xref:components:outputs/sql.adoc[`sql_insert`]" + ` output. For more complex queries use the ` + "xref:components:outputs/sql_raw.adoc[`sql_raw`]" + ` output.`). Field(driverField). Field(service.NewStringField("data_source_name").Description("Data source name.")). Field(rawQueryField(). Example("INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?);")). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`). Optional()). diff --git a/internal/impl/sql/output_sql_insert.go b/internal/impl/sql/output_sql_insert.go index b1741a6c96..25eccf5007 100644 --- a/internal/impl/sql/output_sql_insert.go +++ b/internal/impl/sql/output_sql_insert.go @@ -29,7 +29,7 @@ func sqlInsertOutputConfig() *service.ConfigSpec { Description("A list of columns to insert."). Example([]string{"foo", "bar", "baz"})). Field(service.NewBloblangField("args_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of columns specified."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of columns specified."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`)). Field(service.NewStringField("prefix"). diff --git a/internal/impl/sql/output_sql_raw.go b/internal/impl/sql/output_sql_raw.go index c04f0e2e01..bc236ad936 100644 --- a/internal/impl/sql/output_sql_raw.go +++ b/internal/impl/sql/output_sql_raw.go @@ -23,11 +23,11 @@ func sqlRawOutputConfig() *service.ConfigSpec { Field(rawQueryField(). Example("INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?);")). Field(service.NewBoolField("unsafe_dynamic_query"). - Description("Whether to enable [interpolation functions](/docs/configuration/interpolation/#bloblang-queries) in the query. Great care should be made to ensure your queries are defended against injection attacks."). + Description("Whether to enable xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the query. Great care should be made to ensure your queries are defended against injection attacks."). Advanced(). Default(false)). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`). Optional()). diff --git a/internal/impl/sql/processor_sql_deprecated.go b/internal/impl/sql/processor_sql_deprecated.go index 299ae54f59..f31d48af14 100644 --- a/internal/impl/sql/processor_sql_deprecated.go +++ b/internal/impl/sql/processor_sql_deprecated.go @@ -12,21 +12,21 @@ func DeprecatedProcessorConfig() *service.ConfigSpec { Categories("Integration"). Summary("Runs an arbitrary SQL query against a database and (optionally) returns the result as an array of objects, one for each row returned."). Description(` -If the query fails to execute then the message will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). +If the query fails to execute then the message will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. -## Alternatives +== Alternatives -For basic inserts or select queries use either the ` + "[`sql_insert`](/docs/components/processors/sql_insert)" + ` or the ` + "[`sql_select`](/docs/components/processors/sql_select)" + ` processor. For more complex queries use the ` + "[`sql_raw`](/docs/components/processors/sql_raw)" + ` processor.`). +For basic inserts or select queries use either the ` + "xref:components:processors/sql_insert.adoc[`sql_insert`]" + ` or the ` + "xref:components:processors/sql_select.adoc[`sql_select`]" + ` processor. For more complex queries use the ` + "xref:components:processors/sql_raw.adoc[`sql_raw`]" + ` processor.`). Field(driverField). Field(service.NewStringField("data_source_name").Description("Data source name.")). Field(rawQueryField(). Example("INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?);")). Field(service.NewBoolField("unsafe_dynamic_query"). - Description("Whether to enable [interpolation functions](/docs/configuration/interpolation/#bloblang-queries) in the query. Great care should be made to ensure your queries are defended against injection attacks."). + Description("Whether to enable xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the query. Great care should be made to ensure your queries are defended against injection attacks."). Advanced(). Default(false)). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`). Optional()). diff --git a/internal/impl/sql/processor_sql_insert.go b/internal/impl/sql/processor_sql_insert.go index 20e2ea9972..f53070f110 100644 --- a/internal/impl/sql/processor_sql_insert.go +++ b/internal/impl/sql/processor_sql_insert.go @@ -21,7 +21,7 @@ func InsertProcessorConfig() *service.ConfigSpec { Categories("Integration"). Summary("Inserts rows into an SQL database for each message, and leaves the message unchanged."). Description(` -If the insert fails to execute then the message will still remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling).`). +If the insert fails to execute then the message will still remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods].`). Field(driverField). Field(dsnField). Field(service.NewStringField("table"). @@ -31,7 +31,7 @@ If the insert fails to execute then the message will still remain unchanged and Description("A list of columns to insert."). Example([]string{"foo", "bar", "baz"})). Field(service.NewBloblangField("args_mapping"). - Description("A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of columns specified."). + Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of columns specified."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`)). Field(service.NewStringField("prefix"). diff --git a/internal/impl/sql/processor_sql_raw.go b/internal/impl/sql/processor_sql_raw.go index fe5b91f065..c7b5edebcb 100644 --- a/internal/impl/sql/processor_sql_raw.go +++ b/internal/impl/sql/processor_sql_raw.go @@ -19,18 +19,18 @@ func RawProcessorConfig() *service.ConfigSpec { Categories("Integration"). Summary("Runs an arbitrary SQL query against a database and (optionally) returns the result as an array of objects, one for each row returned."). Description(` -If the query fails to execute then the message will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling).`). +If the query fails to execute then the message will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods].`). Field(driverField). Field(dsnField). Field(rawQueryField(). Example("INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?);"). Example("SELECT * FROM footable WHERE user_id = $1;")). Field(service.NewBoolField("unsafe_dynamic_query"). - Description("Whether to enable [interpolation functions](/docs/configuration/interpolation/#bloblang-queries) in the query. Great care should be made to ensure your queries are defended against injection attacks."). + Description("Whether to enable xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the query. Great care should be made to ensure your queries are defended against injection attacks."). Advanced(). Default(false)). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`). Optional()). @@ -59,7 +59,7 @@ pipeline: ). Example( "Table Query (PostgreSQL)", - `Here we query a database for columns of footable that share a `+"`user_id`"+` with the message field `+"`user.id`"+`. A `+"[`branch` processor](/docs/components/processors/branch)"+` is used in order to insert the resulting array into the original message at the path `+"`foo_rows`"+`.`, + `Here we query a database for columns of footable that share a `+"`user_id`"+` with the message field `+"`user.id`"+`. A `+"xref:components:processors/branch.adoc[`branch` processor]"+` is used in order to insert the resulting array into the original message at the path `+"`foo_rows`"+`.`, ` pipeline: processors: diff --git a/internal/impl/sql/processor_sql_select.go b/internal/impl/sql/processor_sql_select.go index b9dcc48448..0dea57f63e 100644 --- a/internal/impl/sql/processor_sql_select.go +++ b/internal/impl/sql/processor_sql_select.go @@ -21,7 +21,7 @@ func SelectProcessorConfig() *service.ConfigSpec { Categories("Integration"). Summary("Runs an SQL select query against a database and returns the result as an array of objects, one for each row returned, containing a key for each column queried and its value."). Description(` -If the query fails to execute then the message will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling).`). +If the query fails to execute then the message will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods].`). Field(driverField). Field(dsnField). Field(service.NewStringField("table"). @@ -37,7 +37,7 @@ If the query fails to execute then the message will remain unchanged and the err Example("user_id = ?"). Optional()). Field(service.NewBloblangField("args_mapping"). - Description("An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). + Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`."). Example("root = [ this.cat.meow, this.doc.woofs[0] ]"). Example(`root = [ meta("user.id") ]`). Optional()). @@ -58,7 +58,7 @@ If the query fails to execute then the message will remain unchanged and the err Example("Table Query (PostgreSQL)", ` Here we query a database for columns of footable that share a `+"`user_id`"+` -with the message `+"`user.id`"+`. A `+"[`branch` processor](/docs/components/processors/branch)"+` +with the message `+"`user.id`"+`. A `+"xref:components:processors/branch.adoc[`branch` processor]"+` is used in order to insert the resulting array into the original message at the path `+"`foo_rows`"+`:`, ` diff --git a/internal/impl/statsd/metrics_statsd.go b/internal/impl/statsd/metrics_statsd.go index e263705ade..61ec7e7a21 100644 --- a/internal/impl/statsd/metrics_statsd.go +++ b/internal/impl/statsd/metrics_statsd.go @@ -20,7 +20,7 @@ const ( func statsdSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). - Summary("Pushes metrics using the [StatsD protocol](https://github.com/statsd/statsd). Supported tagging formats are 'none', 'datadog' and 'influxdb'."). + Summary("Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. Supported tagging formats are 'none', 'datadog' and 'influxdb'."). Fields( service.NewStringField(smFieldAddress). Description("The address to send metrics to."), diff --git a/internal/impl/wasm/processor_wazero.go b/internal/impl/wasm/processor_wazero.go index a5db40281b..0d88010854 100644 --- a/internal/impl/wasm/processor_wazero.go +++ b/internal/impl/wasm/processor_wazero.go @@ -20,15 +20,15 @@ func wazeroAllocProcessorConfig() *service.ConfigSpec { Categories("Utility"). Summary("Executes a function exported by a WASM module for each message."). Description(` -This processor uses [Wazero](https://github.com/tetratelabs/wazero) to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. +This processor uses https://github.com/tetratelabs/wazero[Wazero] to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. -This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in [the codebase](https://github.com/benthosdev/benthos/tree/main/public/wasm/README.md). +This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in https://github.com/benthosdev/benthos/tree/main/public/wasm/README.md[the codebase]. These examples, as well as the processor itself, is a work in progress. -### Parallelism +== Parallelism -It's not currently possible to execute a single WASM runtime across parallel threads with this processor. Therefore, in order to support parallel processing this processor implements pooling of module runtimes. Ideally your WASM module shouldn't depend on any global state, but if it does then you need to ensure the processor [is only run on a single thread](/docs/configuration/processing_pipelines). +It's not currently possible to execute a single WASM runtime across parallel threads with this processor. Therefore, in order to support parallel processing this processor implements pooling of module runtimes. Ideally your WASM module shouldn't depend on any global state, but if it does then you need to ensure the processor xref:configuration:processing_pipelines.adoc[is only run on a single thread]. `). Field(service.NewStringField("module_path"). Description("The path of the target WASM module to execute.")). diff --git a/internal/impl/xml/processor.go b/internal/impl/xml/processor.go index 7cee1f28ad..b3e07f21df 100644 --- a/internal/impl/xml/processor.go +++ b/internal/impl/xml/processor.go @@ -18,9 +18,9 @@ func xmlProcSpec() *service.ConfigSpec { Beta(). Summary(`Parses messages as an XML document, performs a mutation on the data, and then overwrites the previous contents with the new value.`). Description(` -## Operators +== Operators -### `+"`to_json`"+` +=== `+"`to_json`"+` Converts an XML document into a JSON structure, where elements appear as keys of an object according to the following rules: @@ -80,7 +80,7 @@ With cast set to true, the resulting JSON structure would look like this: `+"```"). Fields( service.NewStringEnumField(pFieldOperator, "to_json"). - Description("An XML [operation](#operators) to apply to messages."). + Description("An XML <> to apply to messages."). Default(""), service.NewBoolField(pFieldCast). Description("Whether to try to cast values that are numbers and booleans to the right type. Default: all values are strings."). diff --git a/internal/impl/zeromq/input_zmq4.go b/internal/impl/zeromq/input_zmq4.go index 41adef3998..3393816bec 100644 --- a/internal/impl/zeromq/input_zmq4.go +++ b/internal/impl/zeromq/input_zmq4.go @@ -22,7 +22,7 @@ func zmqInputConfig() *service.ConfigSpec { Description(` By default Benthos does not build with components that require linking to external libraries. If you wish to build Benthos locally with this component then set the build tag ` + "`x_benthos_extra`" + `: -` + "```shell" + ` +` + "```bash" + ` # With go go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest diff --git a/internal/impl/zeromq/output_zmq4.go b/internal/impl/zeromq/output_zmq4.go index 7c4fccdff0..213b898eff 100644 --- a/internal/impl/zeromq/output_zmq4.go +++ b/internal/impl/zeromq/output_zmq4.go @@ -22,7 +22,7 @@ func zmqOutputConfig() *service.ConfigSpec { Description(` By default Benthos does not build with components that require linking to external libraries. If you wish to build Benthos locally with this component then set the build tag ` + "`x_benthos_extra`" + `: -` + "```shell" + ` +` + "```bash" + ` # With go go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest diff --git a/internal/log/docs.adoc b/internal/log/docs.adoc new file mode 100644 index 0000000000..068288b354 --- /dev/null +++ b/internal/log/docs.adoc @@ -0,0 +1,42 @@ += Logger + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/log/docs.adoc +//// + +{page-component-title} logging prints to stdout (or stderr if your output is stdout) and is formatted as https://brandur.org/logfmt[logfmt] by default. Use these configuration options to change both the logging formats as well as the destination of logs. + +[tabs] +====== +Logfmt to Stdout:: ++ +-- +```yaml +logger: + level: INFO + format: logfmt + add_timestamp: false + static_fields: + '@service': benthos +``` +-- +JSON to File:: ++ +-- +```yaml +logger: + level: WARN + format: json + file: + path: ./logs/benthos.ndjson + rotate: true +``` +-- +====== + +== Fields + diff --git a/internal/log/docs.go b/internal/log/docs.go index 63fe99d376..c15ad5edaa 100644 --- a/internal/log/docs.go +++ b/internal/log/docs.go @@ -31,7 +31,7 @@ func Spec() docs.FieldSpecs { } } -//go:embed docs.md +//go:embed docs.adoc var loggerDocs string type loggerContext struct { diff --git a/internal/log/docs.md b/internal/log/docs.md deleted file mode 100644 index 108473dc45..0000000000 --- a/internal/log/docs.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Logger ---- - - - -Benthos logging prints to stdout (or stderr if your output is stdout) and is formatted as [logfmt](https://brandur.org/logfmt) by default. Use these configuration options to change both the logging formats as well as the destination of logs. - -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - - - -```yaml -logger: - level: INFO - format: logfmt - add_timestamp: false - static_fields: - '@service': benthos -``` - - - - -```yaml -logger: - level: WARN - format: json - file: - path: ./logs/benthos.ndjson - rotate: true -``` - - - - - -## Fields - diff --git a/internal/template/config.go b/internal/template/config.go index 309fbbab0c..c89e1c7e5e 100644 --- a/internal/template/config.go +++ b/internal/template/config.go @@ -249,7 +249,7 @@ func ConfigSpec() docs.FieldSpecs { "cache", "input", "output", "processor", "rate_limit", ), docs.FieldString( - "status", "The stability of the template describing the likelihood that the configuration spec of the template, or it's behaviour, will change.", + "status", "The stability of the template describing the likelihood that the configuration spec of the template, or it's behavior, will change.", ).HasAnnotatedOptions( "stable", "This template is stable and will therefore not change in a breaking way outside of major version releases.", "beta", "This template is beta and will therefore not change in a breaking way unless a major problem is found.", @@ -262,7 +262,7 @@ func ConfigSpec() docs.FieldSpecs { docs.FieldString("description", "A longer form description of the component and how to use it.").HasDefault(""), docs.FieldObject("fields", "The configuration fields of the template, fields specified here will be parsed from a Benthos config and will be accessible from the template mapping.").Array().WithChildren(FieldConfigSpec()...), docs.FieldBloblang( - "mapping", "A [Bloblang](/docs/guides/bloblang/about) mapping that translates the fields of the template into a valid Benthos configuration for the target component type.", + "mapping", "A xref:guides:bloblang/about.adoc[Bloblang] mapping that translates the fields of the template into a valid Benthos configuration for the target component type.", ), templateMetricsMappingDocs(), docs.FieldObject( diff --git a/internal/template/docs.adoc b/internal/template/docs.adoc new file mode 100644 index 0000000000..8511332c15 --- /dev/null +++ b/internal/template/docs.adoc @@ -0,0 +1,109 @@ += Templating +:description: Learn how templates work. + + +//// + THIS FILE IS AUTOGENERATED! + + To make changes please edit the contents of: + internal/template/docs.adoc +//// + +[WARNING] +.Experimental +==== +Templates are an experimental feature and therefore subject to change outside of major version releases. +==== + +Templates are a way to define new {page-component-title} components (similar to plugins) that are implemented by generating a {page-component-title} config snippet from pre-defined parameter fields. This is useful when a common pattern of {page-component-title} configuration is used but with varying parameters each time. + +A template is defined in a YAML file that can be imported when {page-component-title} runs using the flag `-t`: + +[source,bash] +---- +benthos -t "./templates/*.yaml" -c ./config.yaml +---- + +The template describes the type of the component and configuration fields that can be used to customize it, followed by a xref:guides:bloblang/about.adoc[Bloblang mapping] that translates an object containing those fields into a benthos config structure. This allows you to use logic to generate more complex configurations: + +[tabs] +====== +Template:: ++ +-- + +[source,yaml] +---- +name: aws_sqs_list +type: input + +fields: + - name: urls + type: string + kind: list + - name: region + type: string + default: us-east-1 + +mapping: | + root.broker.inputs = this.urls.map_each(url -> { + "aws_sqs": { + "url": url, + "region": this.region, + } + }) +---- +-- +Config:: ++ +-- + +[source,yaml] +---- +input: + aws_sqs_list: + urls: + - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 + - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 + +pipeline: + processors: + - mapping: | + root.id = uuid_v4() + root.foo = this.inner.foo + root.body = this.outer +---- +-- +Result:: ++ +-- + +[source,yaml] +---- +input: + broker: + inputs: + - aws_sqs: + url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 + region: us-east-1 + - aws_sqs: + url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 + region: us-east-1 + +pipeline: + processors: + - mapping: | + root.id = uuid_v4() + root.foo = this.inner.foo + root.body = this.outer +---- +-- +====== + +You can see more examples of templates at https://github.com/benthosdev/benthos/tree/main/config/template_examples^. + +== Fields + +The schema of a template file is as follows: + +{{template "field_docs" . -}} diff --git a/internal/template/docs.go b/internal/template/docs.go index 686b9f2e31..c32e81bb97 100644 --- a/internal/template/docs.go +++ b/internal/template/docs.go @@ -9,7 +9,7 @@ import ( _ "embed" ) -//go:embed docs.md +//go:embed docs.adoc var templateDocs string type templateContext struct { diff --git a/internal/template/docs.md b/internal/template/docs.md deleted file mode 100644 index 5be732ade1..0000000000 --- a/internal/template/docs.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Templating -description: Learn how Benthos templates work. ---- - - - -:::warning EXPERIMENTAL -Templates are an experimental feature and therefore subject to change outside of major version releases. -::: - -Templates are a way to define new Benthos components (similar to plugins) that are implemented by generating a Benthos config snippet from pre-defined parameter fields. This is useful when a common pattern of Benthos configuration is used but with varying parameters each time. - -A template is defined in a YAML file that can be imported when Benthos runs using the flag `-t`: - -```sh -benthos -t "./templates/*.yaml" -c ./config.yaml -``` - -The template describes the type of the component and configuration fields that can be used to customize it, followed by a [Bloblang mapping][bloblang.about] that translates an object containing those fields into a benthos config structure. This allows you to use logic to generate more complex configurations: - -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - - - -```yml -name: aws_sqs_list -type: input - -fields: - - name: urls - type: string - kind: list - - name: region - type: string - default: us-east-1 - -mapping: | - root.broker.inputs = this.urls.map_each(url -> { - "aws_sqs": { - "url": url, - "region": this.region, - } - }) -``` - - - - -```yml -input: - aws_sqs_list: - urls: - - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 - - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 - -pipeline: - processors: - - mapping: | - root.id = uuid_v4() - root.foo = this.inner.foo - root.body = this.outer -``` - - - - -```yaml -input: - broker: - inputs: - - aws_sqs: - url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 - region: us-east-1 - - aws_sqs: - url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 - region: us-east-1 - -pipeline: - processors: - - mapping: | - root.id = uuid_v4() - root.foo = this.inner.foo - root.body = this.outer -``` - - - - - -You can see more examples of templates at [https://github.com/benthosdev/benthos/tree/main/config/template_examples](https://github.com/benthosdev/benthos/tree/main/config/template_examples). - -## Fields - -The schema of a template file is as follows: - -{{template "field_docs" . -}} - -[bloblang.about]: /docs/guides/bloblang/about diff --git a/public/service/codec/scanner.go b/public/service/codec/scanner.go index cee5286170..d93bec06af 100644 --- a/public/service/codec/scanner.go +++ b/public/service/codec/scanner.go @@ -23,7 +23,7 @@ func DeprecatedCodecFields(defaultScanner string) []*service.ConfigField { service.NewInternalField(codec.NewReaderDocs(fieldCodecFromString)).Deprecated().Optional(), service.NewIntField(crFieldMaxBuffer).Deprecated().Default(1000000), service.NewScannerField(crFieldCodec). - Description("The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once."). + Description("The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once."). Default(map[string]any{defaultScanner: map[string]any{}}). Version("4.25.0"). Optional(), diff --git a/public/service/config_extract_tracing.go b/public/service/config_extract_tracing.go index d60e0d36bf..aa9792003d 100644 --- a/public/service/config_extract_tracing.go +++ b/public/service/config_extract_tracing.go @@ -20,7 +20,7 @@ const ( // in order to extract distributed tracing information. func NewExtractTracingSpanMappingField() *ConfigField { return NewBloblangField(etsField). - Description("EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer."). + Description("EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer."). Examples(`root = @`, `root = this.meta.span`). Version("3.45.0"). Optional(). diff --git a/public/service/config_inject_tracing.go b/public/service/config_inject_tracing.go index 8dcf6fdc3b..98b0edc18d 100644 --- a/public/service/config_inject_tracing.go +++ b/public/service/config_inject_tracing.go @@ -16,7 +16,7 @@ const ( // tracing span mapping. func NewInjectTracingSpanMappingField() *ConfigField { return NewBloblangField(itsField). - Description("EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer."). + Description("EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer."). Examples( `meta = @.merge(this)`, `root.meta.span = this`, diff --git a/public/service/message.go b/public/service/message.go index 948c22c963..d44f62b066 100644 --- a/public/service/message.go +++ b/public/service/message.go @@ -265,7 +265,7 @@ func (m *Message) SetStructuredMut(i any) { // SetError marks the message as having failed a processing step and adds the // error to it as context. Messages marked with errors can be handled using a -// range of methods outlined in https://www.benthos.dev/docs/configuration/error_handling. +// range of methods outlined in https://www.docs.redpanda.com/redpanda-connect/configuration/error_handling. func (m *Message) SetError(err error) { if m.onErr != nil { m.onErr(err) @@ -275,7 +275,7 @@ func (m *Message) SetError(err error) { // GetError returns an error associated with a message, or nil if there isn't // one. Messages marked with errors can be handled using a range of methods -// outlined in https://www.benthos.dev/docs/configuration/error_handling. +// outlined in https://www.docs.redpanda.com/redpanda-connect/configuration/error_handling. func (m *Message) GetError() error { return m.part.ErrorGet() } diff --git a/public/service/output.go b/public/service/output.go index d6c259dbe2..03f4eeb81b 100644 --- a/public/service/output.go +++ b/public/service/output.go @@ -353,7 +353,7 @@ var docsAsync = ` This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field ` + "`max_in_flight`" + `.` var docsBatches = ` -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching).` +This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc].` // OutputPerformanceDocs returns a string of markdown documentation that can be // added to outputs as standard performance advice based on whether the output @@ -362,7 +362,7 @@ func OutputPerformanceDocs(benefitsFromMaxInFlight, benefitsFromBatching bool) ( if !benefitsFromMaxInFlight && !benefitsFromBatching { return } - content += "\n\n## Performance" + content += "\n\n== Performance" if benefitsFromMaxInFlight { content += "\n" + docsAsync } diff --git a/public/service/processor.go b/public/service/processor.go index aa78f0f25f..9b9b36a607 100644 --- a/public/service/processor.go +++ b/public/service/processor.go @@ -18,7 +18,7 @@ type Processor interface { // When an error is returned the input message will continue down the // pipeline but will be marked with the error with *message.SetError, and // metrics and logs will be emitted. The failed message can then be handled - // with the patterns outlined in https://www.benthos.dev/docs/configuration/error_handling. + // with the patterns outlined in https://www.docs.redpanda.com/redpanda-connect/configuration/error_handling. // // The Message types returned MUST be derived from the provided message, and // CANNOT be custom implementations of Message. In order to copy the From 8a547fc19089c77223950864f99856e60bc3d956 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Thu, 23 May 2024 12:35:07 +0100 Subject: [PATCH 02/17] Remove docusaurus site --- Makefile | 33 +- go.mod | 72 +- go.sum | 62 +- internal/api/api.go | 271 - internal/api/api_test.go | 185 - internal/api/config.go | 71 - internal/api/docs.adoc | 101 - internal/api/docs.go | 93 - internal/api/dynamic_crud.go | 310 - internal/api/dynamic_crud_test.go | 246 - internal/api/package.go | 21 - internal/autoretry/auto_retry_list.go | 258 - internal/autoretry/auto_retry_list_test.go | 209 - internal/batch/combined_ack_func.go | 57 - internal/batch/combined_ack_func_test.go | 135 - internal/batch/count.go | 64 - internal/batch/count_test.go | 32 - internal/batch/error.go | 120 - internal/batch/error_test.go | 141 - internal/batch/package.go | 3 - internal/batch/policy/batchconfig/config.go | 77 - internal/batch/policy/docs.go | 74 - internal/batch/policy/package.go | 3 - internal/batch/policy/policy.go | 208 - internal/batch/policy/policy_test.go | 314 - internal/bloblang/environment.go | 203 - internal/bloblang/field/expression.go | 94 - internal/bloblang/field/expression_test.go | 371 - internal/bloblang/field/package.go | 4 - internal/bloblang/field/resolver.go | 94 - internal/bloblang/mapping/assignment.go | 227 - internal/bloblang/mapping/executor.go | 332 - internal/bloblang/mapping/executor_test.go | 615 -- internal/bloblang/mapping/package.go | 2 - internal/bloblang/mapping/statement.go | 132 - internal/bloblang/mapping/target.go | 28 - internal/bloblang/package.go | 11 - internal/bloblang/package_test.go | 271 - internal/bloblang/parser/combinators.go | 790 -- internal/bloblang/parser/combinators_test.go | 1808 --- internal/bloblang/parser/context.go | 233 - internal/bloblang/parser/context_test.go | 171 - internal/bloblang/parser/dot_env_parser.go | 57 - .../bloblang/parser/dot_env_parser_test.go | 91 - internal/bloblang/parser/error.go | 267 - internal/bloblang/parser/error_test.go | 177 - internal/bloblang/parser/field_parser.go | 90 - internal/bloblang/parser/field_parser_test.go | 220 - internal/bloblang/parser/mapping_parser.go | 479 - .../bloblang/parser/mapping_parser_test.go | 601 - .../parser/query_arithmetic_parser.go | 118 - .../parser/query_arithmetic_parser_test.go | 394 - .../parser/query_expression_parser.go | 265 - .../parser/query_expression_parser_test.go | 288 - .../bloblang/parser/query_function_parser.go | 372 - .../parser/query_function_parser_test.go | 782 -- .../bloblang/parser/query_literal_parser.go | 101 - .../parser/query_literal_parser_test.go | 139 - .../parser/query_method_parser_test.go | 580 - internal/bloblang/parser/query_parser.go | 37 - internal/bloblang/parser/query_parser_test.go | 247 - .../bloblang/parser/root_expression_parser.go | 109 - .../parser/root_expression_parser_test.go | 149 - internal/bloblang/plugins/bloblang.go | 53 - internal/bloblang/query/arithmetic.go | 543 - internal/bloblang/query/arithmetic_test.go | 921 -- internal/bloblang/query/docs.go | 302 - internal/bloblang/query/errors.go | 86 - internal/bloblang/query/errors_test.go | 142 - internal/bloblang/query/expression.go | 184 - internal/bloblang/query/expression_test.go | 445 - internal/bloblang/query/function_ctor.go | 67 - internal/bloblang/query/function_set.go | 203 - internal/bloblang/query/function_set_test.go | 130 - internal/bloblang/query/functions.go | 952 -- internal/bloblang/query/functions_test.go | 493 - internal/bloblang/query/iterator.go | 287 - internal/bloblang/query/iterator_test.go | 216 - internal/bloblang/query/literals.go | 196 - internal/bloblang/query/literals_test.go | 166 - internal/bloblang/query/method_ctor.go | 72 - internal/bloblang/query/method_set.go | 176 - internal/bloblang/query/method_set_test.go | 127 - internal/bloblang/query/methods.go | 512 - internal/bloblang/query/methods_numbers.go | 235 - internal/bloblang/query/methods_strings.go | 2025 ---- internal/bloblang/query/methods_structured.go | 1735 --- .../bloblang/query/methods_structured_test.go | 151 - internal/bloblang/query/methods_test.go | 2109 ---- internal/bloblang/query/package.go | 162 - internal/bloblang/query/params.go | 734 -- internal/bloblang/query/params_test.go | 758 -- internal/bloblang/query/parsed_test.go | 71 - internal/bloblang/query/target.go | 159 - internal/bundle/buffers.go | 98 - internal/bundle/caches.go | 98 - internal/bundle/environment.go | 112 - internal/bundle/inputs.go | 97 - internal/bundle/metrics.go | 111 - internal/bundle/outputs.go | 107 - internal/bundle/package.go | 135 - internal/bundle/processors.go | 100 - internal/bundle/ratelimits.go | 98 - internal/bundle/scanners.go | 97 - internal/bundle/tracers.go | 98 - internal/bundle/tracing/bundle.go | 73 - internal/bundle/tracing/bundle_test.go | 635 -- internal/bundle/tracing/events.go | 233 - internal/bundle/tracing/input.go | 88 - internal/bundle/tracing/output.go | 83 - internal/bundle/tracing/processor.go | 69 - internal/cli/blobl/cli.go | 300 - .../blobl/resources/bloblang_editor_page.html | 203 - internal/cli/blobl/server.go | 242 - internal/cli/common/logger.go | 31 - internal/cli/common/manager.go | 290 - internal/cli/common/opts.go | 70 - internal/cli/common/reader.go | 38 - internal/cli/common/service.go | 231 - internal/cli/common/swappable.go | 94 - internal/cli/create.go | 192 - internal/cli/lint.go | 242 - internal/cli/lint_test.go | 174 - internal/cli/list.go | 125 - internal/cli/run.go | 314 - internal/cli/run_test.go | 44 - internal/cli/studio/cli.go | 30 - internal/cli/studio/logger.go | 49 - internal/cli/studio/metrics/observed.go | 35 - internal/cli/studio/metrics/tracker.go | 204 - internal/cli/studio/pull.go | 135 - internal/cli/studio/pull_runner.go | 481 - internal/cli/studio/pull_runner_test.go | 1262 --- internal/cli/studio/pull_session_fs.go | 118 - internal/cli/studio/pull_session_tracker.go | 525 - internal/cli/studio/sync_schema.go | 82 - internal/cli/studio/tracing/observed.go | 86 - internal/cli/template/cli.go | 28 - internal/cli/template/lint.go | 108 - internal/cli/test/case.go | 107 - internal/cli/test/case_test.go | 405 - internal/cli/test/cli.go | 66 - internal/cli/test/command.go | 199 - internal/cli/test/command_test.go | 259 - internal/cli/test/definition.go | 37 - internal/cli/test/definition_test.go | 169 - internal/cli/test/processors_provider.go | 414 - internal/cli/test/processors_provider_test.go | 410 - internal/codec/reader.go | 1179 -- internal/codec/reader_test.go | 927 -- internal/codec/skip_group_reader.go | 96 - internal/codec/skip_group_reader_test.go | 97 - internal/codec/writer.go | 63 - internal/component/buffer/config.go | 68 - internal/component/buffer/interface.go | 32 - .../component/buffer/memory_buffer_test.go | 67 - internal/component/buffer/stream.go | 261 - internal/component/buffer/stream_test.go | 269 - internal/component/cache/cache_metrics.go | 137 - .../component/cache/cache_metrics_test.go | 255 - internal/component/cache/config.go | 79 - internal/component/cache/interface.go | 40 - internal/component/errors.go | 81 - internal/component/input/async_cut_off.go | 89 - internal/component/input/async_preserver.go | 122 - .../component/input/async_preserver_test.go | 1020 -- internal/component/input/async_reader.go | 283 - internal/component/input/async_reader_test.go | 788 -- internal/component/input/batcher/batcher.go | 188 - .../component/input/batcher/batcher_test.go | 364 - internal/component/input/config.go | 109 - internal/component/input/config/config.go | 76 - internal/component/input/interface.go | 57 - internal/component/input/processors/append.go | 45 - .../component/input/wrap_with_pipeline.go | 80 - .../input/wrap_with_pipeline_test.go | 349 - internal/component/interop/interop.go | 165 - internal/component/metrics/combine.go | 197 - internal/component/metrics/config.go | 78 - internal/component/metrics/config_test.go | 63 - internal/component/metrics/dud_type.go | 80 - internal/component/metrics/local.go | 275 - internal/component/metrics/local_test.go | 133 - internal/component/metrics/mapping.go | 119 - internal/component/metrics/mapping_test.go | 167 - internal/component/metrics/namespaced.go | 243 - internal/component/metrics/namespaced_test.go | 259 - internal/component/metrics/type.go | 102 - internal/component/metrics/vector_util.go | 50 - internal/component/observability.go | 38 - internal/component/output/async_writer.go | 289 - .../component/output/async_writer_test.go | 637 -- internal/component/output/batched_send.go | 56 - .../component/output/batched_send_test.go | 112 - internal/component/output/batcher/batcher.go | 194 - .../component/output/batcher/batcher_test.go | 401 - internal/component/output/config.go | 109 - internal/component/output/interface.go | 49 - internal/component/output/not_batched.go | 167 - internal/component/output/not_batched_test.go | 314 - .../component/output/processors/append.go | 44 - .../component/output/wrap_with_pipeline.go | 75 - .../output/wrap_with_pipeline_test.go | 210 - internal/component/processor/auto_observed.go | 258 - .../component/processor/auto_observed_test.go | 259 - internal/component/processor/config.go | 81 - internal/component/processor/error.go | 28 - internal/component/processor/execute.go | 118 - internal/component/processor/execute_test.go | 162 - internal/component/processor/interface.go | 65 - internal/component/ratelimit/config.go | 81 - internal/component/ratelimit/interface.go | 20 - .../component/ratelimit/rate_limit_metrics.go | 43 - .../ratelimit/rate_limit_metrics_test.go | 33 - internal/component/scanner/config.go | 57 - internal/component/scanner/interface.go | 32 - .../component/scanner/testutil/testutil.go | 263 - internal/component/testutil/from_yaml.go | 123 - internal/component/tracer/config.go | 73 - internal/config/config_test.go | 355 - internal/config/env_vars.go | 75 - internal/config/env_vars_test.go | 65 - internal/config/lint.go | 111 - internal/config/reader.go | 409 - internal/config/reader_test.go | 241 - internal/config/resource_reader.go | 388 - internal/config/resource_reader_test.go | 248 - internal/config/schema.go | 154 - internal/config/schema/schema.go | 196 - internal/config/stream_reader.go | 240 - internal/config/stream_reader_test.go | 118 - internal/config/test/case.go | 161 - internal/config/test/docs.adoc | 301 - internal/config/test/docs.go | 90 - internal/config/test/input.go | 93 - internal/config/test/output.go | 316 - internal/config/test/output_test.go | 581 - internal/config/watcher.go | 202 - internal/config/watcher_test.go | 566 - internal/config/watcher_wasm.go | 14 - internal/cuegen/README.md | 74 - internal/cuegen/component.go | 77 - internal/cuegen/config.go | 21 - internal/cuegen/cue.go | 179 - internal/cuegen/identifiers.go | 34 - internal/cuegen/schema.go | 144 - internal/docs/benchmark_test.go | 66 - internal/docs/bloblang.go | 49 - internal/docs/bloblang_markdown.go | 342 - internal/docs/bloblang_test.go | 117 - internal/docs/component.go | 108 - internal/docs/component_markdown.go | 245 - internal/docs/config.go | 143 - internal/docs/config_test.go | 171 - internal/docs/field.go | 864 -- internal/docs/field_interop.go | 85 - internal/docs/field_template.go | 148 - internal/docs/field_test.go | 215 - internal/docs/format_any.go | 136 - internal/docs/format_yaml.go | 1121 -- internal/docs/format_yaml_path.go | 376 - internal/docs/format_yaml_path_test.go | 837 -- internal/docs/format_yaml_test.go | 1408 --- internal/docs/interop/interop.go | 19 - internal/docs/json_schema.go | 92 - internal/docs/metrics_mapping.go | 16 - internal/docs/package.go | 3 - internal/docs/parsed.go | 483 - internal/docs/registry.go | 147 - internal/filepath/glob.go | 144 - internal/filepath/glob_test.go | 190 - internal/filepath/ifs/http.go | 83 - internal/filepath/ifs/os.go | 95 - internal/filepath/ifs/os_test.go | 36 - internal/httpclient/auth_oauth2.go | 149 - internal/httpclient/client.go | 446 - internal/httpclient/client_test.go | 710 -- internal/httpclient/config.go | 187 - internal/httpclient/config_test.go | 94 - internal/httpclient/errors.go | 20 - internal/httpclient/errors_test.go | 16 - internal/httpclient/logger.go | 163 - internal/httpclient/logger_test.go | 343 - internal/httpclient/request.go | 259 - internal/httpclient/request_test.go | 41 - internal/httpserver/basic_auth.go | 187 - internal/httpserver/cors.go | 64 - internal/httpserver/cors_test.go | 87 - internal/impl/aws/cache_dynamodb.go | 3 +- internal/impl/aws/cache_s3.go | 3 +- internal/impl/aws/input_kinesis.go | 3 +- internal/impl/aws/input_s3.go | 3 +- internal/impl/aws/input_sqs.go | 3 +- internal/impl/aws/integration_sqs_test.go | 2 +- internal/impl/aws/integration_test.go | 2 +- internal/impl/aws/metrics_cloudwatch.go | 3 +- internal/impl/aws/output_dynamodb.go | 5 +- internal/impl/aws/output_kinesis.go | 5 +- internal/impl/aws/output_kinesis_firehose.go | 5 +- internal/impl/aws/output_s3.go | 3 +- internal/impl/aws/output_sns.go | 3 +- internal/impl/aws/output_sqs.go | 5 +- .../impl/aws/processor_dynamodb_partiql.go | 3 +- internal/impl/aws/processor_lambda.go | 3 +- internal/impl/azure/input_cosmosdb.go | 3 +- internal/impl/azure/output_cosmosdb.go | 3 +- internal/impl/azure/processor_cosmosdb.go | 3 +- internal/impl/confluent/serde_protobuf.go | 3 +- internal/impl/couchbase/cache.go | 3 +- internal/impl/couchbase/client.go | 3 +- internal/impl/couchbase/processor.go | 3 +- internal/impl/couchbase/processor_test.go | 3 +- internal/impl/elasticsearch/aws/aws.go | 5 +- .../elasticsearch/aws/integration_test.go | 5 +- internal/impl/elasticsearch/output.go | 5 +- .../elasticsearch/writer_integration_test.go | 3 +- internal/impl/io/bloblang.go | 186 - internal/impl/io/bloblang_test.go | 118 - internal/impl/io/cache_file.go | 93 - internal/impl/io/cache_file_test.go | 50 - internal/impl/io/input_csv.go | 480 - .../impl/io/input_csv_integration_test.go | 152 - internal/impl/io/input_csv_test.go | 498 - internal/impl/io/input_dynamic.go | 198 - internal/impl/io/input_dynamic_fan_in.go | 219 - internal/impl/io/input_dynamic_fan_in_test.go | 320 - internal/impl/io/input_dynamic_test.go | 214 - internal/impl/io/input_file.go | 237 - internal/impl/io/input_file_test.go | 201 - internal/impl/io/input_http_client.go | 287 - internal/impl/io/input_http_client_test.go | 898 -- internal/impl/io/input_http_server.go | 960 -- internal/impl/io/input_http_server_test.go | 1338 --- internal/impl/io/input_socket.go | 151 - internal/impl/io/input_socket_server.go | 377 - internal/impl/io/input_socket_server_test.go | 1272 --- internal/impl/io/input_socket_test.go | 1021 -- internal/impl/io/input_stdin.go | 92 - internal/impl/io/input_stdin_test.go | 25 - internal/impl/io/input_subprocess.go | 252 - internal/impl/io/input_subprocess_test.go | 173 - internal/impl/io/input_websocket.go | 232 - internal/impl/io/input_websocket_test.go | 242 - internal/impl/io/metrics_json_api.go | 96 - internal/impl/io/output_dynamic.go | 210 - internal/impl/io/output_dynamic_fan_out.go | 307 - .../impl/io/output_dynamic_fan_out_test.go | 437 - internal/impl/io/output_dynamic_test.go | 80 - internal/impl/io/output_file.go | 192 - internal/impl/io/output_http_client.go | 178 - internal/impl/io/output_http_client_test.go | 664 -- internal/impl/io/output_http_server.go | 498 - internal/impl/io/output_http_server_test.go | 161 - internal/impl/io/output_socket.go | 140 - internal/impl/io/output_socket_test.go | 225 - internal/impl/io/output_stdout.go | 82 - internal/impl/io/output_subprocess.go | 175 - internal/impl/io/output_subprocess_test.go | 195 - internal/impl/io/output_websocket.go | 188 - internal/impl/io/output_websocket_test.go | 107 - internal/impl/io/package.go | 4 - internal/impl/io/processor_command.go | 164 - internal/impl/io/processor_command_test.go | 102 - internal/impl/io/processor_http.go | 240 - internal/impl/io/processor_http_test.go | 434 - internal/impl/io/processor_subprocess.go | 523 - internal/impl/io/processor_subprocess_test.go | 305 - internal/impl/kafka/aws/aws.go | 5 +- .../impl/kafka/integration_sarama_test.go | 3 +- internal/impl/kafka/sasl.go | 3 +- internal/impl/kafka/sasl_test.go | 2 +- internal/impl/mongodb/output.go | 3 +- internal/impl/mongodb/processor.go | 3 +- internal/impl/mongodb/processor_test.go | 3 +- internal/impl/opensearch/aws/aws.go | 5 +- internal/impl/opensearch/integration_test.go | 3 +- internal/impl/opensearch/output.go | 3 +- internal/impl/pure/algorithms.go | 298 - internal/impl/pure/bloblang_encoding.go | 84 - internal/impl/pure/bloblang_encoding_test.go | 42 - internal/impl/pure/bloblang_general.go | 162 - internal/impl/pure/bloblang_general_test.go | 96 - internal/impl/pure/bloblang_numbers.go | 176 - internal/impl/pure/bloblang_objects.go | 180 - internal/impl/pure/bloblang_objects_test.go | 164 - internal/impl/pure/bloblang_string.go | 53 - internal/impl/pure/bloblang_string_test.go | 65 - internal/impl/pure/bloblang_time.go | 620 -- internal/impl/pure/bloblang_time_test.go | 331 - internal/impl/pure/buffer_memory.go | 291 - internal/impl/pure/buffer_memory_test.go | 478 - internal/impl/pure/buffer_none.go | 25 - internal/impl/pure/buffer_system_window.go | 458 - .../impl/pure/buffer_system_window_test.go | 646 -- internal/impl/pure/cache_integration_test.go | 31 - internal/impl/pure/cache_lru.go | 298 - internal/impl/pure/cache_lru_test.go | 207 - internal/impl/pure/cache_memory.go | 244 - internal/impl/pure/cache_memory_test.go | 255 - internal/impl/pure/cache_multilevel.go | 201 - internal/impl/pure/cache_multilevel_test.go | 363 - internal/impl/pure/cache_noop.go | 73 - internal/impl/pure/cache_noop_test.go | 27 - internal/impl/pure/cache_ttlru.go | 221 - internal/impl/pure/cache_ttlru_test.go | 129 - internal/impl/pure/extended/zstd.go | 26 - internal/impl/pure/extended/zstd_test.go | 31 - internal/impl/pure/input_batched.go | 46 - internal/impl/pure/input_batched_test.go | 121 - internal/impl/pure/input_broker.go | 144 - internal/impl/pure/input_broker_fan_in.go | 137 - .../impl/pure/input_broker_fan_in_test.go | 268 - internal/impl/pure/input_broker_test.go | 125 - internal/impl/pure/input_generate.go | 278 - internal/impl/pure/input_generate_test.go | 223 - internal/impl/pure/input_inproc.go | 130 - internal/impl/pure/input_inproc_test.go | 53 - internal/impl/pure/input_read_until.go | 343 - internal/impl/pure/input_read_until_test.go | 296 - internal/impl/pure/input_resource.go | 171 - internal/impl/pure/input_resource_test.go | 128 - internal/impl/pure/input_sequence.go | 627 -- internal/impl/pure/input_sequence_test.go | 492 - internal/impl/pure/metrics_logger.go | 157 - internal/impl/pure/metrics_none.go | 50 - internal/impl/pure/output_broker.go | 211 - internal/impl/pure/output_broker_fan_out.go | 131 - .../pure/output_broker_fan_out_sequential.go | 138 - .../output_broker_fan_out_sequential_test.go | 127 - .../impl/pure/output_broker_fan_out_test.go | 325 - internal/impl/pure/output_broker_greedy.go | 51 - .../impl/pure/output_broker_greedy_test.go | 102 - .../impl/pure/output_broker_round_robin.go | 102 - .../pure/output_broker_round_robin_test.go | 151 - internal/impl/pure/output_broker_test.go | 394 - internal/impl/pure/output_cache.go | 216 - internal/impl/pure/output_cache_test.go | 238 - internal/impl/pure/output_drop.go | 52 - internal/impl/pure/output_drop_on.go | 303 - internal/impl/pure/output_drop_on_test.go | 374 - internal/impl/pure/output_fallback.go | 273 - internal/impl/pure/output_fallback_test.go | 440 - internal/impl/pure/output_inproc.go | 123 - internal/impl/pure/output_inproc_test.go | 67 - internal/impl/pure/output_reject.go | 104 - internal/impl/pure/output_reject_errored.go | 280 - .../impl/pure/output_reject_errored_test.go | 468 - internal/impl/pure/output_resource.go | 169 - internal/impl/pure/output_resource_test.go | 86 - internal/impl/pure/output_retry.go | 270 - internal/impl/pure/output_retry_test.go | 410 - internal/impl/pure/output_switch.go | 483 - internal/impl/pure/output_switch_test.go | 1016 -- internal/impl/pure/output_sync_response.go | 77 - .../impl/pure/output_sync_response_test.go | 52 - internal/impl/pure/package.go | 4 - internal/impl/pure/processor_archive.go | 300 - internal/impl/pure/processor_archive_test.go | 264 - internal/impl/pure/processor_bloblang.go | 166 - internal/impl/pure/processor_bloblang_test.go | 218 - internal/impl/pure/processor_bounds_check.go | 142 - .../impl/pure/processor_bounds_check_test.go | 90 - internal/impl/pure/processor_branch.go | 521 - internal/impl/pure/processor_branch_test.go | 275 - internal/impl/pure/processor_cache.go | 343 - internal/impl/pure/processor_cache_test.go | 216 - internal/impl/pure/processor_cached.go | 364 - internal/impl/pure/processor_cached_test.go | 231 - internal/impl/pure/processor_catch.go | 116 - internal/impl/pure/processor_catch_test.go | 261 - internal/impl/pure/processor_compress.go | 89 - internal/impl/pure/processor_compress_test.go | 316 - internal/impl/pure/processor_decompress.go | 78 - .../impl/pure/processor_decompress_test.go | 325 - internal/impl/pure/processor_dedupe.go | 176 - internal/impl/pure/processor_dedupe_test.go | 109 - internal/impl/pure/processor_for_each.go | 90 - internal/impl/pure/processor_for_each_test.go | 173 - internal/impl/pure/processor_grok.go | 246 - internal/impl/pure/processor_grok_test.go | 168 - internal/impl/pure/processor_group_by.go | 196 - internal/impl/pure/processor_group_by_test.go | 83 - .../impl/pure/processor_group_by_value.go | 126 - .../pure/processor_group_by_value_test.go | 76 - internal/impl/pure/processor_insert_part.go | 123 - .../impl/pure/processor_insert_part_test.go | 242 - internal/impl/pure/processor_jmespath.go | 163 - internal/impl/pure/processor_jmespath_test.go | 191 - internal/impl/pure/processor_jq.go | 264 - internal/impl/pure/processor_jq_test.go | 291 - internal/impl/pure/processor_jsonschema.go | 184 - .../impl/pure/processor_jsonschema_test.go | 401 - internal/impl/pure/processor_log.go | 230 - internal/impl/pure/processor_log_test.go | 242 - internal/impl/pure/processor_mapping.go | 168 - internal/impl/pure/processor_mapping_test.go | 231 - internal/impl/pure/processor_metric.go | 414 - internal/impl/pure/processor_metric_test.go | 243 - internal/impl/pure/processor_mutation.go | 172 - internal/impl/pure/processor_mutation_test.go | 233 - internal/impl/pure/processor_noop.go | 34 - internal/impl/pure/processor_parallel.go | 128 - internal/impl/pure/processor_parallel_test.go | 171 - internal/impl/pure/processor_parse_log.go | 317 - .../impl/pure/processor_parse_log_test.go | 130 - internal/impl/pure/processor_processors.go | 100 - .../impl/pure/processor_processors_test.go | 49 - internal/impl/pure/processor_rate_limit.go | 106 - .../impl/pure/processor_rate_limit_test.go | 170 - internal/impl/pure/processor_resource.go | 94 - internal/impl/pure/processor_resource_test.go | 64 - internal/impl/pure/processor_retry.go | 241 - internal/impl/pure/processor_retry_test.go | 224 - internal/impl/pure/processor_select_parts.go | 85 - .../impl/pure/processor_select_parts_test.go | 149 - internal/impl/pure/processor_sleep.go | 98 - internal/impl/pure/processor_sleep_test.go | 111 - internal/impl/pure/processor_split.go | 95 - internal/impl/pure/processor_split_test.go | 181 - internal/impl/pure/processor_switch.go | 245 - internal/impl/pure/processor_switch_test.go | 315 - internal/impl/pure/processor_sync_response.go | 45 - internal/impl/pure/processor_try.go | 131 - internal/impl/pure/processor_try_test.go | 217 - internal/impl/pure/processor_unarchive.go | 360 - .../impl/pure/processor_unarchive_test.go | 362 - internal/impl/pure/processor_while.go | 187 - internal/impl/pure/processor_while_test.go | 181 - internal/impl/pure/processor_workflow.go | 591 - .../pure/processor_workflow_branch_map.go | 312 - internal/impl/pure/processor_workflow_test.go | 1052 -- internal/impl/pure/rate_limit_local.go | 93 - internal/impl/pure/rate_limit_local_test.go | 140 - internal/impl/pure/scanner_chunker.go | 94 - internal/impl/pure/scanner_chunker_test.go | 25 - internal/impl/pure/scanner_csv.go | 163 - internal/impl/pure/scanner_csv_test.go | 83 - internal/impl/pure/scanner_decompress.go | 72 - internal/impl/pure/scanner_decompress_test.go | 32 - internal/impl/pure/scanner_json.go | 71 - internal/impl/pure/scanner_json_test.go | 273 - internal/impl/pure/scanner_lines.go | 119 - internal/impl/pure/scanner_lines_test.go | 73 - internal/impl/pure/scanner_re_match.go | 128 - internal/impl/pure/scanner_re_match_test.go | 46 - internal/impl/pure/scanner_skip_bom.go | 153 - internal/impl/pure/scanner_skip_bom_test.go | 97 - internal/impl/pure/scanner_switch.go | 132 - internal/impl/pure/scanner_switch_test.go | 52 - internal/impl/pure/scanner_tar.go | 86 - internal/impl/pure/scanner_tar_test.go | 50 - internal/impl/pure/scanner_to_the_end.go | 70 - internal/impl/pure/scanner_to_the_end_test.go | 67 - internal/impl/pure/tracer_none.go | 24 - internal/impl/sql/buffer_sqlite_test.go | 3 +- internal/impl/sql/integration_test.go | 3 +- internal/log/config.go | 110 - internal/log/docs.adoc | 42 - internal/log/docs.go | 51 - internal/log/interface.go | 14 - internal/log/logrus.go | 171 - internal/log/logrus_test.go | 170 - internal/log/slog.go | 64 - internal/log/slog_test.go | 85 - internal/log/tee.go | 53 - internal/log/tee_test.go | 69 - internal/log/wrap.go | 143 - internal/manager/config.go | 136 - internal/manager/config_test.go | 84 - internal/manager/docs.go | 48 - internal/manager/initialization_test.go | 159 - internal/manager/input_wrapper.go | 161 - internal/manager/input_wrapper_test.go | 80 - internal/manager/live_resources.go | 159 - internal/manager/mock/cache.go | 72 - internal/manager/mock/cache_test.go | 8 - internal/manager/mock/input.go | 57 - internal/manager/mock/input_test.go | 8 - internal/manager/mock/manager.go | 371 - internal/manager/mock/manager_test.go | 8 - internal/manager/mock/output.go | 59 - internal/manager/mock/output_test.go | 8 - internal/manager/mock/processor.go | 20 - internal/manager/mock/processor_test.go | 8 - internal/manager/mock/ratelimit.go | 19 - internal/manager/mock/ratelimit_test.go | 8 - internal/manager/output_wrapper.go | 70 - internal/manager/output_wrapper_test.go | 51 - internal/manager/package.go | 3 - internal/manager/type.go | 998 -- internal/manager/type_stream_test.go | 91 - internal/manager/type_test.go | 541 - internal/message/data.go | 201 - internal/message/data_test.go | 117 - internal/message/errors.go | 11 - internal/message/message.go | 177 - internal/message/message_test.go | 524 - internal/message/part.go | 171 - internal/message/part_test.go | 104 - internal/message/part_with_context_test.go | 69 - internal/message/sort_group.go | 109 - internal/message/sort_group_test.go | 49 - internal/message/transaction.go | 96 - internal/message/util.go | 152 - internal/message/util_test.go | 184 - internal/metadata/exclude_filter.go | 76 - internal/metadata/exclude_filter_test.go | 81 - internal/metadata/include_filter.go | 109 - internal/metadata/include_filter_test.go | 127 - internal/old/util/throttle/package.go | 2 - internal/old/util/throttle/type.go | 142 - internal/old/util/throttle/type_test.go | 164 - internal/pipeline/config_test.go | 58 - internal/pipeline/constructor.go | 126 - internal/pipeline/package.go | 4 - internal/pipeline/pool.go | 167 - internal/pipeline/pool_test.go | 280 - internal/pipeline/processor.go | 177 - internal/pipeline/processor_test.go | 462 - internal/serverless/handler.go | 198 - internal/serverless/handler_test.go | 204 - internal/serverless/lambda/config.go | 46 - internal/serverless/lambda/config_test.go | 19 - internal/serverless/lambda/lambda.go | 96 - internal/serverless/lambda/package.go | 3 - internal/serverless/package.go | 3 - internal/stream/config.go | 64 - internal/stream/config_test.go | 65 - internal/stream/docs.go | 33 - internal/stream/manager/api.go | 654 -- internal/stream/manager/api_test.go | 1013 -- internal/stream/manager/package.go | 3 - internal/stream/manager/type.go | 263 - internal/stream/manager/type_stress_test.go | 64 - internal/stream/manager/type_test.go | 115 - internal/stream/type.go | 273 - internal/stream/type_test.go | 233 - internal/template/config.go | 276 - internal/template/docs.adoc | 109 - internal/template/docs.go | 29 - internal/template/template.go | 239 - internal/template/template_test.go | 301 - internal/tls/docs.go | 52 - internal/tls/package.go | 3 - internal/tls/type.go | 214 - internal/tls/type_test.go | 262 - internal/tracing/otel.go | 158 - internal/tracing/otel_test.go | 40 - internal/tracing/package.go | 3 - internal/tracing/span.go | 75 - internal/tracing/v2/otel.go | 155 - internal/tracing/v2/otel_test.go | 40 - internal/tracing/v2/package.go | 3 - internal/tracing/v2/span.go | 75 - internal/transaction/benchmarks_test.go | 152 - internal/transaction/result_store.go | 102 - internal/transaction/result_store_test.go | 40 - internal/transaction/tracked.go | 85 - internal/transaction/tracked_test.go | 270 - internal/value/errors.go | 72 - internal/value/type_helpers.go | 901 -- internal/value/type_helpers_test.go | 71 - public/bloblang/arguments.go | 167 - public/bloblang/arguments_test.go | 99 - public/bloblang/context.go | 93 - public/bloblang/context_test.go | 82 - public/bloblang/environment.go | 438 - public/bloblang/environment_test.go | 108 - public/bloblang/environment_unwrapper.go | 23 - public/bloblang/example_plugins_v2_test.go | 195 - public/bloblang/executor.go | 87 - public/bloblang/executor_test.go | 219 - public/bloblang/executor_unwrapper.go | 16 - public/bloblang/function.go | 41 - public/bloblang/method.go | 148 - public/bloblang/method_test.go | 137 - public/bloblang/package.go | 11 - public/bloblang/parse_error.go | 34 - public/bloblang/parse_error_test.go | 53 - public/bloblang/spec.go | 397 - public/bloblang/spec_test.go | 174 - public/bloblang/util.go | 50 - public/bloblang/view.go | 48 - public/components/all/package.go | 92 +- public/components/all/x_benthos_extra.go | 2 +- public/components/amqp09/package.go | 2 +- public/components/amqp1/package.go | 2 +- public/components/avro/package.go | 2 +- public/components/aws/package.go | 8 +- public/components/aws/serverless.go | 13 - public/components/azure/package.go | 2 +- public/components/beanstalkd/package.go | 2 +- public/components/cassandra/package.go | 2 +- public/components/changelog/package.go | 2 +- public/components/cockroachdb/package.go | 2 +- public/components/confluent/package.go | 2 +- public/components/couchbase/package.go | 2 +- public/components/crypto/package.go | 2 +- public/components/dgraph/package.go | 2 +- public/components/discord/package.go | 2 +- public/components/elasticsearch/package.go | 2 +- public/components/gcp/package.go | 2 +- public/components/hdfs/package.go | 2 +- public/components/influxdb/package.go | 2 +- public/components/io/package.go | 2 +- public/components/jaeger/package.go | 2 +- public/components/javascript/package.go | 2 +- public/components/kafka/package.go | 2 +- public/components/maxmind/package.go | 2 +- public/components/memcached/package.go | 2 +- public/components/mongodb/package.go | 2 +- public/components/mqtt/package.go | 2 +- public/components/msgpack/package.go | 2 +- public/components/nanomsg/package.go | 2 +- public/components/nats/package.go | 2 +- public/components/nsq/package.go | 2 +- public/components/opensearch/package.go | 2 +- public/components/otlp/package.go | 2 +- public/components/prometheus/package.go | 2 +- public/components/pulsar/package.go | 2 +- public/components/pure/extended/package.go | 17 +- public/components/pure/package.go | 2 +- public/components/pusher/package.go | 2 +- public/components/redis/package.go | 2 +- public/components/sentry/package.go | 2 +- public/components/sftp/package.go | 2 +- public/components/snowflake/package.go | 2 +- public/components/splunk/package.go | 2 +- public/components/sql/base/package.go | 2 +- public/components/statsd/package.go | 2 +- public/components/twitter/package.go | 2 +- public/components/wasm/package.go | 2 +- public/service/benchmark_test.go | 451 - public/service/buffer.go | 113 - public/service/buffer_test.go | 261 - public/service/cache.go | 156 - public/service/cache_test.go | 492 - public/service/chaos_test.go | 147 - public/service/codec/scanner.go | 126 - public/service/codec/scanner_test.go | 134 - public/service/config.go | 744 -- public/service/config_backoff.go | 140 - public/service/config_backoff_test.go | 57 - public/service/config_batch_policy.go | 161 - public/service/config_batch_policy_test.go | 140 - public/service/config_bloblang.go | 38 - public/service/config_bloblang_test.go | 33 - public/service/config_extract_tracing.go | 170 - public/service/config_extract_tracing_test.go | 197 - public/service/config_http.go | 418 - public/service/config_inject_tracing.go | 127 - public/service/config_inject_tracing_test.go | 175 - public/service/config_input.go | 138 - public/service/config_input_test.go | 92 - public/service/config_interpolated_string.go | 145 - .../config_interpolated_string_test.go | 52 - public/service/config_max_in_flight.go | 18 - public/service/config_metadata_filter.go | 181 - public/service/config_metadata_filter_test.go | 45 - public/service/config_output.go | 129 - public/service/config_output_test.go | 83 - public/service/config_processor.go | 93 - public/service/config_processor_test.go | 85 - public/service/config_scanner.go | 43 - public/service/config_test.go | 434 - public/service/config_tls.go | 91 - public/service/config_url.go | 112 - public/service/config_urls_test.go | 81 - public/service/config_util.go | 43 - public/service/environment.go | 689 -- public/service/environment_schema.go | 44 - public/service/environment_test.go | 222 - public/service/errors.go | 201 - public/service/errors_test.go | 124 - public/service/example_buffer_plugin_test.go | 92 - public/service/example_cache_plugin_test.go | 87 - public/service/example_input_plugin_test.go | 62 - .../example_output_batched_plugin_test.go | 106 - public/service/example_output_plugin_test.go | 82 - .../service/example_processor_plugin_test.go | 94 - .../service/example_rate_limit_plugin_test.go | 56 - .../example_stream_builder_yaml_test.go | 253 - public/service/input.go | 269 - public/service/input_auto_retry.go | 83 - public/service/input_auto_retry_batched.go | 138 - .../service/input_auto_retry_batched_test.go | 370 - public/service/input_auto_retry_test.go | 423 - public/service/input_max_in_flight.go | 105 - public/service/input_max_in_flight_test.go | 216 - public/service/input_test.go | 250 - .../integration/cache_test_definitions.go | 124 - .../service/integration/cache_test_helpers.go | 218 - .../stream_benchmark_definitions.go | 106 - .../integration/stream_test_definitions.go | 835 -- .../integration/stream_test_helpers.go | 588 - public/service/interpolated_string.go | 73 - public/service/interpolated_string_test.go | 107 - public/service/lints.go | 170 - public/service/logger.go | 121 - public/service/logger_test.go | 68 - public/service/message.go | 632 -- public/service/message_test.go | 492 - public/service/metrics.go | 320 - public/service/metrics_test.go | 221 - public/service/output.go | 373 - public/service/output_test.go | 308 - public/service/package.go | 24 - public/service/plugins.go | 207 - public/service/plugins_test.go | 568 - public/service/processor.go | 234 - public/service/processor_test.go | 217 - public/service/rate_limit.go | 45 - public/service/rate_limit_test.go | 88 - public/service/resources.go | 281 - public/service/resources_test.go | 152 - public/service/scanner.go | 245 - public/service/service.go | 126 - public/service/servicetest/service.go | 23 - public/service/servicetest/service_test.go | 42 - public/service/stream.go | 181 - public/service/stream_builder.go | 1014 -- public/service/stream_builder_test.go | 1321 --- public/service/stream_config_linter.go | 109 - public/service/stream_config_linter_test.go | 174 - public/service/stream_config_marshaller.go | 94 - .../service/stream_config_marshaller_test.go | 148 - public/service/stream_schema.go | 419 - public/service/stream_schema_test.go | 227 - public/service/stream_template_tester.go | 60 - public/service/tracing.go | 137 - public/service/tracing_test.go | 249 - public/service/util.go | 45 - public/wasm/README.md | 10 - public/wasm/examples/rust/.gitignore | 2 - public/wasm/examples/rust/Cargo.toml | 23 - public/wasm/examples/rust/louder.rs | 105 - public/wasm/examples/tinygo/README.md | 20 - public/wasm/examples/tinygo/main.go | 28 - public/wasm/tinygo/package.go | 6 - public/wasm/tinygo/tinygo.go | 66 - website/.gitignore | 20 - website/README.md | 33 - .../2019-05-27-compiling-benthos-to-wasm.md | 176 - .../2019-06-17-introducing-benthos-lab.md | 107 - .../blog/2019-08-20-write-a-benthos-plugin.md | 306 - .../blog/2020-04-18-sneak-peek-at-bloblang.md | 145 - website/blog/2020-05-10-bloblang-beta.md | 85 - website/blog/2020-08-30-improved-workflows.md | 146 - website/blog/2021-01-04-v4-roadmap.md | 99 - website/blog/2021-03-09-redpanda.md | 124 - .../2021-06-02-new-plugins-and-templates.md | 139 - website/blog/2021-10-12-new-plugins-stable.md | 31 - website/blog/2022-03-03-v4-coming.md | 66 - website/blog/2022-11-07-whats-next.md | 85 - website/build_plugins.sh | 3 - website/cookbooks/custom_metrics.md | 384 - website/cookbooks/discord_bot.md | 193 - website/cookbooks/enrichments.md | 325 - website/cookbooks/filtering.md | 67 - website/cookbooks/joining_streams.md | 236 - website/docs/about.md | 54 - website/docs/components/about.md | 120 - website/docs/components/buffers/about.md | 20 - website/docs/components/buffers/memory.md | 169 - website/docs/components/buffers/none.md | 29 - website/docs/components/buffers/sqlite.md | 96 - .../docs/components/buffers/system_window.md | 206 - website/docs/components/caches/about.md | 43 - .../docs/components/caches/aws_dynamodb.md | 259 - website/docs/components/caches/aws_s3.md | 231 - website/docs/components/caches/couchbase.md | 141 - website/docs/components/caches/file.md | 37 - .../components/caches/gcp_cloud_storage.md | 48 - website/docs/components/caches/lru.md | 136 - website/docs/components/caches/memcached.md | 134 - website/docs/components/caches/memory.md | 112 - website/docs/components/caches/mongodb.md | 99 - website/docs/components/caches/multilevel.md | 65 - website/docs/components/caches/nats_kv.md | 330 - website/docs/components/caches/noop.md | 27 - website/docs/components/caches/redis.md | 324 - website/docs/components/caches/ristretto.md | 135 - website/docs/components/caches/sql.md | 281 - website/docs/components/caches/ttlru.md | 124 - website/docs/components/http/about.md | 261 - website/docs/components/inputs/about.md | 79 - website/docs/components/inputs/amqp_0_9.md | 381 - website/docs/components/inputs/amqp_1.md | 356 - website/docs/components/inputs/aws_kinesis.md | 401 - website/docs/components/inputs/aws_s3.md | 328 - website/docs/components/inputs/aws_sqs.md | 210 - .../components/inputs/azure_blob_storage.md | 201 - .../docs/components/inputs/azure_cosmosdb.md | 289 - .../components/inputs/azure_queue_storage.md | 142 - .../components/inputs/azure_table_storage.md | 158 - website/docs/components/inputs/batched.md | 170 - website/docs/components/inputs/beanstalkd.md | 48 - website/docs/components/inputs/broker.md | 215 - website/docs/components/inputs/cassandra.md | 369 - .../inputs/cockroachdb_changefeed.md | 269 - website/docs/components/inputs/csv.md | 201 - website/docs/components/inputs/discord.md | 101 - website/docs/components/inputs/dynamic.md | 68 - website/docs/components/inputs/file.md | 125 - .../components/inputs/gcp_bigquery_select.md | 170 - .../components/inputs/gcp_cloud_storage.md | 116 - website/docs/components/inputs/gcp_pubsub.md | 157 - website/docs/components/inputs/generate.md | 150 - website/docs/components/inputs/hdfs.md | 72 - website/docs/components/inputs/http_client.md | 783 -- website/docs/components/inputs/http_server.md | 394 - website/docs/components/inputs/inproc.md | 30 - website/docs/components/inputs/kafka.md | 642 -- website/docs/components/inputs/kafka_franz.md | 625 -- website/docs/components/inputs/mongodb.md | 217 - website/docs/components/inputs/mqtt.md | 389 - website/docs/components/inputs/nanomsg.md | 111 - website/docs/components/inputs/nats.md | 404 - .../docs/components/inputs/nats_jetstream.md | 453 - website/docs/components/inputs/nats_kv.md | 404 - website/docs/components/inputs/nats_stream.md | 426 - website/docs/components/inputs/nsq.md | 276 - website/docs/components/inputs/parquet.md | 99 - website/docs/components/inputs/pulsar.md | 238 - website/docs/components/inputs/read_until.md | 129 - website/docs/components/inputs/redis_list.md | 300 - .../docs/components/inputs/redis_pubsub.md | 288 - website/docs/components/inputs/redis_scan.md | 301 - .../docs/components/inputs/redis_streams.md | 348 - website/docs/components/inputs/resource.md | 67 - website/docs/components/inputs/sequence.md | 241 - website/docs/components/inputs/sftp.md | 238 - website/docs/components/inputs/socket.md | 74 - .../docs/components/inputs/socket_server.md | 116 - website/docs/components/inputs/sql_raw.md | 278 - website/docs/components/inputs/sql_select.md | 320 - website/docs/components/inputs/stdin.md | 49 - website/docs/components/inputs/subprocess.md | 118 - .../docs/components/inputs/twitter_search.md | 146 - website/docs/components/inputs/websocket.md | 421 - website/docs/components/inputs/zmq4.md | 126 - website/docs/components/logger/about.md | 140 - website/docs/components/metrics/about.md | 188 - .../docs/components/metrics/aws_cloudwatch.md | 183 - website/docs/components/metrics/influxdb.md | 349 - website/docs/components/metrics/json_api.md | 28 - website/docs/components/metrics/logger.md | 52 - website/docs/components/metrics/none.md | 26 - website/docs/components/metrics/prometheus.md | 201 - website/docs/components/metrics/statsd.md | 55 - website/docs/components/outputs/about.md | 118 - website/docs/components/outputs/amqp_0_9.md | 462 - website/docs/components/outputs/amqp_1.md | 342 - .../docs/components/outputs/aws_dynamodb.md | 413 - .../docs/components/outputs/aws_kinesis.md | 354 - .../outputs/aws_kinesis_firehose.md | 326 - website/docs/components/outputs/aws_s3.md | 502 - website/docs/components/outputs/aws_sns.md | 223 - website/docs/components/outputs/aws_sqs.md | 378 - .../components/outputs/azure_blob_storage.md | 183 - .../docs/components/outputs/azure_cosmosdb.md | 492 - .../components/outputs/azure_queue_storage.md | 252 - .../components/outputs/azure_table_storage.md | 348 - website/docs/components/outputs/beanstalkd.md | 57 - website/docs/components/outputs/broker.md | 237 - website/docs/components/outputs/cache.md | 134 - website/docs/components/outputs/cassandra.md | 527 - website/docs/components/outputs/discord.md | 53 - website/docs/components/outputs/drop.md | 27 - website/docs/components/outputs/drop_on.md | 127 - website/docs/components/outputs/dynamic.md | 70 - .../docs/components/outputs/elasticsearch.md | 632 -- website/docs/components/outputs/fallback.md | 75 - website/docs/components/outputs/file.md | 79 - .../docs/components/outputs/gcp_bigquery.md | 375 - .../components/outputs/gcp_cloud_storage.md | 320 - website/docs/components/outputs/gcp_pubsub.md | 340 - website/docs/components/outputs/hdfs.md | 219 - .../docs/components/outputs/http_client.md | 873 -- .../docs/components/outputs/http_server.md | 175 - website/docs/components/outputs/inproc.md | 30 - website/docs/components/outputs/kafka.md | 740 -- .../docs/components/outputs/kafka_franz.md | 672 -- website/docs/components/outputs/mongodb.md | 350 - website/docs/components/outputs/mqtt.md | 413 - website/docs/components/outputs/nanomsg.md | 80 - website/docs/components/outputs/nats.md | 429 - .../docs/components/outputs/nats_jetstream.md | 434 - website/docs/components/outputs/nats_kv.md | 366 - .../docs/components/outputs/nats_stream.md | 367 - website/docs/components/outputs/nsq.md | 241 - website/docs/components/outputs/opensearch.md | 568 - website/docs/components/outputs/pulsar.md | 219 - website/docs/components/outputs/pusher.md | 245 - website/docs/components/outputs/redis_hash.md | 343 - website/docs/components/outputs/redis_list.md | 409 - .../docs/components/outputs/redis_pubsub.md | 386 - .../docs/components/outputs/redis_streams.md | 427 - website/docs/components/outputs/reject.md | 60 - .../docs/components/outputs/reject_errored.md | 85 - website/docs/components/outputs/resource.md | 65 - website/docs/components/outputs/retry.md | 109 - website/docs/components/outputs/sftp.md | 143 - .../docs/components/outputs/snowflake_put.md | 748 -- website/docs/components/outputs/socket.md | 81 - website/docs/components/outputs/splunk_hec.md | 181 - website/docs/components/outputs/sql.md | 239 - website/docs/components/outputs/sql_insert.md | 411 - website/docs/components/outputs/sql_raw.md | 391 - website/docs/components/outputs/stdout.md | 57 - website/docs/components/outputs/subprocess.md | 65 - website/docs/components/outputs/switch.md | 198 - .../docs/components/outputs/sync_response.md | 51 - website/docs/components/outputs/websocket.md | 356 - website/docs/components/outputs/zmq4.md | 116 - website/docs/components/processors/about.md | 93 - website/docs/components/processors/archive.md | 99 - website/docs/components/processors/avro.md | 91 - website/docs/components/processors/awk.md | 401 - .../processors/aws_dynamodb_partiql.md | 202 - .../docs/components/processors/aws_lambda.md | 251 - .../components/processors/azure_cosmosdb.md | 386 - .../docs/components/processors/bloblang.md | 126 - .../components/processors/bounds_check.md | 86 - website/docs/components/processors/branch.md | 211 - website/docs/components/processors/cache.md | 221 - website/docs/components/processors/cached.md | 159 - website/docs/components/processors/catch.md | 45 - website/docs/components/processors/command.md | 117 - .../docs/components/processors/compress.md | 48 - .../docs/components/processors/couchbase.md | 180 - .../docs/components/processors/decompress.md | 37 - website/docs/components/processors/dedupe.md | 102 - .../docs/components/processors/for_each.md | 30 - .../processors/gcp_bigquery_select.md | 153 - website/docs/components/processors/grok.md | 149 - .../docs/components/processors/group_by.md | 96 - .../components/processors/group_by_value.md | 67 - website/docs/components/processors/http.md | 735 -- .../docs/components/processors/insert_part.md | 53 - .../docs/components/processors/javascript.md | 227 - .../docs/components/processors/jmespath.md | 81 - website/docs/components/processors/jq.md | 136 - .../docs/components/processors/json_schema.md | 96 - website/docs/components/processors/log.md | 90 - website/docs/components/processors/mapping.md | 140 - website/docs/components/processors/metric.md | 176 - website/docs/components/processors/mongodb.md | 240 - website/docs/components/processors/msgpack.md | 47 - .../docs/components/processors/mutation.md | 144 - website/docs/components/processors/nats_kv.md | 433 - .../processors/nats_request_reply.md | 447 - website/docs/components/processors/noop.md | 25 - .../docs/components/processors/parallel.md | 49 - website/docs/components/processors/parquet.md | 138 - .../components/processors/parquet_decode.md | 64 - .../components/processors/parquet_encode.md | 168 - .../docs/components/processors/parse_log.md | 130 - .../docs/components/processors/processors.md | 53 - .../docs/components/processors/protobuf.md | 188 - .../docs/components/processors/rate_limit.md | 36 - website/docs/components/processors/redis.md | 368 - .../components/processors/redis_script.md | 357 - .../docs/components/processors/resource.md | 53 - website/docs/components/processors/retry.md | 183 - .../processors/schema_registry_decode.md | 383 - .../processors/schema_registry_encode.md | 435 - .../components/processors/select_parts.md | 45 - .../components/processors/sentry_capture.md | 139 - website/docs/components/processors/sleep.md | 37 - website/docs/components/processors/split.md | 50 - website/docs/components/processors/sql.md | 141 - .../docs/components/processors/sql_insert.md | 295 - website/docs/components/processors/sql_raw.md | 301 - .../docs/components/processors/sql_select.md | 311 - .../docs/components/processors/subprocess.md | 128 - website/docs/components/processors/switch.md | 101 - .../components/processors/sync_response.md | 30 - website/docs/components/processors/try.md | 66 - .../docs/components/processors/unarchive.md | 56 - website/docs/components/processors/wasm.md | 61 - website/docs/components/processors/while.md | 102 - .../docs/components/processors/workflow.md | 390 - website/docs/components/processors/xml.md | 111 - website/docs/components/rate_limits/about.md | 49 - website/docs/components/rate_limits/local.md | 45 - website/docs/components/rate_limits/redis.md | 282 - website/docs/components/scanners/about.md | 56 - website/docs/components/scanners/avro.md | 70 - website/docs/components/scanners/chunker.md | 34 - website/docs/components/scanners/csv.md | 69 - .../docs/components/scanners/decompress.md | 44 - .../components/scanners/json_documents.md | 26 - website/docs/components/scanners/lines.md | 43 - website/docs/components/scanners/re_match.md | 49 - website/docs/components/scanners/skip_bom.md | 36 - website/docs/components/scanners/switch.md | 87 - website/docs/components/scanners/tar.md | 32 - .../docs/components/scanners/to_the_end.md | 29 - website/docs/components/tracers/about.md | 32 - .../docs/components/tracers/gcp_cloudtrace.md | 95 - website/docs/components/tracers/jaeger.md | 122 - website/docs/components/tracers/none.md | 25 - .../tracers/open_telemetry_collector.md | 156 - website/docs/configuration/about.md | 306 - website/docs/configuration/batching.md | 207 - .../dynamic_inputs_and_outputs.md | 96 - website/docs/configuration/error_handling.md | 161 - website/docs/configuration/field_paths.md | 61 - website/docs/configuration/interpolation.md | 95 - website/docs/configuration/metadata.md | 113 - .../configuration/processing_pipelines.md | 29 - website/docs/configuration/resources.md | 148 - website/docs/configuration/secrets.md | 49 - website/docs/configuration/templating.md | 285 - website/docs/configuration/unit_testing.md | 602 - website/docs/configuration/using_cue.md | 317 - .../docs/configuration/windowed_processing.md | 115 - website/docs/guides/bloblang/about.md | 427 - website/docs/guides/bloblang/advanced.md | 158 - website/docs/guides/bloblang/arithmetic.md | 43 - website/docs/guides/bloblang/functions.md | 727 -- website/docs/guides/bloblang/methods.md | 3834 ------- website/docs/guides/bloblang/walkthrough.md | 766 -- website/docs/guides/cloud/aws.md | 77 - website/docs/guides/cloud/gcp.md | 19 - website/docs/guides/getting_started.md | 172 - website/docs/guides/migration/v2.md | 84 - website/docs/guides/migration/v3.md | 87 - website/docs/guides/migration/v4.md | 306 - website/docs/guides/monitoring.md | 24 - website/docs/guides/performance_tuning.md | 88 - website/docs/guides/serverless/about.md | 24 - website/docs/guides/serverless/lambda.md | 205 - website/docs/guides/streams_mode/about.md | 64 - .../docs/guides/streams_mode/streams_api.md | 199 - .../guides/streams_mode/using_config_files.md | 134 - .../guides/streams_mode/using_rest_api.md | 229 - website/docs/guides/sync_responses.md | 134 - website/docusaurus.config.js | 167 - website/package.json | 46 - website/sidebars.js | 151 - website/src/css/custom.css | 168 - website/src/exports/redirect.js | 19 - website/src/pages/blobfish.module.css | 15 - website/src/pages/blobfish.tsx | 99 - website/src/pages/community.module.css | 72 - website/src/pages/community.tsx | 92 - website/src/pages/index.module.css | 106 - website/src/pages/index.tsx | 413 - website/src/pages/support.module.css | 17 - website/src/pages/support.tsx | 124 - website/src/pages/videos.module.css | 9 - website/src/pages/videos.tsx | 44 - website/src/plugins/analytics/client.js | 25 - website/src/plugins/analytics/index.js | 12 - website/src/plugins/components/index.js | 53 - website/src/plugins/cookbooks/.gitignore | 1 - .../src/plugins/cookbooks/cookbookUtils.ts | 200 - website/src/plugins/cookbooks/frontMatter.ts | 30 - website/src/plugins/cookbooks/index.ts | 210 - .../src/plugins/cookbooks/markdownLoader.ts | 29 - website/src/plugins/cookbooks/tsconfig.json | 7 - website/src/plugins/cookbooks/types.ts | 55 - .../src/plugins/prism_themes/github/index.js | 75 - .../src/plugins/prism_themes/monokai/index.js | 39 - website/src/theme/ComponentCard/index.js | 19 - .../src/theme/ComponentCard/styles.module.css | 19 - website/src/theme/ComponentSelect/index.js | 32 - .../theme/ComponentSelect/styles.module.css | 4 - .../src/theme/ComponentsByCategory/index.js | 163 - website/src/theme/CookbookItem/index.js | 29 - .../src/theme/CookbookItem/styles.module.css | 22 - website/src/theme/CookbookListPage/index.js | 113 - .../theme/CookbookListPage/styles.module.css | 45 - website/src/theme/CookbookPage/index.js | 47 - .../src/theme/CookbookPage/styles.module.css | 52 - website/src/theme/NotFound/index.tsx | 30 - website/src/theme/NotFound/styles.module.css | 7 - website/static/img/Blobartist.svg | 249 - website/static/img/Blobborg.svg | 228 - website/static/img/Blobboring.svg | 449 - website/static/img/Blobchef.svg | 233 - website/static/img/Blobextended.svg | 231 - website/static/img/Blobgangsta.svg | 607 - website/static/img/Blobninja.svg | 264 - website/static/img/Blobpirate.svg | 218 - website/static/img/Blobscales.svg | 47 - website/static/img/Blobsherlock.svg | 266 - website/static/img/Blobsocial.svg | 227 - website/static/img/Blobviking.svg | 286 - website/static/img/ash.jpg | Bin 38487 -> 0 bytes website/static/img/blobheart.svg | 236 - website/static/img/emojis/blob.png | Bin 7341 -> 0 bytes website/static/img/emojis/blobbot.png | Bin 9243 -> 0 bytes website/static/img/emojis/blobbounce.gif | Bin 22348 -> 0 bytes website/static/img/emojis/blobbug.png | Bin 7717 -> 0 bytes website/static/img/emojis/blobcool.png | Bin 6115 -> 0 bytes website/static/img/emojis/blobcrying.png | Bin 7371 -> 0 bytes website/static/img/emojis/blobcrylaugh.png | Bin 7275 -> 0 bytes website/static/img/emojis/blobemo.png | Bin 7411 -> 0 bytes website/static/img/emojis/blobheart.png | Bin 7044 -> 0 bytes website/static/img/emojis/blobheartpuke.png | Bin 8918 -> 0 bytes website/static/img/emojis/blobhug.png | Bin 9311 -> 0 bytes website/static/img/emojis/blobkiss.png | Bin 6891 -> 0 bytes website/static/img/emojis/blobmad.png | Bin 7222 -> 0 bytes website/static/img/emojis/blobnaughty.png | Bin 7175 -> 0 bytes website/static/img/emojis/blobnerd.png | Bin 7335 -> 0 bytes website/static/img/emojis/blobnervous.png | Bin 9293 -> 0 bytes website/static/img/emojis/blobno.png | Bin 7860 -> 0 bytes website/static/img/emojis/blobok.png | Bin 8155 -> 0 bytes website/static/img/emojis/blobpalm.png | Bin 6854 -> 0 bytes website/static/img/emojis/blobpirate.png | Bin 10572 -> 0 bytes website/static/img/emojis/blobpuke.png | Bin 8808 -> 0 bytes website/static/img/emojis/blobshrug.png | Bin 8920 -> 0 bytes website/static/img/emojis/blobswag.png | Bin 8208 -> 0 bytes website/static/img/emojis/blobsweat.png | Bin 7748 -> 0 bytes website/static/img/emojis/blobsweatsmile.png | Bin 7304 -> 0 bytes website/static/img/emojis/blobthanks.png | Bin 7921 -> 0 bytes website/static/img/emojis/blobthinking.png | Bin 10296 -> 0 bytes website/static/img/emojis/blobtrance.gif | Bin 20442 -> 0 bytes website/static/img/emojis/blobwave.png | Bin 7907 -> 0 bytes website/static/img/emojis/blobyes.png | Bin 7031 -> 0 bytes website/static/img/emojis/cowblob.png | Bin 8157 -> 0 bytes website/static/img/favicon.ico | Bin 5430 -> 0 bytes .../img/introducing-benthos-lab/banner.svg | 506 - .../img/introducing-benthos-lab/genteel.jpg | Bin 106013 -> 0 bytes .../img/introducing-benthos-lab/slamslack.jpg | Bin 79875 -> 0 bytes website/static/img/logo.svg | 166 - website/static/img/logo_hero.svg | 194 - website/static/img/love-bg-dark.svg | 410 - website/static/img/nav-logo.svg | 466 - website/static/img/og_img.png | Bin 33808 -> 0 bytes website/static/img/og_img.svg | 214 - website/static/img/sponsors/HUMAN_logo.png | Bin 29925 -> 0 bytes website/static/img/sponsors/aurora.svg | 71 - website/static/img/sponsors/community.svg | 95 - website/static/img/sponsors/formance.svg | 4 - website/static/img/sponsors/mw_logo.png | Bin 14218 -> 0 bytes website/static/img/sponsors/opala.svg | 57 - website/static/img/sponsors/optum_logo.png | Bin 7908 -> 0 bytes website/static/img/sponsors/synadia.svg | 21 - website/static/img/sponsors/umh_logo.svg | 1 - .../static/img/sponsors/warpstream_logo.svg | 20 - website/static/img/teacher-blob.svg | 645 -- website/static/img/what-is-blob.svg | 2427 ---- .../benthos-plugged.png | Bin 51404 -> 0 bytes .../img/write-a-benthos-plugin/blobfish.jpg | Bin 25373 -> 0 bytes website/static/sh/install | 166 - website/tsconfig.json | 10 - website/yarn.lock | 9721 ----------------- 1251 files changed, 243 insertions(+), 244026 deletions(-) delete mode 100644 internal/api/api.go delete mode 100644 internal/api/api_test.go delete mode 100644 internal/api/config.go delete mode 100644 internal/api/docs.adoc delete mode 100644 internal/api/docs.go delete mode 100644 internal/api/dynamic_crud.go delete mode 100644 internal/api/dynamic_crud_test.go delete mode 100644 internal/api/package.go delete mode 100644 internal/autoretry/auto_retry_list.go delete mode 100644 internal/autoretry/auto_retry_list_test.go delete mode 100644 internal/batch/combined_ack_func.go delete mode 100644 internal/batch/combined_ack_func_test.go delete mode 100644 internal/batch/count.go delete mode 100644 internal/batch/count_test.go delete mode 100644 internal/batch/error.go delete mode 100644 internal/batch/error_test.go delete mode 100644 internal/batch/package.go delete mode 100644 internal/batch/policy/batchconfig/config.go delete mode 100644 internal/batch/policy/docs.go delete mode 100644 internal/batch/policy/package.go delete mode 100644 internal/batch/policy/policy.go delete mode 100644 internal/batch/policy/policy_test.go delete mode 100644 internal/bloblang/environment.go delete mode 100644 internal/bloblang/field/expression.go delete mode 100644 internal/bloblang/field/expression_test.go delete mode 100644 internal/bloblang/field/package.go delete mode 100644 internal/bloblang/field/resolver.go delete mode 100644 internal/bloblang/mapping/assignment.go delete mode 100644 internal/bloblang/mapping/executor.go delete mode 100644 internal/bloblang/mapping/executor_test.go delete mode 100644 internal/bloblang/mapping/package.go delete mode 100644 internal/bloblang/mapping/statement.go delete mode 100644 internal/bloblang/mapping/target.go delete mode 100644 internal/bloblang/package.go delete mode 100644 internal/bloblang/package_test.go delete mode 100644 internal/bloblang/parser/combinators.go delete mode 100644 internal/bloblang/parser/combinators_test.go delete mode 100644 internal/bloblang/parser/context.go delete mode 100644 internal/bloblang/parser/context_test.go delete mode 100644 internal/bloblang/parser/dot_env_parser.go delete mode 100644 internal/bloblang/parser/dot_env_parser_test.go delete mode 100644 internal/bloblang/parser/error.go delete mode 100644 internal/bloblang/parser/error_test.go delete mode 100644 internal/bloblang/parser/field_parser.go delete mode 100644 internal/bloblang/parser/field_parser_test.go delete mode 100644 internal/bloblang/parser/mapping_parser.go delete mode 100644 internal/bloblang/parser/mapping_parser_test.go delete mode 100644 internal/bloblang/parser/query_arithmetic_parser.go delete mode 100644 internal/bloblang/parser/query_arithmetic_parser_test.go delete mode 100644 internal/bloblang/parser/query_expression_parser.go delete mode 100644 internal/bloblang/parser/query_expression_parser_test.go delete mode 100644 internal/bloblang/parser/query_function_parser.go delete mode 100644 internal/bloblang/parser/query_function_parser_test.go delete mode 100644 internal/bloblang/parser/query_literal_parser.go delete mode 100644 internal/bloblang/parser/query_literal_parser_test.go delete mode 100644 internal/bloblang/parser/query_method_parser_test.go delete mode 100644 internal/bloblang/parser/query_parser.go delete mode 100644 internal/bloblang/parser/query_parser_test.go delete mode 100644 internal/bloblang/parser/root_expression_parser.go delete mode 100644 internal/bloblang/parser/root_expression_parser_test.go delete mode 100644 internal/bloblang/plugins/bloblang.go delete mode 100644 internal/bloblang/query/arithmetic.go delete mode 100644 internal/bloblang/query/arithmetic_test.go delete mode 100644 internal/bloblang/query/docs.go delete mode 100644 internal/bloblang/query/errors.go delete mode 100644 internal/bloblang/query/errors_test.go delete mode 100644 internal/bloblang/query/expression.go delete mode 100644 internal/bloblang/query/expression_test.go delete mode 100644 internal/bloblang/query/function_ctor.go delete mode 100644 internal/bloblang/query/function_set.go delete mode 100644 internal/bloblang/query/function_set_test.go delete mode 100644 internal/bloblang/query/functions.go delete mode 100644 internal/bloblang/query/functions_test.go delete mode 100644 internal/bloblang/query/iterator.go delete mode 100644 internal/bloblang/query/iterator_test.go delete mode 100644 internal/bloblang/query/literals.go delete mode 100644 internal/bloblang/query/literals_test.go delete mode 100644 internal/bloblang/query/method_ctor.go delete mode 100644 internal/bloblang/query/method_set.go delete mode 100644 internal/bloblang/query/method_set_test.go delete mode 100644 internal/bloblang/query/methods.go delete mode 100644 internal/bloblang/query/methods_numbers.go delete mode 100644 internal/bloblang/query/methods_strings.go delete mode 100644 internal/bloblang/query/methods_structured.go delete mode 100644 internal/bloblang/query/methods_structured_test.go delete mode 100644 internal/bloblang/query/methods_test.go delete mode 100644 internal/bloblang/query/package.go delete mode 100644 internal/bloblang/query/params.go delete mode 100644 internal/bloblang/query/params_test.go delete mode 100644 internal/bloblang/query/parsed_test.go delete mode 100644 internal/bloblang/query/target.go delete mode 100644 internal/bundle/buffers.go delete mode 100644 internal/bundle/caches.go delete mode 100644 internal/bundle/environment.go delete mode 100644 internal/bundle/inputs.go delete mode 100644 internal/bundle/metrics.go delete mode 100644 internal/bundle/outputs.go delete mode 100644 internal/bundle/package.go delete mode 100644 internal/bundle/processors.go delete mode 100644 internal/bundle/ratelimits.go delete mode 100644 internal/bundle/scanners.go delete mode 100644 internal/bundle/tracers.go delete mode 100644 internal/bundle/tracing/bundle.go delete mode 100644 internal/bundle/tracing/bundle_test.go delete mode 100644 internal/bundle/tracing/events.go delete mode 100644 internal/bundle/tracing/input.go delete mode 100644 internal/bundle/tracing/output.go delete mode 100644 internal/bundle/tracing/processor.go delete mode 100644 internal/cli/blobl/cli.go delete mode 100644 internal/cli/blobl/resources/bloblang_editor_page.html delete mode 100644 internal/cli/blobl/server.go delete mode 100644 internal/cli/common/logger.go delete mode 100644 internal/cli/common/manager.go delete mode 100644 internal/cli/common/opts.go delete mode 100644 internal/cli/common/reader.go delete mode 100644 internal/cli/common/service.go delete mode 100644 internal/cli/common/swappable.go delete mode 100644 internal/cli/create.go delete mode 100644 internal/cli/lint.go delete mode 100644 internal/cli/lint_test.go delete mode 100644 internal/cli/list.go delete mode 100644 internal/cli/run.go delete mode 100644 internal/cli/run_test.go delete mode 100644 internal/cli/studio/cli.go delete mode 100644 internal/cli/studio/logger.go delete mode 100644 internal/cli/studio/metrics/observed.go delete mode 100644 internal/cli/studio/metrics/tracker.go delete mode 100644 internal/cli/studio/pull.go delete mode 100644 internal/cli/studio/pull_runner.go delete mode 100644 internal/cli/studio/pull_runner_test.go delete mode 100644 internal/cli/studio/pull_session_fs.go delete mode 100644 internal/cli/studio/pull_session_tracker.go delete mode 100644 internal/cli/studio/sync_schema.go delete mode 100644 internal/cli/studio/tracing/observed.go delete mode 100644 internal/cli/template/cli.go delete mode 100644 internal/cli/template/lint.go delete mode 100644 internal/cli/test/case.go delete mode 100644 internal/cli/test/case_test.go delete mode 100644 internal/cli/test/cli.go delete mode 100644 internal/cli/test/command.go delete mode 100644 internal/cli/test/command_test.go delete mode 100644 internal/cli/test/definition.go delete mode 100644 internal/cli/test/definition_test.go delete mode 100644 internal/cli/test/processors_provider.go delete mode 100644 internal/cli/test/processors_provider_test.go delete mode 100644 internal/codec/reader.go delete mode 100644 internal/codec/reader_test.go delete mode 100644 internal/codec/skip_group_reader.go delete mode 100644 internal/codec/skip_group_reader_test.go delete mode 100644 internal/codec/writer.go delete mode 100644 internal/component/buffer/config.go delete mode 100644 internal/component/buffer/interface.go delete mode 100644 internal/component/buffer/memory_buffer_test.go delete mode 100644 internal/component/buffer/stream.go delete mode 100644 internal/component/buffer/stream_test.go delete mode 100644 internal/component/cache/cache_metrics.go delete mode 100644 internal/component/cache/cache_metrics_test.go delete mode 100644 internal/component/cache/config.go delete mode 100644 internal/component/cache/interface.go delete mode 100644 internal/component/errors.go delete mode 100644 internal/component/input/async_cut_off.go delete mode 100644 internal/component/input/async_preserver.go delete mode 100644 internal/component/input/async_preserver_test.go delete mode 100644 internal/component/input/async_reader.go delete mode 100644 internal/component/input/async_reader_test.go delete mode 100644 internal/component/input/batcher/batcher.go delete mode 100644 internal/component/input/batcher/batcher_test.go delete mode 100644 internal/component/input/config.go delete mode 100644 internal/component/input/config/config.go delete mode 100644 internal/component/input/interface.go delete mode 100644 internal/component/input/processors/append.go delete mode 100644 internal/component/input/wrap_with_pipeline.go delete mode 100644 internal/component/input/wrap_with_pipeline_test.go delete mode 100644 internal/component/interop/interop.go delete mode 100644 internal/component/metrics/combine.go delete mode 100644 internal/component/metrics/config.go delete mode 100644 internal/component/metrics/config_test.go delete mode 100644 internal/component/metrics/dud_type.go delete mode 100644 internal/component/metrics/local.go delete mode 100644 internal/component/metrics/local_test.go delete mode 100644 internal/component/metrics/mapping.go delete mode 100644 internal/component/metrics/mapping_test.go delete mode 100644 internal/component/metrics/namespaced.go delete mode 100644 internal/component/metrics/namespaced_test.go delete mode 100644 internal/component/metrics/type.go delete mode 100644 internal/component/metrics/vector_util.go delete mode 100644 internal/component/observability.go delete mode 100644 internal/component/output/async_writer.go delete mode 100644 internal/component/output/async_writer_test.go delete mode 100644 internal/component/output/batched_send.go delete mode 100644 internal/component/output/batched_send_test.go delete mode 100644 internal/component/output/batcher/batcher.go delete mode 100644 internal/component/output/batcher/batcher_test.go delete mode 100644 internal/component/output/config.go delete mode 100644 internal/component/output/interface.go delete mode 100644 internal/component/output/not_batched.go delete mode 100644 internal/component/output/not_batched_test.go delete mode 100644 internal/component/output/processors/append.go delete mode 100644 internal/component/output/wrap_with_pipeline.go delete mode 100644 internal/component/output/wrap_with_pipeline_test.go delete mode 100644 internal/component/processor/auto_observed.go delete mode 100644 internal/component/processor/auto_observed_test.go delete mode 100644 internal/component/processor/config.go delete mode 100644 internal/component/processor/error.go delete mode 100644 internal/component/processor/execute.go delete mode 100644 internal/component/processor/execute_test.go delete mode 100644 internal/component/processor/interface.go delete mode 100644 internal/component/ratelimit/config.go delete mode 100644 internal/component/ratelimit/interface.go delete mode 100644 internal/component/ratelimit/rate_limit_metrics.go delete mode 100644 internal/component/ratelimit/rate_limit_metrics_test.go delete mode 100644 internal/component/scanner/config.go delete mode 100644 internal/component/scanner/interface.go delete mode 100644 internal/component/scanner/testutil/testutil.go delete mode 100644 internal/component/testutil/from_yaml.go delete mode 100644 internal/component/tracer/config.go delete mode 100644 internal/config/config_test.go delete mode 100644 internal/config/env_vars.go delete mode 100644 internal/config/env_vars_test.go delete mode 100644 internal/config/lint.go delete mode 100644 internal/config/reader.go delete mode 100644 internal/config/reader_test.go delete mode 100644 internal/config/resource_reader.go delete mode 100644 internal/config/resource_reader_test.go delete mode 100644 internal/config/schema.go delete mode 100644 internal/config/schema/schema.go delete mode 100644 internal/config/stream_reader.go delete mode 100644 internal/config/stream_reader_test.go delete mode 100644 internal/config/test/case.go delete mode 100644 internal/config/test/docs.adoc delete mode 100644 internal/config/test/docs.go delete mode 100644 internal/config/test/input.go delete mode 100644 internal/config/test/output.go delete mode 100644 internal/config/test/output_test.go delete mode 100644 internal/config/watcher.go delete mode 100644 internal/config/watcher_test.go delete mode 100644 internal/config/watcher_wasm.go delete mode 100644 internal/cuegen/README.md delete mode 100644 internal/cuegen/component.go delete mode 100644 internal/cuegen/config.go delete mode 100644 internal/cuegen/cue.go delete mode 100644 internal/cuegen/identifiers.go delete mode 100644 internal/cuegen/schema.go delete mode 100644 internal/docs/benchmark_test.go delete mode 100644 internal/docs/bloblang.go delete mode 100644 internal/docs/bloblang_markdown.go delete mode 100644 internal/docs/bloblang_test.go delete mode 100644 internal/docs/component.go delete mode 100644 internal/docs/component_markdown.go delete mode 100644 internal/docs/config.go delete mode 100644 internal/docs/config_test.go delete mode 100644 internal/docs/field.go delete mode 100644 internal/docs/field_interop.go delete mode 100644 internal/docs/field_template.go delete mode 100644 internal/docs/field_test.go delete mode 100644 internal/docs/format_any.go delete mode 100644 internal/docs/format_yaml.go delete mode 100644 internal/docs/format_yaml_path.go delete mode 100644 internal/docs/format_yaml_path_test.go delete mode 100644 internal/docs/format_yaml_test.go delete mode 100644 internal/docs/interop/interop.go delete mode 100644 internal/docs/json_schema.go delete mode 100644 internal/docs/metrics_mapping.go delete mode 100644 internal/docs/package.go delete mode 100644 internal/docs/parsed.go delete mode 100644 internal/docs/registry.go delete mode 100644 internal/filepath/glob.go delete mode 100644 internal/filepath/glob_test.go delete mode 100644 internal/filepath/ifs/http.go delete mode 100644 internal/filepath/ifs/os.go delete mode 100644 internal/filepath/ifs/os_test.go delete mode 100644 internal/httpclient/auth_oauth2.go delete mode 100644 internal/httpclient/client.go delete mode 100644 internal/httpclient/client_test.go delete mode 100644 internal/httpclient/config.go delete mode 100644 internal/httpclient/config_test.go delete mode 100644 internal/httpclient/errors.go delete mode 100644 internal/httpclient/errors_test.go delete mode 100644 internal/httpclient/logger.go delete mode 100644 internal/httpclient/logger_test.go delete mode 100644 internal/httpclient/request.go delete mode 100644 internal/httpclient/request_test.go delete mode 100644 internal/httpserver/basic_auth.go delete mode 100644 internal/httpserver/cors.go delete mode 100644 internal/httpserver/cors_test.go delete mode 100644 internal/impl/io/bloblang.go delete mode 100644 internal/impl/io/bloblang_test.go delete mode 100644 internal/impl/io/cache_file.go delete mode 100644 internal/impl/io/cache_file_test.go delete mode 100644 internal/impl/io/input_csv.go delete mode 100644 internal/impl/io/input_csv_integration_test.go delete mode 100644 internal/impl/io/input_csv_test.go delete mode 100644 internal/impl/io/input_dynamic.go delete mode 100644 internal/impl/io/input_dynamic_fan_in.go delete mode 100644 internal/impl/io/input_dynamic_fan_in_test.go delete mode 100644 internal/impl/io/input_dynamic_test.go delete mode 100644 internal/impl/io/input_file.go delete mode 100644 internal/impl/io/input_file_test.go delete mode 100644 internal/impl/io/input_http_client.go delete mode 100644 internal/impl/io/input_http_client_test.go delete mode 100644 internal/impl/io/input_http_server.go delete mode 100644 internal/impl/io/input_http_server_test.go delete mode 100644 internal/impl/io/input_socket.go delete mode 100644 internal/impl/io/input_socket_server.go delete mode 100644 internal/impl/io/input_socket_server_test.go delete mode 100644 internal/impl/io/input_socket_test.go delete mode 100644 internal/impl/io/input_stdin.go delete mode 100644 internal/impl/io/input_stdin_test.go delete mode 100644 internal/impl/io/input_subprocess.go delete mode 100644 internal/impl/io/input_subprocess_test.go delete mode 100644 internal/impl/io/input_websocket.go delete mode 100644 internal/impl/io/input_websocket_test.go delete mode 100644 internal/impl/io/metrics_json_api.go delete mode 100644 internal/impl/io/output_dynamic.go delete mode 100644 internal/impl/io/output_dynamic_fan_out.go delete mode 100644 internal/impl/io/output_dynamic_fan_out_test.go delete mode 100644 internal/impl/io/output_dynamic_test.go delete mode 100644 internal/impl/io/output_file.go delete mode 100644 internal/impl/io/output_http_client.go delete mode 100644 internal/impl/io/output_http_client_test.go delete mode 100644 internal/impl/io/output_http_server.go delete mode 100644 internal/impl/io/output_http_server_test.go delete mode 100644 internal/impl/io/output_socket.go delete mode 100644 internal/impl/io/output_socket_test.go delete mode 100644 internal/impl/io/output_stdout.go delete mode 100644 internal/impl/io/output_subprocess.go delete mode 100644 internal/impl/io/output_subprocess_test.go delete mode 100644 internal/impl/io/output_websocket.go delete mode 100644 internal/impl/io/output_websocket_test.go delete mode 100644 internal/impl/io/package.go delete mode 100644 internal/impl/io/processor_command.go delete mode 100644 internal/impl/io/processor_command_test.go delete mode 100644 internal/impl/io/processor_http.go delete mode 100644 internal/impl/io/processor_http_test.go delete mode 100644 internal/impl/io/processor_subprocess.go delete mode 100644 internal/impl/io/processor_subprocess_test.go delete mode 100644 internal/impl/pure/algorithms.go delete mode 100644 internal/impl/pure/bloblang_encoding.go delete mode 100644 internal/impl/pure/bloblang_encoding_test.go delete mode 100644 internal/impl/pure/bloblang_general.go delete mode 100644 internal/impl/pure/bloblang_general_test.go delete mode 100644 internal/impl/pure/bloblang_numbers.go delete mode 100644 internal/impl/pure/bloblang_objects.go delete mode 100644 internal/impl/pure/bloblang_objects_test.go delete mode 100644 internal/impl/pure/bloblang_string.go delete mode 100644 internal/impl/pure/bloblang_string_test.go delete mode 100644 internal/impl/pure/bloblang_time.go delete mode 100644 internal/impl/pure/bloblang_time_test.go delete mode 100644 internal/impl/pure/buffer_memory.go delete mode 100644 internal/impl/pure/buffer_memory_test.go delete mode 100644 internal/impl/pure/buffer_none.go delete mode 100644 internal/impl/pure/buffer_system_window.go delete mode 100644 internal/impl/pure/buffer_system_window_test.go delete mode 100644 internal/impl/pure/cache_integration_test.go delete mode 100644 internal/impl/pure/cache_lru.go delete mode 100644 internal/impl/pure/cache_lru_test.go delete mode 100644 internal/impl/pure/cache_memory.go delete mode 100644 internal/impl/pure/cache_memory_test.go delete mode 100644 internal/impl/pure/cache_multilevel.go delete mode 100644 internal/impl/pure/cache_multilevel_test.go delete mode 100644 internal/impl/pure/cache_noop.go delete mode 100644 internal/impl/pure/cache_noop_test.go delete mode 100644 internal/impl/pure/cache_ttlru.go delete mode 100644 internal/impl/pure/cache_ttlru_test.go delete mode 100644 internal/impl/pure/extended/zstd.go delete mode 100644 internal/impl/pure/extended/zstd_test.go delete mode 100644 internal/impl/pure/input_batched.go delete mode 100644 internal/impl/pure/input_batched_test.go delete mode 100644 internal/impl/pure/input_broker.go delete mode 100644 internal/impl/pure/input_broker_fan_in.go delete mode 100644 internal/impl/pure/input_broker_fan_in_test.go delete mode 100644 internal/impl/pure/input_broker_test.go delete mode 100644 internal/impl/pure/input_generate.go delete mode 100644 internal/impl/pure/input_generate_test.go delete mode 100644 internal/impl/pure/input_inproc.go delete mode 100644 internal/impl/pure/input_inproc_test.go delete mode 100644 internal/impl/pure/input_read_until.go delete mode 100644 internal/impl/pure/input_read_until_test.go delete mode 100644 internal/impl/pure/input_resource.go delete mode 100644 internal/impl/pure/input_resource_test.go delete mode 100644 internal/impl/pure/input_sequence.go delete mode 100644 internal/impl/pure/input_sequence_test.go delete mode 100644 internal/impl/pure/metrics_logger.go delete mode 100644 internal/impl/pure/metrics_none.go delete mode 100644 internal/impl/pure/output_broker.go delete mode 100644 internal/impl/pure/output_broker_fan_out.go delete mode 100644 internal/impl/pure/output_broker_fan_out_sequential.go delete mode 100644 internal/impl/pure/output_broker_fan_out_sequential_test.go delete mode 100644 internal/impl/pure/output_broker_fan_out_test.go delete mode 100644 internal/impl/pure/output_broker_greedy.go delete mode 100644 internal/impl/pure/output_broker_greedy_test.go delete mode 100644 internal/impl/pure/output_broker_round_robin.go delete mode 100644 internal/impl/pure/output_broker_round_robin_test.go delete mode 100644 internal/impl/pure/output_broker_test.go delete mode 100644 internal/impl/pure/output_cache.go delete mode 100644 internal/impl/pure/output_cache_test.go delete mode 100644 internal/impl/pure/output_drop.go delete mode 100644 internal/impl/pure/output_drop_on.go delete mode 100644 internal/impl/pure/output_drop_on_test.go delete mode 100644 internal/impl/pure/output_fallback.go delete mode 100644 internal/impl/pure/output_fallback_test.go delete mode 100644 internal/impl/pure/output_inproc.go delete mode 100644 internal/impl/pure/output_inproc_test.go delete mode 100644 internal/impl/pure/output_reject.go delete mode 100644 internal/impl/pure/output_reject_errored.go delete mode 100644 internal/impl/pure/output_reject_errored_test.go delete mode 100644 internal/impl/pure/output_resource.go delete mode 100644 internal/impl/pure/output_resource_test.go delete mode 100644 internal/impl/pure/output_retry.go delete mode 100644 internal/impl/pure/output_retry_test.go delete mode 100644 internal/impl/pure/output_switch.go delete mode 100644 internal/impl/pure/output_switch_test.go delete mode 100644 internal/impl/pure/output_sync_response.go delete mode 100644 internal/impl/pure/output_sync_response_test.go delete mode 100644 internal/impl/pure/package.go delete mode 100644 internal/impl/pure/processor_archive.go delete mode 100644 internal/impl/pure/processor_archive_test.go delete mode 100644 internal/impl/pure/processor_bloblang.go delete mode 100644 internal/impl/pure/processor_bloblang_test.go delete mode 100644 internal/impl/pure/processor_bounds_check.go delete mode 100644 internal/impl/pure/processor_bounds_check_test.go delete mode 100644 internal/impl/pure/processor_branch.go delete mode 100644 internal/impl/pure/processor_branch_test.go delete mode 100644 internal/impl/pure/processor_cache.go delete mode 100644 internal/impl/pure/processor_cache_test.go delete mode 100644 internal/impl/pure/processor_cached.go delete mode 100644 internal/impl/pure/processor_cached_test.go delete mode 100644 internal/impl/pure/processor_catch.go delete mode 100644 internal/impl/pure/processor_catch_test.go delete mode 100644 internal/impl/pure/processor_compress.go delete mode 100644 internal/impl/pure/processor_compress_test.go delete mode 100644 internal/impl/pure/processor_decompress.go delete mode 100644 internal/impl/pure/processor_decompress_test.go delete mode 100644 internal/impl/pure/processor_dedupe.go delete mode 100644 internal/impl/pure/processor_dedupe_test.go delete mode 100644 internal/impl/pure/processor_for_each.go delete mode 100644 internal/impl/pure/processor_for_each_test.go delete mode 100644 internal/impl/pure/processor_grok.go delete mode 100644 internal/impl/pure/processor_grok_test.go delete mode 100644 internal/impl/pure/processor_group_by.go delete mode 100644 internal/impl/pure/processor_group_by_test.go delete mode 100644 internal/impl/pure/processor_group_by_value.go delete mode 100644 internal/impl/pure/processor_group_by_value_test.go delete mode 100644 internal/impl/pure/processor_insert_part.go delete mode 100644 internal/impl/pure/processor_insert_part_test.go delete mode 100644 internal/impl/pure/processor_jmespath.go delete mode 100644 internal/impl/pure/processor_jmespath_test.go delete mode 100644 internal/impl/pure/processor_jq.go delete mode 100644 internal/impl/pure/processor_jq_test.go delete mode 100644 internal/impl/pure/processor_jsonschema.go delete mode 100644 internal/impl/pure/processor_jsonschema_test.go delete mode 100644 internal/impl/pure/processor_log.go delete mode 100644 internal/impl/pure/processor_log_test.go delete mode 100644 internal/impl/pure/processor_mapping.go delete mode 100644 internal/impl/pure/processor_mapping_test.go delete mode 100644 internal/impl/pure/processor_metric.go delete mode 100644 internal/impl/pure/processor_metric_test.go delete mode 100644 internal/impl/pure/processor_mutation.go delete mode 100644 internal/impl/pure/processor_mutation_test.go delete mode 100644 internal/impl/pure/processor_noop.go delete mode 100644 internal/impl/pure/processor_parallel.go delete mode 100644 internal/impl/pure/processor_parallel_test.go delete mode 100644 internal/impl/pure/processor_parse_log.go delete mode 100644 internal/impl/pure/processor_parse_log_test.go delete mode 100644 internal/impl/pure/processor_processors.go delete mode 100644 internal/impl/pure/processor_processors_test.go delete mode 100644 internal/impl/pure/processor_rate_limit.go delete mode 100644 internal/impl/pure/processor_rate_limit_test.go delete mode 100644 internal/impl/pure/processor_resource.go delete mode 100644 internal/impl/pure/processor_resource_test.go delete mode 100644 internal/impl/pure/processor_retry.go delete mode 100644 internal/impl/pure/processor_retry_test.go delete mode 100644 internal/impl/pure/processor_select_parts.go delete mode 100644 internal/impl/pure/processor_select_parts_test.go delete mode 100644 internal/impl/pure/processor_sleep.go delete mode 100644 internal/impl/pure/processor_sleep_test.go delete mode 100644 internal/impl/pure/processor_split.go delete mode 100644 internal/impl/pure/processor_split_test.go delete mode 100644 internal/impl/pure/processor_switch.go delete mode 100644 internal/impl/pure/processor_switch_test.go delete mode 100644 internal/impl/pure/processor_sync_response.go delete mode 100644 internal/impl/pure/processor_try.go delete mode 100644 internal/impl/pure/processor_try_test.go delete mode 100644 internal/impl/pure/processor_unarchive.go delete mode 100644 internal/impl/pure/processor_unarchive_test.go delete mode 100644 internal/impl/pure/processor_while.go delete mode 100644 internal/impl/pure/processor_while_test.go delete mode 100644 internal/impl/pure/processor_workflow.go delete mode 100644 internal/impl/pure/processor_workflow_branch_map.go delete mode 100644 internal/impl/pure/processor_workflow_test.go delete mode 100644 internal/impl/pure/rate_limit_local.go delete mode 100644 internal/impl/pure/rate_limit_local_test.go delete mode 100644 internal/impl/pure/scanner_chunker.go delete mode 100644 internal/impl/pure/scanner_chunker_test.go delete mode 100644 internal/impl/pure/scanner_csv.go delete mode 100644 internal/impl/pure/scanner_csv_test.go delete mode 100644 internal/impl/pure/scanner_decompress.go delete mode 100644 internal/impl/pure/scanner_decompress_test.go delete mode 100644 internal/impl/pure/scanner_json.go delete mode 100644 internal/impl/pure/scanner_json_test.go delete mode 100644 internal/impl/pure/scanner_lines.go delete mode 100644 internal/impl/pure/scanner_lines_test.go delete mode 100644 internal/impl/pure/scanner_re_match.go delete mode 100644 internal/impl/pure/scanner_re_match_test.go delete mode 100644 internal/impl/pure/scanner_skip_bom.go delete mode 100644 internal/impl/pure/scanner_skip_bom_test.go delete mode 100644 internal/impl/pure/scanner_switch.go delete mode 100644 internal/impl/pure/scanner_switch_test.go delete mode 100644 internal/impl/pure/scanner_tar.go delete mode 100644 internal/impl/pure/scanner_tar_test.go delete mode 100644 internal/impl/pure/scanner_to_the_end.go delete mode 100644 internal/impl/pure/scanner_to_the_end_test.go delete mode 100644 internal/impl/pure/tracer_none.go delete mode 100644 internal/log/config.go delete mode 100644 internal/log/docs.adoc delete mode 100644 internal/log/docs.go delete mode 100644 internal/log/interface.go delete mode 100644 internal/log/logrus.go delete mode 100644 internal/log/logrus_test.go delete mode 100644 internal/log/slog.go delete mode 100644 internal/log/slog_test.go delete mode 100644 internal/log/tee.go delete mode 100644 internal/log/tee_test.go delete mode 100644 internal/log/wrap.go delete mode 100644 internal/manager/config.go delete mode 100644 internal/manager/config_test.go delete mode 100644 internal/manager/docs.go delete mode 100644 internal/manager/initialization_test.go delete mode 100644 internal/manager/input_wrapper.go delete mode 100644 internal/manager/input_wrapper_test.go delete mode 100644 internal/manager/live_resources.go delete mode 100644 internal/manager/mock/cache.go delete mode 100644 internal/manager/mock/cache_test.go delete mode 100644 internal/manager/mock/input.go delete mode 100644 internal/manager/mock/input_test.go delete mode 100644 internal/manager/mock/manager.go delete mode 100644 internal/manager/mock/manager_test.go delete mode 100644 internal/manager/mock/output.go delete mode 100644 internal/manager/mock/output_test.go delete mode 100644 internal/manager/mock/processor.go delete mode 100644 internal/manager/mock/processor_test.go delete mode 100644 internal/manager/mock/ratelimit.go delete mode 100644 internal/manager/mock/ratelimit_test.go delete mode 100644 internal/manager/output_wrapper.go delete mode 100644 internal/manager/output_wrapper_test.go delete mode 100644 internal/manager/package.go delete mode 100644 internal/manager/type.go delete mode 100644 internal/manager/type_stream_test.go delete mode 100644 internal/manager/type_test.go delete mode 100644 internal/message/data.go delete mode 100644 internal/message/data_test.go delete mode 100644 internal/message/errors.go delete mode 100644 internal/message/message.go delete mode 100644 internal/message/message_test.go delete mode 100644 internal/message/part.go delete mode 100644 internal/message/part_test.go delete mode 100644 internal/message/part_with_context_test.go delete mode 100644 internal/message/sort_group.go delete mode 100644 internal/message/sort_group_test.go delete mode 100644 internal/message/transaction.go delete mode 100644 internal/message/util.go delete mode 100644 internal/message/util_test.go delete mode 100644 internal/metadata/exclude_filter.go delete mode 100644 internal/metadata/exclude_filter_test.go delete mode 100644 internal/metadata/include_filter.go delete mode 100644 internal/metadata/include_filter_test.go delete mode 100644 internal/old/util/throttle/package.go delete mode 100644 internal/old/util/throttle/type.go delete mode 100644 internal/old/util/throttle/type_test.go delete mode 100644 internal/pipeline/config_test.go delete mode 100644 internal/pipeline/constructor.go delete mode 100644 internal/pipeline/package.go delete mode 100644 internal/pipeline/pool.go delete mode 100644 internal/pipeline/pool_test.go delete mode 100644 internal/pipeline/processor.go delete mode 100644 internal/pipeline/processor_test.go delete mode 100644 internal/serverless/handler.go delete mode 100644 internal/serverless/handler_test.go delete mode 100644 internal/serverless/lambda/config.go delete mode 100644 internal/serverless/lambda/config_test.go delete mode 100644 internal/serverless/lambda/lambda.go delete mode 100644 internal/serverless/lambda/package.go delete mode 100644 internal/serverless/package.go delete mode 100644 internal/stream/config.go delete mode 100644 internal/stream/config_test.go delete mode 100644 internal/stream/docs.go delete mode 100644 internal/stream/manager/api.go delete mode 100644 internal/stream/manager/api_test.go delete mode 100644 internal/stream/manager/package.go delete mode 100644 internal/stream/manager/type.go delete mode 100644 internal/stream/manager/type_stress_test.go delete mode 100644 internal/stream/manager/type_test.go delete mode 100644 internal/stream/type.go delete mode 100644 internal/stream/type_test.go delete mode 100644 internal/template/config.go delete mode 100644 internal/template/docs.adoc delete mode 100644 internal/template/docs.go delete mode 100644 internal/template/template.go delete mode 100644 internal/template/template_test.go delete mode 100644 internal/tls/docs.go delete mode 100644 internal/tls/package.go delete mode 100644 internal/tls/type.go delete mode 100644 internal/tls/type_test.go delete mode 100644 internal/tracing/otel.go delete mode 100644 internal/tracing/otel_test.go delete mode 100644 internal/tracing/package.go delete mode 100644 internal/tracing/span.go delete mode 100644 internal/tracing/v2/otel.go delete mode 100644 internal/tracing/v2/otel_test.go delete mode 100644 internal/tracing/v2/package.go delete mode 100644 internal/tracing/v2/span.go delete mode 100644 internal/transaction/benchmarks_test.go delete mode 100644 internal/transaction/result_store.go delete mode 100644 internal/transaction/result_store_test.go delete mode 100644 internal/transaction/tracked.go delete mode 100644 internal/transaction/tracked_test.go delete mode 100644 internal/value/errors.go delete mode 100644 internal/value/type_helpers.go delete mode 100644 internal/value/type_helpers_test.go delete mode 100644 public/bloblang/arguments.go delete mode 100644 public/bloblang/arguments_test.go delete mode 100644 public/bloblang/context.go delete mode 100644 public/bloblang/context_test.go delete mode 100644 public/bloblang/environment.go delete mode 100644 public/bloblang/environment_test.go delete mode 100644 public/bloblang/environment_unwrapper.go delete mode 100644 public/bloblang/example_plugins_v2_test.go delete mode 100644 public/bloblang/executor.go delete mode 100644 public/bloblang/executor_test.go delete mode 100644 public/bloblang/executor_unwrapper.go delete mode 100644 public/bloblang/function.go delete mode 100644 public/bloblang/method.go delete mode 100644 public/bloblang/method_test.go delete mode 100644 public/bloblang/package.go delete mode 100644 public/bloblang/parse_error.go delete mode 100644 public/bloblang/parse_error_test.go delete mode 100644 public/bloblang/spec.go delete mode 100644 public/bloblang/spec_test.go delete mode 100644 public/bloblang/util.go delete mode 100644 public/bloblang/view.go delete mode 100644 public/components/aws/serverless.go delete mode 100644 public/service/benchmark_test.go delete mode 100644 public/service/buffer.go delete mode 100644 public/service/buffer_test.go delete mode 100644 public/service/cache.go delete mode 100644 public/service/cache_test.go delete mode 100644 public/service/chaos_test.go delete mode 100644 public/service/codec/scanner.go delete mode 100644 public/service/codec/scanner_test.go delete mode 100644 public/service/config.go delete mode 100644 public/service/config_backoff.go delete mode 100644 public/service/config_backoff_test.go delete mode 100644 public/service/config_batch_policy.go delete mode 100644 public/service/config_batch_policy_test.go delete mode 100644 public/service/config_bloblang.go delete mode 100644 public/service/config_bloblang_test.go delete mode 100644 public/service/config_extract_tracing.go delete mode 100644 public/service/config_extract_tracing_test.go delete mode 100644 public/service/config_http.go delete mode 100644 public/service/config_inject_tracing.go delete mode 100644 public/service/config_inject_tracing_test.go delete mode 100644 public/service/config_input.go delete mode 100644 public/service/config_input_test.go delete mode 100644 public/service/config_interpolated_string.go delete mode 100644 public/service/config_interpolated_string_test.go delete mode 100644 public/service/config_max_in_flight.go delete mode 100644 public/service/config_metadata_filter.go delete mode 100644 public/service/config_metadata_filter_test.go delete mode 100644 public/service/config_output.go delete mode 100644 public/service/config_output_test.go delete mode 100644 public/service/config_processor.go delete mode 100644 public/service/config_processor_test.go delete mode 100644 public/service/config_scanner.go delete mode 100644 public/service/config_test.go delete mode 100644 public/service/config_tls.go delete mode 100644 public/service/config_url.go delete mode 100644 public/service/config_urls_test.go delete mode 100644 public/service/config_util.go delete mode 100644 public/service/environment.go delete mode 100644 public/service/environment_schema.go delete mode 100644 public/service/environment_test.go delete mode 100644 public/service/errors.go delete mode 100644 public/service/errors_test.go delete mode 100644 public/service/example_buffer_plugin_test.go delete mode 100644 public/service/example_cache_plugin_test.go delete mode 100644 public/service/example_input_plugin_test.go delete mode 100644 public/service/example_output_batched_plugin_test.go delete mode 100644 public/service/example_output_plugin_test.go delete mode 100644 public/service/example_processor_plugin_test.go delete mode 100644 public/service/example_rate_limit_plugin_test.go delete mode 100644 public/service/example_stream_builder_yaml_test.go delete mode 100644 public/service/input.go delete mode 100644 public/service/input_auto_retry.go delete mode 100644 public/service/input_auto_retry_batched.go delete mode 100644 public/service/input_auto_retry_batched_test.go delete mode 100644 public/service/input_auto_retry_test.go delete mode 100644 public/service/input_max_in_flight.go delete mode 100644 public/service/input_max_in_flight_test.go delete mode 100644 public/service/input_test.go delete mode 100644 public/service/integration/cache_test_definitions.go delete mode 100644 public/service/integration/cache_test_helpers.go delete mode 100644 public/service/integration/stream_benchmark_definitions.go delete mode 100644 public/service/integration/stream_test_definitions.go delete mode 100644 public/service/integration/stream_test_helpers.go delete mode 100644 public/service/interpolated_string.go delete mode 100644 public/service/interpolated_string_test.go delete mode 100644 public/service/lints.go delete mode 100644 public/service/logger.go delete mode 100644 public/service/logger_test.go delete mode 100644 public/service/message.go delete mode 100644 public/service/message_test.go delete mode 100644 public/service/metrics.go delete mode 100644 public/service/metrics_test.go delete mode 100644 public/service/output.go delete mode 100644 public/service/output_test.go delete mode 100644 public/service/package.go delete mode 100644 public/service/plugins.go delete mode 100644 public/service/plugins_test.go delete mode 100644 public/service/processor.go delete mode 100644 public/service/processor_test.go delete mode 100644 public/service/rate_limit.go delete mode 100644 public/service/rate_limit_test.go delete mode 100644 public/service/resources.go delete mode 100644 public/service/resources_test.go delete mode 100644 public/service/scanner.go delete mode 100644 public/service/service.go delete mode 100644 public/service/servicetest/service.go delete mode 100644 public/service/servicetest/service_test.go delete mode 100644 public/service/stream.go delete mode 100644 public/service/stream_builder.go delete mode 100644 public/service/stream_builder_test.go delete mode 100644 public/service/stream_config_linter.go delete mode 100644 public/service/stream_config_linter_test.go delete mode 100644 public/service/stream_config_marshaller.go delete mode 100644 public/service/stream_config_marshaller_test.go delete mode 100644 public/service/stream_schema.go delete mode 100644 public/service/stream_schema_test.go delete mode 100644 public/service/stream_template_tester.go delete mode 100644 public/service/tracing.go delete mode 100644 public/service/tracing_test.go delete mode 100644 public/service/util.go delete mode 100644 public/wasm/README.md delete mode 100644 public/wasm/examples/rust/.gitignore delete mode 100644 public/wasm/examples/rust/Cargo.toml delete mode 100644 public/wasm/examples/rust/louder.rs delete mode 100644 public/wasm/examples/tinygo/README.md delete mode 100644 public/wasm/examples/tinygo/main.go delete mode 100644 public/wasm/tinygo/package.go delete mode 100644 public/wasm/tinygo/tinygo.go delete mode 100755 website/.gitignore delete mode 100755 website/README.md delete mode 100644 website/blog/2019-05-27-compiling-benthos-to-wasm.md delete mode 100644 website/blog/2019-06-17-introducing-benthos-lab.md delete mode 100644 website/blog/2019-08-20-write-a-benthos-plugin.md delete mode 100644 website/blog/2020-04-18-sneak-peek-at-bloblang.md delete mode 100644 website/blog/2020-05-10-bloblang-beta.md delete mode 100644 website/blog/2020-08-30-improved-workflows.md delete mode 100644 website/blog/2021-01-04-v4-roadmap.md delete mode 100644 website/blog/2021-03-09-redpanda.md delete mode 100644 website/blog/2021-06-02-new-plugins-and-templates.md delete mode 100644 website/blog/2021-10-12-new-plugins-stable.md delete mode 100644 website/blog/2022-03-03-v4-coming.md delete mode 100644 website/blog/2022-11-07-whats-next.md delete mode 100755 website/build_plugins.sh delete mode 100644 website/cookbooks/custom_metrics.md delete mode 100644 website/cookbooks/discord_bot.md delete mode 100644 website/cookbooks/enrichments.md delete mode 100644 website/cookbooks/filtering.md delete mode 100644 website/cookbooks/joining_streams.md delete mode 100644 website/docs/about.md delete mode 100644 website/docs/components/about.md delete mode 100644 website/docs/components/buffers/about.md delete mode 100644 website/docs/components/buffers/memory.md delete mode 100644 website/docs/components/buffers/none.md delete mode 100644 website/docs/components/buffers/sqlite.md delete mode 100644 website/docs/components/buffers/system_window.md delete mode 100644 website/docs/components/caches/about.md delete mode 100644 website/docs/components/caches/aws_dynamodb.md delete mode 100644 website/docs/components/caches/aws_s3.md delete mode 100644 website/docs/components/caches/couchbase.md delete mode 100644 website/docs/components/caches/file.md delete mode 100644 website/docs/components/caches/gcp_cloud_storage.md delete mode 100644 website/docs/components/caches/lru.md delete mode 100644 website/docs/components/caches/memcached.md delete mode 100644 website/docs/components/caches/memory.md delete mode 100644 website/docs/components/caches/mongodb.md delete mode 100644 website/docs/components/caches/multilevel.md delete mode 100644 website/docs/components/caches/nats_kv.md delete mode 100644 website/docs/components/caches/noop.md delete mode 100644 website/docs/components/caches/redis.md delete mode 100644 website/docs/components/caches/ristretto.md delete mode 100644 website/docs/components/caches/sql.md delete mode 100644 website/docs/components/caches/ttlru.md delete mode 100644 website/docs/components/http/about.md delete mode 100644 website/docs/components/inputs/about.md delete mode 100644 website/docs/components/inputs/amqp_0_9.md delete mode 100644 website/docs/components/inputs/amqp_1.md delete mode 100644 website/docs/components/inputs/aws_kinesis.md delete mode 100644 website/docs/components/inputs/aws_s3.md delete mode 100644 website/docs/components/inputs/aws_sqs.md delete mode 100644 website/docs/components/inputs/azure_blob_storage.md delete mode 100644 website/docs/components/inputs/azure_cosmosdb.md delete mode 100644 website/docs/components/inputs/azure_queue_storage.md delete mode 100644 website/docs/components/inputs/azure_table_storage.md delete mode 100644 website/docs/components/inputs/batched.md delete mode 100644 website/docs/components/inputs/beanstalkd.md delete mode 100644 website/docs/components/inputs/broker.md delete mode 100644 website/docs/components/inputs/cassandra.md delete mode 100644 website/docs/components/inputs/cockroachdb_changefeed.md delete mode 100644 website/docs/components/inputs/csv.md delete mode 100644 website/docs/components/inputs/discord.md delete mode 100644 website/docs/components/inputs/dynamic.md delete mode 100644 website/docs/components/inputs/file.md delete mode 100644 website/docs/components/inputs/gcp_bigquery_select.md delete mode 100644 website/docs/components/inputs/gcp_cloud_storage.md delete mode 100644 website/docs/components/inputs/gcp_pubsub.md delete mode 100644 website/docs/components/inputs/generate.md delete mode 100644 website/docs/components/inputs/hdfs.md delete mode 100644 website/docs/components/inputs/http_client.md delete mode 100644 website/docs/components/inputs/http_server.md delete mode 100644 website/docs/components/inputs/inproc.md delete mode 100644 website/docs/components/inputs/kafka.md delete mode 100644 website/docs/components/inputs/kafka_franz.md delete mode 100644 website/docs/components/inputs/mongodb.md delete mode 100644 website/docs/components/inputs/mqtt.md delete mode 100644 website/docs/components/inputs/nanomsg.md delete mode 100644 website/docs/components/inputs/nats.md delete mode 100644 website/docs/components/inputs/nats_jetstream.md delete mode 100644 website/docs/components/inputs/nats_kv.md delete mode 100644 website/docs/components/inputs/nats_stream.md delete mode 100644 website/docs/components/inputs/nsq.md delete mode 100644 website/docs/components/inputs/parquet.md delete mode 100644 website/docs/components/inputs/pulsar.md delete mode 100644 website/docs/components/inputs/read_until.md delete mode 100644 website/docs/components/inputs/redis_list.md delete mode 100644 website/docs/components/inputs/redis_pubsub.md delete mode 100644 website/docs/components/inputs/redis_scan.md delete mode 100644 website/docs/components/inputs/redis_streams.md delete mode 100644 website/docs/components/inputs/resource.md delete mode 100644 website/docs/components/inputs/sequence.md delete mode 100644 website/docs/components/inputs/sftp.md delete mode 100644 website/docs/components/inputs/socket.md delete mode 100644 website/docs/components/inputs/socket_server.md delete mode 100644 website/docs/components/inputs/sql_raw.md delete mode 100644 website/docs/components/inputs/sql_select.md delete mode 100644 website/docs/components/inputs/stdin.md delete mode 100644 website/docs/components/inputs/subprocess.md delete mode 100644 website/docs/components/inputs/twitter_search.md delete mode 100644 website/docs/components/inputs/websocket.md delete mode 100644 website/docs/components/inputs/zmq4.md delete mode 100644 website/docs/components/logger/about.md delete mode 100644 website/docs/components/metrics/about.md delete mode 100644 website/docs/components/metrics/aws_cloudwatch.md delete mode 100644 website/docs/components/metrics/influxdb.md delete mode 100644 website/docs/components/metrics/json_api.md delete mode 100644 website/docs/components/metrics/logger.md delete mode 100644 website/docs/components/metrics/none.md delete mode 100644 website/docs/components/metrics/prometheus.md delete mode 100644 website/docs/components/metrics/statsd.md delete mode 100644 website/docs/components/outputs/about.md delete mode 100644 website/docs/components/outputs/amqp_0_9.md delete mode 100644 website/docs/components/outputs/amqp_1.md delete mode 100644 website/docs/components/outputs/aws_dynamodb.md delete mode 100644 website/docs/components/outputs/aws_kinesis.md delete mode 100644 website/docs/components/outputs/aws_kinesis_firehose.md delete mode 100644 website/docs/components/outputs/aws_s3.md delete mode 100644 website/docs/components/outputs/aws_sns.md delete mode 100644 website/docs/components/outputs/aws_sqs.md delete mode 100644 website/docs/components/outputs/azure_blob_storage.md delete mode 100644 website/docs/components/outputs/azure_cosmosdb.md delete mode 100644 website/docs/components/outputs/azure_queue_storage.md delete mode 100644 website/docs/components/outputs/azure_table_storage.md delete mode 100644 website/docs/components/outputs/beanstalkd.md delete mode 100644 website/docs/components/outputs/broker.md delete mode 100644 website/docs/components/outputs/cache.md delete mode 100644 website/docs/components/outputs/cassandra.md delete mode 100644 website/docs/components/outputs/discord.md delete mode 100644 website/docs/components/outputs/drop.md delete mode 100644 website/docs/components/outputs/drop_on.md delete mode 100644 website/docs/components/outputs/dynamic.md delete mode 100644 website/docs/components/outputs/elasticsearch.md delete mode 100644 website/docs/components/outputs/fallback.md delete mode 100644 website/docs/components/outputs/file.md delete mode 100644 website/docs/components/outputs/gcp_bigquery.md delete mode 100644 website/docs/components/outputs/gcp_cloud_storage.md delete mode 100644 website/docs/components/outputs/gcp_pubsub.md delete mode 100644 website/docs/components/outputs/hdfs.md delete mode 100644 website/docs/components/outputs/http_client.md delete mode 100644 website/docs/components/outputs/http_server.md delete mode 100644 website/docs/components/outputs/inproc.md delete mode 100644 website/docs/components/outputs/kafka.md delete mode 100644 website/docs/components/outputs/kafka_franz.md delete mode 100644 website/docs/components/outputs/mongodb.md delete mode 100644 website/docs/components/outputs/mqtt.md delete mode 100644 website/docs/components/outputs/nanomsg.md delete mode 100644 website/docs/components/outputs/nats.md delete mode 100644 website/docs/components/outputs/nats_jetstream.md delete mode 100644 website/docs/components/outputs/nats_kv.md delete mode 100644 website/docs/components/outputs/nats_stream.md delete mode 100644 website/docs/components/outputs/nsq.md delete mode 100644 website/docs/components/outputs/opensearch.md delete mode 100644 website/docs/components/outputs/pulsar.md delete mode 100644 website/docs/components/outputs/pusher.md delete mode 100644 website/docs/components/outputs/redis_hash.md delete mode 100644 website/docs/components/outputs/redis_list.md delete mode 100644 website/docs/components/outputs/redis_pubsub.md delete mode 100644 website/docs/components/outputs/redis_streams.md delete mode 100644 website/docs/components/outputs/reject.md delete mode 100644 website/docs/components/outputs/reject_errored.md delete mode 100644 website/docs/components/outputs/resource.md delete mode 100644 website/docs/components/outputs/retry.md delete mode 100644 website/docs/components/outputs/sftp.md delete mode 100644 website/docs/components/outputs/snowflake_put.md delete mode 100644 website/docs/components/outputs/socket.md delete mode 100644 website/docs/components/outputs/splunk_hec.md delete mode 100644 website/docs/components/outputs/sql.md delete mode 100644 website/docs/components/outputs/sql_insert.md delete mode 100644 website/docs/components/outputs/sql_raw.md delete mode 100644 website/docs/components/outputs/stdout.md delete mode 100644 website/docs/components/outputs/subprocess.md delete mode 100644 website/docs/components/outputs/switch.md delete mode 100644 website/docs/components/outputs/sync_response.md delete mode 100644 website/docs/components/outputs/websocket.md delete mode 100644 website/docs/components/outputs/zmq4.md delete mode 100644 website/docs/components/processors/about.md delete mode 100644 website/docs/components/processors/archive.md delete mode 100644 website/docs/components/processors/avro.md delete mode 100644 website/docs/components/processors/awk.md delete mode 100644 website/docs/components/processors/aws_dynamodb_partiql.md delete mode 100644 website/docs/components/processors/aws_lambda.md delete mode 100644 website/docs/components/processors/azure_cosmosdb.md delete mode 100644 website/docs/components/processors/bloblang.md delete mode 100644 website/docs/components/processors/bounds_check.md delete mode 100644 website/docs/components/processors/branch.md delete mode 100644 website/docs/components/processors/cache.md delete mode 100644 website/docs/components/processors/cached.md delete mode 100644 website/docs/components/processors/catch.md delete mode 100644 website/docs/components/processors/command.md delete mode 100644 website/docs/components/processors/compress.md delete mode 100644 website/docs/components/processors/couchbase.md delete mode 100644 website/docs/components/processors/decompress.md delete mode 100644 website/docs/components/processors/dedupe.md delete mode 100644 website/docs/components/processors/for_each.md delete mode 100644 website/docs/components/processors/gcp_bigquery_select.md delete mode 100644 website/docs/components/processors/grok.md delete mode 100644 website/docs/components/processors/group_by.md delete mode 100644 website/docs/components/processors/group_by_value.md delete mode 100644 website/docs/components/processors/http.md delete mode 100644 website/docs/components/processors/insert_part.md delete mode 100644 website/docs/components/processors/javascript.md delete mode 100644 website/docs/components/processors/jmespath.md delete mode 100644 website/docs/components/processors/jq.md delete mode 100644 website/docs/components/processors/json_schema.md delete mode 100644 website/docs/components/processors/log.md delete mode 100644 website/docs/components/processors/mapping.md delete mode 100644 website/docs/components/processors/metric.md delete mode 100644 website/docs/components/processors/mongodb.md delete mode 100644 website/docs/components/processors/msgpack.md delete mode 100644 website/docs/components/processors/mutation.md delete mode 100644 website/docs/components/processors/nats_kv.md delete mode 100644 website/docs/components/processors/nats_request_reply.md delete mode 100644 website/docs/components/processors/noop.md delete mode 100644 website/docs/components/processors/parallel.md delete mode 100644 website/docs/components/processors/parquet.md delete mode 100644 website/docs/components/processors/parquet_decode.md delete mode 100644 website/docs/components/processors/parquet_encode.md delete mode 100644 website/docs/components/processors/parse_log.md delete mode 100644 website/docs/components/processors/processors.md delete mode 100644 website/docs/components/processors/protobuf.md delete mode 100644 website/docs/components/processors/rate_limit.md delete mode 100644 website/docs/components/processors/redis.md delete mode 100644 website/docs/components/processors/redis_script.md delete mode 100644 website/docs/components/processors/resource.md delete mode 100644 website/docs/components/processors/retry.md delete mode 100644 website/docs/components/processors/schema_registry_decode.md delete mode 100644 website/docs/components/processors/schema_registry_encode.md delete mode 100644 website/docs/components/processors/select_parts.md delete mode 100644 website/docs/components/processors/sentry_capture.md delete mode 100644 website/docs/components/processors/sleep.md delete mode 100644 website/docs/components/processors/split.md delete mode 100644 website/docs/components/processors/sql.md delete mode 100644 website/docs/components/processors/sql_insert.md delete mode 100644 website/docs/components/processors/sql_raw.md delete mode 100644 website/docs/components/processors/sql_select.md delete mode 100644 website/docs/components/processors/subprocess.md delete mode 100644 website/docs/components/processors/switch.md delete mode 100644 website/docs/components/processors/sync_response.md delete mode 100644 website/docs/components/processors/try.md delete mode 100644 website/docs/components/processors/unarchive.md delete mode 100644 website/docs/components/processors/wasm.md delete mode 100644 website/docs/components/processors/while.md delete mode 100644 website/docs/components/processors/workflow.md delete mode 100644 website/docs/components/processors/xml.md delete mode 100644 website/docs/components/rate_limits/about.md delete mode 100644 website/docs/components/rate_limits/local.md delete mode 100644 website/docs/components/rate_limits/redis.md delete mode 100644 website/docs/components/scanners/about.md delete mode 100644 website/docs/components/scanners/avro.md delete mode 100644 website/docs/components/scanners/chunker.md delete mode 100644 website/docs/components/scanners/csv.md delete mode 100644 website/docs/components/scanners/decompress.md delete mode 100644 website/docs/components/scanners/json_documents.md delete mode 100644 website/docs/components/scanners/lines.md delete mode 100644 website/docs/components/scanners/re_match.md delete mode 100644 website/docs/components/scanners/skip_bom.md delete mode 100644 website/docs/components/scanners/switch.md delete mode 100644 website/docs/components/scanners/tar.md delete mode 100644 website/docs/components/scanners/to_the_end.md delete mode 100644 website/docs/components/tracers/about.md delete mode 100644 website/docs/components/tracers/gcp_cloudtrace.md delete mode 100644 website/docs/components/tracers/jaeger.md delete mode 100644 website/docs/components/tracers/none.md delete mode 100644 website/docs/components/tracers/open_telemetry_collector.md delete mode 100644 website/docs/configuration/about.md delete mode 100644 website/docs/configuration/batching.md delete mode 100644 website/docs/configuration/dynamic_inputs_and_outputs.md delete mode 100644 website/docs/configuration/error_handling.md delete mode 100644 website/docs/configuration/field_paths.md delete mode 100644 website/docs/configuration/interpolation.md delete mode 100644 website/docs/configuration/metadata.md delete mode 100644 website/docs/configuration/processing_pipelines.md delete mode 100644 website/docs/configuration/resources.md delete mode 100644 website/docs/configuration/secrets.md delete mode 100644 website/docs/configuration/templating.md delete mode 100644 website/docs/configuration/unit_testing.md delete mode 100644 website/docs/configuration/using_cue.md delete mode 100644 website/docs/configuration/windowed_processing.md delete mode 100644 website/docs/guides/bloblang/about.md delete mode 100644 website/docs/guides/bloblang/advanced.md delete mode 100644 website/docs/guides/bloblang/arithmetic.md delete mode 100644 website/docs/guides/bloblang/functions.md delete mode 100644 website/docs/guides/bloblang/methods.md delete mode 100644 website/docs/guides/bloblang/walkthrough.md delete mode 100644 website/docs/guides/cloud/aws.md delete mode 100644 website/docs/guides/cloud/gcp.md delete mode 100644 website/docs/guides/getting_started.md delete mode 100644 website/docs/guides/migration/v2.md delete mode 100644 website/docs/guides/migration/v3.md delete mode 100644 website/docs/guides/migration/v4.md delete mode 100644 website/docs/guides/monitoring.md delete mode 100644 website/docs/guides/performance_tuning.md delete mode 100644 website/docs/guides/serverless/about.md delete mode 100644 website/docs/guides/serverless/lambda.md delete mode 100644 website/docs/guides/streams_mode/about.md delete mode 100644 website/docs/guides/streams_mode/streams_api.md delete mode 100644 website/docs/guides/streams_mode/using_config_files.md delete mode 100644 website/docs/guides/streams_mode/using_rest_api.md delete mode 100644 website/docs/guides/sync_responses.md delete mode 100755 website/docusaurus.config.js delete mode 100755 website/package.json delete mode 100755 website/sidebars.js delete mode 100755 website/src/css/custom.css delete mode 100644 website/src/exports/redirect.js delete mode 100755 website/src/pages/blobfish.module.css delete mode 100644 website/src/pages/blobfish.tsx delete mode 100644 website/src/pages/community.module.css delete mode 100644 website/src/pages/community.tsx delete mode 100755 website/src/pages/index.module.css delete mode 100755 website/src/pages/index.tsx delete mode 100755 website/src/pages/support.module.css delete mode 100644 website/src/pages/support.tsx delete mode 100644 website/src/pages/videos.module.css delete mode 100755 website/src/pages/videos.tsx delete mode 100644 website/src/plugins/analytics/client.js delete mode 100644 website/src/plugins/analytics/index.js delete mode 100644 website/src/plugins/components/index.js delete mode 100644 website/src/plugins/cookbooks/.gitignore delete mode 100644 website/src/plugins/cookbooks/cookbookUtils.ts delete mode 100644 website/src/plugins/cookbooks/frontMatter.ts delete mode 100644 website/src/plugins/cookbooks/index.ts delete mode 100644 website/src/plugins/cookbooks/markdownLoader.ts delete mode 100644 website/src/plugins/cookbooks/tsconfig.json delete mode 100644 website/src/plugins/cookbooks/types.ts delete mode 100644 website/src/plugins/prism_themes/github/index.js delete mode 100644 website/src/plugins/prism_themes/monokai/index.js delete mode 100755 website/src/theme/ComponentCard/index.js delete mode 100644 website/src/theme/ComponentCard/styles.module.css delete mode 100644 website/src/theme/ComponentSelect/index.js delete mode 100644 website/src/theme/ComponentSelect/styles.module.css delete mode 100644 website/src/theme/ComponentsByCategory/index.js delete mode 100755 website/src/theme/CookbookItem/index.js delete mode 100644 website/src/theme/CookbookItem/styles.module.css delete mode 100755 website/src/theme/CookbookListPage/index.js delete mode 100644 website/src/theme/CookbookListPage/styles.module.css delete mode 100755 website/src/theme/CookbookPage/index.js delete mode 100644 website/src/theme/CookbookPage/styles.module.css delete mode 100644 website/src/theme/NotFound/index.tsx delete mode 100644 website/src/theme/NotFound/styles.module.css delete mode 100644 website/static/img/Blobartist.svg delete mode 100644 website/static/img/Blobborg.svg delete mode 100644 website/static/img/Blobboring.svg delete mode 100644 website/static/img/Blobchef.svg delete mode 100644 website/static/img/Blobextended.svg delete mode 100644 website/static/img/Blobgangsta.svg delete mode 100644 website/static/img/Blobninja.svg delete mode 100644 website/static/img/Blobpirate.svg delete mode 100644 website/static/img/Blobscales.svg delete mode 100644 website/static/img/Blobsherlock.svg delete mode 100644 website/static/img/Blobsocial.svg delete mode 100644 website/static/img/Blobviking.svg delete mode 100644 website/static/img/ash.jpg delete mode 100644 website/static/img/blobheart.svg delete mode 100644 website/static/img/emojis/blob.png delete mode 100644 website/static/img/emojis/blobbot.png delete mode 100644 website/static/img/emojis/blobbounce.gif delete mode 100644 website/static/img/emojis/blobbug.png delete mode 100644 website/static/img/emojis/blobcool.png delete mode 100644 website/static/img/emojis/blobcrying.png delete mode 100644 website/static/img/emojis/blobcrylaugh.png delete mode 100644 website/static/img/emojis/blobemo.png delete mode 100644 website/static/img/emojis/blobheart.png delete mode 100644 website/static/img/emojis/blobheartpuke.png delete mode 100644 website/static/img/emojis/blobhug.png delete mode 100644 website/static/img/emojis/blobkiss.png delete mode 100644 website/static/img/emojis/blobmad.png delete mode 100644 website/static/img/emojis/blobnaughty.png delete mode 100644 website/static/img/emojis/blobnerd.png delete mode 100644 website/static/img/emojis/blobnervous.png delete mode 100644 website/static/img/emojis/blobno.png delete mode 100644 website/static/img/emojis/blobok.png delete mode 100644 website/static/img/emojis/blobpalm.png delete mode 100644 website/static/img/emojis/blobpirate.png delete mode 100644 website/static/img/emojis/blobpuke.png delete mode 100644 website/static/img/emojis/blobshrug.png delete mode 100644 website/static/img/emojis/blobswag.png delete mode 100644 website/static/img/emojis/blobsweat.png delete mode 100644 website/static/img/emojis/blobsweatsmile.png delete mode 100644 website/static/img/emojis/blobthanks.png delete mode 100644 website/static/img/emojis/blobthinking.png delete mode 100644 website/static/img/emojis/blobtrance.gif delete mode 100644 website/static/img/emojis/blobwave.png delete mode 100644 website/static/img/emojis/blobyes.png delete mode 100644 website/static/img/emojis/cowblob.png delete mode 100644 website/static/img/favicon.ico delete mode 100644 website/static/img/introducing-benthos-lab/banner.svg delete mode 100644 website/static/img/introducing-benthos-lab/genteel.jpg delete mode 100644 website/static/img/introducing-benthos-lab/slamslack.jpg delete mode 100644 website/static/img/logo.svg delete mode 100644 website/static/img/logo_hero.svg delete mode 100644 website/static/img/love-bg-dark.svg delete mode 100644 website/static/img/nav-logo.svg delete mode 100644 website/static/img/og_img.png delete mode 100644 website/static/img/og_img.svg delete mode 100644 website/static/img/sponsors/HUMAN_logo.png delete mode 100644 website/static/img/sponsors/aurora.svg delete mode 100644 website/static/img/sponsors/community.svg delete mode 100644 website/static/img/sponsors/formance.svg delete mode 100644 website/static/img/sponsors/mw_logo.png delete mode 100644 website/static/img/sponsors/opala.svg delete mode 100644 website/static/img/sponsors/optum_logo.png delete mode 100644 website/static/img/sponsors/synadia.svg delete mode 100644 website/static/img/sponsors/umh_logo.svg delete mode 100644 website/static/img/sponsors/warpstream_logo.svg delete mode 100644 website/static/img/teacher-blob.svg delete mode 100644 website/static/img/what-is-blob.svg delete mode 100644 website/static/img/write-a-benthos-plugin/benthos-plugged.png delete mode 100644 website/static/img/write-a-benthos-plugin/blobfish.jpg delete mode 100755 website/static/sh/install delete mode 100644 website/tsconfig.json delete mode 100644 website/yarn.lock diff --git a/Makefile b/Makefile index 6180b6aee0..c115051156 100644 --- a/Makefile +++ b/Makefile @@ -4,13 +4,13 @@ TAGS ?= GOMAXPROCS ?= 1 INSTALL_DIR ?= $(GOPATH)/bin -WEBSITE_DIR ?= ./website +WEBSITE_DIR ?= ./docs/modules DEST_DIR ?= ./target PATHINSTBIN = $(DEST_DIR)/bin PATHINSTTOOLS = $(DEST_DIR)/tools PATHINSTSERVERLESS = $(DEST_DIR)/serverless PATHINSTDOCKER = $(DEST_DIR)/docker -DOCKER_IMAGE ?= ghcr.io/benthosdev/benthos +DOCKER_IMAGE ?= ghcr.io/redpanda-data/connect VERSION := $(shell git describe --tags || echo "v0.0.0") VER_CUT := $(shell echo $(VERSION) | cut -c2-) @@ -20,14 +20,13 @@ VER_PATCH := $(shell echo $(VER_CUT) | cut -f3 -d.) VER_RC := $(shell echo $(VER_PATCH) | cut -f2 -d-) DATE := $(shell date +"%Y-%m-%dT%H:%M:%SZ") -VER_FLAGS = -X github.com/benthosdev/benthos/v4/internal/cli.Version=$(VERSION) \ - -X github.com/benthosdev/benthos/v4/internal/cli.DateBuilt=$(DATE) +VER_FLAGS = -X main.Version=$(VERSION) -X main.DateBuilt=$(DATE) LD_FLAGS ?= -w -s GO_FLAGS ?= DOCS_FLAGS ?= -APPS = benthos +APPS = redpanda-connect all: $(APPS) install: $(APPS) @@ -46,16 +45,16 @@ $(PATHINSTBIN)/%: $(SOURCE_FILES) $(APPS): %: $(PATHINSTBIN)/% -TOOLS = benthos_docs_gen -tools: $(TOOLS) +# TOOLS = redpanda-docs TODO +# tools: $(TOOLS) $(PATHINSTTOOLS)/%: $(SOURCE_FILES) @go build $(GO_FLAGS) -tags "$(TAGS)" -ldflags "$(LD_FLAGS) $(VER_FLAGS)" -o $@ ./cmd/tools/$* $(TOOLS): %: $(PATHINSTTOOLS)/% -SERVERLESS = benthos-lambda -serverless: $(SERVERLESS) +# SERVERLESS = redpanda-connect-lambda TODO +# serverless: $(SERVERLESS) $(PATHINSTSERVERLESS)/%: $(SOURCE_FILES) @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ @@ -83,7 +82,7 @@ docker-cgo: fmt: @go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} - @go list -f {{.Dir}} ./... | xargs -I{} goimports -w -local github.com/benthosdev/benthos/v4 {} + @go list -f {{.Dir}} ./... | xargs -I{} goimports -w -local github.com/redpanda-data/connect/v4 {} @go mod tidy lint: @@ -92,8 +91,8 @@ lint: test: $(APPS) @go test $(GO_FLAGS) -ldflags "$(LD_FLAGS)" -timeout 3m ./... - @$(PATHINSTBIN)/benthos template lint $(TEMPLATE_FILES) - @$(PATHINSTBIN)/benthos test ./config/test/... + @$(PATHINSTBIN)/redpanda-connect template lint $(TEMPLATE_FILES) + @$(PATHINSTBIN)/redpanda-connect test ./config/test/... test-race: $(APPS) @go test $(GO_FLAGS) -ldflags "$(LD_FLAGS)" -timeout 3m -race ./... @@ -109,9 +108,7 @@ clean: rm -rf $(DEST_DIR)/serverless rm -rf $(PATHINSTDOCKER) -docs: $(APPS) $(TOOLS) - @$(PATHINSTTOOLS)/benthos_docs_gen $(DOCS_FLAGS) - @$(PATHINSTBIN)/benthos lint --deprecated "./config/examples/*.yaml" \ - "$(WEBSITE_DIR)/cookbooks/**/*.md" \ - "$(WEBSITE_DIR)/docs/**/*.md" - @$(PATHINSTBIN)/benthos template lint "./config/template_examples/*.yaml" +docs: $(APPS) $(TOOLS) # TODO: Add docs generation back in + @$(PATHINSTBIN)/redpanda-connect lint --deprecated "./config/examples/*.yaml" \ + "$(WEBSITE_DIR)/**/*.md" + @$(PATHINSTBIN)/redpanda-connect template lint "./config/template_examples/*.yaml" diff --git a/go.mod b/go.mod index c227bbb80c..3ac2a26713 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/benthosdev/benthos/v4 +module github.com/redpanda-data/connect/v4 replace github.com/99designs/keyring => github.com/Jeffail/keyring v1.2.3 @@ -6,7 +6,6 @@ require ( cloud.google.com/go/bigquery v1.59.0 cloud.google.com/go/pubsub v1.36.1 cloud.google.com/go/storage v1.37.0 - cuelang.org/go v0.7.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 @@ -19,14 +18,11 @@ require ( github.com/IBM/sarama v1.42.2 github.com/Jeffail/checkpoint v1.0.1 github.com/Jeffail/gabs/v2 v2.7.0 - github.com/Jeffail/grok v1.1.0 github.com/Jeffail/shutdown v1.0.0 github.com/Masterminds/squirrel v1.5.4 - github.com/OneOfOne/xxhash v1.2.8 github.com/PaesslerAG/gval v1.2.2 github.com/PaesslerAG/jsonpath v0.1.1 github.com/apache/pulsar-client-go v0.12.0 - github.com/aws/aws-lambda-go v1.46.0 github.com/aws/aws-sdk-go-v2 v1.25.0 github.com/aws/aws-sdk-go-v2/config v1.26.6 github.com/aws/aws-sdk-go-v2/credentials v1.16.16 @@ -43,6 +39,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 github.com/beanstalkd/go-beanstalk v0.2.0 github.com/benhoyt/goawk v1.25.0 + github.com/benthosdev/benthos/v4 v4.27.1-0.20240522131344-c11a4d51792f github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/snowflake v0.3.0 @@ -56,8 +53,6 @@ require ( github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c github.com/dustin/go-humanize v1.0.1 github.com/eclipse/paho.mqtt.golang v1.4.3 - github.com/fatih/color v1.16.0 - github.com/fsnotify/fsnotify v1.7.0 github.com/generikvault/gvalstrings v0.0.0-20180926130504-471f38f0112a github.com/getsentry/sentry-go v0.27.0 github.com/go-faker/faker/v4 v4.3.0 @@ -65,25 +60,13 @@ require ( github.com/gocql/gocql v1.6.0 github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/gorilla/handlers v1.5.2 - github.com/gorilla/mux v1.8.1 - github.com/gorilla/websocket v1.5.1 github.com/gosimple/slug v1.13.1 - github.com/hashicorp/golang-lru/arc/v2 v2.0.7 - github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/influxdata/go-syslog/v3 v3.0.0 github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c - github.com/itchyny/gojq v0.12.14 - github.com/itchyny/timefmt-go v0.1.5 github.com/jackc/pgx/v4 v4.18.2 github.com/jhump/protoreflect v1.15.6 - github.com/jmespath/go-jmespath v0.4.0 - github.com/klauspost/compress v1.17.7 - github.com/klauspost/pgzip v1.2.6 github.com/lib/pq v1.10.9 github.com/linkedin/goavro/v2 v2.12.0 github.com/matoous/go-nanoid/v2 v2.0.0 - github.com/microcosm-cc/bluemonday v1.0.25 github.com/microsoft/gocosmos v1.1.1 github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats.go v1.32.0 @@ -98,31 +81,23 @@ require ( github.com/oschwald/geoip2-golang v1.9.0 github.com/parquet-go/parquet-go v0.20.0 github.com/pebbe/zmq4 v1.2.10 - github.com/pierrec/lz4/v4 v4.1.21 github.com/pkg/sftp v1.13.6 github.com/prometheus/client_golang v1.18.0 github.com/prometheus/common v0.46.0 github.com/pusher/pusher-http-go v4.0.1+incompatible - github.com/quipo/dependencysolver v0.0.0-20170801134659-2b009cb4ddcc github.com/r3labs/diff/v3 v3.0.1 github.com/rabbitmq/amqp091-go v1.9.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/redis/go-redis/v9 v9.4.0 - github.com/rickb777/date v1.20.5 - github.com/robfig/cron/v3 v3.0.1 - github.com/segmentio/ksuid v1.0.4 github.com/sijms/go-ora/v2 v2.8.7 - github.com/sirupsen/logrus v1.9.3 github.com/smira/go-statsd v1.3.3 github.com/snowflakedb/gosnowflake v1.7.2 github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.9.0 github.com/tetratelabs/wazero v1.6.0 - github.com/tilinna/z85 v1.0.0 github.com/trinodb/trino-go-client v0.313.0 github.com/twmb/franz-go v1.16.1 github.com/twmb/franz-go/pkg/kmsg v1.7.0 - github.com/urfave/cli/v2 v2.27.1 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/xdg-go/scram v1.1.2 github.com/xeipuuv/gojsonschema v1.2.0 @@ -142,13 +117,10 @@ require ( golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.23.0 - golang.org/x/oauth2 v0.17.0 golang.org/x/sync v0.6.0 golang.org/x/text v0.14.0 google.golang.org/api v0.162.0 google.golang.org/protobuf v1.33.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 - gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.28.0 ) @@ -158,6 +130,7 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.6 // indirect cloud.google.com/go/trace v1.10.4 // indirect + cuelang.org/go v0.7.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect @@ -169,15 +142,16 @@ require ( github.com/ClickHouse/ch-go v0.61.5 // indirect github.com/DataDog/zstd v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.45.0 // indirect + github.com/Jeffail/grok v1.1.0 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/apache/arrow/go/v14 v14.0.2 // indirect github.com/apache/thrift v0.18.1 // indirect github.com/ardielle/ardielle-go v1.5.2 // indirect - github.com/armon/go-metrics v0.3.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 // indirect github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.12.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect @@ -224,9 +198,10 @@ require ( github.com/eapache/go-resiliency v1.5.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect - github.com/frankban/quicktest v1.14.6 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect @@ -250,15 +225,24 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.0 // indirect + github.com/hashicorp/go-hclog v1.1.0 // indirect + github.com/hashicorp/go-msgpack v1.1.5 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/raft v1.3.9 // indirect + github.com/influxdata/go-syslog/v3 v3.0.0 // indirect + github.com/itchyny/gojq v0.12.14 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -272,9 +256,12 @@ require ( github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -283,12 +270,13 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect + github.com/minio/highwayhash v1.0.2 // indirect github.com/moby/term v0.5.0 // indirect github.com/montanaflynn/stats v0.7.0 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/mtibben/percent v0.2.1 // indirect - github.com/nats-io/nats-server/v2 v2.9.23 // indirect - github.com/nats-io/nats-streaming-server v0.24.6 // indirect + github.com/nats-io/jwt/v2 v2.5.0 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -297,20 +285,28 @@ require ( github.com/oschwald/maxminddb-golang v1.11.0 // indirect github.com/paulmach/orb v0.11.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/quipo/dependencysolver v0.0.0-20170801134659-2b009cb4ddcc // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rickb777/date v1.20.5 // indirect github.com/rickb777/plural v1.4.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/encoding v0.3.6 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect github.com/shopspring/decimal v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tilinna/z85 v1.0.0 // indirect + github.com/urfave/cli/v2 v2.27.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -318,6 +314,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect + go.etcd.io/bbolt v1.3.6 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect @@ -326,6 +323,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.14.0 // indirect + golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect @@ -341,7 +339,9 @@ require ( gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect gopkg.in/jcmturner/gokrb5.v6 v6.1.1 // indirect gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.3.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect diff --git a/go.sum b/go.sum index 1c90f1a39e..1337aefc69 100644 --- a/go.sum +++ b/go.sum @@ -99,7 +99,6 @@ github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9z github.com/ClickHouse/clickhouse-go/v2 v2.21.1 h1:x8wZEMOHDh4K8kLQBtGMeIIguejiaj8/bUiF2VzG6n4= github.com/ClickHouse/clickhouse-go/v2 v2.21.1/go.mod h1:hTWNkV9mkQwiQ/df0rbN17VXF05UTResY4krnjbzVZA= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.21.0 h1:OEgjQy1rH4Fbn5IpuI9d0uhLl+j6DkDvh9Q2Ucd6GK8= @@ -140,10 +139,6 @@ github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -164,8 +159,6 @@ github.com/ardielle/ardielle-tools v1.5.4/go.mod h1:oZN+JRMnqGiIhrzkRN9l26Cej9dE github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= github.com/armon/go-metrics v0.3.4 h1:Xqf+7f2Vhl9tsqDYmXhnXInUdcrtgpRNpIA15/uldSc= github.com/armon/go-metrics v0.3.4/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/aws/aws-lambda-go v1.46.0 h1:UWVnvh2h2gecOlFhHQfIPQcD8pL/f7pVCutmFl+oXU8= -github.com/aws/aws-lambda-go v1.46.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= github.com/aws/aws-sdk-go v1.30.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250= @@ -248,8 +241,9 @@ github.com/beanstalkd/go-beanstalk v0.2.0/go.mod h1:/G8YTyChOtpOArwLTQPY1CHB+i21 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benhoyt/goawk v1.25.0 h1:DW4DCn2IrVp6FUar2W404G1YyQDXseWAVDwb11PUL+I= github.com/benhoyt/goawk v1.25.0/go.mod h1:FjIAicXvrv3wbqAhSTo5bn4mIM5y1iy3lcnIynlJvoI= +github.com/benthosdev/benthos/v4 v4.27.1-0.20240522131344-c11a4d51792f h1:1nuINiKbTpJUhKSTBTzTrbDHElAwXDa1mr9K/7qldWg= +github.com/benthosdev/benthos/v4 v4.27.1-0.20240522131344-c11a4d51792f/go.mod h1:KbKrzlHGhf67eIdUqk4pjT5yaD8nTPb7vczP5/twTOc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= @@ -445,12 +439,8 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -462,7 +452,6 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -473,7 +462,6 @@ github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJr github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -537,10 +525,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= @@ -709,11 +695,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -723,7 +706,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= @@ -734,7 +716,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -757,7 +738,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= @@ -799,10 +779,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= @@ -811,18 +787,12 @@ github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9 github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= -github.com/nats-io/nats-server/v2 v2.8.2/go.mod h1:vIdpKz3OG+DCg4q/xVPdXHoztEyKDWRtykQ4N7hd7C4= github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU= github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0= github.com/nats-io/nats-streaming-server v0.24.6 h1:iIZXuPSznnYkiy0P3L0AP9zEN9Etp+tITbbX1KKeq4Q= github.com/nats-io/nats-streaming-server v0.24.6/go.mod h1:tdKXltY3XLeBJ21sHiZiaPl+j8sK3vcCKBWVyxeQs10= -github.com/nats-io/nats.go v1.13.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nats.go v1.14.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nats.go v1.15.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= github.com/nats-io/nats.go v1.32.0 h1:Bx9BZS+aXYlxW08k8Gd3yR2s73pV5XSoAQUyp1Kwvp0= github.com/nats-io/nats.go v1.32.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= @@ -831,7 +801,6 @@ github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nats-io/stan.go v0.10.2/go.mod h1:vo2ax8K2IxaR3JtEMLZRFKIdoK/3o1/PKueapB7ezX0= github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw= github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k= github.com/ncw/swift v1.0.52/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= @@ -888,8 +857,6 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -897,28 +864,17 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= @@ -949,7 +905,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk= github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -972,7 +927,6 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sijms/go-ora/v2 v2.8.7 h1:lkbCuXqd5/wn8niyJs/qvfTcSAfi8wBbzc5LYz41g5g= github.com/sijms/go-ora/v2 v2.8.7/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -1132,7 +1086,6 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1150,7 +1103,6 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= @@ -1209,7 +1161,6 @@ golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1218,7 +1169,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1260,7 +1210,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1268,7 +1217,6 @@ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1311,9 +1259,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1356,7 +1302,6 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1497,7 +1442,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1525,10 +1469,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/api/api.go b/internal/api/api.go deleted file mode 100644 index b6c79a9456..0000000000 --- a/internal/api/api.go +++ /dev/null @@ -1,271 +0,0 @@ -package api - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/pprof" - "runtime" - "sync" - - "github.com/gorilla/mux" - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// OptFunc applies an option to an API type during construction. -type OptFunc func(t *Type) - -// OptWithMiddleware adds an HTTP middleware to the Benthos API. -func OptWithMiddleware(m func(http.Handler) http.Handler) OptFunc { - return func(t *Type) { - t.server.Handler = m(t.server.Handler) - } -} - -// OptWithTLS replaces the tls options of the HTTP server. -func OptWithTLS(tls *tls.Config) OptFunc { - return func(t *Type) { - t.server.TLSConfig = tls - } -} - -//------------------------------------------------------------------------------ - -// Type implements the Benthos HTTP API. -type Type struct { - conf Config - endpoints map[string]string - endpointsMut sync.Mutex - - ctx context.Context - cancel func() - - handlers map[string]http.HandlerFunc - handlersMut sync.RWMutex - - log log.Modular - mux *mux.Router - server *http.Server -} - -// New creates a new Benthos HTTP API. -func New( - version string, - dateBuilt string, - conf Config, - wholeConf any, - log log.Modular, - stats metrics.Type, - opts ...OptFunc, -) (*Type, error) { - gMux := mux.NewRouter() - server := &http.Server{Addr: conf.Address} - - var err error - if server.Handler, err = conf.CORS.WrapHandler(gMux); err != nil { - return nil, fmt.Errorf("bad CORS configuration: %w", err) - } - - if conf.CertFile != "" || conf.KeyFile != "" { - if conf.CertFile == "" || conf.KeyFile == "" { - return nil, errors.New("both cert_file and key_file must be specified, or neither") - } - } - - if err := conf.BasicAuth.Validate(); err != nil { - return nil, err - } - - t := &Type{ - conf: conf, - endpoints: map[string]string{}, - handlers: map[string]http.HandlerFunc{}, - mux: gMux, - server: server, - log: log, - } - t.ctx, t.cancel = context.WithCancel(context.Background()) - - handlePing := func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("pong")) - } - - handleStackTrace := func(w http.ResponseWriter, r *http.Request) { - stackSlice := make([]byte, 1024*100) - s := runtime.Stack(stackSlice, true) - _, _ = w.Write(stackSlice[:s]) - } - - handlePrintJSONConfig := func(w http.ResponseWriter, r *http.Request) { - var g any - var err error - if node, ok := wholeConf.(yaml.Node); ok { - err = node.Decode(&g) - } else { - g = node - } - var resBytes []byte - if err == nil { - resBytes, err = json.Marshal(g) - } - if err != nil { - w.WriteHeader(http.StatusBadGateway) - return - } - _, _ = w.Write(resBytes) - } - - handlePrintYAMLConfig := func(w http.ResponseWriter, r *http.Request) { - resBytes, err := yaml.Marshal(wholeConf) - if err != nil { - w.WriteHeader(http.StatusBadGateway) - return - } - _, _ = w.Write(resBytes) - } - - handleVersion := func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "{\"version\":\"%v\", \"built\":\"%v\"}", version, dateBuilt) - } - - handleEndpoints := func(w http.ResponseWriter, r *http.Request) { - t.endpointsMut.Lock() - defer t.endpointsMut.Unlock() - - resBytes, err := json.Marshal(t.endpoints) - if err != nil { - w.WriteHeader(http.StatusBadGateway) - } else { - _, _ = w.Write(resBytes) - } - } - - if t.conf.DebugEndpoints { - t.RegisterEndpoint( - "/debug/config/json", "DEBUG: Returns the loaded config as JSON.", - handlePrintJSONConfig, - ) - t.RegisterEndpoint( - "/debug/config/yaml", "DEBUG: Returns the loaded config as YAML.", - handlePrintYAMLConfig, - ) - t.RegisterEndpoint( - "/debug/stack", "DEBUG: Returns a snapshot of the current service stack trace.", - handleStackTrace, - ) - t.RegisterEndpoint( - "/debug/pprof/profile", "DEBUG: Responds with a pprof-formatted cpu profile.", - pprof.Profile, - ) - t.RegisterEndpoint( - "/debug/pprof/heap", "DEBUG: Responds with a pprof-formatted heap profile.", - pprof.Index, - ) - t.RegisterEndpoint( - "/debug/pprof/goroutine", "DEBUG: Responds with a pprof-formatted goroutine profile.", - pprof.Index, - ) - t.RegisterEndpoint( - "/debug/pprof/block", "DEBUG: Responds with a pprof-formatted block profile.", - pprof.Index, - ) - t.RegisterEndpoint( - "/debug/pprof/mutex", "DEBUG: Responds with a pprof-formatted mutex profile.", - pprof.Index, - ) - t.RegisterEndpoint( - "/debug/pprof/allocs", "DEBUG: Responds with a pprof-formatted allocs profile.", - pprof.Index, - ) - t.RegisterEndpoint( - "/debug/pprof/symbol", "DEBUG: looks up the program counters listed"+ - " in the request, responding with a table mapping program"+ - " counters to function names.", - pprof.Symbol, - ) - t.RegisterEndpoint( - "/debug/pprof/trace", - "DEBUG: Responds with the execution trace in binary form."+ - " Tracing lasts for duration specified in seconds GET"+ - " parameter, or for 1 second if not specified.", - pprof.Trace, - ) - } - - t.RegisterEndpoint("/ping", "Ping me.", handlePing) - t.RegisterEndpoint("/version", "Returns the service version.", handleVersion) - t.RegisterEndpoint("/endpoints", "Returns this map of endpoints.", handleEndpoints) - - // If we want to expose a stats endpoint we register the endpoints. - if wHandlerFunc := stats.HandlerFunc(); wHandlerFunc != nil { - t.RegisterEndpoint("/stats", "Exposes service-wide metrics in the format configured.", wHandlerFunc) - t.RegisterEndpoint("/metrics", "Exposes service-wide metrics in the format configured.", wHandlerFunc) - } - - for _, opt := range opts { - opt(t) - } - - return t, nil -} - -// Handler returns the underlying http.Hander where paths are registered. -func (t *Type) Handler() http.Handler { - return t.server.Handler -} - -// RegisterEndpoint registers a http.HandlerFunc under a path with a -// description that will be displayed under the /endpoints path. -func (t *Type) RegisterEndpoint(path, desc string, handlerFunc http.HandlerFunc) { - t.endpointsMut.Lock() - defer t.endpointsMut.Unlock() - - t.endpoints[path] = desc - - t.handlersMut.Lock() - defer t.handlersMut.Unlock() - - if _, exists := t.handlers[path]; !exists { - wrapHandler := t.conf.BasicAuth.WrapHandler(func(w http.ResponseWriter, r *http.Request) { - t.handlersMut.RLock() - h := t.handlers[path] - t.handlersMut.RUnlock() - h(w, r) - }) - - GetMuxRoute(t.mux, path).Handler(wrapHandler) - GetMuxRoute(t.mux, t.conf.RootPath+path).Handler(wrapHandler) - } - t.handlers[path] = handlerFunc -} - -// ListenAndServe launches the API and blocks until the server closes or fails. -func (t *Type) ListenAndServe() error { - if !t.conf.Enabled { - <-t.ctx.Done() - return nil - } - t.log.Info( - "Listening for HTTP requests at: %v\n", - "http://"+t.conf.Address, - ) - if t.server.TLSConfig != nil { - return t.server.ListenAndServeTLS("", "") - } - if t.conf.CertFile != "" { - return t.server.ListenAndServeTLS(t.conf.CertFile, t.conf.KeyFile) - } - return t.server.ListenAndServe() -} - -// Shutdown attempts to close the http server. -func (t *Type) Shutdown(ctx context.Context) error { - t.cancel() - return t.server.Shutdown(ctx) -} diff --git a/internal/api/api_test.go b/internal/api/api_test.go deleted file mode 100644 index cd9a1a443c..0000000000 --- a/internal/api/api_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package api_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestAPIEnableCORS(t *testing.T) { - conf := api.NewConfig() - conf.CORS.Enabled = true - conf.CORS.AllowedOrigins = []string{"*"} - - s, err := api.New("", "", conf, nil, log.Noop(), metrics.Noop()) - require.NoError(t, err) - - handler := s.Handler() - - request, _ := http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "meow") - request.Header.Add("Access-Control-Request-Method", "POST") - - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "*", response.Header().Get("Access-Control-Allow-Origin")) -} - -func TestAPIEnableCORSOrigins(t *testing.T) { - conf := api.NewConfig() - conf.CORS.Enabled = true - conf.CORS.AllowedOrigins = []string{"foo", "bar"} - - s, err := api.New("", "", conf, nil, log.Noop(), metrics.Noop()) - require.NoError(t, err) - - handler := s.Handler() - - request, _ := http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "foo") - request.Header.Add("Access-Control-Request-Method", "POST") - - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "foo", response.Header().Get("Access-Control-Allow-Origin")) - - request, _ = http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "bar") - request.Header.Add("Access-Control-Request-Method", "POST") - - response = httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "bar", response.Header().Get("Access-Control-Allow-Origin")) - - request, _ = http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "baz") - request.Header.Add("Access-Control-Request-Method", "POST") - - response = httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "", response.Header().Get("Access-Control-Allow-Origin")) -} - -func TestAPIEnableCORSNoHeaders(t *testing.T) { - conf := api.NewConfig() - conf.CORS.Enabled = true - - _, err := api.New("", "", conf, nil, log.Noop(), metrics.Noop()) - require.Error(t, err) - assert.Contains(t, err.Error(), "must specify at least one allowed origin") -} - -func TestAPIBasicAuth(t *testing.T) { - secret256 := "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" - secretMD5 := "Xr4ilOzQ4PCOq3aQ0qbuaQ==" - secretBcrypt := "JDJhJDEwJHRDMWs2NHc5VjA0R3JIdEE2QzQ2SC5na3o3WWNUUWlQc1RNbVNCUU8yOG8uSWg4YUUvaUcu" - secretScrypt := "eJ01V0+YCJxHraokLp79ijKS0z1jzVQ3vFtGx5UEEiY=" - type testCase struct { - name string - enabled bool - algorithm string - correctUser string - correctPass string - givenUser string - givenPass string - expectedErr assert.ErrorAssertionFunc - expectedCode int - } - - tests := []testCase{ - { - name: "validAuth", enabled: true, algorithm: "sha256", - correctUser: "myuser", correctPass: secret256, - givenUser: "myuser", givenPass: "secret", - expectedErr: assert.NoError, expectedCode: http.StatusOK, - }, - { - name: "validAuthMD5", enabled: true, algorithm: "md5", - correctUser: "myuser", correctPass: secretMD5, - givenUser: "myuser", givenPass: "secret", - expectedErr: assert.NoError, expectedCode: http.StatusOK, - }, - { - name: "validAuthBcrypt", enabled: true, algorithm: "bcrypt", - correctUser: "myuser", correctPass: secretBcrypt, - givenUser: "myuser", givenPass: "secret", - expectedErr: assert.NoError, expectedCode: http.StatusOK, - }, - { - name: "validAuthScrypt", enabled: true, algorithm: "scrypt", - correctUser: "myuser", correctPass: secretScrypt, - givenUser: "myuser", givenPass: "secret", - expectedErr: assert.NoError, expectedCode: http.StatusOK, - }, - { - name: "invalidAuth", enabled: true, algorithm: "sha256", - correctUser: "myuser", correctPass: secret256, - givenUser: "myuser", givenPass: "wrong", - expectedErr: assert.NoError, expectedCode: http.StatusUnauthorized, - }, - { - name: "noAuthGiven", enabled: true, algorithm: "sha256", - correctUser: "myuser", correctPass: secret256, - givenUser: "", givenPass: "", expectedErr: assert.NoError, - expectedCode: http.StatusUnauthorized, - }, - { - name: "disabledAuthWrong", enabled: false, algorithm: "sha256", - correctUser: "myuser", correctPass: secret256, - givenUser: "myuser", givenPass: "wrong", - expectedErr: assert.NoError, expectedCode: http.StatusOK, - }, - { - name: "disabledAuthNoneGiven", enabled: false, algorithm: "sha256", - correctUser: "myuser", correctPass: secret256, - givenUser: "", givenPass: "", - expectedErr: assert.NoError, expectedCode: http.StatusOK, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - conf := api.NewConfig() - conf.BasicAuth.Enabled = tc.enabled - conf.BasicAuth.Algorithm = tc.algorithm - conf.BasicAuth.Username = tc.correctUser - conf.BasicAuth.PasswordHash = tc.correctPass - conf.BasicAuth.Salt = "EzrwNJYw2wkErVVV1P36FQ==" - - s, err := api.New("", "", conf, nil, log.Noop(), metrics.Noop()) - if ok := tc.expectedErr(t, err); !(ok && err == nil) { - return - } - - handler := s.Handler() - - request, _ := http.NewRequest("GET", "/version", http.NoBody) - if tc.givenUser != "" || tc.givenPass != "" { - request.SetBasicAuth(tc.givenUser, tc.givenPass) - } - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, tc.expectedCode, response.Code) - } - }(tc)) - } -} diff --git a/internal/api/config.go b/internal/api/config.go deleted file mode 100644 index 81d6c0b5d7..0000000000 --- a/internal/api/config.go +++ /dev/null @@ -1,71 +0,0 @@ -package api - -import ( - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/httpserver" -) - -const ( - fieldAddress = "address" - fieldEnabled = "enabled" - fieldRootPath = "root_path" - fieldDebugEndpoints = "debug_endpoints" - fieldCertFile = "cert_file" - fieldKeyFile = "key_file" - fieldCORS = "cors" - fieldBasicAuth = "basic_auth" -) - -// Config contains the configuration fields for the Benthos API. -type Config struct { - Address string `json:"address" yaml:"address"` - Enabled bool `json:"enabled" yaml:"enabled"` - RootPath string `json:"root_path" yaml:"root_path"` - DebugEndpoints bool `json:"debug_endpoints" yaml:"debug_endpoints"` - CertFile string `json:"cert_file" yaml:"cert_file"` - KeyFile string `json:"key_file" yaml:"key_file"` - CORS httpserver.CORSConfig `json:"cors" yaml:"cors"` - BasicAuth httpserver.BasicAuthConfig `json:"basic_auth" yaml:"basic_auth"` -} - -// NewConfig creates a new API config with default values. -func NewConfig() Config { - return Config{ - Address: "0.0.0.0:4195", - Enabled: true, - RootPath: "/benthos", - DebugEndpoints: false, - CertFile: "", - KeyFile: "", - CORS: httpserver.NewServerCORSConfig(), - BasicAuth: httpserver.NewBasicAuthConfig(), - } -} - -func FromParsed(pConf *docs.ParsedConfig) (conf Config, err error) { - if conf.Address, err = pConf.FieldString(fieldAddress); err != nil { - return - } - if conf.Enabled, err = pConf.FieldBool(fieldEnabled); err != nil { - return - } - if conf.RootPath, err = pConf.FieldString(fieldRootPath); err != nil { - return - } - if conf.DebugEndpoints, err = pConf.FieldBool(fieldDebugEndpoints); err != nil { - return - } - if conf.CertFile, err = pConf.FieldString(fieldCertFile); err != nil { - return - } - if conf.KeyFile, err = pConf.FieldString(fieldKeyFile); err != nil { - return - } - if conf.CORS, err = httpserver.CORSConfigFromParsed(pConf); err != nil { - return - } - if conf.BasicAuth, err = httpserver.BasicAuthConfigFromParsed(pConf); err != nil { - return - } - return -} diff --git a/internal/api/docs.adoc b/internal/api/docs.adoc deleted file mode 100644 index 9ef81f0af2..0000000000 --- a/internal/api/docs.adoc +++ /dev/null @@ -1,101 +0,0 @@ -= HTTP - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the contents of: - internal/api/docs.adoc -//// - -When {page-component-title} runs it kicks off an HTTP server that provides a few generally useful endpoints and is also where configured components such as the xref:components:inputs/http_server.adoc[`http_server` input] xref:components:outputs/http_server.adoc[and output] can register their own endpoints if they don't require their own host/port. - -The configuration for this server lives under the `http` namespace, with the following default values: - -{{if eq .CommonConfig .AdvancedConfig -}} -```yaml -# Config fields, showing default values -{{.CommonConfig -}} -``` -{{else}} - -[tabs] -====== -Common:: -+ --- - -```yaml -# Common config fields, showing default values -{{.CommonConfig -}} -``` - --- -Advanced:: -+ --- - -```yaml -# All config fields, showing default values -{{.AdvancedConfig -}} -``` --- -====== -{{end -}} - -The field `enabled` can be set to `false` in order to disable the server. - -The field `root_path` specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a `root_path` set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`. - -== Enabling HTTPS - -By default {page-component-title} will serve traffic over HTTP. In order to enforce TLS and serve traffic exclusively over HTTPS you must provide a `cert_file` and `key_file` path in your config, which point to a file containing a certificate and a matching private key for the server respectively. - -If the certificate is signed by a certificate authority, the `cert_file` should be the concatenation of the server's certificate, any intermediates, and the CA's certificate. - -== Enabling basic authentication - -By default {page-component-title} does not do any sort of authentication for the service-wide HTTP server. However, it's possible to configure basic authentication with the <> field. Passwords configured must be hashed according to the specified algorithm and base64 encoded, for some hashing algorithms you can do this using {page-component-title} itself: - -```sh -echo mynewpassword | benthos blobl 'root = content().hash("sha256").encode("base64")' -``` - -== Endpoints - -The following endpoints will be generally available when the HTTP server is enabled: - -- `/version` provides version info. -- `/ping` can be used as a liveness probe as it always returns a 200. -- `/ready` can be used as a readiness probe as it serves a 200 only when both the input and output are connected, otherwise a 503 is returned. -- `/metrics`, `/stats` both provide metrics when the metrics type is either xref:components:metrics/json_api.adoc[`json_api`] or xref:components:metrics/prometheus.adoc[`prometheus`]. -- `/endpoints` provides a JSON object containing a list of available endpoints, including those registered by configured components. - -== CORS - -In order to serve Cross-Origin Resource Sharing headers, which instruct browsers to allow CORS requests, set the subfield `cors.enabled` to `true`. - -=== allowed_origins - -A list of allowed origins to connect from. The literal value `*` can be specified as a wildcard. Note `cors.enabled` must be set to `true` for this list to take effect. - -== Debug endpoints - -The field `debug_endpoints` when set to `true` prompts {page-component-title} to register a few extra endpoints that can be useful for debugging performance or behavioral problems: - -- `/debug/config/json` returns the loaded config as JSON. -- `/debug/config/yaml` returns the loaded config as YAML. -- `/debug/pprof/block` responds with a pprof-formatted block profile. -- `/debug/pprof/heap` responds with a pprof-formatted heap profile. -- `/debug/pprof/mutex` responds with a pprof-formatted mutex profile. -- `/debug/pprof/profile` responds with a pprof-formatted cpu profile. -- `/debug/pprof/goroutine` responds with a pprof-formatted goroutine profile. -- `/debug/pprof/symbol` looks up the program counters listed in the request, responding with a table mapping program counters to function names. -- `/debug/pprof/trace` responds with the execution trace in binary form. Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified. -- `/debug/stack` returns a snapshot of the current service stack trace. - -== Fields - -The schema of the `http` section is as follows: - -{{template "field_docs" . -}} diff --git a/internal/api/docs.go b/internal/api/docs.go deleted file mode 100644 index 21153ecffc..0000000000 --- a/internal/api/docs.go +++ /dev/null @@ -1,93 +0,0 @@ -package api - -import ( - "bytes" - "text/template" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/httpserver" - - _ "embed" -) - -// Spec returns a field spec for the API configuration fields. -func Spec() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldBool(fieldEnabled, "Whether to enable to HTTP server.").HasDefault(true), - docs.FieldString(fieldAddress, "The address to bind to.").HasDefault("0.0.0.0:4195"), - docs.FieldString( - fieldRootPath, "Specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a root_path set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`.", - ).HasDefault("/benthos"), - docs.FieldBool( - fieldDebugEndpoints, "Whether to register a few extra endpoints that can be useful for debugging performance or behavioral problems.", - ).HasDefault(false), - docs.FieldString(fieldCertFile, "An optional certificate file for enabling TLS.").Advanced().HasDefault(""), - docs.FieldString(fieldKeyFile, "An optional key file for enabling TLS.").Advanced().HasDefault(""), - httpserver.ServerCORSFieldSpec(), - httpserver.BasicAuthFieldSpec(), - } -} - -//go:embed docs.adoc -var httpDocs string - -type templateContext struct { - Fields []docs.FieldSpecCtx - CommonConfig string - AdvancedConfig string -} - -// DocsMarkdown returns a markdown document for the http documentation. -func DocsMarkdown() ([]byte, error) { - httpDocsTemplate := docs.FieldsTemplate(false) + httpDocs - - var buf bytes.Buffer - err := template.Must(template.New("http").Parse(httpDocsTemplate)).Execute(&buf, templateContext{ - Fields: docs.FieldObject("", "").WithChildren(Spec()...).FlattenChildrenForDocs(), - CommonConfig: ` -http: - address: 0.0.0.0:4195 - enabled: true - root_path: /benthos - debug_endpoints: false -`, - AdvancedConfig: ` -http: - address: 0.0.0.0:4195 - enabled: true - root_path: /benthos - debug_endpoints: false - cert_file: "" - key_file: "" - cors: - enabled: false - allowed_origins: [] - basic_auth: - enabled: false - username: "" - password_hash: "" - algorithm: "sha256" - salt: "" -`, - }) - - return buf.Bytes(), err -} - -// EndpointCaveats is a documentation section for HTTP components that explains -// some of the caveats in registering endpoints due to their non-deterministic -// ordering and lack of explicit path terminators. -func EndpointCaveats() string { - return ` -[CAUTION] -.Endpoint caveats -==== -Components within a Benthos config will register their respective endpoints in a non-deterministic order. This means that establishing precedence of endpoints that are registered via multiple ` + "`http_server`" + ` inputs or outputs (either within brokers or from cohabiting streams) is not possible in a predictable way. - -This ambiguity makes it difficult to ensure that paths which are both a subset of a path registered by a separate component, and end in a slash (` + "`/`" + `) and will therefore match against all extensions of that path, do not prevent the more specific path from matching against requests. - -It is therefore recommended that you ensure paths of separate components do not collide unless they are explicitly non-competing. - -For example, if you were to deploy two separate ` + "`http_server`" + ` inputs, one with a path ` + "`/foo/`" + ` and the other with a path ` + "`/foo/bar`" + `, it would not be possible to ensure that the path ` + "`/foo/`" + ` does not swallow requests made to ` + "`/foo/bar`" + `. -====` -} diff --git a/internal/api/dynamic_crud.go b/internal/api/dynamic_crud.go deleted file mode 100644 index 7fde2e44c5..0000000000 --- a/internal/api/dynamic_crud.go +++ /dev/null @@ -1,310 +0,0 @@ -package api - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "net/http" - "sync" - "time" - - "github.com/gorilla/mux" - "gopkg.in/yaml.v3" -) - -// dynamicConfMgr maintains a map of config hashes to ids for dynamic -// inputs/outputs and thereby tracks whether a new configuration has changed for -// a particular id. -type dynamicConfMgr struct { - configHashes map[string]string -} - -func newDynamicConfMgr() *dynamicConfMgr { - return &dynamicConfMgr{ - configHashes: map[string]string{}, - } -} - -// Set will cache the config hash as the latest for the id and returns whether -// this hash is different to the previous config. -func (d *dynamicConfMgr) Set(id string, conf []byte) bool { - hasher := sha256.New() - _, _ = hasher.Write(conf) - newHash := hex.EncodeToString(hasher.Sum(nil)) - - if hash, exists := d.configHashes[id]; exists { - if hash == newHash { - // Same config as before, ignore. - return false - } - } - - d.configHashes[id] = newHash - return true -} - -// Matches checks whether a provided config matches an existing config for the -// same id. -func (d *dynamicConfMgr) Matches(id string, conf []byte) bool { - if hash, exists := d.configHashes[id]; exists { - hasher := sha256.New() - _, _ = hasher.Write(conf) - newHash := hex.EncodeToString(hasher.Sum(nil)) - - if hash == newHash { - // Same config as before. - return true - } - } - - return false -} - -// Remove will delete a cached hash for id if there is one. -func (d *dynamicConfMgr) Remove(id string) { - delete(d.configHashes, id) -} - -//------------------------------------------------------------------------------ - -// Dynamic is a type for exposing CRUD operations on dynamic broker -// configurations as an HTTP interface. Events can be registered for listening -// to configuration changes, and these events should be forwarded to the -// dynamic broker. -type Dynamic struct { - onUpdate func(ctx context.Context, id string, conf []byte) error - onDelete func(ctx context.Context, id string) error - - // configs is a map of the latest sanitised configs from our CRUD clients. - configs map[string][]byte - configHashes *dynamicConfMgr - configsMut sync.Mutex - - // ids is a map of dynamic components that are currently active and their - // start times. - ids map[string]time.Time - idsMut sync.Mutex -} - -// NewDynamic creates a new Dynamic API type. -func NewDynamic() *Dynamic { - return &Dynamic{ - onUpdate: func(ctx context.Context, id string, conf []byte) error { return nil }, - onDelete: func(ctx context.Context, id string) error { return nil }, - configs: map[string][]byte{}, - configHashes: newDynamicConfMgr(), - ids: map[string]time.Time{}, - } -} - -//------------------------------------------------------------------------------ - -// OnUpdate registers a func to handle CRUD events where a request wants to set -// a new value for a dynamic configuration. An error should be returned if the -// configuration is invalid or the component failed. -func (d *Dynamic) OnUpdate(onUpdate func(ctx context.Context, id string, conf []byte) error) { - d.onUpdate = onUpdate -} - -// OnDelete registers a func to handle CRUD events where a request wants to -// remove a dynamic configuration. An error should be returned if the component -// failed to close. -func (d *Dynamic) OnDelete(onDelete func(ctx context.Context, id string) error) { - d.onDelete = onDelete -} - -// Stopped should be called whenever an active dynamic component has closed, -// whether by naturally winding down or from a request. -func (d *Dynamic) Stopped(id string) { - d.idsMut.Lock() - defer d.idsMut.Unlock() - - delete(d.ids, id) -} - -// Started should be called whenever an active dynamic component has started -// with a new configuration. A normalised form of the configuration should be -// provided and will be delivered to clients that query the component contents. -func (d *Dynamic) Started(id string, config []byte) { - d.idsMut.Lock() - d.ids[id] = time.Now() - d.idsMut.Unlock() - - if len(config) > 0 { - d.configsMut.Lock() - d.configs[id] = config - d.configsMut.Unlock() - } -} - -//------------------------------------------------------------------------------ - -// HandleList is an http.HandleFunc for returning maps of active dynamic -// components by their id to uptime. -func (d *Dynamic) HandleList(w http.ResponseWriter, r *http.Request) { - var httpErr error - defer func() { - if r.Body != nil { - r.Body.Close() - } - if httpErr != nil { - http.Error(w, "Internal server error", http.StatusBadGateway) - return - } - }() - - type confInfo struct { - Uptime string `json:"uptime"` - Config any `json:"config"` - ConfigRaw string `json:"config_raw"` - } - uptimes := map[string]confInfo{} - - d.idsMut.Lock() - for k, v := range d.ids { - uptimes[k] = confInfo{ - Uptime: time.Since(v).String(), - Config: nil, - ConfigRaw: "", - } - } - d.idsMut.Unlock() - - d.configsMut.Lock() - for k, v := range d.configs { - var confStructured any - if httpErr = yaml.Unmarshal(v, &confStructured); httpErr != nil { - return - } - info := confInfo{ - Uptime: "stopped", - Config: confStructured, - ConfigRaw: string(v), - } - if existingInfo, exists := uptimes[k]; exists { - info.Uptime = existingInfo.Uptime - } - uptimes[k] = info - } - d.configsMut.Unlock() - - var resBytes []byte - if resBytes, httpErr = json.Marshal(uptimes); httpErr == nil { - _, _ = w.Write(resBytes) - } -} - -func (d *Dynamic) HandleUptime(w http.ResponseWriter, r *http.Request) { - id := mux.Vars(r)["id"] - - var uptimeStr string - - d.idsMut.Lock() - if startedAt, exists := d.ids[id]; exists { - uptimeStr = time.Since(startedAt).String() - } - d.idsMut.Unlock() - - if uptimeStr == "" { - d.configsMut.Lock() - if _, exists := d.configs[id]; exists { - uptimeStr = "stopped" - } - d.configsMut.Unlock() - } - if uptimeStr == "" { - http.Error(w, fmt.Sprintf("Dynamic component '%v' is unknown", id), http.StatusNotFound) - return - } - - _, _ = w.Write([]byte(uptimeStr)) -} - -func (d *Dynamic) handleGETInput(w http.ResponseWriter, r *http.Request) error { - id := mux.Vars(r)["id"] - - d.configsMut.Lock() - conf, exists := d.configs[id] - d.configsMut.Unlock() - if !exists { - http.Error(w, fmt.Sprintf("Dynamic component '%v' is not active", id), http.StatusNotFound) - return nil - } - _, _ = w.Write(conf) - return nil -} - -func (d *Dynamic) handlePOSTInput(w http.ResponseWriter, r *http.Request) error { - id := mux.Vars(r)["id"] - - reqBytes, err := io.ReadAll(r.Body) - if err != nil { - return err - } - - d.configsMut.Lock() - matched := d.configHashes.Matches(id, reqBytes) - d.configsMut.Unlock() - if matched { - return nil - } - - if err := d.onUpdate(r.Context(), id, reqBytes); err != nil { - return err - } - - d.configsMut.Lock() - d.configHashes.Set(id, reqBytes) - d.configsMut.Unlock() - return nil -} - -func (d *Dynamic) handleDELInput(w http.ResponseWriter, r *http.Request) error { - id := mux.Vars(r)["id"] - - if err := d.onDelete(r.Context(), id); err != nil { - return err - } - - d.configsMut.Lock() - d.configHashes.Remove(id) - delete(d.configs, id) - d.configsMut.Unlock() - - return nil -} - -// HandleCRUD is an http.HandleFunc for performing CRUD operations on dynamic -// components by their ids. -func (d *Dynamic) HandleCRUD(w http.ResponseWriter, r *http.Request) { - var httpErr error - defer func() { - if r.Body != nil { - r.Body.Close() - } - if httpErr != nil { - http.Error(w, fmt.Sprintf("Error: %v", httpErr), http.StatusBadGateway) - return - } - }() - - id := mux.Vars(r)["id"] - if id == "" { - http.Error(w, "Var `id` must be set", http.StatusBadRequest) - return - } - - switch r.Method { - case "POST": - httpErr = d.handlePOSTInput(w, r) - case "GET": - httpErr = d.handleGETInput(w, r) - case "DELETE": - httpErr = d.handleDELInput(w, r) - default: - httpErr = fmt.Errorf("verb not supported: %v", r.Method) - } -} diff --git a/internal/api/dynamic_crud_test.go b/internal/api/dynamic_crud_test.go deleted file mode 100644 index 3596dc7a54..0000000000 --- a/internal/api/dynamic_crud_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package api - -import ( - "bytes" - "context" - "errors" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" -) - -func TestDynamicConfMgr(t *testing.T) { - hasher := newDynamicConfMgr() - hasher.Remove("foo") - - if hasher.Matches("foo", []byte("test")) { - t.Error("matched hash on non-existing id") - } - - if !hasher.Set("foo", []byte("test")) { - t.Error("Collision on new id") - } - - if !hasher.Matches("foo", []byte("test")) { - t.Error("Non-matched on same content") - } - - if hasher.Matches("foo", []byte("test 2")) { - t.Error("Matched on different content") - } - - if hasher.Set("foo", []byte("test")) { - t.Error("Non-collision on existing id") - } - - if !hasher.Set("foo", []byte("test 2")) { - t.Error("Collision on new content") - } - - if !hasher.Matches("foo", []byte("test 2")) { - t.Error("Non-matched on same content") - } - - if hasher.Matches("foo", []byte("test")) { - t.Error("Matched on different content") - } - - if hasher.Set("foo", []byte("test 2")) { - t.Error("Non-collision on existing content") - } -} - -//------------------------------------------------------------------------------ - -func router(dAPI *Dynamic) *mux.Router { - router := mux.NewRouter() - router.HandleFunc("/inputs", dAPI.HandleList) - router.HandleFunc("/input/{id}", dAPI.HandleCRUD) - return router -} - -func TestDynamicCRUDBadReqs(t *testing.T) { - dAPI := NewDynamic() - r := router(dAPI) - - request, _ := http.NewRequest("DERP", "/input/foo", http.NoBody) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusBadGateway, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } -} - -func TestDynamicDelete(t *testing.T) { - dAPI := NewDynamic() - r := router(dAPI) - - expRemoved := []string{} - removed := []string{} - failRemove := true - - dAPI.OnDelete(func(ctx context.Context, id string) error { - if failRemove { - return errors.New("foo err") - } - removed = append(removed, id) - return nil - }) - dAPI.OnUpdate(func(ctx context.Context, id string, content []byte) error { - t.Error("Unexpected update called") - return nil - }) - - request, _ := http.NewRequest("DELETE", "/input/foo", http.NoBody) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusBadGateway, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - if exp, act := expRemoved, removed; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong collection of removed configs: %v != %v", act, exp) - } - - failRemove = false - expRemoved = append(expRemoved, "foo") - request, _ = http.NewRequest("DELETE", "/input/foo", http.NoBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - if exp, act := expRemoved, removed; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong collection of removed configs: %v != %v", act, exp) - } -} - -func TestDynamicBasicCRUD(t *testing.T) { - dAPI := NewDynamic() - r := router(dAPI) - - deleteExp := "" - var deleteErr error - dAPI.OnDelete(func(ctx context.Context, id string) error { - if exp, act := deleteExp, id; exp != act { - t.Errorf("Wrong content on delete: %v != %v", act, exp) - } - return deleteErr - }) - - updateExp := []byte("hello world") - var updateErr error - dAPI.OnUpdate(func(ctx context.Context, id string, content []byte) error { - if exp, act := updateExp, content; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong content on update: %s != %s", act, exp) - } - return updateErr - }) - - request, _ := http.NewRequest("GET", "/input/foo", http.NoBody) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusNotFound, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - - request, _ = http.NewRequest("POST", "/input/foo", bytes.NewReader([]byte("hello world"))) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - - dAPI.Started("foo", []byte("foo bar")) - - request, _ = http.NewRequest("GET", "/input/foo", http.NoBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - if exp, act := []byte("foo bar"), response.Body.Bytes(); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong content on GET: %s != %s", act, exp) - } - - updateErr = errors.New("this shouldnt happen") - request, _ = http.NewRequest("POST", "/input/foo", bytes.NewReader([]byte("hello world"))) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - - request, _ = http.NewRequest("GET", "/input/foo", http.NoBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected response code: %v != %v", act, exp) - } - if exp, act := []byte("foo bar"), response.Body.Bytes(); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong content on GET: %s != %s", act, exp) - } -} - -func TestDynamicListing(t *testing.T) { - dAPI := NewDynamic() - r := router(dAPI) - - dAPI.OnDelete(func(ctx context.Context, id string) error { - return nil - }) - - dAPI.OnUpdate(func(ctx context.Context, id string, content []byte) error { - return nil - }) - - dAPI.Started("bar", []byte(` -test: sanitised -`)) - - request, _ := http.NewRequest("POST", "/input/foo", bytes.NewReader([]byte(` -test: from crud raw -`))) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - dAPI.Started("foo", []byte(` -test: second sanitised -`)) - - request, _ = http.NewRequest("GET", "/inputs", http.NoBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - expSections := []string{ - `{"bar":{"uptime":"`, - `","config":{"test":"sanitised"}`, - `"foo":{"uptime":"`, - `","config":{"test":"second sanitised"}`, - } - res := response.Body.String() - for _, exp := range expSections { - assert.Contains(t, res, exp) - } - - dAPI.Stopped("foo") - - request, _ = http.NewRequest("DELETE", "/input/bar", http.NoBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - dAPI.Stopped("bar") - - request, _ = http.NewRequest("GET", "/inputs", http.NoBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - assert.Equal(t, `{"foo":{"uptime":"stopped","config":{"test":"second sanitised"},"config_raw":"\ntest: second sanitised\n"}}`, response.Body.String()) -} diff --git a/internal/api/package.go b/internal/api/package.go deleted file mode 100644 index b181837b26..0000000000 --- a/internal/api/package.go +++ /dev/null @@ -1,21 +0,0 @@ -// Package api implements a type used for creating the Benthos HTTP API. -package api - -import ( - "github.com/gorilla/mux" -) - -// GetMuxRoute returns a *mux.Route (the result of calling .Path or .PathPrefix -// on the provided router), where in cases where the path ends in a slash it -// will be treated as a prefix. This isn't ideal but it's as close as we can -// realistically get to the http.ServeMux behaviour with added path variables. -// -// NOTE: Eventually we can move back to http.ServeMux once -// https://github.com/golang/go/issues/61410 is available, and that'll allow us -// to make all paths explicit. -func GetMuxRoute(gMux *mux.Router, path string) *mux.Route { - if path != "" && path[len(path)-1] == '/' { - return gMux.PathPrefix(path) - } - return gMux.Path(path) -} diff --git a/internal/autoretry/auto_retry_list.go b/internal/autoretry/auto_retry_list.go deleted file mode 100644 index 54315670db..0000000000 --- a/internal/autoretry/auto_retry_list.go +++ /dev/null @@ -1,258 +0,0 @@ -package autoretry - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/cenkalti/backoff/v4" -) - -// ErrExhausted is returned by shift calls when there are no pending messages -// and new reads are disabled. -var ErrExhausted = errors.New("retry list is exhausted") - -// AckFunc is a synchronous function that matches the standard acknowledgment -// signature in Benthos. -type AckFunc func(context.Context, error) error - -// ReadFunc is a closure used to obtain a new T, this is done asynchronously -// from retries but the result is given lower priority than retries. If the read -// func returns an error then it is returned as a highest priority. -type ReadFunc[T any] func(context.Context) (t T, aFn AckFunc, err error) - -// MutatorFunc is an optional closure used to mutate a T about to be scheduled -// for retry based on the returned error. This is useful for reducing a batch -// based on a batch error, etc. -type MutatorFunc[T any] func(t T, err error) T - -// The result of a read attempt for a new T, the result of this is lower -// priority than pendingT. -type readT[T any] struct { - t T - aFn AckFunc - err error -} - -type pendingT[T any] struct { - t T - aFn AckFunc - boff backoff.BackOff - attempts int -} - -// List contains a slice of items that are pending an acknowledgement, once -// items are added it's required that all rejected adopted T values are -// recirculated either via TryShift (non-blocking) or Shift. -type List[T any] struct { - // Send is a lower priority than retry - pendingRead *readT[T] - pendingRetry []*pendingT[T] - - reader ReadFunc[T] - mutator MutatorFunc[T] - - readInFlight int - retryInFlight int - cond sync.Cond - readCtx context.Context - readDone func() -} - -// NewList returns a new list of Ts requiring automatic retries. -func NewList[T any](reader ReadFunc[T], mutator MutatorFunc[T]) *List[T] { - if mutator == nil { - mutator = func(t T, err error) T { return t } - } - readCtx, readDone := context.WithCancel(context.Background()) - return &List[T]{ - cond: *sync.NewCond(&sync.Mutex{}), - reader: reader, - mutator: mutator, - readCtx: readCtx, - readDone: readDone, - } -} - -// Adopt a T and its acknowledgement function so that a rejected T is added to -// retry list. Returns a new acknowledgment function that should be propagated -// as it encapsulates the retry logic. -func (l *List[T]) adopt(t T, aFn AckFunc) AckFunc { - l.retryInFlight++ - - boff := backoff.NewExponentialBackOff() - boff.InitialInterval = time.Millisecond - boff.MaxInterval = time.Second - boff.Multiplier = 1.1 - boff.MaxElapsedTime = 0 - - return l.wrapPendingAck(&pendingT[T]{ - t: t, - aFn: aFn, - attempts: 0, - boff: boff, - }) -} - -func (l *List[T]) wrapPendingAck(t *pendingT[T]) AckFunc { - return func(ctx context.Context, err error) error { - l.cond.L.Lock() - defer func() { - // Either outcome is worth broadcasting. - l.cond.Broadcast() - l.cond.L.Unlock() - }() - - if err != nil { - t.t = l.mutator(t.t, err) - l.pendingRetry = append(l.pendingRetry, t) - return nil - } - l.retryInFlight-- - return t.aFn(ctx, nil) - } -} - -func (l *List[T]) dispatchReader() { - var next readT[T] - next.t, next.aFn, next.err = l.reader(l.readCtx) - - l.cond.L.Lock() - l.pendingRead = &next - l.readInFlight-- - l.cond.Broadcast() - l.cond.L.Unlock() -} - -// Shift blocks until either a T needs retrying and returns it, enableRead is -// set true and a new T is ready, or the context is cancelled. Returns -// ErrExhausted if all messages are exhausted and enableRead is set to false. -func (l *List[T]) Shift(ctx context.Context, enableRead bool) (t T, fn AckFunc, err error) { - l.cond.L.Lock() - defer l.cond.L.Unlock() - - ctx, done := context.WithCancel(ctx) - defer done() - go func() { - <-ctx.Done() - l.cond.Broadcast() - }() - - if enableRead && l.readInFlight == 0 && l.pendingRead == nil { - l.readInFlight++ - go l.dispatchReader() - } - - for { - // If we've exhausted all in flight and pending Ts return ErrExhausted. - if !enableRead && l.exhausted() { - err = ErrExhausted - return - } - - // If the context is cancelled return its error. - if err = ctx.Err(); err != nil { - return - } - - // If there's a retry ready to unshift then return it. - var unshifted bool - if t, fn, unshifted = l.tryShift(ctx); unshifted { - return - } - - // If the read attempt succeeded then return it. - if pendingRead := l.pendingRead; pendingRead != nil { - if pendingRead.err == nil { - pendingRead.aFn = l.adopt(pendingRead.t, pendingRead.aFn) - } - l.pendingRead = nil - return pendingRead.t, pendingRead.aFn, pendingRead.err - } - - // Otherwise, wait for either a context cancel or other activity. - l.cond.Wait() - } -} - -func (l *List[T]) tryShift(ctx context.Context) (t T, fn AckFunc, ok bool) { - var resend *pendingT[T] - func() { - lPending := len(l.pendingRetry) - if lPending == 0 { - return - } - - resend = l.pendingRetry[0] - if lPending > 1 { - l.pendingRetry = l.pendingRetry[1:] - } else { - l.pendingRetry = nil - } - }() - - if resend == nil { - return - } - - resend.attempts++ - if resend.attempts > 2 { - // This sleep prevents a busy loop on permanently failed messages. - if tout := resend.boff.NextBackOff(); tout > 0 { - select { - case <-time.After(tout): - case <-ctx.Done(): - return - } - } - } - return resend.t, l.wrapPendingAck(resend), true -} - -// Exhausted returns true if all adopted Ts have been acknowledged. -func (l *List[T]) Exhausted() bool { - l.cond.L.Lock() - defer l.cond.L.Unlock() - return l.exhausted() -} - -func (l *List[T]) exhausted() bool { - return l.readInFlight == 0 && l.retryInFlight == 0 && len(l.pendingRetry) == 0 -} - -// Close any pending read attempts that could be dangling from prior shifts. -func (l *List[T]) Close(ctx context.Context) error { - l.readDone() - return nil -} - -// TODO: Ensure docs around auto retry and all implementations are okay with -// nacks on termination, otherwise we leave them. -// -//nolint:unused // Keeping this around for now. -func (l *List[T]) nackAllPending() error { - l.cond.L.Lock() - defer l.cond.L.Unlock() - - rejectAck := func(fn AckFunc) { - _ = fn(context.Background(), errors.New("message rejected")) - } - - // Wait for all pending activity to end. - for { - if l.retryInFlight <= 0 && l.readInFlight <= 0 { - for _, r := range l.pendingRetry { - go rejectAck(r.aFn) - l.pendingRead = nil - } - l.pendingRetry = nil - if l.pendingRead != nil && l.pendingRead.aFn != nil { - go rejectAck(l.pendingRead.aFn) - l.pendingRead = nil - } - return nil - } - l.cond.Wait() - } -} diff --git a/internal/autoretry/auto_retry_list_test.go b/internal/autoretry/auto_retry_list_test.go deleted file mode 100644 index c45770cf32..0000000000 --- a/internal/autoretry/auto_retry_list_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package autoretry - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var errCustomEOF = errors.New("custom EOF") - -func TestRetryListAllAcks(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - var acked []string - - data := []string{"foo", "bar", "baz"} - l := NewList(func(ctx context.Context) (t string, aFn AckFunc, err error) { - if len(data) == 0 { - err = errCustomEOF - return - } - next := data[0] - data = data[1:] - return next, func(ctx context.Context, err error) error { - acked = append(acked, next) - return nil - }, nil - }, nil) - - res, fooFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "foo", res) - - res, barFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "bar", res) - - res, bazFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "baz", res) - - _, _, err = l.Shift(tCtx, true) - require.Equal(t, errCustomEOF, err) - - assert.NoError(t, bazFn(tCtx, nil)) - assert.NoError(t, barFn(tCtx, nil)) - assert.NoError(t, fooFn(tCtx, nil)) - - assert.Equal(t, []string{ - "baz", "bar", "foo", - }, acked) - - fmt.Println("last shift") - _, _, err = l.Shift(tCtx, false) - assert.Equal(t, ErrExhausted, err) - - require.NoError(t, l.Close(tCtx)) -} - -func TestRetryListNacks(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - var acked []string - - data := []string{"foo", "bar", "baz"} - l := NewList(func(ctx context.Context) (t string, aFn AckFunc, err error) { - if len(data) == 0 { - err = errCustomEOF - return - } - next := data[0] - data = data[1:] - return next, func(ctx context.Context, err error) error { - acked = append(acked, next) - return nil - }, nil - }, nil) - - v, fooFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, barFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "bar", v) - - v, bazFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "baz", v) - - _, _, err = l.Shift(tCtx, true) - require.Equal(t, errCustomEOF, err) - - assert.NoError(t, bazFn(tCtx, errors.New("baz nope"))) - assert.NoError(t, barFn(tCtx, errors.New("bar nope"))) - assert.NoError(t, fooFn(tCtx, errors.New("foo nope"))) - - assert.Equal(t, []string(nil), acked) - - v, bazFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "baz", v) - - v, barFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "bar", v) - assert.NoError(t, barFn(tCtx, errors.New("bar nope again"))) - - v, fooFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "foo", v) - - assert.NoError(t, fooFn(tCtx, nil)) - assert.NoError(t, bazFn(tCtx, nil)) - - assert.Equal(t, []string{ - "foo", "baz", - }, acked) - - v, barFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "bar", v) - - cancelledCtx, done := context.WithTimeout(tCtx, time.Millisecond*50) - defer done() - - _, _, err = l.Shift(cancelledCtx, false) - assert.Equal(t, cancelledCtx.Err(), err) - - assert.NoError(t, barFn(tCtx, nil)) - - assert.Equal(t, []string{ - "foo", "baz", "bar", - }, acked) - - _, _, err = l.Shift(tCtx, false) - assert.Equal(t, ErrExhausted, err) - - require.NoError(t, l.Close(tCtx)) -} - -func TestRetryListNackMutator(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - var acked []string - - data := []string{"foo"} - l := NewList(func(ctx context.Context) (t string, aFn AckFunc, err error) { - if len(data) == 0 { - err = errCustomEOF - return - } - next := data[0] - data = data[1:] - return next, func(ctx context.Context, err error) error { - acked = append(acked, next) - return nil - }, nil - }, func(t string, err error) string { - return t + " and " + err.Error() - }) - - v, fooFn, err := l.Shift(tCtx, true) - require.NoError(t, err) - assert.Equal(t, "foo", v) - - _, _, err = l.Shift(tCtx, true) - require.Equal(t, errCustomEOF, err) - - assert.NoError(t, fooFn(tCtx, errors.New("first error"))) - assert.Equal(t, []string(nil), acked) - - v, fooFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "foo and first error", v) - - assert.NoError(t, fooFn(tCtx, errors.New("second error"))) - assert.Equal(t, []string(nil), acked) - - v, fooFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "foo and first error and second error", v) - - assert.NoError(t, fooFn(tCtx, errors.New("third error"))) - assert.Equal(t, []string(nil), acked) - - v, fooFn, err = l.Shift(tCtx, false) - require.NoError(t, err) - assert.Equal(t, "foo and first error and second error and third error", v) - - assert.NoError(t, fooFn(tCtx, nil)) - - assert.Equal(t, []string{ - "foo", - }, acked) - - _, _, err = l.Shift(tCtx, false) - assert.Equal(t, ErrExhausted, err) - - require.NoError(t, l.Close(tCtx)) -} diff --git a/internal/batch/combined_ack_func.go b/internal/batch/combined_ack_func.go deleted file mode 100644 index 17e603ef2f..0000000000 --- a/internal/batch/combined_ack_func.go +++ /dev/null @@ -1,57 +0,0 @@ -package batch - -import ( - "context" - "sync" -) - -// AckFunc is a common function signature for acknowledging receipt of messages. -type AckFunc func(context.Context, error) error - -// CombinedAcker creates a single ack func closure that aggregates one or more -// derived closures such that only once each derived closure is called the -// singular ack func will trigger. If at least one derived closure receives an -// error the singular ack func will send the first non-nil error received. -type CombinedAcker struct { - mut sync.Mutex - remainingAcks int - err error - root AckFunc -} - -// NewCombinedAcker creates an aggregated that derives one or more ack funcs -// that, once all of which have been called, the provided root ack func is -// called. -func NewCombinedAcker(aFn AckFunc) *CombinedAcker { - return &CombinedAcker{ - remainingAcks: 0, - root: aFn, - } -} - -// Derive creates a new ack func that must be called before the origin ack func -// will be called. It is invalid to derive an ack func after any other -// previously derived funcs have been called. -func (c *CombinedAcker) Derive() AckFunc { - c.mut.Lock() - c.remainingAcks++ - c.mut.Unlock() - - var decrementOnce sync.Once - return func(ctx context.Context, ackErr error) (err error) { - decrementOnce.Do(func() { - c.mut.Lock() - c.remainingAcks-- - remaining := c.remainingAcks - if ackErr != nil { - c.err = ackErr - } - ackErr = c.err - c.mut.Unlock() - if remaining == 0 { - err = c.root(ctx, ackErr) - } - }) - return - } -} diff --git a/internal/batch/combined_ack_func_test.go b/internal/batch/combined_ack_func_test.go deleted file mode 100644 index 9d9c889103..0000000000 --- a/internal/batch/combined_ack_func_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package batch - -import ( - "context" - "errors" - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCombinedAckFunc(t *testing.T) { - var ackCalled int - var ackErr error - combined := NewCombinedAcker(func(c context.Context, e error) error { - ackCalled++ - ackErr = e - return nil - }) - - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - first := combined.Derive() - second := combined.Derive() - third := combined.Derive() - - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - assert.NoError(t, first(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - assert.NoError(t, second(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - assert.NoError(t, third(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 1, ackCalled) - - // Call multiple times - assert.NoError(t, first(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 1, ackCalled) - - assert.NoError(t, second(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 1, ackCalled) - - assert.NoError(t, third(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 1, ackCalled) -} - -func TestCombinedAckError(t *testing.T) { - var ackCalled int - var ackErr error - combined := NewCombinedAcker(func(c context.Context, e error) error { - ackCalled++ - ackErr = e - return nil - }) - - testErr := errors.New("test error") - - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - first := combined.Derive() - second := combined.Derive() - third := combined.Derive() - - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - assert.NoError(t, first(context.Background(), nil)) - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - assert.NoError(t, second(context.Background(), testErr)) - assert.NoError(t, ackErr) - assert.Equal(t, 0, ackCalled) - - assert.NoError(t, third(context.Background(), nil)) - assert.Equal(t, testErr, ackErr) - assert.Equal(t, 1, ackCalled) - - // Call multiple times - assert.NoError(t, first(context.Background(), nil)) - assert.Equal(t, testErr, ackErr) - assert.Equal(t, 1, ackCalled) - - assert.NoError(t, second(context.Background(), nil)) - assert.Equal(t, testErr, ackErr) - assert.Equal(t, 1, ackCalled) - - assert.NoError(t, third(context.Background(), nil)) - assert.Equal(t, testErr, ackErr) - assert.Equal(t, 1, ackCalled) -} - -func TestCombinedAckErrorSync(t *testing.T) { - var ackCalled bool - combined := NewCombinedAcker(func(c context.Context, e error) error { - ackCalled = true - return nil - }) - - var derivedFuncs []AckFunc - for i := 0; i < 1000; i++ { - derivedFuncs = append(derivedFuncs, combined.Derive()) - } - - startChan := make(chan struct{}) - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - i := i - go func() { - defer wg.Done() - <-startChan - - for j := 0; j < 100; j++ { - assert.NoError(t, derivedFuncs[(i*100)+j](context.Background(), nil)) - } - }() - } - - close(startChan) - wg.Wait() - - assert.True(t, ackCalled) -} diff --git a/internal/batch/count.go b/internal/batch/count.go deleted file mode 100644 index a4b07a3ab0..0000000000 --- a/internal/batch/count.go +++ /dev/null @@ -1,64 +0,0 @@ -package batch - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -type batchedCountKeyType int - -const batchedCountKey batchedCountKeyType = iota - -// CtxCollapsedCount attempts to extract the actual number of messages that were -// collapsed into the resulting message part. This value could be greater than 1 -// when users configure processors that archive batched message parts. -func CtxCollapsedCount(ctx context.Context) int { - if v, ok := ctx.Value(batchedCountKey).(int); ok { - return v - } - return 1 -} - -// CollapsedCount attempts to extract the actual number of messages that were -// collapsed into the resulting message part. This value could be greater than 1 -// when users configure processors that archive batched message parts. -func CollapsedCount(p *message.Part) int { - return CtxCollapsedCount(message.GetContext(p)) -} - -// MessageCollapsedCount attempts to extract the actual number of messages that -// were combined into the resulting batched message parts. This value could -// differ from message.Len() when users configure processors that archive -// batched message parts. -func MessageCollapsedCount(m message.Batch) int { - total := 0 - _ = m.Iter(func(i int, p *message.Part) error { - total += CollapsedCount(p) - return nil - }) - return total -} - -// WithCollapsedCount returns a message part with a context indicating that this -// message is the result of collapsing a number of messages. This allows -// downstream components to know how many total messages were combined. -func WithCollapsedCount(p *message.Part, count int) *message.Part { - // Start with the previous length which could also be >1. - ctx := CtxWithCollapsedCount(message.GetContext(p), count) - return message.WithContext(ctx, p) -} - -// CtxWithCollapsedCount returns a message part with a context indicating that this -// message is the result of collapsing a number of messages. This allows -// downstream components to know how many total messages were combined. -func CtxWithCollapsedCount(ctx context.Context, count int) context.Context { - base := 1 - if v, ok := ctx.Value(batchedCountKey).(int); ok { - base = v - } - - // The new length is the previous length plus the total messages put into - // this batch (minus one to prevent double counting the original part). - return context.WithValue(ctx, batchedCountKey, base-1+count) -} diff --git a/internal/batch/count_test.go b/internal/batch/count_test.go deleted file mode 100644 index 3bee4a45b5..0000000000 --- a/internal/batch/count_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package batch - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestCount(t *testing.T) { - p1 := message.NewPart([]byte("foo bar")) - - p2 := WithCollapsedCount(p1, 2) - p3 := WithCollapsedCount(p2, 3) - p4 := WithCollapsedCount(p1, 4) - - assert.Equal(t, 1, CollapsedCount(p1)) - assert.Equal(t, 2, CollapsedCount(p2)) - assert.Equal(t, 4, CollapsedCount(p3)) - assert.Equal(t, 4, CollapsedCount(p4)) -} - -func TestMessageCount(t *testing.T) { - m := message.QuickBatch([][]byte{ - []byte("FOO"), - []byte("BAR"), - []byte("BAZ"), - }) - - assert.Equal(t, 3, MessageCollapsedCount(m)) -} diff --git a/internal/batch/error.go b/internal/batch/error.go deleted file mode 100644 index d2177b0ffd..0000000000 --- a/internal/batch/error.go +++ /dev/null @@ -1,120 +0,0 @@ -// Package batch contains internal utilities for interacting with message -// batches. -package batch - -import ( - "errors" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Error is an error type that also allows storing granular errors for each -// message of a batch. -type Error struct { - err error - erroredBatch message.Batch - partErrors map[int]error -} - -// NewError creates a new batch-wide error, where it's possible to add granular -// errors for individual messages of the batch. -func NewError(msg message.Batch, err error) *Error { - if berr, ok := err.(*Error); ok { - err = berr.Unwrap() - } - return &Error{ - err: err, - erroredBatch: msg, - } -} - -// Failed stores an error state for a particular message of a batch. Returns a -// pointer to the underlying error, allowing the method to be chained. -// -// If Failed is not called then all messages are assumed to have failed. If it -// is called at least once then all message indexes that aren't explicitly -// failed are assumed to have been processed successfully. -func (e *Error) Failed(i int, err error) *Error { - if len(e.erroredBatch) <= i { - return e - } - if e.partErrors == nil { - e.partErrors = make(map[int]error) - } - e.partErrors[i] = err - return e -} - -// IndexedErrors returns the number of indexed errors that have been registered -// for the batch. -func (e *Error) IndexedErrors() int { - return len(e.partErrors) -} - -// XErroredBatch returns the underlying batch associated with the error. -func (e *Error) XErroredBatch() message.Batch { - return e.erroredBatch -} - -// WalkPartsBySource applies a closure to each message that was part of the -// request that caused this error. The closure is provided the message part -// index, a pointer to the part, and its individual error, which may be nil if -// the message itself was processed successfully. The closure returns a bool -// which indicates whether the iteration should be continued. -// -// Important! The order to parts walked is not guaranteed to match that of the -// source batch. It is also possible for any given index to be represented zero, -// one or more times. -func (e *Error) WalkPartsBySource(sourceSortGroup *message.SortGroup, sourceBatch message.Batch, fn func(int, *message.Part, error) bool) { - _ = e.erroredBatch.Iter(func(i int, p *message.Part) error { - index := sourceSortGroup.GetIndex(p) - if index < 0 || index >= len(sourceBatch) { - return nil - } - - var err error - if e.partErrors == nil { - err = e.err - } else { - err = e.partErrors[i] - } - if !fn(index, sourceBatch[index], err) { - return errors.New("stop") - } - return nil - }) -} - -// WalkPartsNaively applies a closure to each message that was part of the -// request that caused this error. The closure is provided the message part -// index, a pointer to the part, and its individual error, which may be nil if -// the message itself was processed successfully. The closure returns a bool -// which indicates whether the iteration should be continued. -// -// WARNING: The shape and order of the errored batch is not guaranteed to match -// that of an origin batch and therefore cannot be used to associate batch -// errors with the origin. Instead, use WalkPartsBySource. -func (e *Error) WalkPartsNaively(fn func(int, *message.Part, error) bool) { - _ = e.erroredBatch.Iter(func(i int, p *message.Part) error { - var err error - if e.partErrors == nil { - err = e.err - } else { - err = e.partErrors[i] - } - if !fn(i, p, err) { - return errors.New("stop") - } - return nil - }) -} - -// Error implements the common error interface. -func (e *Error) Error() string { - return e.err.Error() -} - -// Unwrap returns the underlying common error. -func (e *Error) Unwrap() error { - return e.err -} diff --git a/internal/batch/error_test.go b/internal/batch/error_test.go deleted file mode 100644 index b947a37293..0000000000 --- a/internal/batch/error_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package batch - -import ( - "errors" - "fmt" - "testing" - - "github.com/benthosdev/benthos/v4/internal/message" - - "github.com/stretchr/testify/assert" -) - -func TestErrorBatchAssociation(t *testing.T) { - batch := message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - } - - group, batch := message.NewSortGroup(batch) - - bErr := NewError(batch, errors.New("nope")) - bErr = bErr.Failed(1, errors.New("yas")) - - partsWalked := map[int]string{} - bErr.WalkPartsBySource(group, batch, func(i int, p *message.Part, err error) bool { - partsWalked[i] = fmt.Sprintf("%s - %v", p.AsBytes(), err) - return true - }) - - assert.Equal(t, map[int]string{ - 0: "foo - ", - 1: "bar - yas", - 2: "baz - ", - }, partsWalked) -} - -func TestErrorBatchAssociationShallowCopy(t *testing.T) { - batch := message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - } - - group, batch := message.NewSortGroup(batch) - - bErr := NewError(batch.ShallowCopy(), errors.New("nope")) - bErr = bErr.Failed(1, errors.New("yas")) - - partsWalked := map[int]string{} - bErr.WalkPartsBySource(group, batch, func(i int, p *message.Part, err error) bool { - partsWalked[i] = fmt.Sprintf("%s - %v", p.AsBytes(), err) - return true - }) - - assert.Equal(t, map[int]string{ - 0: "foo - ", - 1: "bar - yas", - 2: "baz - ", - }, partsWalked) -} - -func TestErrorBatchAssociationDeepCopy(t *testing.T) { - batch := message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - } - - group, batch := message.NewSortGroup(batch) - - bErr := NewError(batch.DeepCopy(), errors.New("nope")) - bErr = bErr.Failed(1, errors.New("yas")) - - partsWalked := map[int]string{} - bErr.WalkPartsBySource(group, batch, func(i int, p *message.Part, err error) bool { - partsWalked[i] = fmt.Sprintf("%s - %v", p.AsBytes(), err) - return true - }) - - assert.Equal(t, map[int]string{ - 0: "foo - ", - 1: "bar - yas", - 2: "baz - ", - }, partsWalked) -} - -func TestErrorBatchAssociationNested(t *testing.T) { - batch := message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - } - - group, batch := message.NewSortGroup(batch) - - _, batch2 := message.NewSortGroup(batch.ShallowCopy().DeepCopy()) - - bErr := NewError(batch2, errors.New("nope")) - bErr = bErr.Failed(1, errors.New("yas")) - - partsWalked := map[int]string{} - bErr.WalkPartsBySource(group, batch, func(i int, p *message.Part, err error) bool { - partsWalked[i] = fmt.Sprintf("%s - %v", p.AsBytes(), err) - return true - }) - - assert.Equal(t, map[int]string{ - 0: "foo - ", - 1: "bar - yas", - 2: "baz - ", - }, partsWalked) -} - -func TestErrorBatchAssociationDoubleNested(t *testing.T) { - batch := message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - } - - group, batch := message.NewSortGroup(batch) - - _, batch2 := message.NewSortGroup(batch.ShallowCopy().DeepCopy()) - _, batch3 := message.NewSortGroup(batch2.ShallowCopy().DeepCopy()) - - bErr := NewError(batch3, errors.New("nope")) - bErr = bErr.Failed(1, errors.New("yas")) - - partsWalked := map[int]string{} - bErr.WalkPartsBySource(group, batch, func(i int, p *message.Part, err error) bool { - partsWalked[i] = fmt.Sprintf("%s - %v", p.AsBytes(), err) - return true - }) - - assert.Equal(t, map[int]string{ - 0: "foo - ", - 1: "bar - yas", - 2: "baz - ", - }, partsWalked) -} diff --git a/internal/batch/package.go b/internal/batch/package.go deleted file mode 100644 index 26ae588aa6..0000000000 --- a/internal/batch/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package batch contains internal utilities for interacting with message -// batches. -package batch diff --git a/internal/batch/policy/batchconfig/config.go b/internal/batch/policy/batchconfig/config.go deleted file mode 100644 index 15d9a816ba..0000000000 --- a/internal/batch/policy/batchconfig/config.go +++ /dev/null @@ -1,77 +0,0 @@ -package batchconfig - -import ( - "github.com/benthosdev/benthos/v4/internal/component/processor" -) - -// Config contains configuration parameters for a batch policy. -type Config struct { - ByteSize int `json:"byte_size" yaml:"byte_size"` - Count int `json:"count" yaml:"count"` - Check string `json:"check" yaml:"check"` - Period string `json:"period" yaml:"period"` - Processors []processor.Config `json:"processors" yaml:"processors"` -} - -// NewConfig creates a default PolicyConfig. -func NewConfig() Config { - return Config{ - ByteSize: 0, - Count: 0, - Check: "", - Period: "", - Processors: []processor.Config{}, - } -} - -// IsNoop returns true if this batch policy configuration does nothing. -func (p Config) IsNoop() bool { - if p.ByteSize > 0 { - return false - } - if p.Count > 1 { - return false - } - if p.Check != "" { - return false - } - if p.Period != "" { - return false - } - if len(p.Processors) > 0 { - return false - } - return true -} - -// IsLimited returns true if there's any limit on the batching policy. -func (p Config) IsLimited() bool { - if p.ByteSize > 0 { - return true - } - if p.Count > 0 { - return true - } - if p.Period != "" { - return true - } - if p.Check != "" { - return true - } - return false -} - -// IsHardLimited returns true if there's a realistic limit on the batching -// policy, where checks are not included. -func (p Config) IsHardLimited() bool { - if p.ByteSize > 0 { - return true - } - if p.Count > 0 { - return true - } - if p.Period != "" { - return true - } - return false -} diff --git a/internal/batch/policy/docs.go b/internal/batch/policy/docs.go deleted file mode 100644 index 01454bbb19..0000000000 --- a/internal/batch/policy/docs.go +++ /dev/null @@ -1,74 +0,0 @@ -package policy - -import "github.com/benthosdev/benthos/v4/internal/docs" - -// FieldSpec returns a spec for a common batching field. -func FieldSpec() docs.FieldSpec { - return docs.FieldSpec{ - Name: "batching", - Type: docs.FieldTypeObject, - Description: ` -Allows you to configure a xref:configuration:batching.adoc[batching policy].`, - Examples: []any{ - map[string]any{ - "count": 0, - "byte_size": 5000, - "period": "1s", - }, - map[string]any{ - "count": 10, - "period": "1s", - }, - map[string]any{ - "count": 0, - "period": "1m", - "check": `this.contains("END BATCH")`, - }, - }, - Children: docs.FieldSpecs{ - docs.FieldInt( - "count", - "A number of messages at which the batch should be flushed. If `0` disables count based batching.", - ).HasDefault(0), - docs.FieldInt( - "byte_size", - "An amount of bytes at which the batch should be flushed. If `0` disables size based batching.", - ).HasDefault(0), - docs.FieldString( - "period", - "A period in which an incomplete batch should be flushed regardless of its size.", - "1s", "1m", "500ms", - ).HasDefault(""), - docs.FieldBloblang( - "check", - "A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should end a batch.", - `this.type == "end_of_transaction"`, - ).HasDefault(""), - docs.FieldProcessor( - "processors", - "A list of xref:components:processors/about.adoc[processors] to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op.", - []map[string]any{ - { - "archive": map[string]any{ - "format": "concatenate", - }, - }, - }, - []map[string]any{ - { - "archive": map[string]any{ - "format": "lines", - }, - }, - }, - []map[string]any{ - { - "archive": map[string]any{ - "format": "json_array", - }, - }, - }, - ).Array().Advanced().Optional(), - }, - } -} diff --git a/internal/batch/policy/package.go b/internal/batch/policy/package.go deleted file mode 100644 index c11362431a..0000000000 --- a/internal/batch/policy/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package policy provides tooling for creating and executing Benthos message -// batch policies. -package policy diff --git a/internal/batch/policy/policy.go b/internal/batch/policy/policy.go deleted file mode 100644 index 7387c34e1e..0000000000 --- a/internal/batch/policy/policy.go +++ /dev/null @@ -1,208 +0,0 @@ -package policy - -import ( - "context" - "errors" - "fmt" - "strconv" - "time" - - "github.com/benthosdev/benthos/v4/internal/batch/policy/batchconfig" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - iprocessor "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Batcher implements a batching policy by buffering messages until, based on a -// set of rules, the buffered messages are ready to be sent onwards as a batch. -type Batcher struct { - log log.Modular - - byteSize int - count int - period time.Duration - check *mapping.Executor - procs []iprocessor.V1 - sizeTally int - parts []*message.Part - - triggered bool - lastBatch time.Time - - mSizeBatch metrics.StatCounter - mCountBatch metrics.StatCounter - mPeriodBatch metrics.StatCounter - mCheckBatch metrics.StatCounter -} - -// New creates an empty policy with default rules. -func New(conf batchconfig.Config, mgr bundle.NewManagement) (*Batcher, error) { - if !conf.IsLimited() { - return nil, errors.New("batch policy must have at least one active trigger") - } - if !conf.IsHardLimited() { - mgr.Logger().Warn("Batch policy should have at least one of count, period or byte_size set in order to provide a hard batch ceiling.") - } - var err error - var check *mapping.Executor - if conf.Check != "" { - if check, err = mgr.BloblEnvironment().NewMapping(conf.Check); err != nil { - return nil, fmt.Errorf("failed to parse check: %v", err) - } - } - var period time.Duration - if conf.Period != "" { - if period, err = time.ParseDuration(conf.Period); err != nil { - return nil, fmt.Errorf("failed to parse duration string: %v", err) - } - } - var procs []iprocessor.V1 - for i, pconf := range conf.Processors { - pMgr := mgr.IntoPath("processors", strconv.Itoa(i)) - proc, err := pMgr.NewProcessor(pconf) - if err != nil { - return nil, err - } - procs = append(procs, proc) - } - - batchOn := mgr.Metrics().GetCounterVec("batch_created", "mechanism") - return &Batcher{ - log: mgr.Logger(), - - byteSize: conf.ByteSize, - count: conf.Count, - period: period, - check: check, - procs: procs, - - lastBatch: time.Now(), - - mSizeBatch: batchOn.With("size"), - mCountBatch: batchOn.With("count"), - mPeriodBatch: batchOn.With("period"), - mCheckBatch: batchOn.With("check"), - }, nil -} - -//------------------------------------------------------------------------------ - -// Add a new message part to this batch policy. Returns true if this part -// triggers the conditions of the policy. -func (p *Batcher) Add(part *message.Part) bool { - if p.byteSize > 0 { - // This calculation (serialisation into bytes) is potentially expensive - // so we only do it when there's a byte size based trigger. - p.sizeTally += len(part.AsBytes()) - } - p.parts = append(p.parts, part) - - if !p.triggered && p.count > 0 && len(p.parts) >= p.count { - p.triggered = true - p.mCountBatch.Incr(1) - p.log.Trace("Batching based on count") - } - if !p.triggered && p.byteSize > 0 && p.sizeTally >= p.byteSize { - p.triggered = true - p.mSizeBatch.Incr(1) - p.log.Trace("Batching based on byte_size") - } - if p.check != nil && !p.triggered { - tmpMsg := message.Batch(p.parts) - test, err := p.check.QueryPart(tmpMsg.Len()-1, tmpMsg) - if err != nil { - test = false - p.log.Error("Failed to execute batch check query: %v\n", err) - } - if test { - p.triggered = true - p.mCheckBatch.Incr(1) - p.log.Trace("Batching based on check query") - } - } - return p.triggered || (p.period > 0 && time.Since(p.lastBatch) > p.period) -} - -// Flush clears all messages stored by this batch policy. Returns nil if the -// policy is currently empty. -func (p *Batcher) Flush(ctx context.Context) message.Batch { - var newMsg message.Batch - - resultMsgs := p.flushAny(ctx) - if len(resultMsgs) == 1 { - newMsg = resultMsgs[0] - } else if len(resultMsgs) > 1 { - for _, m := range resultMsgs { - _ = m.Iter(func(_ int, p *message.Part) error { - newMsg = append(newMsg, p) - return nil - }) - } - } - return newMsg -} - -func (p *Batcher) flushAny(ctx context.Context) []message.Batch { - var newMsg message.Batch - if len(p.parts) > 0 { - if !p.triggered && p.period > 0 && time.Since(p.lastBatch) > p.period { - p.mPeriodBatch.Incr(1) - p.log.Trace("Batching based on period") - } - newMsg = message.Batch(p.parts) - } - p.parts = nil - p.sizeTally = 0 - p.lastBatch = time.Now() - p.triggered = false - - if newMsg == nil { - return nil - } - - if len(p.procs) > 0 { - resultMsgs, err := iprocessor.ExecuteAll(ctx, p.procs, newMsg) - if err != nil { - p.log.Error("Batch processors resulted in error: %v, the batch has been dropped.", err) - return nil - } - return resultMsgs - } - - return []message.Batch{newMsg} -} - -// Count returns the number of currently buffered message parts within this -// policy. -func (p *Batcher) Count() int { - return len(p.parts) -} - -// UntilNext returns a duration indicating how long until the current batch -// should be flushed due to a configured period. A negative duration indicates -// a period has not been set. -func (p *Batcher) UntilNext() time.Duration { - if p.period <= 0 { - return -1 - } - tUntil := time.Until(p.lastBatch.Add(p.period)) - if tUntil <= 0 { - tUntil = 1 - } - return tUntil -} - -//------------------------------------------------------------------------------ - -// Close shuts down the policy resources. -func (p *Batcher) Close(ctx context.Context) error { - for _, c := range p.procs { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/batch/policy/policy_test.go b/internal/batch/policy/policy_test.go deleted file mode 100644 index 11b92b596a..0000000000 --- a/internal/batch/policy/policy_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package policy_test - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/batch/policy/batchconfig" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestPolicyNoop(t *testing.T) { - conf := batchconfig.NewConfig() - assert.True(t, conf.IsNoop()) - - conf = batchconfig.NewConfig() - conf.Count = 2 - assert.False(t, conf.IsNoop()) - - conf = batchconfig.NewConfig() - conf.Check = "foo.bar" - assert.False(t, conf.IsNoop()) - - conf = batchconfig.NewConfig() - conf.ByteSize = 10 - assert.False(t, conf.IsNoop()) - - conf = batchconfig.NewConfig() - conf.Period = "10s" - assert.False(t, conf.IsNoop()) -} - -func TestPolicyBasic(t *testing.T) { - conf := batchconfig.NewConfig() - conf.Count = 2 - conf.ByteSize = 0 - - pol, err := policy.New(conf, mock.NewManager()) - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - if v := pol.UntilNext(); v >= 0 { - t.Errorf("Non-negative period: %v", v) - } - - if exp, act := 0, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - - if pol.Add(message.NewPart(exp[0])) { - t.Error("Unexpected batch") - } - if exp, act := 1, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - if !pol.Add(message.NewPart(exp[1])) { - t.Error("Expected batch") - } - if exp, act := 2, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - - msg := pol.Flush(tCtx) - if !reflect.DeepEqual(exp, message.GetAllBytes(msg)) { - t.Errorf("Wrong result: %s != %s", message.GetAllBytes(msg), exp) - } - if exp, act := 0, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - - if msg = pol.Flush(tCtx); msg != nil { - t.Error("Non-nil empty flush") - } -} - -func TestPolicyPeriod(t *testing.T) { - conf := batchconfig.NewConfig() - conf.Period = "300ms" - - pol, err := policy.New(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - if pol.Add(message.NewPart(nil)) { - t.Error("Unexpected batch ready") - } - - if v := pol.UntilNext(); v >= (time.Millisecond*300) || v < (time.Millisecond*100) { - t.Errorf("Wrong period: %v", v) - } - - <-time.After(time.Millisecond * 500) - if v := pol.UntilNext(); v >= (time.Millisecond * 100) { - t.Errorf("Wrong period: %v", v) - } - - if v := pol.Flush(tCtx); v == nil { - t.Error("Nil msgs from flush") - } - - if v := pol.UntilNext(); v >= (time.Millisecond*300) || v < (time.Millisecond*100) { - t.Errorf("Wrong period: %v", v) - } -} - -func TestPolicySize(t *testing.T) { - conf := batchconfig.NewConfig() - conf.ByteSize = 10 - - pol, err := policy.New(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - exp := [][]byte{[]byte("foo bar"), []byte("baz qux")} - - if pol.Add(message.NewPart(exp[0])) { - t.Error("Unexpected batch") - } - if !pol.Add(message.NewPart(exp[1])) { - t.Error("Expected batch") - } - - msg := pol.Flush(tCtx) - if !reflect.DeepEqual(exp, message.GetAllBytes(msg)) { - t.Errorf("Wrong result: %s != %s", message.GetAllBytes(msg), exp) - } - - if msg = pol.Flush(tCtx); msg != nil { - t.Error("Non-nil empty flush") - } -} - -func TestPolicyCheck(t *testing.T) { - conf := batchconfig.NewConfig() - conf.Check = `content() == "bar"` - - pol, err := policy.New(conf, mock.NewManager()) - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - exp := [][]byte{[]byte("foo"), []byte("bar")} - - if pol.Add(message.NewPart(exp[0])) { - t.Error("Unexpected batch") - } - if !pol.Add(message.NewPart(exp[1])) { - t.Error("Expected batch") - } - - msg := pol.Flush(tCtx) - if !reflect.DeepEqual(exp, message.GetAllBytes(msg)) { - t.Errorf("Wrong result: %s != %s", message.GetAllBytes(msg), exp) - } - - if msg = pol.Flush(tCtx); msg != nil { - t.Error("Non-nil empty flush") - } -} - -func TestPolicyCheckAdvanced(t *testing.T) { - conf := batchconfig.NewConfig() - conf.Check = `batch_size() >= 3` - - pol, err := policy.New(conf, mock.NewManager()) - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - exp := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")} - - if pol.Add(message.NewPart(exp[0])) { - t.Error("Unexpected batch") - } - if pol.Add(message.NewPart(exp[1])) { - t.Error("Expected batch") - } - if !pol.Add(message.NewPart(exp[2])) { - t.Error("Expected batch") - } - - msg := pol.Flush(tCtx) - if !reflect.DeepEqual(exp, message.GetAllBytes(msg)) { - t.Errorf("Wrong result: %s != %s", message.GetAllBytes(msg), exp) - } - - if msg = pol.Flush(tCtx); msg != nil { - t.Error("Non-nil empty flush") - } -} - -func TestPolicyArchived(t *testing.T) { - conf := batchconfig.NewConfig() - conf.Count = 2 - conf.ByteSize = 0 - - procConf, err := testutil.ProcessorFromYAML(` -archive: - format: lines -`) - require.NoError(t, err) - - conf.Processors = append(conf.Processors, procConf) - - pol, err := policy.New(conf, mock.NewManager()) - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - exp := [][]byte{[]byte("foo\nbar")} - - assert.False(t, pol.Add(message.NewPart([]byte("foo")))) - assert.Equal(t, 1, pol.Count()) - - assert.True(t, pol.Add(message.NewPart([]byte("bar")))) - assert.Equal(t, 2, pol.Count()) - - msg := pol.Flush(tCtx) - assert.Equal(t, exp, message.GetAllBytes(msg)) - assert.Equal(t, 0, pol.Count()) - - msg = pol.Flush(tCtx) - assert.Nil(t, msg) -} - -func TestPolicySplit(t *testing.T) { - conf := batchconfig.NewConfig() - conf.Count = 2 - conf.ByteSize = 0 - - procConf := processor.NewConfig() - procConf.Type = "split" - - conf.Processors = append(conf.Processors, procConf) - - pol, err := policy.New(conf, mock.NewManager()) - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(func() { - require.NoError(t, pol.Close(tCtx)) - done() - }) - - exp := [][]byte{[]byte("foo"), []byte("bar")} - - if pol.Add(message.NewPart([]byte("foo"))) { - t.Error("Unexpected batch") - } - if exp, act := 1, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - if !pol.Add(message.NewPart([]byte("bar"))) { - t.Error("Expected batch") - } - if exp, act := 2, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - - msg := pol.Flush(tCtx) - if !reflect.DeepEqual(exp, message.GetAllBytes(msg)) { - t.Errorf("Wrong result: %s != %s", message.GetAllBytes(msg), exp) - } - if exp, act := 0, pol.Count(); exp != act { - t.Errorf("Wrong count: %v != %v", act, exp) - } - - if msg = pol.Flush(tCtx); msg != nil { - t.Error("Non-nil empty flush") - } -} diff --git a/internal/bloblang/environment.go b/internal/bloblang/environment.go deleted file mode 100644 index 5164a2daad..0000000000 --- a/internal/bloblang/environment.go +++ /dev/null @@ -1,203 +0,0 @@ -package bloblang - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -// Environment provides an isolated Bloblang environment where the available -// features, functions and methods can be modified. -type Environment struct { - pCtx parser.Context - maxMapRecursion int -} - -// GlobalEnvironment returns the global default environment. Modifying this -// environment will impact all Bloblang parses that aren't initialized with an -// isolated environment, as well as any new environments initialized after the -// changes. -func GlobalEnvironment() *Environment { - return &Environment{ - pCtx: parser.GlobalContext(), - } -} - -// NewEnvironment creates a fresh Bloblang environment, starting with the full -// range of globally defined features (functions and methods), and provides APIs -// for expanding or contracting the features available to this environment. -// -// It's worth using an environment when you need to restrict the access or -// capabilities that certain bloblang mappings have versus others. -// -// For example, an environment could be created that removes any functions for -// accessing environment variables or reading data from the host disk, which -// could be used in certain situations without removing those functions globally -// for all mappings. -func NewEnvironment() *Environment { - return GlobalEnvironment().WithoutFunctions().WithoutMethods() -} - -// NewEmptyEnvironment creates a fresh Bloblang environment starting completely -// empty, where no functions or methods are initially available. -func NewEmptyEnvironment() *Environment { - return &Environment{ - pCtx: parser.EmptyContext(), - } -} - -// NewField attempts to parse and create a dynamic field expression from a -// string. If the expression is invalid an error is returned. -// -// When a parsing error occurs the returned error will be a *parser.Error type, -// which allows you to gain positional and structured error messages. -func (e *Environment) NewField(expr string) (*field.Expression, error) { - f, err := parser.ParseField(e.pCtx, expr) - if err != nil { - return nil, err - } - return f, nil -} - -// NewMapping parses a Bloblang mapping using the Environment to determine the -// features (functions and methods) available to the mapping. -// -// When a parsing error occurs the error will be the type *parser.Error, which -// gives access to the line and column where the error occurred, as well as a -// method for creating a well formatted error message. -func (e *Environment) NewMapping(blobl string) (*mapping.Executor, error) { - exec, err := parser.ParseMapping(e.pCtx, blobl) - if err != nil { - return nil, err - } - if e.maxMapRecursion > 0 { - exec.SetMaxMapRecursion(e.maxMapRecursion) - } - return exec, nil -} - -// Deactivated returns a version of the environment where constructors are -// disabled for all functions and methods, allowing mappings to be parsed and -// validated but not executed. -// -// The underlying register of functions and methods is shared with the target -// environment, and therefore functions/methods registered to this set will also -// be added to the still activated environment. Use the Without methods (with -// empty args if applicable) in order to create a deep copy of the environment -// that is independent of the source. -func (e *Environment) Deactivated() *Environment { - env := *e - env.pCtx = env.pCtx.Deactivated() - return &env -} - -// OnlyPure removes any methods and functions that have been registered but are -// marked as impure. Impure in this context means the method/function is able to -// mutate global state or access machine state (read environment variables, -// files, etc). Note that methods/functions that access the machine clock are -// not marked as pure, so timestamp functions will still work. -func (e *Environment) OnlyPure() *Environment { - env := *e - env.pCtx.Functions = env.pCtx.Functions.OnlyPure() - env.pCtx.Methods = env.pCtx.Methods.OnlyPure() - return &env -} - -// RegisterMethod adds a new Bloblang method to the environment. -func (e *Environment) RegisterMethod(spec query.MethodSpec, ctor query.MethodCtor) error { - return e.pCtx.Methods.Add(spec, ctor) -} - -// RegisterFunction adds a new Bloblang function to the environment. -func (e *Environment) RegisterFunction(spec query.FunctionSpec, ctor query.FunctionCtor) error { - return e.pCtx.Functions.Add(spec, ctor) -} - -// WithImporter returns a new environment where Bloblang imports are performed -// from a new importer. -func (e *Environment) WithImporter(importer parser.Importer) *Environment { - env := *e - env.pCtx = env.pCtx.WithImporter(importer) - return &env -} - -// WithImporterRelativeToFile returns a new environment where any relative -// imports will be made from the directory of the provided file path. The -// provided path can itself be relative (to the current importer directory) or -// absolute. -func (e *Environment) WithImporterRelativeToFile(filePath string) *Environment { - env := *e - env.pCtx = env.pCtx.WithImporterRelativeToFile(filePath) - return &env -} - -// WithDisabledImports returns a version of the environment where imports within -// mappings are disabled entirely. This prevents mappings from accessing files -// from the host disk. -func (e *Environment) WithDisabledImports() *Environment { - env := *e - env.pCtx = env.pCtx.DisabledImports() - return &env -} - -// WithCustomImporter returns a version of the environment where file imports -// are done exclusively through a provided closure function, which takes an -// import path (relative or absolute). -func (e *Environment) WithCustomImporter(fn func(name string) ([]byte, error)) *Environment { - env := *e - env.pCtx = env.pCtx.CustomImporter(fn) - return &env -} - -// WithoutMethods returns a copy of the environment but with a variadic list of -// method names removed. Instantiation of these removed methods within a mapping -// will cause errors at parse time. -func (e *Environment) WithoutMethods(names ...string) *Environment { - env := *e - env.pCtx.Methods = env.pCtx.Methods.Without(names...) - return &env -} - -// WithoutFunctions returns a copy of the environment but with a variadic list -// of function names removed. Instantiation of these removed functions within a -// mapping will cause errors at parse time. -func (e *Environment) WithoutFunctions(names ...string) *Environment { - env := *e - env.pCtx.Functions = env.pCtx.Functions.Without(names...) - return &env -} - -// WithMaxMapRecursion returns a copy of the environment where the maximum -// recursion allowed for maps is set to a given value. If the execution of a -// mapping from this environment matches this number of recursive map calls the -// mapping will error out. -func (e *Environment) WithMaxMapRecursion(n int) *Environment { - env := *e - env.maxMapRecursion = n - return &env -} - -// WalkFunctions executes a provided function argument for every function that -// has been registered to the environment. -func (e *Environment) WalkFunctions(fn func(name string, spec query.FunctionSpec)) { - for _, f := range e.pCtx.Functions.Docs() { - fn(f.Name, f) - } -} - -// WalkMethods executes a provided function argument for every method that has -// been registered to the environment. -func (e *Environment) WalkMethods(fn func(name string, spec query.MethodSpec)) { - for _, m := range e.pCtx.Methods.Docs() { - fn(m.Name, m) - } -} - -// ImportFile attempts to read a file from the target filesystem. This defaults -// to the operating system disk where paths are relative to the bloblang file -// being parsed. However, custom importers can be configured at the environment -// level in order to change where and how files are accessed. -func (e *Environment) ImportFile(name string) ([]byte, error) { - return e.pCtx.ImportFile(name) -} diff --git a/internal/bloblang/field/expression.go b/internal/bloblang/field/expression.go deleted file mode 100644 index 57948ba538..0000000000 --- a/internal/bloblang/field/expression.go +++ /dev/null @@ -1,94 +0,0 @@ -package field - -import ( - "bytes" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Message is an interface type to be given to a function interpolator, it -// allows the function to resolve fields and metadata from a message. -type Message interface { - Get(p int) *message.Part - Len() int -} - -//------------------------------------------------------------------------------ - -// NewExpression creates a field expression from a slice of resolvers. -func NewExpression(resolvers ...Resolver) *Expression { - e := &Expression{ - resolvers: resolvers, - } - var staticBuf bytes.Buffer - for _, r := range resolvers { - if s, is := r.(StaticResolver); is { - _, _ = staticBuf.Write([]byte(s)) - } else { - e.dynamicExpressions++ - } - } - if e.dynamicExpressions > 0 || staticBuf.Len() == 0 { - return e - } - return &Expression{ - static: staticBuf.String(), - } -} - -//------------------------------------------------------------------------------ - -// Expression represents a Benthos dynamic field expression, used to configure -// string fields where the contents should change based on the contents of -// messages and other factors. -// -// Each function here resolves the expression for a particular message of a -// batch, this is why an index is expected. -type Expression struct { - static string - resolvers []Resolver - dynamicExpressions int -} - -func (e *Expression) resolve(index int, msg Message, escaped bool) ([]byte, error) { - if len(e.resolvers) == 1 { - return e.resolvers[0].ResolveBytes(index, msg, escaped) - } - var buf bytes.Buffer - for _, r := range e.resolvers { - b, err := r.ResolveBytes(index, msg, escaped) - if err != nil { - return nil, err - } - _, _ = buf.Write(b) - } - return buf.Bytes(), nil -} - -// NumDynamicExpressions returns the number of dynamic interpolation functions -// within the expression. -func (e *Expression) NumDynamicExpressions() int { - return e.dynamicExpressions -} - -// Bytes returns a byte slice representing the expression resolved for a message -// of a batch. -func (e *Expression) Bytes(index int, msg Message) ([]byte, error) { - if len(e.resolvers) == 0 { - return []byte(e.static), nil - } - return e.resolve(index, msg, false) -} - -// String returns a string representing the expression resolved for a message of -// a batch. -func (e *Expression) String(index int, msg Message) (string, error) { - if len(e.resolvers) == 0 { - return e.static, nil - } - b, err := e.Bytes(index, msg) - if err != nil { - return "", err - } - return string(b), nil -} diff --git a/internal/bloblang/field/expression_test.go b/internal/bloblang/field/expression_test.go deleted file mode 100644 index 7ee80d7acd..0000000000 --- a/internal/bloblang/field/expression_test.go +++ /dev/null @@ -1,371 +0,0 @@ -package field - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestStaticExpressionOptimization(t *testing.T) { - tests := []struct { - input []Resolver - output string - }{ - { - input: []Resolver{ - StaticResolver("a static string"), - }, - output: "a static string", - }, - { - input: []Resolver{ - StaticResolver("multiple "), - StaticResolver("static "), - StaticResolver("strings"), - }, - output: "multiple static strings", - }, - } - - for _, test := range tests { - test := test - t.Run(test.output, func(t *testing.T) { - e := NewExpression(test.input...) - assert.Equal(t, test.output, e.static) - assert.Empty(t, e.resolvers) - }) - } -} - -func TestExpressions(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - tests := map[string]struct { - expression *Expression - output string - numDyn int - messages []easyMsg - index int - errContains string - }{ - "static string": { - expression: NewExpression( - StaticResolver("static string hello world"), - ), - output: `static string hello world`, - }, - "unsuspicious string": { - expression: NewExpression( - StaticResolver("${{! not a thing"), - ), - output: `${{! not a thing`, - }, - "unsuspicious string 2": { - expression: NewExpression( - StaticResolver("${! not a thing"), - ), - output: `${! not a thing`, - }, - "dollar on its own": { - expression: NewExpression( - StaticResolver("hello $ world"), - ), - output: `hello $ world`, - }, - "json function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `{"foo":"bar"}`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - {content: `not json`}, - }, - }, - "json function 2": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `bar`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - }, - }, - "json function 3": { - expression: NewExpression(NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - return fn - }())), - numDyn: 1, - output: `bar`, - index: 1, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json function 4": { - expression: NewExpression(NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - return fn - }())), - numDyn: 1, - output: `{"bar":"baz"}`, - index: 0, - messages: []easyMsg{ - {content: `{"foo":{"bar":"baz"}}`}, - }, - }, - "two json functions": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - return fn - }()), - StaticResolver(" and "), - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "bar") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 2, - output: `foo value and bar value`, - index: 0, - messages: []easyMsg{ - {content: `{"foo":"foo value","bar":"bar value"}`}, - }, - }, - "json_from function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - fn, err = query.InitMethodHelper("from", fn, int64(1)) - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `bar`, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json function gone wrong": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - messages: []easyMsg{ - {content: `not valid json`}, - }, - errContains: "invalid character 'o'", - }, - "json_from function 2": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - fn, err = query.InitMethodHelper("from", fn, int64(0)) - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `null`, - messages: []easyMsg{ - {content: `{}`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json_from function 3": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("json", "foo") - require.NoError(t, err) - fn, err = query.InitMethodHelper("from", fn, int64(-1)) - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `bar`, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "this expression": { - expression: NewExpression( - NewQueryResolver(query.NewFieldFunction("foo")), - ), - numDyn: 1, - output: `bar`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - {content: `not json`}, - }, - }, - "meta function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("meta", "foo") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `from foo`, - messages: []easyMsg{ - {content: `hello world`, meta: map[string]any{ - "foo": "from foo", - "bar": "from bar", - }}, - }, - }, - "metadata function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("meta", "foo") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `from foo`, - messages: []easyMsg{ - {content: `hello world`, meta: map[string]any{ - "foo": "from foo", - "bar": "from bar", - }}, - }, - }, - "metadata function not exist": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("meta", "foo") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `null`, - messages: []easyMsg{ - {content: `hello world`, meta: map[string]any{ - "bar": "from bar", - }}, - }, - }, - "all meta function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("meta") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `{"bar":"from bar","foo":"from foo"}`, - messages: []easyMsg{ - {content: `hello world`, meta: map[string]any{ - "foo": "from foo", - "bar": "from bar", - }}, - }, - }, - "all metadata function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("meta") - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `{"bar":"from bar","foo":"from foo"}`, - messages: []easyMsg{ - {content: `hello world`, meta: map[string]any{ - "foo": "from foo", - "bar": "from bar", - }}, - }, - }, - "meta_from function": { - expression: NewExpression( - NewQueryResolver(func() query.Function { - fn, err := query.InitFunctionHelper("meta", "bar") - require.NoError(t, err) - fn, err = query.InitMethodHelper("from", fn, int64(1)) - require.NoError(t, err) - return fn - }()), - ), - numDyn: 1, - output: `from bar from 1`, - messages: []easyMsg{ - {content: `first`, meta: map[string]any{ - "foo": "from foo from 0", - "bar": "from bar from 0", - }}, - {content: `second`, meta: map[string]any{ - "foo": "from foo from 1", - "bar": "from bar from 1", - }}, - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - assert.Equal(t, test.numDyn, test.expression.NumDynamicExpressions()) - res, err := test.expression.String(test.index, msg) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } - }) - } -} diff --git a/internal/bloblang/field/package.go b/internal/bloblang/field/package.go deleted file mode 100644 index dffa5e443f..0000000000 --- a/internal/bloblang/field/package.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package field implements a bloblang interpolation function templating syntax -// used in some dynamic fields within Benthos. Only the query (right-hand side) -// part of the bloblang spec is supported within interpolation functions. -package field diff --git a/internal/bloblang/field/resolver.go b/internal/bloblang/field/resolver.go deleted file mode 100644 index ac364d44b3..0000000000 --- a/internal/bloblang/field/resolver.go +++ /dev/null @@ -1,94 +0,0 @@ -package field - -import ( - "strconv" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Resolver is an interface for resolving a string containing Bloblang function -// interpolations into either a string or bytes. -type Resolver interface { - ResolveString(index int, msg Message, escaped bool) (string, error) - ResolveBytes(index int, msg Message, escaped bool) ([]byte, error) -} - -//------------------------------------------------------------------------------ - -// StaticResolver is a Resolver implementation that simply returns a static -// string. -type StaticResolver string - -// ResolveString returns a string. -func (s StaticResolver) ResolveString(index int, msg Message, escaped bool) (string, error) { - return string(s), nil -} - -// ResolveBytes returns a byte slice. -func (s StaticResolver) ResolveBytes(index int, msg Message, escaped bool) ([]byte, error) { - return []byte(s), nil -} - -//------------------------------------------------------------------------------ - -// QueryResolver executes a query and returns a string representation of the -// result. -type QueryResolver struct { - fn query.Function -} - -// NewQueryResolver creates a field query resolver that returns the result of a -// query function. -func NewQueryResolver(fn query.Function) *QueryResolver { - return &QueryResolver{fn} -} - -// ResolveString returns a string. -func (q QueryResolver) ResolveString(index int, msg Message, escaped bool) (string, error) { - if msg == nil { - msg = message.QuickBatch(nil) - } - return query.ExecToString(q.fn, query.FunctionContext{ - Index: index, - MsgBatch: msg, - NewMeta: msg.Get(index), - }.WithValueFunc(func() *any { - if jObj, err := msg.Get(index).AsStructured(); err == nil { - return &jObj - } - return nil - })) -} - -// ResolveBytes returns a byte slice. -func (q QueryResolver) ResolveBytes(index int, msg Message, escaped bool) ([]byte, error) { - if msg == nil { - msg = message.QuickBatch(nil) - } - bs, err := query.ExecToBytes(q.fn, query.FunctionContext{ - Index: index, - MsgBatch: msg, - NewMeta: msg.Get(index), - }.WithValueFunc(func() *any { - if jObj, err := msg.Get(index).AsStructured(); err == nil { - return &jObj - } - return nil - })) - if err != nil { - return nil, err - } - if escaped { - bs = escapeBytes(bs) - } - return bs, nil -} - -func escapeBytes(in []byte) []byte { - quoted := strconv.Quote(string(in)) - if len(quoted) < 3 { - return in - } - return []byte(quoted[1 : len(quoted)-1]) -} diff --git a/internal/bloblang/mapping/assignment.go b/internal/bloblang/mapping/assignment.go deleted file mode 100644 index ea626b94ba..0000000000 --- a/internal/bloblang/mapping/assignment.go +++ /dev/null @@ -1,227 +0,0 @@ -package mapping - -import ( - "errors" - "fmt" - "strconv" - "strings" - - "github.com/Jeffail/gabs/v2" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" -) - -//------------------------------------------------------------------------------ - -type metaMsg interface { - MetaSetMut(key string, value any) - MetaDelete(key string) - MetaIterMut(f func(k string, v any) error) error -} - -// AssignmentContext contains references to all potential assignment -// destinations of a given mapping. -type AssignmentContext struct { - Vars map[string]any - Meta metaMsg - Value *any -} - -// Assignment represents a way of assigning a queried value to something within -// an assignment context. This could be a Benthos message, a variable, a -// metadata field, etc. -type Assignment interface { - Apply(value any, ctx AssignmentContext) error - Target() TargetPath -} - -//------------------------------------------------------------------------------ - -// VarAssignment creates a variable and assigns it a value. -type VarAssignment struct { - name string -} - -// NewVarAssignment creates a new variable assignment. -func NewVarAssignment(name string) *VarAssignment { - return &VarAssignment{ - name: name, - } -} - -// Apply a value to a variable. -func (v *VarAssignment) Apply(val any, ctx AssignmentContext) error { - if _, deleted := val.(value.Delete); deleted { - delete(ctx.Vars, v.name) - } else { - ctx.Vars[v.name] = val - } - return nil -} - -// Target returns a representation of what the assignment targets. -func (v *VarAssignment) Target() TargetPath { - return NewTargetPath(TargetVariable, v.name) -} - -//------------------------------------------------------------------------------ - -// MetaAssignment assigns a value to a metadata key of a message. If the key is -// omitted and the value is an object then the metadata of the message is reset -// to the contents of the value. -type MetaAssignment struct { - key *string -} - -// NewMetaAssignment creates a new meta assignment. -func NewMetaAssignment(key *string) *MetaAssignment { - return &MetaAssignment{ - key: key, - } -} - -// Apply a value to a metadata key. -func (m *MetaAssignment) Apply(val any, ctx AssignmentContext) error { - if ctx.Meta == nil { - return errors.New("unable to assign metadata in the current context") - } - _, deleted := val.(value.Delete) - if !deleted { - val = value.IClone(val) - } - if m.key == nil { - if deleted { - _ = ctx.Meta.MetaIterMut(func(k string, _ any) error { - ctx.Meta.MetaDelete(k) - return nil - }) - } else { - if m, ok := val.(map[string]any); ok { - _ = ctx.Meta.MetaIterMut(func(k string, _ any) error { - ctx.Meta.MetaDelete(k) - return nil - }) - for k, v := range m { - ctx.Meta.MetaSetMut(k, v) - } - } else { - return fmt.Errorf("setting root meta object requires object value, received: %T", val) - } - } - return nil - } - if deleted { - ctx.Meta.MetaDelete(*m.key) - } else { - ctx.Meta.MetaSetMut(*m.key, val) - } - return nil -} - -// Target returns a representation of what the assignment targets. -func (m *MetaAssignment) Target() TargetPath { - var path []string - if m.key != nil { - path = []string{*m.key} - } - return NewTargetPath(TargetMetadata, path...) -} - -//------------------------------------------------------------------------------ - -// JSONAssignment creates a path within the structured message and assigns it a -// value. -type JSONAssignment struct { - path []string -} - -// NewJSONAssignment creates a new JSON assignment. -func NewJSONAssignment(path ...string) *JSONAssignment { - return &JSONAssignment{ - path: path, - } -} - -func findTheNonObject(gObj *gabs.Container, allowArray bool, paths ...string) (culprit, typeStr string) { - if _, isObj := gObj.Data().(map[string]any); !isObj { - return "", string(value.ITypeOf(gObj.Data())) - } - - var culpritSlice []string - for _, path := range paths { - culpritSlice = append(culpritSlice, query.SliceToDotPath(path)) - gObj = gObj.S(path) - - _, isObj := gObj.Data().(map[string]any) - _, isArray := gObj.Data().([]any) - if !isObj && (!isArray || !allowArray) { - return strings.Join(culpritSlice, "."), string(value.ITypeOf(gObj.Data())) - } - } - - return strings.Join(culpritSlice, "."), string(value.ITypeOf(gObj.Data())) -} - -// Apply a value to the target JSON path. -func (j *JSONAssignment) Apply(val any, ctx AssignmentContext) error { - _, deleted := val.(value.Delete) - if !deleted { - val = value.IClone(val) - } - if len(j.path) == 0 { - *ctx.Value = val - return nil - } - if _, isNothing := (*ctx.Value).(value.Nothing); isNothing || *ctx.Value == nil { - *ctx.Value = map[string]any{} - } - - gObj := gabs.Wrap(*ctx.Value) - if deleted { - if len(j.path) > 0 { - _ = gObj.Delete(j.path...) - } - } else { - _, err := gObj.Set(val, j.path...) - if err != nil && err.Error() == "unable to append new array index at root of path" { - if s, ok := (*ctx.Value).([]any); ok { - newPath := make([]string, len(j.path)) - copy(newPath, j.path) - newPath[0] = strconv.Itoa(len(s)) - gObj = gabs.Wrap(append(s, map[string]any{})) - _, err = gObj.Set(val, newPath...) - } - } - if err != nil { - if errors.Is(err, gabs.ErrPathCollision) { - culprit, typeStr := findTheNonObject(gObj, false, j.path...) - if culprit == "" { - return fmt.Errorf( - "unable to set target path %v as the value of the root was a non-object type (%v)", - query.SliceToDotPath(j.path...), typeStr, - ) - } - return fmt.Errorf( - "unable to set target path %v as the value of %v was a non-object type (%v)", - query.SliceToDotPath(j.path...), culprit, typeStr, - ) - } - return fmt.Errorf("unable to set target path %v: %w", query.SliceToDotPath(j.path...), err) - } - } - *ctx.Value = gObj.Data() - return nil -} - -// Target returns a representation of what the assignment targets. -func (j *JSONAssignment) Target() TargetPath { - var path []string - if len(j.path) > 0 { - path = make([]string, len(j.path)) - copy(path, j.path) - } - return NewTargetPath(TargetValue, path...) -} - -//------------------------------------------------------------------------------ diff --git a/internal/bloblang/mapping/executor.go b/internal/bloblang/mapping/executor.go deleted file mode 100644 index f4e00ba6fd..0000000000 --- a/internal/bloblang/mapping/executor.go +++ /dev/null @@ -1,332 +0,0 @@ -package mapping - -import ( - "errors" - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -// Message is an interface type to be given to a query function, it allows the -// function to resolve fields and metadata from a message. -type Message interface { - Get(p int) *message.Part - Len() int -} - -//------------------------------------------------------------------------------ - -// LineAndColOf returns the line and column position of a tailing clip from an -// input. -func LineAndColOf(input, clip []rune) (line, col int) { - char := len(input) - len(clip) - - lines := strings.Split(string(input), "\n") - for ; line < len(lines); line++ { - if char < (len(lines[line]) + 1) { - break - } - char = char - len(lines[line]) - 1 - } - - return line + 1, char + 1 -} - -//------------------------------------------------------------------------------ - -// Executor is a parsed bloblang mapping that can be executed on a Benthos -// message. -type Executor struct { - annotation string - input []rune - maps map[string]query.Function - statements []Statement - - maxMapStacks int -} - -const defaultMaxMapStacks = 5000 - -// NewExecutor initialises a new mapping executor from a map of query functions, -// and a list of assignments to be executed on each mapping. The input parameter -// is an optional slice pointing to the parsed expression that created the -// executor. -func NewExecutor(annotation string, input []rune, maps map[string]query.Function, statements ...Statement) *Executor { - return &Executor{ - annotation: annotation, - input: input, - maps: maps, - statements: statements, - maxMapStacks: defaultMaxMapStacks, - } -} - -// SetMaxMapRecursion configures the maximum recursion allowed for maps, if the -// execution of this mapping matches this number of recursive map calls the -// mapping will error out. -func (e *Executor) SetMaxMapRecursion(m int) { - e.maxMapStacks = m -} - -// Annotation returns a string annotation that describes the mapping executor. -func (e *Executor) Annotation() string { - return e.annotation -} - -// Maps returns any map definitions contained within the mapping. -func (e *Executor) Maps() map[string]query.Function { - return e.maps -} - -// QueryPart executes the bloblang mapping on a particular message index of a -// batch. The message is parsed as a JSON document in order to provide the -// mapping context. The result of the mapping is expected to be a boolean value -// at the root, this is not the case, or if any stage of the mapping fails to -// execute, an error is returned. -func (e *Executor) QueryPart(index int, msg Message) (bool, error) { - newPart, err := e.MapPart(index, msg) - if err != nil { - return false, err - } - if newPart == nil { - return false, errors.New("query mapping resulted in deleted message, expected a boolean value") - } - newValue, err := newPart.AsStructured() - if err != nil { - return false, err - } - if b, ok := newValue.(bool); ok { - return b, nil - } - return false, value.NewTypeErrorFrom("mapping", newValue, value.TBool) -} - -// MapPart executes the bloblang mapping on a particular message index of a -// batch. The message is parsed as a JSON document in order to provide the -// mapping context. Returns an error if any stage of the mapping fails to -// execute. -// -// A resulting mapped message part is returned, unless the mapping results in a -// value.Delete value, in which case nil is returned and the part should be -// discarded. -func (e *Executor) MapPart(index int, msg Message) (*message.Part, error) { - return e.mapPart(nil, index, msg) -} - -// MapOnto maps into an existing message part, where mappings are appended to -// the message rather than being used to construct a new message. -func (e *Executor) MapOnto(part *message.Part, index int, msg Message) (*message.Part, error) { - return e.mapPart(part, index, msg) -} - -func (e *Executor) mapPart(appendTo *message.Part, index int, reference Message) (*message.Part, error) { - var valuePtr *any - var parseErr error - - lazyValue := func() *any { - if valuePtr == nil && parseErr == nil { - if jObj, err := reference.Get(index).AsStructured(); err == nil { - valuePtr = &jObj - } else { - if errors.Is(err, message.ErrMessagePartNotExist) { - parseErr = errors.New("message is empty") - } else { - parseErr = fmt.Errorf("parse as json: %w", err) - } - } - } - return valuePtr - } - - var newPart *message.Part - var newValue any = value.Nothing(nil) - - if appendTo == nil { - newPart = reference.Get(index).ShallowCopy() - } else { - newPart = appendTo - if appendObj, err := appendTo.AsStructuredMut(); err == nil { - newValue = appendObj - } - } - - vars := map[string]any{} - - for _, stmt := range e.statements { - err := stmt.Execute(query.FunctionContext{ - Maps: e.maps, - Vars: vars, - Index: index, - MsgBatch: reference, - NewMeta: newPart, - NewValue: &newValue, - }.WithValueFunc(lazyValue), - AssignmentContext{ - Vars: vars, - Meta: newPart, - Value: &newValue, - }, - ) - if err != nil { - var line int - if len(e.input) > 0 && len(stmt.Input()) > 0 { - line, _ = LineAndColOf(e.input, stmt.Input()) - } - var ctxErr query.ErrNoContext - if parseErr != nil && errors.As(err, &ctxErr) { - if ctxErr.FieldName != "" { - err = fmt.Errorf("unable to reference message as structured (with 'this.%v'): %w", ctxErr.FieldName, parseErr) - } else { - err = fmt.Errorf("unable to reference message as structured (with 'this'): %w", parseErr) - } - } - return nil, fmt.Errorf("failed assignment (line %v): %w", line, err) - } - } - - switch newValue.(type) { - case value.Delete: - // Return nil (filter the message part) - return nil, nil - case value.Nothing: - // Do not change the original contents - default: - switch t := newValue.(type) { - case string: - newPart.SetBytes([]byte(t)) - case []byte: - newPart.SetBytes(t) - default: - newPart.SetStructuredMut(newValue) - } - } - return newPart, nil -} - -// QueryTargets returns a slice of all targets referenced by queries within the -// mapping. -func (e *Executor) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) { - // Reset maps to our own. - childCtx := ctx - childCtx.Maps = e.maps - - var paths []query.TargetPath - for _, stmt := range e.statements { - _, tmpPaths := stmt.QueryTargets(childCtx) - paths = append(paths, tmpPaths...) - } - - return ctx, paths -} - -// AssignmentTargets returns a slice of all targets assigned to by statements -// within the mapping. -func (e *Executor) AssignmentTargets() []TargetPath { - var paths []TargetPath - for _, stmt := range e.statements { - paths = append(paths, stmt.AssignmentTargets()...) - } - return paths -} - -// Exec this function with a context struct. -func (e *Executor) Exec(ctx query.FunctionContext) (any, error) { - ctx, stackCount := ctx.IncrStackCount() - if stackCount > e.maxMapStacks { - return nil, &errStacks{annotation: e.annotation, maxStacks: e.maxMapStacks} - } - - var newObj any = value.Nothing(nil) - ctx.NewValue = &newObj - - for _, stmt := range e.statements { - if err := stmt.Execute(ctx, AssignmentContext{ - Vars: ctx.Vars, - // Meta: meta, Prevented for now due to .from(int) - Value: &newObj, - }); err != nil { - return nil, formatExecErr(err, e.input, stmt.Input()) - } - } - - return newObj, nil -} - -// ExecOnto a provided assignment context. -func (e *Executor) ExecOnto(ctx query.FunctionContext, onto AssignmentContext) error { - for _, stmt := range e.statements { - if err := stmt.Execute(ctx, onto); err != nil { - return formatExecErr(err, e.input, stmt.Input()) - } - } - return nil -} - -// ToBytes executes this function for a message of a batch and returns the -// result marshalled into a byte slice. -func (e *Executor) ToBytes(ctx query.FunctionContext) ([]byte, error) { - v, err := e.Exec(ctx) - if err != nil { - return nil, err - } - return value.IToBytes(v), nil -} - -// ToString executes this function for a message of a batch and returns the -// result marshalled into a string. -func (e *Executor) ToString(ctx query.FunctionContext) (string, error) { - v, err := e.Exec(ctx) - if err != nil { - return "", err - } - return value.IToString(v), nil -} - -//------------------------------------------------------------------------------ - -type failedAssignmentErr struct { - line int - err error -} - -func (f *failedAssignmentErr) Unwrap() error { - return f.err -} - -func (f *failedAssignmentErr) Error() string { - return fmt.Sprintf("failed assignment (line %v): %v", f.line, f.err) -} - -type errStacks struct { - annotation string - maxStacks int -} - -func (e *errStacks) Error() string { - return fmt.Sprintf("entering %v exceeded maximum allowed stacks of %v, this could be due to unbounded recursion", e.annotation, e.maxStacks) -} - -func formatExecErr(err error, input, stmtInput []rune) error { - var u *failedAssignmentErr - if errors.As(err, &u) { - return u - } - - var line int - if len(input) > 0 && len(stmtInput) > 0 { - line, _ = LineAndColOf(input, stmtInput) - } - - var e *errStacks - if errors.As(err, &e) { - err = e - } - - return &failedAssignmentErr{ - line: line, - err: err, - } -} diff --git a/internal/bloblang/mapping/executor_test.go b/internal/bloblang/mapping/executor_test.go deleted file mode 100644 index 3266f58b6d..0000000000 --- a/internal/bloblang/mapping/executor_test.go +++ /dev/null @@ -1,615 +0,0 @@ -package mapping - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestAssignments(t *testing.T) { - type part struct { - Content string - Meta map[string]any - } - - metaKey := func(k string) *string { - return &k - } - - initFunc := func(name string, args ...any) query.Function { - t.Helper() - fn, err := query.InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - index int - input []part - mapping *Executor - output *part - err error - }{ - "simple json map": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - NewSingleStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), - NewSingleStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), - ), - input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, - output: &part{Content: `{"bar":"test2","foo":"test1"}`}, - }, - "map to root": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", "bar")), - ), - input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, - output: &part{Content: `bar`}, - }, - "append array at root": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", []any{})), - NewSingleStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "foo")), - NewSingleStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "bar")), - NewSingleStatement(nil, NewJSONAssignment("-"), query.NewLiteralFunction("", "baz")), - ), - input: []part{{Content: `[]`}}, - output: &part{Content: `["foo","bar","baz"]`}, - }, - "append array at root nested": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", []any{})), - NewSingleStatement(nil, NewJSONAssignment("-", "A"), query.NewLiteralFunction("", "foo")), - NewSingleStatement(nil, NewJSONAssignment("-", "B"), query.NewLiteralFunction("", "bar")), - NewSingleStatement(nil, NewJSONAssignment("-", "C"), query.NewLiteralFunction("", "baz")), - ), - input: []part{{Content: `{}`}}, - output: &part{Content: `[{"A":"foo"},{"B":"bar"},{"C":"baz"}]`}, - }, - "delete root": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", value.Delete(nil))), - ), - input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, - output: nil, - }, - "no mapping to root": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", value.Nothing(nil))), - ), - input: []part{{Content: `{"bar":"test1","zed":"gone"}`}}, - output: &part{Content: `{"bar":"test1","zed":"gone"}`}, - }, - "variable error DNE": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewVarFunction("doesnt exist")), - ), - input: []part{{Content: `{}`}}, - err: errors.New("failed assignment (line 0): variable 'doesnt exist' undefined"), - }, - "variable assignment": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewVarAssignment("foo"), query.NewLiteralFunction("", "does exist")), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewVarFunction("foo")), - ), - input: []part{{Content: `{}`}}, - output: &part{Content: `{"foo":"does exist"}`}, - }, - "meta query error": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), initFunc("meta", "foo")), - ), - input: []part{{Content: `{}`}}, - output: &part{Content: `{"foo":null}`}, - }, - "meta assignment": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "exists now")), - ), - input: []part{{Content: `{}`}}, - output: &part{ - Content: `{}`, - Meta: map[string]any{ - "foo": "exists now", - }, - }, - }, - "meta deletion": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(metaKey("and")), query.NewLiteralFunction("", value.Delete(nil))), - ), - input: []part{{ - Content: `{}`, - Meta: map[string]any{ - "ignore": "me", - "and": "delete me", - }, - }}, - output: &part{ - Content: `{}`, - Meta: map[string]any{ - "ignore": "me", - }, - }, - }, - "meta set all error wrong type": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", "foo")), - ), - input: []part{{Content: `{}`}}, - err: errors.New("failed assignment (line 0): setting root meta object requires object value, received: string"), - }, - "meta set all": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", map[string]any{ - "new1": "value1", - "new2": "value2", - })), - ), - input: []part{{ - Content: `{}`, - Meta: map[string]any{ - "foo": "first", - "bar": "second", - }, - }}, - output: &part{ - Content: `{}`, - Meta: map[string]any{ - "new1": "value1", - "new2": "value2", - }, - }, - }, - "meta delete all": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", value.Delete(nil))), - ), - input: []part{{ - Content: `{}`, - Meta: map[string]any{ - "foo": "first", - "bar": "second", - }, - }}, - output: &part{Content: `{}`}, - }, - "metadata assignment": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "new value")), - NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), initFunc("meta", "foo")), - ), - input: []part{{ - Content: `{}`, - Meta: map[string]any{ - "foo": "old value", - }, - }}, - output: &part{ - Content: `{}`, - Meta: map[string]any{ - "foo": "new value", - "bar": "old value", - }, - }, - }, - "root_metadata assignment": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "exists now")), - NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), initFunc("root_meta", "foo")), - ), - input: []part{{Content: `{}`}}, - output: &part{ - Content: `{}`, - Meta: map[string]any{ - "foo": "exists now", - "bar": "exists now", - }, - }, - }, - "invalid json message": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - NewSingleStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), - ), - input: []part{{Content: `{@#$ not valid json`}}, - err: errors.New("failed assignment (line 0): unable to reference message as structured (with 'this.bar'): parse as json: invalid character '@' looking for beginning of object key string"), - }, - "json parse empty message": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("bar"), query.NewLiteralFunction("", "test2")), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - NewSingleStatement(nil, NewJSONAssignment("zed"), query.NewLiteralFunction("", value.Delete(nil))), - ), - input: []part{{Content: ``}}, - err: errors.New("failed assignment (line 0): unable to reference message as structured (with 'this.bar'): message is empty"), - }, - "root if statements": { - mapping: NewExecutor("", nil, nil, - NewRootLevelIfStatement(nil).Add( - query.NewLiteralFunction("", true), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "bar")), - ), - ), - input: []part{{Content: `{}`}}, - output: &part{Content: `{"foo":"bar"}`}, - }, - "root if statements if/else": { - mapping: NewExecutor("", nil, nil, - NewRootLevelIfStatement(nil).Add( - query.NewLiteralFunction("", false), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "a")), - ).Add( - query.NewLiteralFunction("", true), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "b")), - ).Add( - nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "c")), - ), - ), - input: []part{{Content: `{}`}}, - output: &part{Content: `{"foo":"b"}`}, - }, - "root if statements else": { - mapping: NewExecutor("", nil, nil, - NewRootLevelIfStatement(nil).Add( - query.NewLiteralFunction("", false), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "a")), - ).Add( - query.NewLiteralFunction("", false), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "b")), - ).Add( - nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "c")), - ), - ), - input: []part{{Content: `{}`}}, - output: &part{Content: `{"foo":"c"}`}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - msg := message.QuickBatch(nil) - for _, p := range test.input { - part := message.NewPart([]byte(p.Content)) - if p.Content == "" { - part = message.NewPart(nil) - } - for k, v := range p.Meta { - part.MetaSetMut(k, v) - } - msg = append(msg, part) - } - - resPart, err := test.mapping.MapPart(test.index, msg) - if test.err != nil { - assert.EqualError(t, err, test.err.Error()) - return - } - - require.NoError(t, err) - - if test.output != nil { - if test.output.Meta == nil { - test.output.Meta = map[string]any{} - } - - newPart := part{ - Content: string(resPart.AsBytes()), - Meta: map[string]any{}, - } - _ = resPart.MetaIterMut(func(k string, v any) error { - newPart.Meta[k] = v - return nil - }) - - assert.Equal(t, *test.output, newPart) - } else { - assert.Nil(t, resPart) - } - }) - } -} - -func TestTargets(t *testing.T) { - function := func(name string, args ...any) query.Function { - t.Helper() - fn, err := query.InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - - metaKey := func(k string) *string { - return &k - } - - tests := []struct { - mapping *Executor - queryTargets []query.TargetPath - assignmentTargets []TargetPath - }{ - { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("first")), - NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), query.NewLiteralFunction("", "second")), - NewSingleStatement(nil, NewVarAssignment("baz"), function("meta", "third")), - ), - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue, "first"), - query.NewTargetPath(query.TargetMetadata, "third"), - }, - assignmentTargets: []TargetPath{ - NewTargetPath(TargetValue, "foo"), - NewTargetPath(TargetMetadata, "bar"), - NewTargetPath(TargetVariable, "baz"), - }, - }, - { - mapping: NewExecutor("", nil, nil, - NewRootLevelIfStatement(nil).Add(query.NewLiteralFunction("", false), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("first")), - NewSingleStatement(nil, NewMetaAssignment(metaKey("bar")), query.NewLiteralFunction("", "second")), - NewSingleStatement(nil, NewVarAssignment("baz"), function("meta", "third")), - ), - ), - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue, "first"), - query.NewTargetPath(query.TargetMetadata, "third"), - }, - assignmentTargets: []TargetPath{ - NewTargetPath(TargetValue, "foo"), - NewTargetPath(TargetMetadata, "bar"), - NewTargetPath(TargetVariable, "baz"), - }, - }, - { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("first")), - NewSingleStatement(nil, NewMetaAssignment(nil), query.NewLiteralFunction("", "second")), - NewSingleStatement(nil, NewVarAssignment("baz"), function("meta", "third")), - ), - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue, "first"), - query.NewTargetPath(query.TargetMetadata, "third"), - }, - assignmentTargets: []TargetPath{ - NewTargetPath(TargetValue), - NewTargetPath(TargetMetadata), - NewTargetPath(TargetVariable, "baz"), - }, - }, - } - - for i, test := range tests { - test := test - t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { - _, targets := test.mapping.QueryTargets(query.TargetsContext{ - Maps: map[string]query.Function{}, - }) - assert.Equal(t, test.queryTargets, targets) - assert.Equal(t, test.assignmentTargets, test.mapping.AssignmentTargets()) - }) - } -} - -func TestExec(t *testing.T) { - metaKey := func(k string) *string { - return &k - } - - function := func(name string, args ...any) query.Function { - t.Helper() - fn, err := query.InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - mapping *Executor - input any - output any - outputString string - err string - }{ - "cant set meta": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewMetaAssignment(metaKey("foo")), query.NewLiteralFunction("", "bar")), - ), - err: "failed assignment (line 0): unable to assign metadata in the current context", - }, - "cant use json": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), function("json", "bar")), - ), - err: "failed assignment (line 0): target message part does not exist", - }, - "simple root get and set": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("")), - ), - input: "foobar", - output: "foobar", - outputString: "foobar", - }, - "nested get and set": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - ), - input: map[string]any{"bar": "baz"}, - output: map[string]any{"foo": "baz"}, - outputString: `{"foo":"baz"}`, - }, - "failed get": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), function("json", "bar.baz")), - ), - input: map[string]any{"nope": "baz"}, - err: "failed assignment (line 0): target message part does not exist", - outputString: "", - }, - "null get and set": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("does.not.exist")), - ), - input: `{"message":"hello world"}`, - output: map[string]any{"foo": nil}, - outputString: `{"foo":null}`, - }, - "null get and set root": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("does.not.exist")), - ), - input: `{"message":"hello world"}`, - output: nil, - outputString: `null`, - }, - "colliding set at root": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("", "hello world")), - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewFieldFunction("bar")), - ), - input: map[string]any{"bar": "baz"}, - err: "failed assignment (line 0): unable to set target path foo as the value of the root was a non-object type (string)", - }, - "colliding set at path": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), query.NewLiteralFunction("", "hello world")), - NewSingleStatement(nil, NewJSONAssignment("foo", "bar"), query.NewFieldFunction("bar")), - ), - input: map[string]any{"bar": "baz"}, - err: "failed assignment (line 0): unable to set target path foo.bar as the value of foo was a non-object type (string)", - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - res, err := test.mapping.Exec(query.FunctionContext{ - MsgBatch: message.QuickBatch(nil), - }.WithValue(test.input)) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - assert.Equal(t, test.output, res) - } - - resString, err := test.mapping.ToString(query.FunctionContext{ - MsgBatch: message.QuickBatch(nil), - }.WithValue(test.input)) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - assert.Equal(t, test.outputString, resString) - } - - resBytes, err := test.mapping.ToBytes(query.FunctionContext{ - MsgBatch: message.QuickBatch(nil), - }.WithValue(test.input)) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - assert.Equal(t, test.outputString, string(resBytes)) - } - }) - } -} - -func TestQueries(t *testing.T) { - type part struct { - Content string - Meta map[string]any - } - - initFunc := func(name string, args ...any) query.Function { - t.Helper() - fn, err := query.InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - index int - input []part - mapping *Executor - output bool - err error - }{ - "simple json query": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), - ), - input: []part{{Content: `{"bar":true}`}}, - output: true, - }, - "simple json query 2": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), - ), - input: []part{{Content: `{"bar":false}`}}, - output: false, - }, - "json query deleted message": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewLiteralFunction("delete", value.Delete(nil))), - ), - input: []part{{Content: `{"bar":{"is":"an object"}}`}}, - err: errors.New("query mapping resulted in deleted message, expected a boolean value"), - }, - "simple json query bad type": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment(), query.NewFieldFunction("bar")), - ), - input: []part{{Content: `{"bar":{"is":"an object"}}`}}, - err: errors.New("expected bool value, got object from mapping"), - }, - "var assignment": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewVarAssignment("foo"), query.NewLiteralFunction("", true)), - NewSingleStatement(nil, NewJSONAssignment(), initFunc("var", "foo")), - ), - input: []part{{Content: `not valid json`}}, - output: true, - }, - "meta query error": { - mapping: NewExecutor("", nil, nil, - NewSingleStatement(nil, NewJSONAssignment("foo"), initFunc("meta", "foo")), - ), - input: []part{{Content: `{}`}}, - err: errors.New("expected bool value, got object from mapping"), - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - msg := message.QuickBatch(nil) - for _, p := range test.input { - part := message.NewPart([]byte(p.Content)) - for k, v := range p.Meta { - part.MetaSetMut(k, v) - } - msg = append(msg, part) - } - - res, err := test.mapping.QueryPart(test.index, msg) - if test.err != nil { - assert.EqualError(t, err, test.err.Error()) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } - }) - } -} diff --git a/internal/bloblang/mapping/package.go b/internal/bloblang/mapping/package.go deleted file mode 100644 index 54f97163c2..0000000000 --- a/internal/bloblang/mapping/package.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package mapping provides a parser for the full bloblang mapping spec. -package mapping diff --git a/internal/bloblang/mapping/statement.go b/internal/bloblang/mapping/statement.go deleted file mode 100644 index 40e668c11a..0000000000 --- a/internal/bloblang/mapping/statement.go +++ /dev/null @@ -1,132 +0,0 @@ -package mapping - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" -) - -type Statement interface { - QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) - AssignmentTargets() []TargetPath - Input() []rune - Execute(fnContext query.FunctionContext, asContext AssignmentContext) error -} - -//------------------------------------------------------------------------------ - -type SingleStatement struct { - input []rune - assignment Assignment - query query.Function -} - -func NewSingleStatement(input []rune, assignment Assignment, query query.Function) *SingleStatement { - return &SingleStatement{ - input: input, - assignment: assignment, - query: query, - } -} - -func (s *SingleStatement) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) { - return s.query.QueryTargets(ctx) -} - -func (s *SingleStatement) AssignmentTargets() []TargetPath { - return []TargetPath{s.assignment.Target()} -} - -func (s *SingleStatement) Input() []rune { - return s.input -} - -func (s *SingleStatement) Execute(fnContext query.FunctionContext, asContext AssignmentContext) error { - res, err := s.query.Exec(fnContext) - if err != nil { - return err - } - if _, isNothing := res.(value.Nothing); isNothing { - // Skip assignment entirely - return nil - } - return s.assignment.Apply(res, asContext) -} - -//------------------------------------------------------------------------------ - -type rootLevelIfStatementPair struct { - query query.Function - statements []Statement -} - -type RootLevelIfStatement struct { - input []rune - pairs []rootLevelIfStatementPair -} - -func NewRootLevelIfStatement(input []rune) *RootLevelIfStatement { - return &RootLevelIfStatement{ - input: input, - } -} - -func (r *RootLevelIfStatement) Add(query query.Function, statements ...Statement) *RootLevelIfStatement { - r.pairs = append(r.pairs, rootLevelIfStatementPair{query: query, statements: statements}) - return r -} - -func (r *RootLevelIfStatement) QueryTargets(ctx query.TargetsContext) (query.TargetsContext, []query.TargetPath) { - var paths []query.TargetPath - for _, p := range r.pairs { - if p.query != nil { - _, tmp := p.query.QueryTargets(ctx) - paths = append(paths, tmp...) - } - for _, s := range p.statements { - _, tmp := s.QueryTargets(ctx) - paths = append(paths, tmp...) - } - } - return ctx, paths -} - -func (r *RootLevelIfStatement) AssignmentTargets() []TargetPath { - var paths []TargetPath - for _, p := range r.pairs { - for _, s := range p.statements { - paths = append(paths, s.AssignmentTargets()...) - } - } - return paths -} - -func (r *RootLevelIfStatement) Input() []rune { - return r.input -} - -func (r *RootLevelIfStatement) Execute(fnContext query.FunctionContext, asContext AssignmentContext) error { - for i, p := range r.pairs { - if p.query != nil { - queryVal, err := p.query.Exec(fnContext) - if err != nil { - return fmt.Errorf("failed to check if condition %v: %w", i+1, err) - } - queryRes, isBool := queryVal.(bool) - if !isBool { - return fmt.Errorf("%v resolved to a non-boolean value %v (%T)", p.query.Annotation(), queryVal, queryVal) - } - if !queryRes { - continue - } - } - for _, stmt := range p.statements { - if err := stmt.Execute(fnContext, asContext); err != nil { - return err - } - } - return nil - } - return nil -} diff --git a/internal/bloblang/mapping/target.go b/internal/bloblang/mapping/target.go deleted file mode 100644 index ef0941ef1f..0000000000 --- a/internal/bloblang/mapping/target.go +++ /dev/null @@ -1,28 +0,0 @@ -package mapping - -// TargetType represents a mapping target type, which is a destination for a -// query result to be mapped into a message. -type TargetType int - -// TargetTypes. -const ( - TargetMetadata TargetType = iota - TargetValue - TargetVariable -) - -// TargetPath represents a target type and segmented path that a query function -// references. An empty path indicates the root of the type is targeted. -type TargetPath struct { - Type TargetType - Path []string -} - -// NewTargetPath constructs a new target path from a type and zero or more path -// segments. -func NewTargetPath(t TargetType, path ...string) TargetPath { - return TargetPath{ - Type: t, - Path: path, - } -} diff --git a/internal/bloblang/package.go b/internal/bloblang/package.go deleted file mode 100644 index 19330a4e83..0000000000 --- a/internal/bloblang/package.go +++ /dev/null @@ -1,11 +0,0 @@ -package bloblang - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/plugins" -) - -func init() { - if err := plugins.Register(); err != nil { - panic(err) - } -} diff --git a/internal/bloblang/package_test.go b/internal/bloblang/package_test.go deleted file mode 100644 index 3619f0b735..0000000000 --- a/internal/bloblang/package_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package bloblang - -import ( - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestMappings(t *testing.T) { - tests := map[string]struct { - mapping string - input any - output any - assignmentTargets []mapping.TargetPath - queryTargets []query.TargetPath - }{ - "basic query": { - mapping: `root = this.foo - let bar = $baz | this.bar.baz`, - input: map[string]any{ - "foo": "bar", - }, - output: "bar", - assignmentTargets: []mapping.TargetPath{ - mapping.NewTargetPath(mapping.TargetValue), - mapping.NewTargetPath(mapping.TargetVariable, "bar"), - }, - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue, "foo"), - query.NewTargetPath(query.TargetVariable, "baz"), - query.NewTargetPath(query.TargetValue, "bar", "baz"), - }, - }, - "metadata stuff": { - mapping: ` -meta foo = this.foo -meta bar = @bar -meta baz = @ -meta buz = @bar.string().length() -root.keys = @.keys().sort() -root.meta = @ -`, - input: map[string]any{ - "foo": "bar", - }, - output: map[string]any{ - "keys": []any{"bar", "baz", "buz", "foo"}, - "meta": map[string]any{ - "foo": "bar", - "bar": nil, - "baz": map[string]any{ - "foo": "bar", - "bar": nil, - }, - "buz": int64(4), - }, - }, - assignmentTargets: []mapping.TargetPath{ - mapping.NewTargetPath(mapping.TargetMetadata, "foo"), - mapping.NewTargetPath(mapping.TargetMetadata, "bar"), - mapping.NewTargetPath(mapping.TargetMetadata, "baz"), - mapping.NewTargetPath(mapping.TargetMetadata, "buz"), - mapping.NewTargetPath(mapping.TargetValue, "keys"), - mapping.NewTargetPath(mapping.TargetValue, "meta"), - }, - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue, "foo"), - query.NewTargetPath(query.TargetMetadata, "bar"), - query.NewTargetPath(query.TargetMetadata), - query.NewTargetPath(query.TargetMetadata, "bar"), - query.NewTargetPath(query.TargetMetadata), - query.NewTargetPath(query.TargetMetadata), - }, - }, - "complex query": { - mapping: `root = match this.foo { - this.bar == "bruh" => this.baz.buz, - _ => $foo - }`, - input: map[string]any{ - "foo": map[string]any{ - "bar": "bruh", - "baz": map[string]any{ - "buz": "the result", - }, - }, - }, - output: "the result", - assignmentTargets: []mapping.TargetPath{ - mapping.NewTargetPath(mapping.TargetValue), - }, - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue, "foo", "bar"), - query.NewTargetPath(query.TargetValue, "foo", "baz", "buz"), - query.NewTargetPath(query.TargetVariable, "foo"), - query.NewTargetPath(query.TargetValue, "foo"), - }, - }, - "long assignment": { - mapping: `root.foo.bar = "this" - root.foo = "that" - root.baz.buz.0.bev = "then this"`, - output: map[string]any{ - "foo": "that", - "baz": map[string]any{ - "buz": map[string]any{ - "0": map[string]any{ - "bev": "then this", - }, - }, - }, - }, - assignmentTargets: []mapping.TargetPath{ - mapping.NewTargetPath(mapping.TargetValue, "foo", "bar"), - mapping.NewTargetPath(mapping.TargetValue, "foo"), - mapping.NewTargetPath(mapping.TargetValue, "baz", "buz", "0", "bev"), - }, - }, - "root copies to root": { - mapping: ` -root = this -root.first = root -root.second = root -`, - input: map[string]any{ - "foo": "bar", - }, - output: map[string]any{ - "foo": "bar", - "first": map[string]any{ - "foo": "bar", - }, - "second": map[string]any{ - "foo": "bar", - "first": map[string]any{ - "foo": "bar", - }, - }, - }, - assignmentTargets: []mapping.TargetPath{ - mapping.NewTargetPath(mapping.TargetValue), - mapping.NewTargetPath(mapping.TargetValue, "first"), - mapping.NewTargetPath(mapping.TargetValue, "second"), - }, - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue), - query.NewTargetPath(query.TargetRoot), - query.NewTargetPath(query.TargetRoot), - }, - }, - "root edit from map": { - mapping: ` -map foo { - root.from_map = "hello world" - root = root.from_map -} -root = this -root.meow = this.apply("foo") -`, - input: map[string]any{ - "foo": "bar", - }, - output: map[string]any{ - "foo": "bar", - "meow": "hello world", - }, - assignmentTargets: []mapping.TargetPath{ - mapping.NewTargetPath(mapping.TargetValue), - mapping.NewTargetPath(mapping.TargetValue, "meow"), - }, - queryTargets: []query.TargetPath{ - query.NewTargetPath(query.TargetValue), - query.NewTargetPath(query.TargetValue), - query.NewTargetPath(query.TargetRoot, "from_map"), - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - m, err := GlobalEnvironment().NewMapping(test.mapping) - require.NoError(t, err) - - assert.Equal(t, test.assignmentTargets, m.AssignmentTargets()) - - _, targets := m.QueryTargets(query.TargetsContext{ - Maps: m.Maps(), - }) - assert.Equal(t, test.queryTargets, targets) - - part := message.NewPart(nil) - part.SetStructuredMut(test.input) - - resPart, err := m.MapPart(0, message.Batch{part}) - require.NoError(t, err) - - res, err := resPart.AsStructured() - if err != nil { - res = string(resPart.AsBytes()) - } - assert.Equal(t, test.output, res) - }) - } -} - -func TestMappingParallelExecution(t *testing.T) { - tests := map[string]struct { - mapping string - input any - output any - }{ - "basic query using vars": { - mapping: `let tmp = this.foo.uppercase() - root.first = $tmp - let tmp = this.foo.lowercase() - root.second = $tmp`, - input: map[string]any{ - "foo": "HELLO world", - }, - output: map[string]any{ - "first": "HELLO WORLD", - "second": "hello world", - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - m, err := GlobalEnvironment().NewMapping(test.mapping) - require.NoError(t, err) - - startChan := make(chan struct{}) - - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-startChan - - for j := 0; j < 100; j++ { - part := message.NewPart(nil) - part.SetStructured(test.input) - - msg := message.QuickBatch(nil) - msg = append(msg, part) - - p, err := m.MapPart(0, msg) - require.NoError(t, err) - - res, err := p.AsStructuredMut() - require.NoError(t, err) - - assert.Equal(t, test.output, res) - } - }() - } - - close(startChan) - wg.Wait() - }) - } -} diff --git a/internal/bloblang/parser/combinators.go b/internal/bloblang/parser/combinators.go deleted file mode 100644 index 76b91675de..0000000000 --- a/internal/bloblang/parser/combinators.go +++ /dev/null @@ -1,790 +0,0 @@ -package parser - -import ( - "bytes" - "errors" - "fmt" - "strconv" - "strings" -) - -// Result represents the result of a parser given an input. -type Result[T any] struct { - Payload T - Err *Error - Remaining []rune -} - -// Func is the common signature of a parser function. -type Func[T any] func([]rune) Result[T] - -// ZeroedFuncAs converts a Func of type Tin into a Func of type Tout. -// -// WARNING: No conversion is made between payloads of Tin to Tout, instead a -// zero value of Tout will be emitted. -func ZeroedFuncAs[Tin, Tout any](f Func[Tin]) Func[Tout] { - return func(r []rune) Result[Tout] { - return ResultInto[Tout](f(r)) - } -} - -// FuncAsAny converts a Func of type Tin into a Func of type any. The payload is -// passed unchanged but cast into an any. -func FuncAsAny[T any](f Func[T]) Func[any] { - return func(r []rune) Result[any] { - tmpRes := f(r) - - outRes := ResultInto[any](tmpRes) - outRes.Payload = tmpRes.Payload - return outRes - } -} - -//------------------------------------------------------------------------------ - -// Success creates a result with a payload from successful parsing. -func Success[T any](payload T, remaining []rune) Result[T] { - return Result[T]{ - Payload: payload, - Remaining: remaining, - } -} - -// Fail creates a result with an error from failed parsing. -func Fail[T any](err *Error, input []rune) Result[T] { - return Result[T]{ - Err: err, - Remaining: input, - } -} - -func ResultInto[T, L any](from Result[L]) Result[T] { - return Result[T]{ - Err: from.Err, - Remaining: from.Remaining, - } -} - -//------------------------------------------------------------------------------ - -// Char parses a single character and expects it to match one candidate. -func Char(c rune) Func[string] { - return func(input []rune) Result[string] { - if len(input) == 0 || input[0] != c { - return Fail[string](NewError(input, string(c)), input) - } - return Success(string(c), input[1:]) - } -} - -var ( - charBracketOpen = Char('(') - charBracketClose = Char(')') - charSquigOpen = Char('{') - charSquigClose = Char('}') - charSquareOpen = Char('[') - charSquareClose = Char(']') - charDot = Char('.') - charUnderscore = Char('_') - charMinus = Char('-') - charEquals = Char('=') - charComma = Char(',') - charColon = Char(':') - charDollar = Char('$') - charHash = Char('#') -) - -// NotChar parses any number of characters until they match a single candidate. -func NotChar(c rune) Func[string] { - exp := "not " + string(c) - return func(input []rune) Result[string] { - if len(input) == 0 || input[0] == c { - return Fail[string](NewError(input, exp), input) - } - i := 0 - for ; i < len(input); i++ { - if input[i] == c { - return Success(string(input[:i]), input[i:]) - } - } - return Success(string(input), nil) - } -} - -// InSet parses any number of characters within a set of runes. -func InSet(set ...rune) Func[string] { - setMap := make(map[rune]struct{}, len(set)) - for _, r := range set { - setMap[r] = struct{}{} - } - exp := fmt.Sprintf("chars(%v)", string(set)) - return func(input []rune) Result[string] { - if len(input) == 0 { - return Fail[string](NewError(input, exp), input) - } - i := 0 - for ; i < len(input); i++ { - if _, exists := setMap[input[i]]; !exists { - if i == 0 { - return Fail[string](NewError(input, exp), input) - } - break - } - } - return Success(string(input[:i]), input[i:]) - } -} - -// NotInSet parses any number of characters until a rune within a given set is -// encountered. -func NotInSet(set ...rune) Func[string] { - setMap := make(map[rune]struct{}, len(set)) - for _, r := range set { - setMap[r] = struct{}{} - } - exp := fmt.Sprintf("not chars(%v)", string(set)) - return func(input []rune) Result[string] { - if len(input) == 0 { - return Fail[string](NewError(input, exp), input) - } - i := 0 - for ; i < len(input); i++ { - if _, exists := setMap[input[i]]; exists { - if i == 0 { - return Fail[string](NewError(input, exp), input) - } - break - } - } - return Success(string(input[:i]), input[i:]) - } -} - -// InRange parses any number of characters between two runes inclusive. -func InRange(lower, upper rune) Func[string] { - exp := fmt.Sprintf("range(%c - %c)", lower, upper) - return func(input []rune) Result[string] { - if len(input) == 0 { - return Fail[string](NewError(input, exp), input) - } - i := 0 - for ; i < len(input); i++ { - if input[i] < lower || input[i] > upper { - if i == 0 { - return Fail[string](NewError(input, exp), input) - } - break - } - } - return Success(string(input[:i]), input[i:]) - } -} - -// SpacesAndTabs parses any number of space or tab characters. -var SpacesAndTabs = Expect(InSet(' ', '\t'), "whitespace") - -// Term parses a single instance of a string. -func Term(term string) Func[string] { - termRunes := []rune(term) - return func(input []rune) Result[string] { - if len(input) < len(termRunes) { - return Fail[string](NewError(input, term), input) - } - for i, c := range termRunes { - if input[i] != c { - return Fail[string](NewError(input, term), input) - } - } - return Success(term, input[len(termRunes):]) - } -} - -// UntilTerm parses any number of characters until an instance of a string is -// met. The provided term is not included in the result. -func UntilTerm(term string) Func[string] { - termRunes := []rune(term) - return func(input []rune) Result[string] { - if len(input) < len(termRunes) { - return Fail[string](NewError(input, term), input) - } - i := 0 - for ; i <= (len(input) - len(termRunes)); i++ { - matched := true - for j := 0; j < len(termRunes); j++ { - if input[i+j] != termRunes[j] { - matched = false - break - } - } - if matched { - return Success(string(input[:i]), input[i:]) - } - } - return Fail[string](NewError(input, term), input) - } -} - -// Number parses any number of numerical characters into either an int64 or, if -// the number contains float characters, a float64. -var Number = func() Func[any] { - digitSet := InSet([]rune("0123456789")...) - dot := charDot - expectNumber := Expect(digitSet, "number") - - return func(input []rune) Result[any] { - var negative bool - res := charMinus(input) - if res.Err == nil { - negative = true - } - res = expectNumber(res.Remaining) - if res.Err != nil { - return ResultInto[any](res) - } - resStr := res.Payload - if resTest := dot(res.Remaining); resTest.Err == nil { - if resTest = digitSet(resTest.Remaining); resTest.Err == nil { - resStr = resStr + "." + resTest.Payload - res = resTest - } - } - - outRes := ResultInto[any](res) - if strings.Contains(resStr, ".") { - f, err := strconv.ParseFloat(resStr, 64) - if err != nil { - err = fmt.Errorf("failed to parse '%v' as float: %v", resStr, err) - return Fail[any](NewFatalError(input, err), input) - } - if negative { - f = -f - } - outRes.Payload = f - } else { - i, err := strconv.ParseInt(resStr, 10, 64) - if err != nil { - err = fmt.Errorf("failed to parse '%v' as integer: %v", resStr, err) - return Fail[any](NewFatalError(input, err), input) - } - if negative { - i = -i - } - outRes.Payload = i - } - return outRes - } -}() - -// Boolean parses either 'true' or 'false' into a boolean value. -var Boolean = func() Func[bool] { - parser := Expect(OneOf(Term("true"), Term("false")), "boolean") - return func(input []rune) Result[bool] { - res := parser(input) - if res.Err == nil { - return Success(res.Payload == "true", res.Remaining) - } - return ResultInto[bool](res) - } -}() - -// Null parses a null literal value. -var Null = func() Func[any] { - nullMatch := Term("null") - return func(input []rune) Result[any] { - res := ResultInto[any](nullMatch(input)) - if res.Err == nil { - res.Payload = nil - } - return res - } -}() - -var DiscardedWhitespaceNewlineComments = DiscardAll(OneOf(SpacesAndTabs, NewlineAllowComment)) - -// Array parses an array literal. -func Array() Func[[]any] { - pattern := DelimitedPattern( - Expect(Sequence(charSquareOpen, DiscardedWhitespaceNewlineComments), "array"), - LiteralValue(), - Sequence( - Discard(SpacesAndTabs), - charComma, - DiscardedWhitespaceNewlineComments, - ), - Sequence(DiscardedWhitespaceNewlineComments, charSquareClose), - ) - - return func(r []rune) Result[[]any] { - return pattern(r) - } -} - -// Object parses an object literal. -func Object() Func[map[string]any] { - pattern := DelimitedPattern( - Expect(Sequence( - charSquigOpen, DiscardedWhitespaceNewlineComments, - ), "object"), - Sequence( - FuncAsAny(QuotedString), - FuncAsAny(Discard(SpacesAndTabs)), - FuncAsAny(charColon), - FuncAsAny(DiscardedWhitespaceNewlineComments), - LiteralValue(), - ), - Sequence( - Discard(SpacesAndTabs), - charComma, - DiscardedWhitespaceNewlineComments, - ), - Sequence(DiscardedWhitespaceNewlineComments, charSquigClose), - ) - - return func(input []rune) Result[map[string]any] { - res := pattern(input) - if res.Err != nil { - return Fail[map[string]any](res.Err, input) - } - - values := map[string]any{} - for _, kv := range res.Payload { - values[kv[0].(string)] = kv[4] - } - - return Success(values, res.Remaining) - } -} - -// LiteralValue parses a literal bool, number, quoted string, null value, array -// of literal values, or object. -func LiteralValue() Func[any] { - return func(r []rune) Result[any] { - return OneOf( - FuncAsAny(Boolean), - FuncAsAny(Number), - FuncAsAny(TripleQuoteString), - FuncAsAny(QuotedString), - FuncAsAny(Null), - FuncAsAny(Array()), - FuncAsAny(Object()), - )(r) - } -} - -// JoinStringPayloads wraps a parser that returns a []interface{} of exclusively -// string values and returns a result of a joined string of all the elements. -// -// Warning! If the result is not a []interface{}, or if an element is not a -// string, then this parser returns a zero value instead. -func JoinStringPayloads(p Func[[]string]) Func[string] { - return func(input []rune) Result[string] { - res := p(input) - if res.Err != nil { - return ResultInto[string](res) - } - - var buf bytes.Buffer - for _, v := range res.Payload { - buf.WriteString(v) - } - - outRes := ResultInto[string](res) - outRes.Payload = buf.String() - return outRes - } -} - -// Comment parses a # comment (always followed by a line break). -var Comment = JoinStringPayloads( - Sequence( - charHash, - JoinStringPayloads( - Optional(UntilFail(NotChar('\n'))), - ), - Newline, - ), -) - -// SnakeCase parses any number of characters of a camel case string. This parser -// is very strict and does not support double underscores, prefix or suffix -// underscores. -var SnakeCase = Expect(JoinStringPayloads(UntilFail(OneOf( - InRange('a', 'z'), - InRange('0', '9'), - charUnderscore, -))), "snake-case") - -// TripleQuoteString parses a single instance of a triple-quoted multiple line -// string. The result is the inner contents. -func TripleQuoteString(input []rune) Result[string] { - if len(input) < 6 || - input[0] != '"' || - input[1] != '"' || - input[2] != '"' { - return Fail[string](NewError(input, "quoted string"), input) - } - for i := 3; i < len(input)-2; i++ { - if input[i] == '"' && - input[i+1] == '"' && - input[i+2] == '"' { - return Success(string(input[3:i]), input[i+3:]) - } - } - return Fail[string](NewFatalError(input[len(input):], errors.New("required"), "end triple-quote"), input) -} - -// QuotedString parses a single instance of a quoted string. The result is the -// inner contents unescaped. -func QuotedString(input []rune) Result[string] { - if len(input) == 0 || input[0] != '"' { - return Fail[string](NewError(input, "quoted string"), input) - } - escaped := false - for i := 1; i < len(input); i++ { - if input[i] == '"' && !escaped { - unquoted, err := strconv.Unquote(string(input[:i+1])) - if err != nil { - err = fmt.Errorf("failed to unescape quoted string contents: %v", err) - return Fail[string](NewFatalError(input, err), input) - } - return Success(unquoted, input[i+1:]) - } - if input[i] == '\n' { - Fail[string](NewFatalError(input[i:], errors.New("required"), "end quote"), input) - } - if input[i] == '\\' { - escaped = !escaped - } else if escaped { - escaped = false - } - } - return Fail[string](NewFatalError(input[len(input):], errors.New("required"), "end quote"), input) -} - -// EmptyLine ensures that a line is empty, but doesn't advance the parser beyond -// the newline char. -func EmptyLine(r []rune) Result[any] { - if len(r) > 0 && r[0] == '\n' { - return Success[any](nil, r) - } - return Fail[any](NewError(r, "Empty line"), r) -} - -// EndOfInput ensures that the input is now empty. -func EndOfInput(r []rune) Result[any] { - if len(r) == 0 { - return Success[any](nil, r) - } - return Fail[any](NewError(r, "End of input"), r) -} - -// Newline parses a line break. -var Newline = Expect( - JoinStringPayloads( - Sequence( - Optional(Char('\r')), - Char('\n'), - ), - ), - "line break", -) - -// NewlineAllowComment parses an optional comment followed by a mandatory line -// break. -var NewlineAllowComment = Expect(OneOf(Comment, Newline), "line break") - -// UntilFail applies a parser until it fails, and returns a slice containing all -// results. If the parser does not succeed at least once an error is returned. -func UntilFail[T any](parser Func[T]) Func[[]T] { - return func(input []rune) Result[[]T] { - res := parser(input) - if res.Err != nil { - return ResultInto[[]T](res) - } - results := []T{res.Payload} - for { - if res = parser(res.Remaining); res.Err != nil { - return Success(results, res.Remaining) - } - results = append(results, res.Payload) - } - } -} - -// DelimitedPattern attempts to parse zero or more primary parsers in between a -// start and stop parser, where after the first parse a delimiter is expected. -// Parsing is stopped only once an explicit stop parser is successful. -// -// If allowTrailing is set to false and a delimiter is parsed but a subsequent -// primary parse fails then an error is returned. -// -// Only the results of the primary parser are returned, the results of the -// start, delimiter and stop parsers are discarded. -func DelimitedPattern[S, P, D, E any]( - start Func[S], primary Func[P], delimiter Func[D], stop Func[E], -) Func[[]P] { - return func(input []rune) Result[[]P] { - var remaining []rune - if res := start(input); res.Err != nil { - return ResultInto[[]P](res) - } else { - remaining = res.Remaining - } - - results := []P{} - - if res := primary(remaining); res.Err != nil { - if resStop := stop(res.Remaining); resStop.Err == nil { - return Success(results, resStop.Remaining) - } - return Fail[[]P](res.Err, input) - } else { - results = append(results, res.Payload) - remaining = res.Remaining - } - - for { - if res := delimiter(remaining); res.Err != nil { - resStop := stop(res.Remaining) - if resStop.Err == nil { - return Success(results, resStop.Remaining) - } - res.Err.Add(resStop.Err) - return Fail[[]P](res.Err, input) - } else { - remaining = res.Remaining - } - - if res := primary(remaining); res.Err != nil { - if resStop := stop(res.Remaining); resStop.Err == nil { - return Success(results, resStop.Remaining) - } - return Fail[[]P](res.Err, input) - } else { - results = append(results, res.Payload) - remaining = res.Remaining - } - } - } -} - -// DelimitedResult is an explicit result struct returned by the Delimited -// parser, containing a slice of primary parser payloads and a slice of -// delimited parser payloads. -type DelimitedResult[P, D any] struct { - Primary []P - Delimiter []D -} - -// Delimited attempts to parse one or more primary parsers, where after the -// first parse a delimiter is expected. Parsing is stopped only once a delimiter -// parse is not successful. -// -// Two slices are returned, the first element being a slice of primary results -// and the second element being the delimiter results. -func Delimited[P, D any](primary Func[P], delimiter Func[D]) Func[DelimitedResult[P, D]] { - return func(input []rune) Result[DelimitedResult[P, D]] { - delimRes := DelimitedResult[P, D]{} - - res := primary(input) - if res.Err != nil { - return ResultInto[DelimitedResult[P, D]](res) - } - delimRes.Primary = append(delimRes.Primary, res.Payload) - - for { - dRes := delimiter(res.Remaining) - if dRes.Err != nil { - return Success(delimRes, dRes.Remaining) - } - delimRes.Delimiter = append(delimRes.Delimiter, dRes.Payload) - - if res = primary(dRes.Remaining); res.Err != nil { - return Fail[DelimitedResult[P, D]](res.Err, input) - } - delimRes.Primary = append(delimRes.Primary, res.Payload) - } - } -} - -// TakeOnly wraps an array based combinator with one that only extracts a single -// element of the resulting values. NOTE: If the index is -func TakeOnly[T any](index int, p Func[[]T]) Func[T] { - return func(input []rune) Result[T] { - res := p(input) - if res.Err != nil { - return Fail[T](res.Err, input) - } - if len(res.Payload) <= index { - return ResultInto[T](res) - } - return Success(res.Payload[index], res.Remaining) - } -} - -// Sequence applies a sequence of parsers and returns either a slice of the -// results or an error if any parser fails. -func Sequence[T any](parsers ...Func[T]) Func[[]T] { - return func(input []rune) Result[[]T] { - results := make([]T, 0, len(parsers)) - - res := Result[T]{ - Remaining: input, - } - for _, p := range parsers { - if res = p(res.Remaining); res.Err != nil { - return Fail[[]T](res.Err, input) - } - results = append(results, res.Payload) - } - return Success(results, res.Remaining) - } -} - -// Optional applies a child parser and if it returns an ExpectedError then it is -// cleared and a zero result is returned instead. Any other form of error will -// be returned unchanged. -func Optional[T any](parser Func[T]) Func[T] { - return func(input []rune) Result[T] { - res := parser(input) - if res.Err != nil && !res.Err.IsFatal() { - res.Err = nil - } - return res - } -} - -// OptionalPtr applies a child parser and if it returns an ExpectedError then it -// is cleared and a nil result is returned instead. Any other form of error will -// be returned unchanged (but converted to a pointer type). -func OptionalPtr[T any](parser Func[T]) Func[*T] { - return func(input []rune) Result[*T] { - res := parser(input) - if res.Err == nil { - return Success(&res.Payload, res.Remaining) - } - if !res.Err.IsFatal() { - res.Err = nil - return Success[*T](nil, res.Remaining) - } - return Fail[*T](res.Err, input) - } -} - -// Discard the result of a child parser, regardless of the result. This has the -// effect of running the parser and returning only Remaining. -func Discard[T any](parser Func[T]) Func[T] { - return func(input []rune) Result[T] { - res := parser(input) - var tmp T - res.Payload = tmp - res.Err = nil - return res - } -} - -// DiscardAll the results of a child parser, applied until it fails. This has -// the effect of running the parser and returning only Remaining. -func DiscardAll[T any](parser Func[T]) Func[T] { - return func(input []rune) Result[T] { - res := parser(input) - for res.Err == nil { - res = parser(res.Remaining) - } - var tmp T - res.Payload = tmp - res.Err = nil - return res - } -} - -// MustBe applies a parser and if the result is a non-fatal error then it is -// upgraded to a fatal one. -func MustBe[T any](parser Func[T]) Func[T] { - return func(input []rune) Result[T] { - res := parser(input) - if res.Err != nil && !res.Err.IsFatal() { - res.Err.Err = errors.New("required") - } - return res - } -} - -// Expect applies a parser and if an error is returned the list of expected candidates is replaced with the given -// strings. This is useful for providing better context to users. -func Expect[T any](parser Func[T], expected ...string) Func[T] { - return func(input []rune) Result[T] { - res := parser(input) - if res.Err != nil && !res.Err.IsFatal() { - res.Err.Expected = expected - } - return res - } -} - -// OneOf accepts one or more parsers and tries them in order against an input. -// If a parser returns an ExpectedError then the next parser is tried and so -// on. Otherwise, the result is returned. -func OneOf[T any](parsers ...Func[T]) Func[T] { - return func(input []rune) Result[T] { - var err *Error - for _, p := range parsers { - res := p(input) - if res.Err == nil || res.Err.IsFatal() { - return res - } - if err == nil || len(err.Input) > len(res.Err.Input) { - err = res.Err - } else if len(err.Input) == len(res.Err.Input) { - err.Add(res.Err) - } - } - return Fail[T](err, input) - } -} - -func bestMatch[T any](left, right Result[T]) Result[T] { - remainingLeft := len(left.Remaining) - remainingRight := len(right.Remaining) - if left.Err != nil { - remainingLeft = len(left.Err.Input) - } - if right.Err != nil { - remainingRight = len(right.Err.Input) - } - if remainingRight == remainingLeft { - if left.Err == nil { - return left - } - if right.Err == nil { - return right - } - } - if remainingRight < remainingLeft { - return right - } - return left -} - -// BestMatch accepts one or more parsers and tries them all against an input. -// If all parsers return either a result or an error then the parser that got -// further through the input will have its result returned. This means that an -// error may be returned even if a parser was successful. -// -// For example, given two parsers, A searching for 'aa', and B searching for -// 'aaaa', if the input 'aaab' were provided then an error from parser B would -// be returned, as although the input didn't match, it matched more of parser B -// than parser A. -func BestMatch[T any](parsers ...Func[T]) Func[T] { - if len(parsers) == 1 { - return parsers[0] - } - return func(input []rune) Result[T] { - res := parsers[0](input) - for _, p := range parsers[1:] { - resTmp := p(input) - res = bestMatch(res, resTmp) - } - return res - } -} diff --git a/internal/bloblang/parser/combinators_test.go b/internal/bloblang/parser/combinators_test.go deleted file mode 100644 index b3c4f7921c..0000000000 --- a/internal/bloblang/parser/combinators_test.go +++ /dev/null @@ -1,1808 +0,0 @@ -package parser - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestChar(t *testing.T) { - char := Char('x') - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "x"), - }, - "only the char": { - input: "x", - result: "x", - }, - "lots of the char": { - input: "xxxx", - result: "x", - remaining: "xxx", - }, - "wrong input": { - input: "not x", - remaining: "not x", - err: NewError([]rune("not x"), "x"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := char([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestNotChar(t *testing.T) { - char := NotChar('x') - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "not x"), - }, - "only the char": { - input: "x", - remaining: "x", - err: NewError([]rune("x"), "not x"), - }, - "lots of the char": { - input: "xxxx", - remaining: "xxxx", - err: NewError([]rune("xxxx"), "not x"), - }, - "only not the char": { - input: "n", - result: "n", - remaining: "", - }, - "not the char": { - input: "not x", - result: "not ", - remaining: "x", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := char([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestInSet(t *testing.T) { - inSet := InSet('a', 'b', 'c') - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "chars(abc)"), - }, - "only a char": { - input: "a", - result: "a", - remaining: "", - }, - "lots of the set": { - input: "abcabc", - remaining: "", - result: "abcabc", - }, - "lots of the set and some": { - input: "abcabc and this", - remaining: " and this", - result: "abcabc", - }, - "not in the set": { - input: "n", - remaining: "n", - err: NewError([]rune("n"), "chars(abc)"), - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - err: NewError([]rune("nononono"), "chars(abc)"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := inSet([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestNotInSet(t *testing.T) { - inSet := NotInSet('#', '\n', ' ') - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "not chars(#\n )"), - }, - "only a char": { - input: "#", - remaining: "#", - err: NewError([]rune("#"), "not chars(#\n )"), - }, - "some text then set": { - input: "abcabc#foo", - remaining: "#foo", - result: "abcabc", - }, - "some text then two from set": { - input: "abcabc#\nfoo", - remaining: "#\nfoo", - result: "abcabc", - }, - "only text not in set": { - input: "abcabc", - remaining: "", - result: "abcabc", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := inSet([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestEmptyLine(t *testing.T) { - parser := EmptyLine - - tests := map[string]struct { - input string - result any - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "Empty line"), - }, - "empty line": { - input: "\n", - result: nil, - remaining: "\n", - }, - "empty line with extra": { - input: "\n foo", - result: nil, - remaining: "\n foo", - }, - "non-empty line": { - input: "foo\n", - err: NewError([]rune("foo\n"), "Empty line"), - remaining: "foo\n", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestInRange(t *testing.T) { - parser := InRange('a', 'c') - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "range(a - c)"), - }, - "only a char": { - input: "a", - result: "a", - remaining: "", - }, - "lots of the set": { - input: "abcabc", - remaining: "", - result: "abcabc", - }, - "lots of the set and some": { - input: "abcabc and this", - remaining: " and this", - result: "abcabc", - }, - "not in the set": { - input: "n", - remaining: "n", - err: NewError([]rune("n"), "range(a - c)"), - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - err: NewError([]rune("nononono"), "range(a - c)"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestOneOfErrors(t *testing.T) { - input := "foo bar baz" - tests := map[string]struct { - resultErrs []*Error - err string - }{ - "One parser fails": { - resultErrs: []*Error{ - NewError([]rune("bar baz"), "foo"), - }, - err: `line 1 char 5: expected foo`, - }, - "Two parsers fail": { - resultErrs: []*Error{ - NewError([]rune("bar baz"), "foo"), - NewError([]rune("bar baz"), "bar"), - }, - err: `line 1 char 5: expected foo or bar`, - }, - "Two parsers fail 2": { - resultErrs: []*Error{ - NewError([]rune("bar baz"), "bar"), - NewError([]rune("o bar baz"), "foo"), - }, - err: `line 1 char 5: expected bar`, - }, - "Two parsers fail 3": { - resultErrs: []*Error{ - NewError([]rune("o bar baz"), "foo"), - NewError([]rune("bar baz"), "bar"), - }, - err: `line 1 char 5: expected bar`, - }, - "Fatal parsers fail": { - resultErrs: []*Error{ - NewFatalError([]rune("bar baz"), errors.New("this is a real error")), - NewError([]rune("bar baz"), "bar"), - }, - err: `line 1 char 5: this is a real error`, - }, - "Fatal parsers fail 2": { - resultErrs: []*Error{ - NewFatalError([]rune("bar baz"), errors.New("this is a real error")), - NewError([]rune("baz"), "bar"), - }, - err: `line 1 char 5: this is a real error`, - }, - "Fatal parsers fail 3": { - resultErrs: []*Error{ - NewError([]rune("bar baz"), "bar"), - NewFatalError([]rune("bar baz"), errors.New("this is a real error")), - }, - err: `line 1 char 5: this is a real error`, - }, - } - - for name, test := range tests { - childParsers := []Func[any]{} - for _, err := range test.resultErrs { - err := err - childParsers = append(childParsers, func([]rune) Result[any] { - return Result[any]{ - Err: err, - } - }) - } - t.Run(name, func(t *testing.T) { - res := OneOf(childParsers...)([]rune(input)) - assert.Equal(t, test.err, res.Err.ErrorAtPosition([]rune(input)), "Error") - }) - } -} - -func TestBestMatch(t *testing.T) { - tests := map[string]struct { - inputResults []Result[any] - result Result[any] - }{ - "Three parsers fail": { - inputResults: []Result[any]{ - { - Err: NewError([]rune("ar"), "foo"), - Remaining: []rune("foobar"), - }, - { - Err: NewError([]rune("obar"), "bar"), - Remaining: []rune("foobar"), - }, - { - Err: NewError([]rune("bar"), "bar"), - Remaining: []rune("foobar"), - }, - }, - result: Result[any]{ - Err: NewError([]rune("ar"), "foo"), - Remaining: []rune("foobar"), - }, - }, - "One parser succeeds": { - inputResults: []Result[any]{ - { - Err: NewError([]rune("ar"), "foo"), - Remaining: []rune("foobar"), - }, - { - Payload: "test", - Remaining: []rune("bar"), - }, - }, - result: Result[any]{ - Err: NewError([]rune("ar"), "foo"), - Remaining: []rune("foobar"), - }, - }, - "One parser succeeds 2": { - inputResults: []Result[any]{ - { - Err: NewError([]rune("ar"), "foo"), - Remaining: []rune("foobar"), - }, - { - Payload: "test", - Remaining: []rune("r"), - }, - }, - result: Result[any]{ - Payload: "test", - Remaining: []rune("r"), - }, - }, - "Three parsers fail one severe": { - inputResults: []Result[any]{ - { - Err: NewError([]rune("ar"), "foo"), - Remaining: []rune("foobar"), - }, - { - Err: NewFatalError([]rune("r"), errors.New("this is a real error")), - Remaining: []rune("foobar"), - }, - { - Err: NewError([]rune("bar"), "bar"), - Remaining: []rune("foobar"), - }, - }, - result: Result[any]{ - Err: NewFatalError([]rune("r"), errors.New("this is a real error")), - Remaining: []rune("foobar"), - }, - }, - } - - for name, test := range tests { - childParsers := []Func[any]{} - for _, res := range test.inputResults { - res := res - childParsers = append(childParsers, func([]rune) Result[any] { - return res - }) - } - t.Run(name, func(t *testing.T) { - res := BestMatch(childParsers...)([]rune("foobar")) - assert.Equal(t, test.result, res) - }) - } -} - -func TestSnakeCase(t *testing.T) { - parser := SnakeCase - - tests := map[string]struct { - input string - result string - remaining string - err string - }{ - "empty input": { - err: `line 1 char 1: expected snake-case`, - }, - "only a char": { - input: "a", - result: "a", - remaining: "", - }, - "lots of the set": { - input: "a1c_a2c", - remaining: "", - result: "a1c_a2c", - }, - "lots of the set and some": { - input: "abc_abc and this", - remaining: " and this", - result: "abc_abc", - }, - "not in the set": { - input: "N", - remaining: "N", - err: `line 1 char 1: expected snake-case`, - }, - "lots not in the set": { - input: "NONONONO", - remaining: "NONONONO", - err: `line 1 char 1: expected snake-case`, - }, - "prefix underscore": { - input: "_foobar baz", - remaining: " baz", - result: "_foobar", - }, - "suffix underscore": { - input: "foo_bar_ baz", - remaining: " baz", - result: "foo_bar_", - }, - "double underscore": { - input: "foo_bar__baz", - remaining: "", - result: "foo_bar__baz", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - if res.Err != nil || test.err != "" { - assert.Equal(t, test.err, res.Err.ErrorAtPosition([]rune(test.input)), "Error") - } - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestTerm(t *testing.T) { - parser := Term("a🥰c") - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "a🥰c"), - }, - "smaller than string": { - input: "a🥰", - remaining: "a🥰", - err: NewError([]rune("a🥰"), "a🥰c"), - }, - "matches first": { - input: "a🥰cNo", - result: "a🥰c", - remaining: "No", - }, - "matches all": { - input: "a🥰c", - remaining: "", - result: "a🥰c", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestUntilTerm(t *testing.T) { - tests := map[string]struct { - parser Func[string] - input string - result string - remaining string - err *Error - }{ - "empty input": { - parser: UntilTerm("abc"), - err: NewError([]rune(""), "abc"), - }, - "smaller than string": { - parser: UntilTerm("abc"), - input: "ab", - remaining: "ab", - err: NewError([]rune("ab"), "abc"), - }, - "matches first": { - parser: UntilTerm("abc"), - input: "abcNo", - result: "", - remaining: "abcNo", - }, - "matches all": { - parser: UntilTerm("abc"), - input: "abc", - remaining: "abc", - result: "", - }, - "matches end": { - parser: UntilTerm("abc"), - input: "hello world abc", - remaining: "abc", - result: "hello world ", - }, - "matches before end": { - parser: UntilTerm("abc"), - input: "hello world abc this is ash", - remaining: "abc this is ash", - result: "hello world ", - }, - "single char term matches all": { - parser: UntilTerm("a"), - input: "a", - remaining: "a", - result: "", - }, - "single char term matches end": { - parser: UntilTerm("a"), - input: "helloa", - remaining: "a", - result: "hello", - }, - "single char term matches before end": { - parser: UntilTerm("a"), - input: "helloabar", - remaining: "abar", - result: "hello", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := test.parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestSequence(t *testing.T) { - parser := Sequence(Term("abc"), Term("def")) - - tests := map[string]struct { - input string - result []string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "abc"), - }, - "smaller than string": { - input: "ab", - remaining: "ab", - err: NewError([]rune("ab"), "abc"), - }, - "matches first": { - input: "abcNo", - remaining: "abcNo", - err: NewError([]rune("No"), "def"), - }, - "matches some of second": { - input: "abcdeNo", - remaining: "abcdeNo", - err: NewError([]rune("deNo"), "def"), - }, - "matches all": { - input: "abcdef", - remaining: "", - result: []string{"abc", "def"}, - }, - "matches some": { - input: "abcdef and this", - remaining: " and this", - result: []string{"abc", "def"}, - }, - "matches only one": { - input: "abcdefabcdef", - remaining: "abcdef", - result: []string{"abc", "def"}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestAllOf(t *testing.T) { - parser := UntilFail(Term("abc")) - - tests := map[string]struct { - input string - result []string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "abc"), - }, - "smaller than string": { - input: "ab", - remaining: "ab", - err: NewError([]rune("ab"), "abc"), - }, - "matches first": { - input: "abcNo", - remaining: "No", - result: []string{"abc"}, - }, - "matches some of second": { - input: "abcabNo", - remaining: "abNo", - result: []string{"abc"}, - }, - "matches all": { - input: "abcabc", - remaining: "", - result: []string{"abc", "abc"}, - }, - "matches some": { - input: "abcabc and this", - remaining: " and this", - result: []string{"abc", "abc"}, - }, - "matches all of these": { - input: "abcabcabcabcdef and this", - remaining: "def and this", - result: []string{"abc", "abc", "abc", "abc"}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestDelimited(t *testing.T) { - parser := Delimited(Term("abc"), charHash) - - tests := map[string]struct { - input string - result DelimitedResult[string, string] - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "abc"), - }, - "no first primary": { - input: "ab", - remaining: "ab", - err: NewError([]rune("ab"), "abc"), - }, - "matches first": { - input: "abc#", - remaining: "abc#", - err: NewError([]rune(""), "abc"), - }, - "matches some of second": { - input: "abc#ab", - remaining: "abc#ab", - err: NewError([]rune("ab"), "abc"), - }, - "matches all": { - input: "abc#abc", - remaining: "", - result: DelimitedResult[string, string]{ - Primary: []string{"abc", "abc"}, - Delimiter: []string{"#"}, - }, - }, - "matches some": { - input: "abc#abc and this", - remaining: " and this", - result: DelimitedResult[string, string]{ - Primary: []string{"abc", "abc"}, - Delimiter: []string{"#"}, - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestDelimitedPatternAllowTrailing(t *testing.T) { - parser := DelimitedPattern(charHash, Term("abc"), charComma, Char('!')) - - tests := map[string]struct { - input string - result []string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "#"), - }, - "no start": { - input: "ab", - remaining: "ab", - err: NewError([]rune("ab"), "#"), - }, - "smaller than string": { - input: "#ab", - remaining: "#ab", - err: NewError([]rune("ab"), "abc"), - }, - "matches first": { - input: "#abc!No", - remaining: "No", - result: []string{"abc"}, - }, - "matches first trailing": { - input: "#abc,!No", - remaining: "No", - result: []string{"abc"}, - }, - "matches some of second": { - input: "#abc,abNo", - remaining: "#abc,abNo", - err: NewError([]rune("abNo"), "abc"), - }, - "matches not stopped": { - input: "#abc,abcNo", - remaining: "#abc,abcNo", - err: NewError([]rune("No"), ",", "!"), - }, - "matches all": { - input: "#abc,abc!", - remaining: "", - result: []string{"abc", "abc"}, - }, - "matches all trailing": { - input: "#abc,abc,!", - remaining: "", - result: []string{"abc", "abc"}, - }, - "matches some": { - input: "#abc,abc! and this", - remaining: " and this", - result: []string{"abc", "abc"}, - }, - "matches all of these": { - input: "#abc,abc,abc,abc!def and this", - remaining: "def and this", - result: []string{"abc", "abc", "abc", "abc"}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestMustBe(t *testing.T) { - tests := map[string]struct { - inputRes Result[any] - outputRes Result[any] - }{ - "No error": { - inputRes: Result[any]{ - Payload: "foo", - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Payload: "foo", - Remaining: []rune("foobar"), - }, - }, - "Real error": { - inputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("testerr")), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("testerr")), - Remaining: []rune("foobar"), - }, - }, - "Expected error": { - inputRes: Result[any]{ - Err: NewError(nil, "testerr"), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("required"), "testerr"), - Remaining: []rune("foobar"), - }, - }, - "Expected already fatal error": { - inputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("testerr"), "testerr"), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("testerr"), "testerr"), - Remaining: []rune("foobar"), - }, - }, - "Expected and positioned error": { - inputRes: Result[any]{ - Err: NewError([]rune("foo"), "testerr"), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewFatalError([]rune("foo"), errors.New("required"), "testerr"), - Remaining: []rune("foobar"), - }, - }, - "Fatal and positioned error": { - inputRes: Result[any]{ - Err: NewFatalError([]rune("foo"), errors.New("testerr")), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewFatalError([]rune("foo"), errors.New("testerr")), - Remaining: []rune("foobar"), - }, - }, - } - - for name, test := range tests { - test := test - childParser := func([]rune) Result[any] { - return test.inputRes - } - t.Run(name, func(t *testing.T) { - res := MustBe(childParser)([]rune("foobar")) - assert.Equal(t, test.outputRes, res) - }) - } -} - -func TestInterceptExpectedError(t *testing.T) { - tests := map[string]struct { - inputRes Result[any] - outputRes Result[any] - }{ - "No error": { - inputRes: Result[any]{ - Payload: "foo", - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Payload: "foo", - Remaining: []rune("foobar"), - }, - }, - "Real error": { - inputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("testerr")), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewFatalError(nil, errors.New("testerr")), - Remaining: []rune("foobar"), - }, - }, - "Expected error": { - inputRes: Result[any]{ - Err: NewError(nil, "testerr"), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewError(nil, "foobar"), - Remaining: []rune("foobar"), - }, - }, - "Expected and positioned error": { - inputRes: Result[any]{ - Err: NewError([]rune("foo"), "testerr"), - Remaining: []rune("foobar"), - }, - outputRes: Result[any]{ - Err: NewError([]rune("foo"), "foobar"), - Remaining: []rune("foobar"), - }, - }, - } - - for name, test := range tests { - test := test - childParser := func([]rune) Result[any] { - return test.inputRes - } - t.Run(name, func(t *testing.T) { - res := Expect(childParser, "foobar")([]rune("foobar")) - assert.Equal(t, test.outputRes, res) - }) - } -} - -func TestMatch(t *testing.T) { - str := Term("abc") - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "abc"), - }, - "smaller than string": { - input: "ab", - remaining: "ab", - err: NewError([]rune("ab"), "abc"), - }, - "only string": { - input: "abc", - result: "abc", - remaining: "", - }, - "lots of the string": { - input: "abcabc", - remaining: "abc", - result: "abc", - }, - "lots of the string and some": { - input: "abcabc and this", - remaining: "abc and this", - result: "abc", - }, - "not in the string": { - input: "n", - remaining: "n", - err: NewError([]rune("n"), "abc"), - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - err: NewError([]rune("nononono"), "abc"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := str([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestDiscard(t *testing.T) { - parser := Discard(Term("abc")) - - tests := map[string]struct { - input string - remaining string - }{ - "empty input": {}, - "smaller than string": { - input: "ab", - remaining: "ab", - }, - "only string": { - input: "abc", - remaining: "", - }, - "lots of the string": { - input: "abcabc", - remaining: "abc", - }, - "lots of the string and some": { - input: "abcabc and this", - remaining: "abc and this", - }, - "not in the string": { - input: "n", - remaining: "n", - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Nil(t, res.Err) - assert.Empty(t, res.Payload) - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestDiscardAll(t *testing.T) { - parser := DiscardAll(Term("abc")) - - tests := map[string]struct { - input string - remaining string - }{ - "empty input": {}, - "smaller than string": { - input: "ab", - remaining: "ab", - }, - "only string": { - input: "abc", - remaining: "", - }, - "lots of the string": { - input: "abcabc", - remaining: "", - }, - "lots of the string broken": { - input: "abcabc abc and this", - remaining: " abc and this", - }, - "lots of the string and some": { - input: "abcabc and this", - remaining: " and this", - }, - "not in the string": { - input: "n", - remaining: "n", - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Nil(t, res.Err) - assert.Empty(t, res.Payload) - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestOptional(t *testing.T) { - parser := Optional( - Sequence( - Term("abc"), - Term("def"), - ), - ) - - tests := map[string]struct { - input string - result []string - remaining string - err *Error - }{ - "empty input": {}, - "smaller than string": { - input: "ab", - remaining: "ab", - }, - "only first string": { - input: "abc", - remaining: "abc", - }, - "bit of second string": { - input: "abcde", - remaining: "abcde", - }, - "full string": { - input: "abcdef", - remaining: "", - result: []string{"abc", "def"}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - assert.Equal(t, test.err, res.Err) - assert.Equal(t, test.result, res.Payload) - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestNumber(t *testing.T) { - p := Number - - tests := map[string]struct { - input string - result any - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "number"), - }, - "just digits": { - input: "123", - result: int64(123), - remaining: "", - }, - "digits plus": { - input: "123 foo", - result: int64(123), - remaining: " foo", - }, - "float number": { - input: "0.123", - result: float64(0.123), - remaining: "", - }, - "float number 2": { - input: "0.123 foo", - result: float64(0.123), - remaining: " foo", - }, - "float number 3": { - input: "1.23.0 notthis", - result: float64(1.23), - remaining: ".0 notthis", - }, - "not a number": { - input: "hello", - remaining: "hello", - err: NewError([]rune("hello"), "number"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := p([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestBoolean(t *testing.T) { - p := Boolean - - tests := map[string]struct { - input string - result bool - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "boolean"), - }, - "just bool": { - input: "true", - result: true, - remaining: "", - }, - "just bool 2": { - input: "false", - result: false, - remaining: "", - }, - "not a bool": { - input: "hello", - remaining: "hello", - err: NewError([]rune("hello"), "boolean"), - }, - "bool and stuff": { - input: "false foo", - result: false, - remaining: " foo", - }, - "bool and stuff 2": { - input: "true foo", - result: true, - remaining: " foo", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := p([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestQuotedString(t *testing.T) { - str := QuotedString - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "quoted string"), - }, - "only quote": { - input: `"foo"`, - result: "foo", - remaining: "", - }, - "quote plus extra": { - input: `"foo" and this`, - result: "foo", - remaining: " and this", - }, - "quote with escapes": { - input: `"foo\u263abar" and this`, - result: "foo☺bar", - remaining: " and this", - }, - "quote with escaped quotes": { - input: `"foo\"bar\"baz" and this`, - result: `foo"bar"baz`, - remaining: " and this", - }, - "quote with escaped quotes 2": { - input: `"foo\\\"bar\\\"baz" and this`, - result: `foo\"bar\"baz`, - remaining: " and this", - }, - "not quoted": { - input: `foo`, - remaining: "foo", - err: NewError([]rune("foo"), "quoted string"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := str([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestQuotedMultilineString(t *testing.T) { - str := TripleQuoteString - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "quoted string"), - }, - "only quote": { - input: `"""foo"""`, - result: "foo", - remaining: "", - }, - "quote plus extra": { - input: `"""foo""" and this`, - result: "foo", - remaining: " and this", - }, - "quote with escapes": { - input: `"""foo\u263abar""" and this`, - result: `foo\u263abar`, - remaining: " and this", - }, - "quote with escaped quotes": { - input: `"""foo"bar"baz""" and this`, - result: `foo"bar"baz`, - remaining: " and this", - }, - "quote with escaped quotes 2": { - input: `"""foo\\\" -bar\\\" - -baz""" and this`, - result: `foo\\\" -bar\\\" - -baz`, - remaining: " and this", - }, - "not quoted": { - input: `foo`, - remaining: "foo", - err: NewError([]rune("foo"), "quoted string"), - }, - "unfinished quotes": { - input: `"""foo\"bar\"baz and this`, - remaining: `"""foo\"bar\"baz and this`, - err: NewFatalError([]rune(``), errors.New("required"), "end triple-quote"), - }, - "unfinished end quotes": { - input: `"""""0`, - remaining: `"""""0`, - err: NewFatalError([]rune(``), errors.New("required"), "end triple-quote"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := str([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestArray(t *testing.T) { - p := LiteralValue() - - tests := map[string]struct { - input string - result any - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "boolean", "number", "quoted string", "quoted string", "null", "array", "object"), - }, - "empty array": { - input: "[] and this", - result: []any{}, - remaining: " and this", - }, - "empty array with whitespace": { - input: "[ \t] and this", - result: []any{}, - remaining: " and this", - }, - "single element array": { - input: `[ "foo" ] and this`, - result: []any{"foo"}, - remaining: " and this", - }, - "tailing comma array": { - input: `[ "foo", ] and this`, - result: []any{"foo"}, - remaining: ` and this`, - }, - "random stuff array": { - input: `[ "foo", whats this ] and this`, - err: NewError([]rune(`whats this ] and this`), "boolean", "number", "quoted string", "quoted string", "null", "array", "object"), - remaining: `[ "foo", whats this ] and this`, - }, - "random stuff array 2": { - input: `[ "foo" whats this ] and this`, - err: NewError([]rune(`whats this ] and this`), ",", "]"), - remaining: `[ "foo" whats this ] and this`, - }, - "multiple elements array": { - input: `[ "foo", false,5.2] and this`, - result: []any{"foo", false, float64(5.2)}, - remaining: " and this", - }, - "multiple elements array line broken": { - input: `[ - "foo", - null, - "bar", - [true,false] -] and this`, - result: []any{"foo", nil, "bar", []any{true, false}}, - remaining: " and this", - }, - "multiple elements array line broken windows style": { - input: "[\r\n \"foo\",\r\n null,\r\n \"bar\",\r\n [true,false]\r\n] and this", - result: []any{"foo", nil, "bar", []any{true, false}}, - remaining: " and this", - }, - "multiple elements array comments": { - input: `[ - "foo", # this is a thing - null, -# the following lines are things - "bar", - [true,false] # and this -] and this`, - result: []any{"foo", nil, "bar", []any{true, false}}, - remaining: " and this", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := p([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestObject(t *testing.T) { - p := LiteralValue() - - tests := map[string]struct { - input string - result any - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "boolean", "number", "quoted string", "quoted string", "null", "array", "object"), - }, - "empty object": { - input: "{} and this", - result: map[string]any{}, - remaining: " and this", - }, - "empty object with whitespace": { - input: "{ \t} and this", - result: map[string]any{}, - remaining: " and this", - }, - "single value object": { - input: `{"foo":"bar"} and this`, - result: map[string]any{"foo": "bar"}, - remaining: " and this", - }, - "unfinished item object": { - input: `{ "foo": } and this`, - err: NewError([]rune("} and this"), "boolean", "number", "quoted string", "quoted string", "null", "array", "object"), - remaining: `{ "foo": } and this`, - }, - "random stuff object": { - input: `{ "foo": whats this } and this`, - err: NewError([]rune("whats this } and this"), "boolean", "number", "quoted string", "quoted string", "null", "array", "object"), - remaining: `{ "foo": whats this } and this`, - }, - "multiple values random stuff object": { - input: `{ "foo":true "bar":5.2 } and this`, - err: NewError([]rune(`"bar":5.2 } and this`), ",", "}"), - remaining: `{ "foo":true "bar":5.2 } and this`, - }, - "multiple values object": { - input: `{ "foo":true, "bar":5.2 } and this`, - result: map[string]any{"foo": true, "bar": 5.2}, - remaining: " and this", - }, - "multiple values trailing comma object": { - input: `{ "foo":true, "bar":5.2, } and this`, - result: map[string]any{"foo": true, "bar": 5.2}, - remaining: " and this", - }, - "multiple values object line broken": { - input: `{ - "foo":2 , - "bar": null, - "baz": - "three", - "quz": [true,false] -} and this`, - result: map[string]any{ - "foo": int64(2), - "bar": nil, - "baz": "three", - "quz": []any{true, false}, - }, - remaining: " and this", - }, - "multiple values object with comments": { - input: `{ # start of object - "foo":2 , # heres a thing - # A comment followed by an empty comment - # - # Followed by another comment - "bar": null, -# now these things are crazy - "baz": - "three", - "quz": [true,false] # woah! -} and this`, - result: map[string]any{ - "foo": int64(2), - "bar": nil, - "baz": "three", - "quz": []any{true, false}, - }, - remaining: " and this", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := p([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestSpacesAndTabs(t *testing.T) { - inSet := SpacesAndTabs - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "whitespace"), - }, - "only a char": { - input: " ", - result: " ", - remaining: "", - }, - "lots of the set": { - input: " \t ", - remaining: "", - result: " \t ", - }, - "lots of the set and some": { - input: " \t\t and this", - remaining: "and this", - result: " \t\t ", - }, - "not in the set": { - input: "n", - remaining: "n", - err: NewError([]rune("n"), "whitespace"), - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - err: NewError([]rune("nononono"), "whitespace"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := inSet([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestNewline(t *testing.T) { - inSet := Newline - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "line break"), - }, - "only a line feed": { - input: "\n", - result: "\n", - remaining: "", - }, - "only a carriage return": { - input: "\r", - remaining: "\r", - err: NewError([]rune(""), "line break"), - }, - "carriage return line feed": { - input: "\r\n", - result: "\r\n", - remaining: "", - }, - "a line feed plus": { - input: "\n foo", - result: "\n", - remaining: " foo", - }, - "crlf plus": { - input: "\r\n foo", - result: "\r\n", - remaining: " foo", - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - err: NewError([]rune("nononono"), "line break"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := inSet([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestAnyOf(t *testing.T) { - anyOf := OneOf( - Char('a'), - Char('b'), - Char('c'), - ) - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "a", "b", "c"), - }, - "only a char": { - input: "a", - result: "a", - remaining: "", - }, - "lots of the set": { - input: "abcabc", - remaining: "bcabc", - result: "a", - }, - "lots of the set 2": { - input: "bcabc", - remaining: "cabc", - result: "b", - }, - "lots of the set 3": { - input: "cabc", - remaining: "abc", - result: "c", - }, - "lots of the set and some": { - input: "a and this", - remaining: " and this", - result: "a", - }, - "lots of the set and some 2": { - input: "b and this", - remaining: " and this", - result: "b", - }, - "lots of the set and some 3": { - input: "c and this", - remaining: " and this", - result: "c", - }, - "not in the set": { - input: "n", - remaining: "n", - err: NewError([]rune("n"), "a", "b", "c"), - }, - "lots not in the set": { - input: "nononono", - remaining: "nononono", - err: NewError([]rune("nononono"), "a", "b", "c"), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := anyOf([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} - -func TestTakeOnly(t *testing.T) { - pattern := TakeOnly(1, UntilFail(OneOf(Char('1'), Char('2'), Char('3')))) - - tests := map[string]struct { - input string - result string - remaining string - err *Error - }{ - "empty input": { - err: NewError([]rune(""), "1", "2", "3"), - }, - "fewer than needed": { - input: "2", - result: "", - remaining: "", - }, - "exactly needed": { - input: "12", - result: "2", - remaining: "", - }, - "more than needed": { - input: "123", - result: "2", - remaining: "", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := pattern([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} diff --git a/internal/bloblang/parser/context.go b/internal/bloblang/parser/context.go deleted file mode 100644 index 3b697941a6..0000000000 --- a/internal/bloblang/parser/context.go +++ /dev/null @@ -1,233 +0,0 @@ -package parser - -import ( - "errors" - "io" - "os" - "path/filepath" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -// Context contains context used throughout a Bloblang parser for -// accessing function and method constructors. -type Context struct { - Functions *query.FunctionSet - Methods *query.MethodSet - namedContext *namedContext - importer Importer -} - -// EmptyContext returns a parser context with no functions, methods or import -// capabilities. -func EmptyContext() Context { - return Context{ - Functions: query.NewFunctionSet(), - Methods: query.NewMethodSet(), - importer: newOSImporter(), - } -} - -// GlobalContext returns a parser context with globally defined functions and -// methods. -func GlobalContext() Context { - return Context{ - Functions: query.AllFunctions, - Methods: query.AllMethods, - importer: newOSImporter(), - } -} - -type namedContext struct { - name string - next *namedContext -} - -// WithNamedContext returns a Context with a named execution context. -func (pCtx Context) WithNamedContext(name string) Context { - next := pCtx.namedContext - pCtx.namedContext = &namedContext{name: name, next: next} - return pCtx -} - -// HasNamedContext returns true if a given name exists as a named context. -func (pCtx Context) HasNamedContext(name string) bool { - tmp := pCtx.namedContext - for tmp != nil { - if tmp.name == name { - return true - } - tmp = tmp.next - } - return false -} - -// InitFunction attempts to initialise a function from the available -// constructors of the parser context. -func (pCtx Context) InitFunction(name string, args *query.ParsedParams) (query.Function, error) { - return pCtx.Functions.Init(name, args) -} - -// InitMethod attempts to initialise a method from the available constructors of -// the parser context. -func (pCtx Context) InitMethod(name string, target query.Function, args *query.ParsedParams) (query.Function, error) { - return pCtx.Methods.Init(name, target, args) -} - -// WithImporter returns a Context where imports are made from the provided -// Importer implementation. -func (pCtx Context) WithImporter(importer Importer) Context { - pCtx.importer = importer - return pCtx -} - -// WithImporterRelativeToFile returns a Context where any relative imports will -// be made from the directory of the provided file path. The provided path can -// itself be relative (to the current importer directory) or absolute. -func (pCtx Context) WithImporterRelativeToFile(pathStr string) Context { - pCtx.importer = pCtx.importer.RelativeToFile(pathStr) - return pCtx -} - -// Deactivated returns a version of the parser context where all functions and -// methods exist but can no longer be instantiated. This means it's possible to -// parse and validate mappings but not execute them. If the context also has an -// importer then it will also be replaced with an implementation that always -// returns empty files. -func (pCtx Context) Deactivated() Context { - nextCtx := pCtx - nextCtx.Functions = pCtx.Functions.Deactivated() - nextCtx.Methods = pCtx.Methods.Deactivated() - return nextCtx -} - -// CustomImporter returns a version of the parser context where file imports are -// done exclusively through a provided closure function, which takes an import -// path (relative or absolute). -func (pCtx Context) CustomImporter(fn func(name string) ([]byte, error)) Context { - nextCtx := pCtx - nextCtx.importer = newCustomImporter(fn) - return nextCtx -} - -// DisabledImports returns a version of the parser context where file imports -// are entirely disabled. Any import statement within parsed mappings will -// return parse errors explaining that file imports are disabled. -func (pCtx Context) DisabledImports() Context { - nextCtx := pCtx - nextCtx.importer = disabledImporter{} - return nextCtx -} - -// ImportFile attempts to read a file for import via the customised Importer. -func (pCtx Context) ImportFile(name string) ([]byte, error) { - return pCtx.importer.Import(name) -} - -//------------------------------------------------------------------------------ - -// Importer represents a repository of bloblang files that can be imported by -// mappings. It's possible for mappings to import files using relative paths, if -// the import is from a mapping which was itself imported then the path should -// be interpretted as relative to that file. -type Importer interface { - // Import a file from a relative or absolute path. - Import(pathStr string) ([]byte, error) - - // Derive a new importer where relative import paths are resolved from the - // directory of the provided file path. The provided path could be absolute, - // or relative itself in which case it should be resolved from the - // pre-existing relative directory. - RelativeToFile(filePath string) Importer -} - -//------------------------------------------------------------------------------ - -type osImporter struct { - relativePath string -} - -func newOSImporter() Importer { - pwd, _ := os.Getwd() - return &osImporter{ - relativePath: pwd, - } -} - -func (i *osImporter) Import(pathStr string) ([]byte, error) { - if !filepath.IsAbs(pathStr) { - pathStr = filepath.Join(i.relativePath, pathStr) - } - - f, err := os.Open(pathStr) - if err != nil { - return nil, err - } - return io.ReadAll(f) -} - -func (i *osImporter) RelativeToFile(filePath string) Importer { - dir := filepath.Dir(filePath) - if dir == "" || dir == "." { - return i - } - - pathStr := filepath.Dir(filePath) - if !filepath.IsAbs(pathStr) && i.relativePath != "" { - pathStr = filepath.Join(i.relativePath, pathStr) - } - - newI := *i - newI.relativePath = pathStr - return &newI -} - -//------------------------------------------------------------------------------ - -type customImporter struct { - relativePath string - readFn func(name string) ([]byte, error) -} - -func newCustomImporter(readFn func(name string) ([]byte, error)) Importer { - return &customImporter{ - relativePath: ".", - readFn: readFn, - } -} - -func (i *customImporter) Import(pathStr string) ([]byte, error) { - if !filepath.IsAbs(pathStr) { - pathStr = filepath.Join(i.relativePath, pathStr) - } - - return i.readFn(pathStr) -} - -func (i *customImporter) RelativeToFile(filePath string) Importer { - dir := filepath.Dir(filePath) - if dir == "" || dir == "." { - return i - } - - pathStr := filepath.Dir(filePath) - if !filepath.IsAbs(pathStr) && i.relativePath != "" { - pathStr = filepath.Join(i.relativePath, pathStr) - } - - newI := *i - newI.relativePath = pathStr - return &newI -} - -//------------------------------------------------------------------------------ - -type disabledImporter struct{} - -func (d disabledImporter) Import(pathStr string) ([]byte, error) { - return nil, errors.New("imports are disabled in this context") -} - -func (d disabledImporter) RelativeToFile(filePath string) Importer { - return d -} diff --git a/internal/bloblang/parser/context_test.go b/internal/bloblang/parser/context_test.go deleted file mode 100644 index 9f932792d3..0000000000 --- a/internal/bloblang/parser/context_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package parser - -import ( - "fmt" - "os" - "path" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestContextImportIsolation(t *testing.T) { - tmpDir := t.TempDir() - - content := `map foo { root = this }` - fileName := "foo.blobl" - fullPath := filepath.Join(tmpDir, fileName) - require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644)) - - for _, srcCtx := range []Context{GlobalContext(), EmptyContext()} { - relCtx := srcCtx.WithImporterRelativeToFile(fullPath) - isoCtx := srcCtx.DisabledImports() - - // Source context only works with full path - _, err := srcCtx.importer.Import(fileName) - assert.Error(t, err) - - out, err := srcCtx.importer.Import(fullPath) - assert.NoError(t, err) - assert.Equal(t, content, string(out)) - - // Relative context works with full or relative path - out, err = relCtx.importer.Import(fullPath) - assert.NoError(t, err) - assert.Equal(t, content, string(out)) - - out, err = relCtx.importer.Import(fileName) - assert.NoError(t, err) - assert.Equal(t, content, string(out)) - - // Isolated context doesn't work with any path - _, err = isoCtx.importer.Import(fileName) - assert.Error(t, err) - - _, err = isoCtx.importer.Import(fullPath) - assert.Error(t, err) - } -} - -func TestContextImportRelativity(t *testing.T) { - tmpDir := t.TempDir() - - for path, content := range map[string]string{ - "mappings/foo.blobl": `map foo { root.foo = this.foo }`, - "mappings/first/bar.blobl": `map bar { root.bar = this.bar }`, - "mappings/first/second/baz.blobl": `map baz { root.baz = this.baz }`, - } { - osPath := filepath.FromSlash(path) - dirPath := filepath.Dir(osPath) - - require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, dirPath), 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(tmpDir, osPath), []byte(content), 0o644)) - } - - for _, srcCtx := range []Context{GlobalContext(), EmptyContext()} { - relCtxOne := srcCtx.WithImporterRelativeToFile(filepath.Join(tmpDir, "mappings", "foo.blobl")) - relCtxTwo := relCtxOne.WithImporterRelativeToFile(filepath.Join("first", "bar.blobl")) - - // foo.blobl - content, err := srcCtx.importer.Import(filepath.Join(tmpDir, "mappings", "foo.blobl")) - require.NoError(t, err) - assert.Equal(t, `map foo { root.foo = this.foo }`, string(content)) - - content, err = relCtxOne.importer.Import("foo.blobl") - require.NoError(t, err) - assert.Equal(t, `map foo { root.foo = this.foo }`, string(content)) - - content, err = relCtxTwo.importer.Import("../foo.blobl") - require.NoError(t, err) - assert.Equal(t, `map foo { root.foo = this.foo }`, string(content)) - - // bar.blobl - content, err = srcCtx.importer.Import(filepath.Join(tmpDir, "mappings", "first", "bar.blobl")) - require.NoError(t, err) - assert.Equal(t, `map bar { root.bar = this.bar }`, string(content)) - - content, err = relCtxOne.importer.Import("./first/bar.blobl") - require.NoError(t, err) - assert.Equal(t, `map bar { root.bar = this.bar }`, string(content)) - - content, err = relCtxTwo.importer.Import("bar.blobl") - require.NoError(t, err) - assert.Equal(t, `map bar { root.bar = this.bar }`, string(content)) - - // baz.blobl - content, err = srcCtx.importer.Import(filepath.Join(tmpDir, "mappings", "first", "second", "baz.blobl")) - require.NoError(t, err) - assert.Equal(t, `map baz { root.baz = this.baz }`, string(content)) - - content, err = relCtxOne.importer.Import("./first/second/baz.blobl") - require.NoError(t, err) - assert.Equal(t, `map baz { root.baz = this.baz }`, string(content)) - - content, err = relCtxTwo.importer.Import("second/baz.blobl") - require.NoError(t, err) - assert.Equal(t, `map baz { root.baz = this.baz }`, string(content)) - } -} - -func TestContextCustomImports(t *testing.T) { - mappings := map[string]string{ - "mappings/foo.blobl": `map foo { root.foo = this.foo }`, - "mappings/first/bar.blobl": `map bar { root.bar = this.bar }`, - "mappings/first/second/baz.blobl": `map baz { root.baz = this.baz }`, - } - - importer := func(name string) ([]byte, error) { - name = path.Clean(name) - s, ok := mappings[name] - if !ok { - return nil, fmt.Errorf("mapping %v not found", name) - } - return []byte(s), nil - } - - for _, srcCtx := range []Context{GlobalContext().CustomImporter(importer), EmptyContext().CustomImporter(importer)} { - relCtxOne := srcCtx.WithImporterRelativeToFile(path.Join("mappings", "foo.blobl")) - relCtxTwo := relCtxOne.WithImporterRelativeToFile(path.Join("first", "bar.blobl")) - - // foo.blobl - content, err := srcCtx.importer.Import(path.Join("mappings", "foo.blobl")) - require.NoError(t, err) - assert.Equal(t, `map foo { root.foo = this.foo }`, string(content)) - - content, err = relCtxOne.importer.Import("foo.blobl") - require.NoError(t, err) - assert.Equal(t, `map foo { root.foo = this.foo }`, string(content)) - - content, err = relCtxTwo.importer.Import("../foo.blobl") - require.NoError(t, err) - assert.Equal(t, `map foo { root.foo = this.foo }`, string(content)) - - // bar.blobl - content, err = srcCtx.importer.Import(path.Join("mappings", "first", "bar.blobl")) - require.NoError(t, err) - assert.Equal(t, `map bar { root.bar = this.bar }`, string(content)) - - content, err = relCtxOne.importer.Import("./first/bar.blobl") - require.NoError(t, err) - assert.Equal(t, `map bar { root.bar = this.bar }`, string(content)) - - content, err = relCtxTwo.importer.Import("bar.blobl") - require.NoError(t, err) - assert.Equal(t, `map bar { root.bar = this.bar }`, string(content)) - - // baz.blobl - content, err = srcCtx.importer.Import(path.Join("mappings", "first", "second", "baz.blobl")) - require.NoError(t, err) - assert.Equal(t, `map baz { root.baz = this.baz }`, string(content)) - - content, err = relCtxOne.importer.Import("./first/second/baz.blobl") - require.NoError(t, err) - assert.Equal(t, `map baz { root.baz = this.baz }`, string(content)) - - content, err = relCtxTwo.importer.Import("second/baz.blobl") - require.NoError(t, err) - assert.Equal(t, `map baz { root.baz = this.baz }`, string(content)) - } -} diff --git a/internal/bloblang/parser/dot_env_parser.go b/internal/bloblang/parser/dot_env_parser.go deleted file mode 100644 index 87a996041d..0000000000 --- a/internal/bloblang/parser/dot_env_parser.go +++ /dev/null @@ -1,57 +0,0 @@ -package parser - -import ( - "fmt" -) - -var dotEnvParser = func() Func[map[string]string] { - assignmentParser := Sequence( - NotInSet('=', ' ', '\n', '#'), - Optional(SpacesAndTabs), - charEquals, - Optional(SpacesAndTabs), - Optional(OneOf(TripleQuoteString, QuotedString, NotInSet('#', ' ', '\n'))), - Optional(SpacesAndTabs), - ) - - envFileParser := Delimited( - Expect(OneOf( - assignmentParser, - ZeroedFuncAs[string, []string](SpacesAndTabs), - ZeroedFuncAs[any, []string](EmptyLine), - ZeroedFuncAs[any, []string](EndOfInput), - ZeroedFuncAs[[]any, []string](Sequence( - FuncAsAny(charHash), - FuncAsAny(Optional(UntilFail(NotChar('\n')))), - )), - ), "Environment variable assignment"), - NewlineAllowComment, - ) - - return func(input []rune) Result[map[string]string] { - res := envFileParser(input) - if res.Err != nil { - return Fail[map[string]string](res.Err, input) - } - vars := map[string]string{} - for _, line := range res.Payload.Primary { - if len(line) != 6 { - continue - } - vars[line[0]] = line[4] - } - return Success(vars, res.Remaining) - } -}() - -// ParseDotEnvFile attempts to parse a .env file containing environment variable -// assignments, and returns either a map of key/value assignments or an error. -func ParseDotEnvFile(envFileBytes []byte) (map[string]string, error) { - input := string(envFileBytes) - res := dotEnvParser([]rune(input)) - if res.Err != nil { - line, _ := LineAndColOf([]rune(input), res.Err.Input) - return nil, fmt.Errorf("%v: %v", line, res.Err.ErrorAtPositionStructured("", []rune(input))) - } - return res.Payload, nil -} diff --git a/internal/bloblang/parser/dot_env_parser_test.go b/internal/bloblang/parser/dot_env_parser_test.go deleted file mode 100644 index 42ba561f17..0000000000 --- a/internal/bloblang/parser/dot_env_parser_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParseDotEnv(t *testing.T) { - parser := dotEnvParser - - tests := map[string]struct { - input string - result map[string]string - remaining string - err *Error - }{ - "empty input": { - result: map[string]string{}, - }, - "empty lines": { - input: "\n \n \t\n\n #foo bar \n\n", - result: map[string]string{}, - }, - "bad assignment": { - input: "not a thing", - err: NewError([]rune("a thing"), "Environment variable assignment"), - remaining: "not a thing", - }, - "single line": { - input: `FOO=bar`, - result: map[string]string{"FOO": "bar"}, - }, - "single assignment with empty lines": { - input: "\n \nFOO=bar\n \n ", - result: map[string]string{"FOO": "bar"}, - }, - "multiple assignments": { - input: "FOO=bar\nBAZ=buz # Cool\n\nBEV=qux", - result: map[string]string{ - "FOO": "bar", - "BAZ": "buz", - "BEV": "qux", - }, - }, - "multiple assignments with quotes": { - input: "FOO=bar\n# Meow meow\nBAZ=\"buz\" # Cool\n\nBEV=qux", - result: map[string]string{ - "FOO": "bar", - "BAZ": "buz", - "BEV": "qux", - }, - }, - "multiple assignments gibberish at the end": { - input: "FOO=bar\nBAZ=buz\n\nBEV=qux\nand some stuff here", - err: NewError([]rune("some stuff here"), "Environment variable assignment"), - remaining: "FOO=bar\nBAZ=buz\n\nBEV=qux\nand some stuff here", - }, - "multiple assignments with spaces": { - input: ` -FIRST = foo - -SECOND= "bar" - -THIRD =b"az - -FOURTH = buz - -FIFTH = - -`, - result: map[string]string{ - "FIRST": "foo", - "SECOND": "bar", - "THIRD": "b\"az", - "FOURTH": "buz", - "FIFTH": "", - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - res := parser([]rune(test.input)) - require.Equal(t, test.err, res.Err, "Error") - assert.Equal(t, test.result, res.Payload, "Result") - assert.Equal(t, test.remaining, string(res.Remaining), "Remaining") - }) - } -} diff --git a/internal/bloblang/parser/error.go b/internal/bloblang/parser/error.go deleted file mode 100644 index 6b7d696c4e..0000000000 --- a/internal/bloblang/parser/error.go +++ /dev/null @@ -1,267 +0,0 @@ -package parser - -import ( - "bytes" - "fmt" - "strconv" - "strings" -) - -// LineAndColOf returns the line and column position of a tailing clip from an -// input. -func LineAndColOf(input, clip []rune) (line, col int) { - char := len(input) - len(clip) - - lines := strings.Split(string(input), "\n") - for ; line < len(lines); line++ { - if char < (len(lines[line]) + 1) { - break - } - char = char - len(lines[line]) - 1 - } - - return line + 1, char + 1 -} - -// Error represents an error that has occurred whilst attempting to apply a -// parser function to a given input. A slice of abstract names should be -// provided outlining tokens or characters that were expected and not found at -// the input in order to provide a useful error message. -// -// The input at the point of the error can be used in order to infer where -// exactly in the input the error occurred with len(input) - len(err.Input). -type Error struct { - Input []rune - Err error - Expected []string -} - -// NewError creates a parser error from the input and a list of expected tokens. -// This is a passive error indicating that this particular parser did not -// succeed, but that other parsers should be tried if applicable. -func NewError(input []rune, expected ...string) *Error { - return &Error{ - Input: input, - Expected: expected, - } -} - -// NewFatalError creates a parser error from the input and a wrapped fatal error -// indicating that this parse succeeded partially, but a requirement was not met -// that means the parsed input is invalid and that all parsing should stop. -func NewFatalError(input []rune, err error, expected ...string) *Error { - return &Error{ - Input: input, - Err: err, - Expected: expected, - } -} - -func (e *Error) errorMsg(includeGot bool) string { - inputSnippet := string(e.Input) - if len(e.Input) > 5 { - inputSnippet = string(e.Input[:5]) - } - - var msg string - if e.Err != nil { - msg = e.Err.Error() - } - - if len(e.Expected) == 0 { - if msg == "" { - if inputSnippet == "" { - msg = "encountered unexpected end of input" - } else { - msg = "encountered unexpected input" - } - } - if includeGot { - if inputSnippet != "" { - msg += fmt.Sprintf(": %v", inputSnippet) - } - } - return msg - } - - if msg != "" { - msg += ": " - } - - if len(e.Expected) == 1 { - msg += fmt.Sprintf("expected %v", e.Expected[0]) - } else { - dedupeMap := make(map[string]struct{}, len(e.Expected)) - expected := make([]string, 0, len(e.Expected)) - for _, exp := range e.Expected { - if _, exists := dedupeMap[exp]; !exists { - expected = append(expected, exp) - } - dedupeMap[exp] = struct{}{} - } - - var buf bytes.Buffer - buf.WriteString("expected ") - for i, exp := range expected { - if i == len(expected)-1 { - if len(expected) > 2 { - buf.WriteByte(',') - } - buf.WriteString(" or ") - } else if i > 0 { - buf.WriteString(", ") - } - buf.WriteString(exp) - } - msg += buf.String() - } - - if includeGot { - if inputSnippet != "" { - msg += fmt.Sprintf(", got: %v", inputSnippet) - } else { - msg += ", but reached end of input" - } - } - return msg -} - -// Error returns a human readable error string. -func (e *Error) Error() string { - return e.errorMsg(true) -} - -// Unwrap returns the underlying fatal error (or nil). -func (e *Error) Unwrap() error { - return e.Err -} - -// IsFatal returns whether this parser error should be considered fatal, and -// therefore sibling parser candidates should not be tried. -func (e *Error) IsFatal() bool { - return e.Err != nil -} - -// Add context from another error into this one. -func (e *Error) Add(from *Error) { - e.Expected = append(e.Expected, from.Expected...) - if e.Err == nil { - e.Err = from.Err - } -} - -// ErrorAtPosition returns a human readable error string including the line and -// character position of the error. -func (e *Error) ErrorAtPosition(input []rune) string { - importErr, isImport := e.Err.(*ImportError) - - var errStr string - if isImport { - errStr = fmt.Sprintf( - "failed to parse import '%v': %v", importErr.filepath, - importErr.perr.ErrorAtPosition(importErr.content), - ) - } else { - errStr = e.errorMsg(false) - } - - line, char := LineAndColOf(input, e.Input) - return fmt.Sprintf("line %v char %v: %v", line, char, errStr) -} - -// ErrorAtChar returns a human readable error string including the character -// position of the error. -func (e *Error) ErrorAtChar(input []rune) string { - importErr, isImport := e.Err.(*ImportError) - - var errStr string - if isImport { - errStr = fmt.Sprintf( - "failed to parse import '%v': %v", importErr.filepath, - importErr.perr.ErrorAtChar(importErr.perr.Input), - ) - } else { - errStr = e.errorMsg(false) - } - - char := len(input) - len(e.Input) - return fmt.Sprintf("char %v: %v", char+1, errStr) -} - -// ErrorAtPositionStructured returns a human readable error string including the -// line and character position of the error formatted in a more structured way, -// this message isn't appropriate to write within structured logs as the -// formatting will be broken. -func (e *Error) ErrorAtPositionStructured(filepath string, input []rune) string { - importErr, isImport := e.Err.(*ImportError) - - var errStr string - if isImport { - errStr = fmt.Sprintf("failed to parse import '%v'", importErr.filepath) - } else { - errStr = e.errorMsg(false) - } - - line, char := 0, len(input)-len(e.Input) - var contextLine string - - lines := strings.Split(string(input), "\n") - for ; line < len(lines); line++ { - if char < (len(lines[line]) + 1) { - maxLen := len(lines[line]) - if char < (maxLen - 60) { - maxLen = char + 60 - } - contextLine = lines[line][:maxLen] - break - } - char = char - len(lines[line]) - 1 - } - - lineStr := strconv.FormatInt(int64(line+1), 10) - linePadding := strings.Repeat(" ", len(lineStr)) - - filepathStr := "" - if filepath != "" { - filepathStr = filepath + ": " - } - - structuredMsg := fmt.Sprintf(`%vline %v char %v: %v -%v | -%v | %v -%v | %v^---`, - filepathStr, - lineStr, char+1, errStr, - linePadding, - lineStr, contextLine, - linePadding, strings.Repeat(" ", char)) - - if isImport { - structuredMsg = structuredMsg + "\n\n" + importErr.perr.ErrorAtPositionStructured(importErr.filepath, importErr.content) - } - - return structuredMsg -} - -// ImportError wraps a parser error with an import file path. When a fatal error -// wraps. -type ImportError struct { - filepath string - content []rune - perr *Error -} - -// NewImportError wraps a parser error with a filepath for when a parser has -// attempted to parse content of an imported file. -func NewImportError(path string, input []rune, err *Error) *ImportError { - return &ImportError{ - filepath: path, - content: input, - perr: err, - } -} - -// Error implements the standard error interface. -func (i *ImportError) Error() string { - return i.perr.Error() -} diff --git a/internal/bloblang/parser/error_test.go b/internal/bloblang/parser/error_test.go deleted file mode 100644 index 5dffafe242..0000000000 --- a/internal/bloblang/parser/error_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package parser - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestErrorStrings(t *testing.T) { - tests := []struct { - err *Error - exp string - }{ - { - err: NewError([]rune("input data"), "foo", "bar", "baz"), - exp: `expected foo, bar, or baz, got: input`, - }, - { - err: NewError([]rune("input data"), "foo", "bar", "foo", "baz"), - exp: `expected foo, bar, or baz, got: input`, - }, - { - err: NewError(nil, "foo", "bar", "baz"), - exp: `expected foo, bar, or baz, but reached end of input`, - }, - { - err: NewError(nil), - exp: `encountered unexpected end of input`, - }, - { - err: NewError([]rune("?!"), "foo", "bar", "baz"), - exp: `expected foo, bar, or baz, got: ?!`, - }, - { - err: NewError([]rune("nope")), - exp: `encountered unexpected input: nope`, - }, - { - err: NewError([]rune("input data"), "foo", "bar"), - exp: `expected foo or bar, got: input`, - }, - { - err: NewError([]rune("input data"), "foo"), - exp: `expected foo, got: input`, - }, - { - err: NewFatalError([]rune("input data"), errors.New("nope"), "foo", "bar", "baz"), - exp: `nope: expected foo, bar, or baz, got: input`, - }, - { - err: NewFatalError([]rune("nope"), errors.New("oh no")), - exp: `oh no: nope`, - }, - } - - for _, test := range tests { - assert.Equal(t, test.exp, test.err.Error()) - } -} - -func TestErrorPositionalStrings(t *testing.T) { - tests := []struct { - input string - err *Error - exp string - }{ - { - input: `hello world input data`, - err: NewError([]rune("input data"), "foo", "bar", "baz"), - exp: `line 1 char 13: expected foo, bar, or baz`, - }, - { - input: `hello world input data`, - err: NewError(nil, "foo", "bar", "baz"), - exp: `line 1 char 23: expected foo, bar, or baz`, - }, - { - input: `hello world input data`, - err: NewError(nil), - exp: `line 1 char 23: encountered unexpected end of input`, - }, - { - input: "hello \nworld \ninput data", - err: NewError([]rune("input data"), "foo", "bar", "baz"), - exp: `line 3 char 1: expected foo, bar, or baz`, - }, - { - input: "hello \nworld \ni", - err: NewError([]rune("i"), "foo", "bar", "baz"), - exp: `line 3 char 1: expected foo, bar, or baz`, - }, - { - input: "hello \nworld \n input data\n foo", - err: NewError([]rune("input data\n foo"), "foo", "bar", "baz"), - exp: `line 3 char 2: expected foo, bar, or baz`, - }, - { - input: "hello \nworld \n i", - err: NewError([]rune("i"), "foo", "bar", "baz"), - exp: `line 3 char 2: expected foo, bar, or baz`, - }, - } - - for _, test := range tests { - assert.Equal(t, test.exp, test.err.ErrorAtPosition([]rune(test.input))) - } -} - -func TestErrorPositionalStringsStructured(t *testing.T) { - tests := []struct { - input string - err *Error - exp string - }{ - { - input: `hello world input data`, - err: NewError([]rune("input data"), "foo", "bar", "baz"), - exp: `line 1 char 13: expected foo, bar, or baz - | -1 | hello world input data - | ^---`, - }, - { - input: `hello world input data`, - err: NewError(nil, "foo", "bar", "baz"), - exp: `line 1 char 23: expected foo, bar, or baz - | -1 | hello world input data - | ^---`, - }, - { - input: `hello world input data`, - err: NewError(nil), - exp: `line 1 char 23: encountered unexpected end of input - | -1 | hello world input data - | ^---`, - }, - { - input: `hello world this is a really long input string input data`, - err: NewError([]rune("input data"), "foo", "bar", "baz"), - exp: `line 1 char 48: expected foo, bar, or baz - | -1 | hello world this is a really long input string input data - | ^---`, - }, - { - input: `hello world this is a really long input string input data with too much information that we dont want to entirely print`, - err: NewError([]rune("this is a really long input string input data with too much information that we dont want to entirely print"), "foo", "bar", "baz"), - exp: `line 1 char 13: expected foo, bar, or baz - | -1 | hello world this is a really long input string input data with too much - | ^---`, - }, - { - input: "hello world\nthis is a really\nlong input string input data", - err: NewError([]rune("input data"), "foo", "bar", "baz"), - exp: `line 3 char 19: expected foo, bar, or baz - | -3 | long input string input data - | ^---`, - }, - { - input: "hello world\n\nthis is a really\nlong input string input\n\ndata", - err: NewError([]rune("input\n\ndata"), "foo", "bar", "baz"), - exp: `line 4 char 19: expected foo, bar, or baz - | -4 | long input string input - | ^---`, - }, - } - - for _, test := range tests { - assert.Equal(t, test.exp, test.err.ErrorAtPositionStructured("", []rune(test.input))) - } -} diff --git a/internal/bloblang/parser/field_parser.go b/internal/bloblang/parser/field_parser.go deleted file mode 100644 index 567a25a096..0000000000 --- a/internal/bloblang/parser/field_parser.go +++ /dev/null @@ -1,90 +0,0 @@ -package parser - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/field" -) - -func intoStaticResolver(p Func[string]) Func[field.Resolver] { - return func(input []rune) Result[field.Resolver] { - res := p(input) - if res.Err != nil { - return Fail[field.Resolver](res.Err, input) - } - return Success[field.Resolver](field.StaticResolver(res.Payload), res.Remaining) - } -} - -var interpStart = Term("${!") - -func aFunction(pCtx Context) Func[field.Resolver] { - pattern := TakeOnly(2, Sequence( - strToQuery(interpStart), - strToQuery(Optional(SpacesAndTabs)), - MustBe(queryParser(pCtx)), - strToQuery(Optional(SpacesAndTabs)), - strToQuery(MustBe(Expect(charSquigClose, "end of expression"))), - )) - return func(input []rune) Result[field.Resolver] { - res := pattern(input) - if res.Err != nil { - return Fail[field.Resolver](res.Err, input) - } - return Success[field.Resolver](field.NewQueryResolver(res.Payload), res.Remaining) - } -} - -var ( - interpEscapedStart = Term("${{!") - interpEscapedEnd = Term("}}") - untilInterpEscapedEnd = UntilTerm("}}") -) - -var escapedBlock = func() Func[field.Resolver] { - pattern := TakeOnly(1, Sequence( - interpEscapedStart, - MustBe(Expect(untilInterpEscapedEnd, "end of escaped expression")), - interpEscapedEnd, - )) - return func(input []rune) Result[field.Resolver] { - res := pattern(input) - if res.Err != nil { - return Fail[field.Resolver](res.Err, input) - } - return Success[field.Resolver](field.StaticResolver("${!"+res.Payload+"}"), res.Remaining) - } -}() - -//------------------------------------------------------------------------------ - -func parseFieldResolvers(pCtx Context, expr string) ([]field.Resolver, *Error) { - var resolvers []field.Resolver - - p := OneOf( - escapedBlock, - aFunction(pCtx), - intoStaticResolver(charDollar), - intoStaticResolver(NotChar('$')), - ) - - remaining := []rune(expr) - for len(remaining) > 0 { - res := p(remaining) - if res.Err != nil { - return nil, res.Err - } - remaining = res.Remaining - resolvers = append(resolvers, res.Payload) - } - - return resolvers, nil -} - -// ParseField attempts to parse a field expression. -func ParseField(pCtx Context, expr string) (*field.Expression, *Error) { - resolvers, err := parseFieldResolvers(pCtx, expr) - if err != nil { - return nil, err - } - e := field.NewExpression(resolvers...) - return e, nil -} diff --git a/internal/bloblang/parser/field_parser_test.go b/internal/bloblang/parser/field_parser_test.go deleted file mode 100644 index 7c3c3d7ba0..0000000000 --- a/internal/bloblang/parser/field_parser_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestFieldStaticExpressionOptimization(t *testing.T) { - tests := map[string]string{ - "a static string": "a static string", - "a string ${{!with escapes}} still static": "a string ${!with escapes} still static", - "a string $ with dollars still static": "a string $ with dollars still static", - " ": " ", - "": "", - } - - for k, v := range tests { - t.Run(k, func(t *testing.T) { - rs, pErr := parseFieldResolvers(GlobalContext(), k) - require.Nil(t, pErr) - - e := field.NewExpression(rs...) - - res, err := e.String(0, message.QuickBatch(nil)) - require.NoError(t, err) - assert.Equal(t, v, res) - - bres, err := e.Bytes(0, message.QuickBatch(nil)) - require.NoError(t, err) - assert.Equal(t, v, string(bres)) - }) - } -} - -func TestFieldExpressionParserErrors(t *testing.T) { - tests := map[string]struct { - input string - err string - }{ - "bad function": { - input: `static string ${!not a function} hello world`, - err: `char 22: required: expected end of expression`, - }, - "bad function 2": { - input: `static string ${!not_a_function()} hello world`, - err: `char 34: unrecognised function 'not_a_function'`, - }, - "bad args": { - input: `foo ${!json("foo") whats this?} bar`, - err: `char 20: required: expected end of expression`, - }, - "bad args 2": { - input: `foo ${!json("foo} bar`, - err: `char 22: required: expected end quote`, - }, - "bad args 3": { - input: `foo ${!json(} bar`, - err: `char 13: required: expected function argument`, - }, - "bad args 4": { - input: `foo ${!json(0,} bar`, - err: `char 15: required: expected function argument`, - }, - "unfinished escape": { - input: `a string that ends ${{!with unfinished escapes`, - err: `char 24: required: expected end of escaped expression`, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - _, err := ParseField(GlobalContext(), test.input) - require.NotNil(t, err) - require.Equal(t, test.err, err.ErrorAtChar([]rune(test.input))) - }) - } -} - -func TestFieldExpressions(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - tests := map[string]struct { - input string - output string - messages []easyMsg - index int - }{ - "static string": { - input: `static string hello world`, - output: `static string hello world`, - }, - "dollar on its own": { - input: `hello $ world`, - output: `hello $ world`, - }, - "dollar on its own 2": { - input: `hello world $`, - output: `hello world $`, - }, - "dollar on its own 3": { - input: `$ hello world`, - output: `$ hello world`, - }, - "escaped string": { - input: `hello ${{!this is escaped}} world`, - output: `hello ${!this is escaped} world`, - }, - "escaped string 2": { - input: `hello world ${{!this is escaped}}`, - output: `hello world ${!this is escaped}`, - }, - "escaped string 3": { - input: `${{!this is escaped}} hello world`, - output: `${!this is escaped} hello world`, - }, - "escaped string 4": { - input: `${{!this is escaped}}`, - output: `${!this is escaped}`, - }, - "json function": { - input: `${!json()}`, - output: `{"foo":"bar"}`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - {content: `not json`}, - }, - }, - "json function 2": { - input: `${!json("foo")}`, - output: `bar`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - }, - }, - "json function 3": { - input: `${!json("foo")}`, - output: `bar`, - index: 1, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json function 4": { - input: `${!json("foo")}`, - output: `{"bar":"baz"}`, - index: 0, - messages: []easyMsg{ - {content: `{"foo":{"bar":"baz"}}`}, - }, - }, - "json function 5": { - input: `${!json("foo") }`, - output: `{"bar":"baz"}`, - index: 0, - messages: []easyMsg{ - {content: `{"foo":{"bar":"baz"}}`}, - }, - }, - "json_from function": { - input: `${!json("foo").from(1)}`, - output: `bar`, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json_from function 2": { - input: `${!json("foo").from(0)}`, - output: `null`, - messages: []easyMsg{ - {content: `{}`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json_from function 3": { - input: `${!json("foo").from(-1)}`, - output: `bar`, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - e, pErr := ParseField(GlobalContext(), test.input) - require.Nil(t, pErr) - - res, err := e.String(test.index, msg) - require.NoError(t, err) - assert.Equal(t, test.output, res) - }) - } -} diff --git a/internal/bloblang/parser/mapping_parser.go b/internal/bloblang/parser/mapping_parser.go deleted file mode 100644 index fd68f3050d..0000000000 --- a/internal/bloblang/parser/mapping_parser.go +++ /dev/null @@ -1,479 +0,0 @@ -package parser - -import ( - "errors" - "fmt" - "strings" - - "github.com/Jeffail/gabs/v2" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -// ParseMapping parses a bloblang mapping and returns an executor to run it, or -// an error if the parsing fails. -// -// The filepath is optional and used for relative file imports and error -// messages. -func ParseMapping(pCtx Context, expr string) (*mapping.Executor, *Error) { - in := []rune(expr) - - resDirectImport := singleRootImport(pCtx)(in) - if resDirectImport.Err != nil && resDirectImport.Err.IsFatal() { - return nil, resDirectImport.Err - } - if resDirectImport.Err == nil && len(resDirectImport.Remaining) == 0 { - return resDirectImport.Payload, nil - } - - resExe := parseExecutor(pCtx)(in) - if resExe.Err != nil && resExe.Err.IsFatal() { - return nil, resExe.Err - } - resSingle := singleRootMapping(pCtx)(in) - - res := bestMatch(resExe, resSingle) - if res.Err != nil { - return nil, res.Err - } - return res.Payload, nil -} - -//------------------------------------------------------------------------------ - -func mappingStatement(pCtx Context, enableMeta bool, maps map[string]query.Function) Func[mapping.Statement] { - toNilStatement := ZeroedFuncAs[string, mapping.Statement] - - var enabledStatements []Func[mapping.Statement] - if maps != nil { - enabledStatements = []Func[mapping.Statement]{ - toNilStatement(importParser(pCtx, maps)), - toNilStatement(mapParser(pCtx, maps)), - } - } - enabledStatements = append(enabledStatements, - letStatementParser(pCtx), - metaStatementParser(pCtx, enableMeta), - plainMappingStatementParser(pCtx), - rootLevelIfExpressionParser(pCtx), - ) - - return OneOf(enabledStatements...) -} - -func parseExecutor(pCtx Context) Func[*mapping.Executor] { - return func(input []rune) Result[*mapping.Executor] { - maps := map[string]query.Function{} - statements := []mapping.Statement{} - - statementPattern := mappingStatement(pCtx, true, maps) - - res := statementPattern(DiscardedWhitespaceNewlineComments(input).Remaining) - if res.Err != nil { - res.Remaining = input - return ResultInto[*mapping.Executor](res) - } - if res.Payload != nil { - statements = append(statements, res.Payload) - } - - for { - if res.Remaining = Discard(SpacesAndTabs)(res.Remaining).Remaining; len(res.Remaining) == 0 { - break - } - - if tmpRes := NewlineAllowComment(res.Remaining); tmpRes.Err != nil { - return Fail[*mapping.Executor](tmpRes.Err, input) - } else { - res.Remaining = tmpRes.Remaining - } - - if res.Remaining = DiscardedWhitespaceNewlineComments(res.Remaining).Remaining; len(res.Remaining) == 0 { - break - } - - if res = statementPattern(res.Remaining); res.Err != nil { - return Fail[*mapping.Executor](res.Err, input) - } - if res.Payload != nil { - statements = append(statements, res.Payload) - } - } - return Success(mapping.NewExecutor("", input, maps, statements...), res.Remaining) - } -} - -func singleRootImport(pCtx Context) Func[*mapping.Executor] { - parser := TakeOnly(3, Sequence( - DiscardedWhitespaceNewlineComments, - Term("from"), - SpacesAndTabs, - QuotedString, - DiscardedWhitespaceNewlineComments, - )) - - return func(input []rune) Result[*mapping.Executor] { - res := parser(input) - if res.Err != nil { - return Fail[*mapping.Executor](res.Err, input) - } - - fpath := res.Payload - contents, err := pCtx.importer.Import(fpath) - if err != nil { - return Fail[*mapping.Executor](NewFatalError(input, fmt.Errorf("failed to read import: %w", err)), input) - } - - nextCtx := pCtx.WithImporterRelativeToFile(fpath) - - importContent := []rune(string(contents)) - execRes := parseExecutor(nextCtx)(importContent) - if execRes.Err != nil { - return Fail[*mapping.Executor](NewFatalError(input, NewImportError(fpath, importContent, execRes.Err)), input) - } - if len(res.Remaining) > 0 { - return Fail[*mapping.Executor](NewFatalError(input, fmt.Errorf("unexpected content after single root import: %s", string(res.Remaining))), input) - } - return Success(execRes.Payload, res.Remaining) - } -} - -func singleRootMapping(pCtx Context) Func[*mapping.Executor] { - return func(input []rune) Result[*mapping.Executor] { - res := queryParser(pCtx)(DiscardedWhitespaceNewlineComments(input).Remaining) - if res.Err != nil { - return Fail[*mapping.Executor](res.Err, input) - } - - fn := res.Payload - assignmentRunes := input[:len(input)-len(res.Remaining)] - - // Remove all tailing whitespace and ensure no remaining input. - testRes := DiscardedWhitespaceNewlineComments(res.Remaining) - if len(testRes.Remaining) > 0 { - assignmentRunes := DiscardedWhitespaceNewlineComments(assignmentRunes).Remaining - - var assignmentStr string - if len(assignmentRunes) > 12 { - assignmentStr = string(assignmentRunes[:12]) + "..." - } else { - assignmentStr = string(assignmentRunes) - } - - expStr := fmt.Sprintf("the mapping to end here as the beginning is shorthand for `root = %v`, but this shorthand form cannot be followed with more assignments", assignmentStr) - return Fail[*mapping.Executor](NewError(testRes.Remaining, expStr), input) - } - - stmt := mapping.NewSingleStatement(input, mapping.NewJSONAssignment(), fn) - return Success(mapping.NewExecutor("", input, map[string]query.Function{}, stmt), nil) - } -} - -//------------------------------------------------------------------------------ - -var varNameParser = JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), -) - -var importParserComb = TakeOnly(2, Sequence( - Term("import"), - SpacesAndTabs, - MustBe( - Expect( - QuotedString, - "filepath", - ), - ), -)) - -func importParser(pCtx Context, maps map[string]query.Function) Func[string] { - return func(input []rune) Result[string] { - res := importParserComb(input) - if res.Err != nil { - return res - } - - if maps == nil { - return Fail[string]( - NewFatalError(input, errors.New("importing mappings is not allowed within this block")), - input, - ) - } - - fpath := res.Payload - contents, err := pCtx.importer.Import(fpath) - if err != nil { - return Fail[string](NewFatalError(input, fmt.Errorf("failed to read import: %w", err)), input) - } - - nextCtx := pCtx.WithImporterRelativeToFile(fpath) - - importContent := []rune(string(contents)) - execRes := parseExecutor(nextCtx)(importContent) - if execRes.Err != nil { - return Fail[string](NewFatalError(input, NewImportError(fpath, importContent, execRes.Err)), input) - } - - exec := execRes.Payload - if len(exec.Maps()) == 0 { - err := fmt.Errorf("no maps to import from '%v'", fpath) - return Fail[string](NewFatalError(input, err), input) - } - - collisions := []string{} - for k, v := range exec.Maps() { - if _, exists := maps[k]; exists { - collisions = append(collisions, k) - } else { - maps[k] = v - } - } - if len(collisions) > 0 { - err := fmt.Errorf("map name collisions from import '%v': %v", fpath, collisions) - return Fail[string](NewFatalError(input, err), input) - } - - return Success(fpath, res.Remaining) - } -} - -func mapParser(pCtx Context, maps map[string]query.Function) Func[string] { - p := Sequence( - FuncAsAny(Term("map")), - FuncAsAny(SpacesAndTabs), - // Prevents a missing path from being captured by the next parser - FuncAsAny(MustBe( - Expect( - OneOf( - QuotedString, - varNameParser, - ), - "map name", - ), - )), - FuncAsAny(SpacesAndTabs), - FuncAsAny(DelimitedPattern( - Sequence( - charSquigOpen, - DiscardedWhitespaceNewlineComments, - ), - // Prevent imports, maps and metadata assignments. - mappingStatement(pCtx, false, nil), - Sequence( - Discard(SpacesAndTabs), - NewlineAllowComment, - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquigClose, - ), - )), - ) - - return func(input []rune) Result[string] { - res := p(input) - if res.Err != nil { - return Fail[string](res.Err, input) - } - - if maps == nil { - return Fail[string]( - NewFatalError(input, errors.New("defining maps is not allowed within this block")), - input, - ) - } - - seqSlice := res.Payload - ident := seqSlice[2].(string) - stmtSlice := seqSlice[4].([]mapping.Statement) - - if _, exists := maps[ident]; exists { - return Fail[string](NewFatalError(input, fmt.Errorf("map name collision: %v", ident)), input) - } - - maps[ident] = mapping.NewExecutor("map "+ident, input, maps, stmtSlice...) - return Success(ident, res.Remaining) - } -} - -func letStatementParser(pCtx Context) Func[mapping.Statement] { - p := Sequence( - FuncAsAny(Expect(Term("let"), "assignment")), - FuncAsAny(SpacesAndTabs), - // Prevents a missing path from being captured by the next parser - FuncAsAny(MustBe( - Expect( - OneOf( - QuotedString, - varNameParser, - ), - "variable name", - ), - )), - FuncAsAny(SpacesAndTabs), - FuncAsAny(charEquals), - FuncAsAny(SpacesAndTabs), - FuncAsAny(queryParser(pCtx)), - ) - - return func(input []rune) Result[mapping.Statement] { - res := p(input) - if res.Err != nil { - return Fail[mapping.Statement](res.Err, input) - } - return Success[mapping.Statement](mapping.NewSingleStatement( - input, - mapping.NewVarAssignment(res.Payload[2].(string)), - res.Payload[6].(query.Function), - ), res.Remaining) - } -} - -var nameLiteralParser = JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), -) - -func metaStatementParser(pCtx Context, enabled bool) Func[mapping.Statement] { - p := Sequence( - FuncAsAny(Expect(Term("meta"), "assignment")), - FuncAsAny(SpacesAndTabs), - FuncAsAny(OptionalPtr(OneOf( - QuotedString, - nameLiteralParser, - ))), - // TODO: Break out root assignment so we can make this mandatory - FuncAsAny(Optional(SpacesAndTabs)), - FuncAsAny(charEquals), - FuncAsAny(SpacesAndTabs), - FuncAsAny(queryParser(pCtx)), - ) - - return func(input []rune) Result[mapping.Statement] { - res := p(input) - if res.Err != nil { - return Fail[mapping.Statement](res.Err, input) - } - if !enabled { - return Fail[mapping.Statement]( - NewFatalError(input, errors.New("setting meta fields is not allowed within this block")), - input, - ) - } - resSlice := res.Payload - - return Success[mapping.Statement](mapping.NewSingleStatement( - input, - mapping.NewMetaAssignment(resSlice[2].(*string)), - resSlice[6].(query.Function), - ), res.Remaining) - } -} - -var pathLiteralSegmentParser = JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), -) - -func quotedPathLiteralSegmentParser(input []rune) Result[string] { - res := QuotedString(input) - if res.Err != nil { - return res - } - - // Convert into a JSON pointer style path string. - rawSegment := strings.ReplaceAll(res.Payload, "~", "~0") - rawSegment = strings.ReplaceAll(rawSegment, ".", "~1") - - return Success(rawSegment, res.Remaining) -} - -var pathParserPattern = Sequence( - FuncAsAny(Expect(pathLiteralSegmentParser, "assignment")), - FuncAsAny(Optional( - TakeOnly(1, Sequence( - ZeroedFuncAs[string, DelimitedResult[string, string]](charDot), - Delimited( - Expect( - OneOf( - quotedPathLiteralSegmentParser, - pathLiteralSegmentParser, - ), - "target path", - ), - charDot, - ), - )), - )), -) - -func pathParser(input []rune) Result[[]string] { - res := pathParserPattern(input) - if res.Err != nil { - return Fail[[]string](res.Err, input) - } - - sequence := res.Payload - path := []string{sequence[0].(string)} - - if sequence[1] != nil { - pathParts := sequence[1].(DelimitedResult[string, string]).Primary - for _, p := range pathParts { - path = append(path, gabs.DotPathToSlice(p)...) - } - } - - return Success(path, res.Remaining) -} - -func plainMappingStatementParser(pCtx Context) Func[mapping.Statement] { - p := Sequence( - FuncAsAny(pathParser), - FuncAsAny(SpacesAndTabs), - FuncAsAny(charEquals), - FuncAsAny(SpacesAndTabs), - FuncAsAny(queryParser(pCtx)), - ) - - return func(input []rune) Result[mapping.Statement] { - res := p(input) - if res.Err != nil { - return Fail[mapping.Statement](res.Err, input) - } - - resSlice := res.Payload - path := resSlice[0].([]string) - - // TODO V5: Enforce root here - if len(path) > 0 && path[0] == "root" { - path = path[1:] - } - - return Success[mapping.Statement](mapping.NewSingleStatement( - input, - mapping.NewJSONAssignment(path...), - resSlice[4].(query.Function), - ), res.Remaining) - } -} diff --git a/internal/bloblang/parser/mapping_parser_test.go b/internal/bloblang/parser/mapping_parser_test.go deleted file mode 100644 index 8256c4da7f..0000000000 --- a/internal/bloblang/parser/mapping_parser_test.go +++ /dev/null @@ -1,601 +0,0 @@ -package parser - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestMappingErrors(t *testing.T) { - dir := t.TempDir() - - badMapFile := filepath.Join(dir, "bad_map.blobl") - noMapsFile := filepath.Join(dir, "no_maps.blobl") - goodMapFile := filepath.Join(dir, "good_map.blobl") - - require.NoError(t, os.WriteFile(badMapFile, []byte(`not a map bruh`), 0o777)) - require.NoError(t, os.WriteFile(noMapsFile, []byte(`foo = "this is valid but has no maps"`), 0o777)) - require.NoError(t, os.WriteFile(goodMapFile, []byte(`map foo { foo = "this is valid" }`), 0o777)) - - tests := map[string]struct { - mapping string - errContains string - }{ - "bad variable name": { - mapping: `let foo+bar = baz`, - errContains: "line 1 char 8: expected whitespace", - }, - "bad meta name": { - mapping: `meta foo+bar = baz`, - errContains: "line 1 char 9: expected =", - }, - "no mappings": { - mapping: ``, - errContains: `line 1 char 1: expected import, map, or assignment`, - }, - "no mappings 2": { - mapping: ` - `, - errContains: `line 2 char 4: expected import, map, or assignment`, - }, - "comment with no mapping": { - mapping: `# foobar`, - errContains: `line 1 char 1: expected import, map, or assignment`, - }, - "double mapping": { - mapping: `foo = bar bar = baz`, - errContains: `line 1 char 11: expected line break`, - }, - "double mapping line breaks": { - mapping: ` - -foo = bar bar = baz - -`, - errContains: `line 3 char 11: expected line break`, - }, - "double mapping line 2": { - mapping: `let a = "a" -foo = bar bar = baz`, - errContains: `line 2 char 11: expected line break`, - }, - "double mapping line 3": { - mapping: `let a = "a" -foo = bar bar = baz - let a = "a"`, - errContains: "line 2 char 11: expected line break", - }, - "bad mapping": { - mapping: `foo wat bar`, - errContains: `line 1 char 5: expected =`, - }, - "bad char": { - mapping: `!foo = bar`, - errContains: "line 1 char 6: expected the mapping to end here as the beginning is shorthand for `root = !foo`, but this shorthand form cannot be followed with more assignments", - }, - "bad inline query": { - mapping: `content().uppercase().lowercase() -meta foo = "bar"`, - errContains: "line 2 char 1: expected the mapping to end here as the beginning is shorthand for `root = content().up...`, but this shorthand form cannot be followed with more assignments", - }, - "bad char 2": { - mapping: `let foo = bar -!foo = bar`, - errContains: `line 2 char 1: expected import, map, or assignment`, - }, - "bad char 3": { - mapping: `let foo = bar -!foo = bar -this = that`, - errContains: `line 2 char 1: expected import, map, or assignment`, - }, - "bad query": { - mapping: `foo = blah.`, - errContains: `line 1 char 12: required: expected method or field path`, - }, - "bad variable assign": { - mapping: `let = blah`, - errContains: `line 1 char 5: required: expected variable name`, - }, - "double map definition": { - mapping: `map foo { - foo = bar -} -map foo { - foo = bar -} -foo = bar.apply("foo")`, - errContains: `line 4 char 1: map name collision: foo`, - }, - "map contains meta assignment": { - mapping: `map foo { - meta foo = "bar" -} -foo = bar.apply("foo")`, - errContains: `line 2 char 3: setting meta fields is not allowed within this block`, - }, - "no name map definition": { - mapping: `map { - foo = bar -} -foo = bar.apply("foo")`, - errContains: `line 1 char 5: required: expected map name`, - }, - "no file import": { - mapping: `import "this file doesn't exist (I hope)" - -foo = bar.apply("from_import")`, - errContains: `this file doesn't exist (I hope): no such file or directory`, - }, - "no file directly imported mapping": { - mapping: `from "this file doesn't exist (I hope)"`, - errContains: `this file doesn't exist (I hope): no such file or directory`, - }, - "bad file import": { - mapping: fmt.Sprintf(`import "%v" - -foo = bar.apply("from_import")`, badMapFile), - errContains: fmt.Sprintf(`line 1 char 1: failed to parse import '%v': line 1 char 5: expected =`, badMapFile), - }, - "bad file directly imported mapping": { - mapping: fmt.Sprintf(`from "%v"`, badMapFile), - errContains: fmt.Sprintf(`line 1 char 1: failed to parse import '%v': line 1 char 5: expected =`, badMapFile), - }, - "unexpected content after directly imported mapping": { - mapping: fmt.Sprintf(`from "%v" -root.foo = "not allowed"`, goodMapFile), - errContains: `line 1 char 1: unexpected content after single root import: root.foo = "not allowed"`, - }, - "no maps file import": { - mapping: fmt.Sprintf(`import "%v" - -foo = bar.apply("from_import")`, noMapsFile), - errContains: fmt.Sprintf(`line 1 char 1: no maps to import from '%v'`, noMapsFile), - }, - "colliding maps file import": { - mapping: fmt.Sprintf(`map "foo" { this = that } - -import "%v" - -foo = bar.apply("foo")`, goodMapFile), - errContains: fmt.Sprintf(`line 3 char 1: map name collisions from import '%v': [foo]`, goodMapFile), - }, - "quotes at root": { - mapping: ` -"root.something" = 5 + 2`, - errContains: "line 2 char 18: expected the mapping to end here as the beginning is shorthand for `root = \"root.someth...`, but this shorthand form cannot be followed with more assignments", - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - exec, err := ParseMapping(GlobalContext(), test.mapping) - require.NotNil(t, err) - assert.Contains(t, err.ErrorAtPosition([]rune(test.mapping)), test.errContains) - assert.Nil(t, exec) - }) - } -} - -func TestMappings(t *testing.T) { - dir := t.TempDir() - - goodMapFile := filepath.Join(dir, "foo_map.blobl") - require.NoError(t, os.WriteFile(goodMapFile, []byte(`map foo { - foo = "this is valid" - nested = this -}`), 0o777)) - - directMapFile := filepath.Join(dir, "direct_map.blobl") - require.NoError(t, os.WriteFile(directMapFile, []byte(`root.nested = this`), 0o777)) - - type part struct { - Content string - Meta map[string]any - } - - tests := map[string]struct { - index int - input []part - mapping string - output part - }{ - "compressed arithmetic": { - mapping: `this.foo+this.bar`, - input: []part{ - {Content: `{"foo":5,"bar":3}`}, - }, - output: part{ - Content: `8`, - }, - }, - "compressed arithmetic 2": { - mapping: `this.foo-this.bar`, - input: []part{ - {Content: `{"foo":5,"bar":3}`}, - }, - output: part{ - Content: `2`, - }, - }, - "simple json map": { - mapping: `foo = foo + 2 -bar = "test1" -zed = deleted()`, - input: []part{{Content: `{"foo":10,"zed":"gone"}`}}, - output: part{Content: `{"bar":"test1","foo":12}`}, - }, - "simple json map 2": { - mapping: ` -foo = foo + 2 - -bar = "test1" - -zed = deleted() -`, - input: []part{{Content: `{"foo":10,"zed":"gone"}`}}, - output: part{Content: `{"bar":"test1","foo":12}`}, - }, - "simple json map 3": { - mapping: ` - foo = foo + 2 - - bar = "test1" - -zed = deleted() - `, - input: []part{{Content: `{"foo":10,"zed":"gone"}`}}, - output: part{Content: `{"bar":"test1","foo":12}`}, - }, - "simple root query": { - mapping: `{"result": foo + 2}`, - input: []part{{Content: `{"foo":10}`}}, - output: part{Content: `{"result":12}`}, - }, - "simple root query 2": { - mapping: `foo.bar`, - input: []part{{Content: `{"foo":{"bar":10}}`}}, - output: part{Content: `10`}, - }, - "simple root query 3": { - mapping: `root = foo.bar`, - input: []part{{Content: `{"foo":{"bar":10}}`}}, - output: part{Content: `10`}, - }, - "simple json map with comments": { - mapping: ` -# Here's a comment -foo = foo + 2 # And here - -bar = "test1" # And one here - -# And here -zed = deleted() -`, - input: []part{{Content: `{"foo":10,"zed":"gone"}`}}, - output: part{Content: `{"bar":"test1","foo":12}`}, - }, - "test mapping metadata and json": { - mapping: `meta foo = foo -bar.baz = meta("bar baz") -meta "bar baz" = deleted()`, - input: []part{ - { - Content: `{"foo":"bar"}`, - Meta: map[string]any{ - "bar baz": "test1", - }, - }, - }, - output: part{ - Content: `{"bar":{"baz":"test1"}}`, - Meta: map[string]any{ - "foo": "bar", - }, - }, - }, - "test mapping metadata empty key": { - mapping: `meta "" = foo.bar -meta "bar baz" = "test1"`, - input: []part{ - {Content: `{"foo":{"bar":"baz"}}`}, - }, - output: part{ - Content: `{"foo":{"bar":"baz"}}`, - Meta: map[string]any{ - "": "baz", - "bar baz": "test1", - }, - }, - }, - "test mapping metadata and json 2": { - mapping: `meta = foo -meta "bar baz" = "test1"`, - input: []part{ - {Content: `{"foo":{"bar":"baz"}}`}, - }, - output: part{ - Content: `{"foo":{"bar":"baz"}}`, - Meta: map[string]any{ - "bar": "baz", - "bar baz": "test1", - }, - }, - }, - "test mapping delete and json": { - mapping: `meta foo = foo -bar.baz = meta("bar baz") -meta = deleted()`, - input: []part{ - { - Content: `{"foo":"bar"}`, - Meta: map[string]any{ - "bar baz": "test1", - }, - }, - }, - output: part{ - Content: `{"bar":{"baz":"test1"}}`, - }, - }, - "test variables and json": { - mapping: `let foo = foo -let "bar baz" = "test1" -bar.baz = var("bar baz")`, - input: []part{ - {Content: `{"foo":"bar"}`}, - }, - output: part{ - Content: `{"bar":{"baz":"test1"}}`, - }, - }, - "map json root": { - mapping: `root = { - "foo": "this is a literal map" -}`, - input: []part{{Content: `{"zed":"gone"}`}}, - output: part{Content: `{"foo":"this is a literal map"}`}, - }, - "map json root 2": { - mapping: `root = { - "foo": "this is a literal map" -} -bar = "this is another thing"`, - input: []part{{Content: `{"zed":"gone"}`}}, - output: part{Content: `{"bar":"this is another thing","foo":"this is a literal map"}`}, - }, - "test mapping metadata without json": { - mapping: `meta foo = "foo" -meta bar = 5 + 2`, - input: []part{ - {Content: `this isn't json`}, - }, - output: part{ - Content: `this isn't json`, - Meta: map[string]any{ - "foo": "foo", - "bar": int64(7), - }, - }, - }, - "field called root": { - mapping: `root.root = "not set at root"`, - input: []part{ - {Content: `this isn't json`}, - }, - output: part{ - Content: `{"root":"not set at root"}`, - }, - }, - "quoted paths": { - mapping: ` -meta "foo bar" = "hello world" -root."bar baz".test = 5 + 2`, - input: []part{ - {Content: `this isn't json`}, - }, - output: part{ - Content: `{"bar baz":{"test":7}}`, - Meta: map[string]any{ - "foo bar": "hello world", - }, - }, - }, - "test mapping raw content": { - mapping: `meta content = content() -foo = "static"`, - input: []part{ - {Content: `hello world`}, - }, - output: part{ - Content: `{"foo":"static"}`, - Meta: map[string]any{ - "content": []byte(`hello world`), - }, - }, - }, - "test mapping raw json content": { - mapping: `meta content = content() -foo = "static"`, - input: []part{ - {Content: `{"foo":{"bar":"baz"}}`}, - }, - output: part{ - Content: `{"foo":"static"}`, - Meta: map[string]any{ - "content": []byte(`{"foo":{"bar":"baz"}}`), - }, - }, - }, - "test mapping to string": { - mapping: `root = "static string"`, - input: []part{ - {Content: `{"this":"is a json doc"}`}, - }, - output: part{ - Content: `static string`, - }, - }, - "test map without mapping": { - mapping: `map foo { - foo = "static foo" -}`, - input: []part{ - {Content: `{"foo":"bar"}`}, - }, - output: part{ - Content: `{"foo":"bar"}`, - }, - }, - "test maps": { - mapping: `map foo { - foo = "static foo" - bar = this - applied = ["foo"] -} -root = this.apply("foo")`, - input: []part{ - {Content: `{"outer":{"inner":"hello world"}}`}, - }, - output: part{ - Content: `{"applied":["foo"],"bar":{"outer":{"inner":"hello world"}},"foo":"static foo"}`, - }, - }, - "test nested maps": { - mapping: `map foo { - let tmp = this.apply("bar") - foo = var("tmp") - applied = var("tmp").applied.merge("foo") - foo.applied = deleted() -} -map bar { - static = "this is valid" - bar = this - applied = ["bar"] -} -root = this.apply("foo")`, - input: []part{ - {Content: `{"outer":{"inner":"hello world"}}`}, - }, - output: part{ - Content: `{"applied":["bar","foo"],"foo":{"bar":{"outer":{"inner":"hello world"}},"static":"this is valid"}}`, - }, - }, - "test single root mapping": { - mapping: `"foo" == "bar"`, - input: []part{ - {Content: ``}, - }, - output: part{ - Content: `false`, - }, - }, - "test single root mapping with blobl shebang": { - mapping: `#!blobl -"foo" == "bar"`, - input: []part{ - {Content: ``}, - }, - output: part{ - Content: `false`, - }, - }, - "test imported map": { - mapping: fmt.Sprintf(`import "%v" - -root = this.apply("foo")`, goodMapFile), - input: []part{ - {Content: `{"outer":{"inner":"hello world"}}`}, - }, - output: part{ - Content: `{"foo":"this is valid","nested":{"outer":{"inner":"hello world"}}}`, - }, - }, - "test imported map with blobl shebang": { - mapping: fmt.Sprintf(`#!blobl -import "%v" - -root = this.apply("foo")`, goodMapFile), - input: []part{ - {Content: `{"outer":{"inner":"hello world"}}`}, - }, - output: part{ - Content: `{"foo":"this is valid","nested":{"outer":{"inner":"hello world"}}}`, - }, - }, - "test directly imported mapping": { - mapping: fmt.Sprintf(`from "%v"`, directMapFile), - input: []part{ - {Content: `{"inner":"hello world"}`}, - }, - output: part{ - Content: `{"nested":{"inner":"hello world"}}`, - }, - }, - "test directly imported mapping with blobl shebang": { - mapping: fmt.Sprintf(`#!blobl - -from "%v"`, directMapFile), - input: []part{ - {Content: `{"inner":"hello world"}`}, - }, - output: part{ - Content: `{"nested":{"inner":"hello world"}}`, - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - msg := message.QuickBatch(nil) - for _, p := range test.input { - part := message.NewPart([]byte(p.Content)) - for k, v := range p.Meta { - part.MetaSetMut(k, v) - } - msg = append(msg, part) - } - if test.output.Meta == nil { - test.output.Meta = map[string]any{} - } - - exec, perr := ParseMapping(GlobalContext(), test.mapping) - require.Nil(t, perr) - - resPart, err := exec.MapPart(test.index, msg) - require.NoError(t, err) - - newPart := part{ - Content: string(resPart.AsBytes()), - Meta: map[string]any{}, - } - _ = resPart.MetaIterMut(func(k string, v any) error { - newPart.Meta[k] = v - return nil - }) - - assert.Equal(t, test.output, newPart) - }) - } -} - -func BenchmarkMappingParser(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := ParseMapping(GlobalContext(), ` -root.foo = this.foo.uppercase() -root.bar = this.bar.lowercase() -root.baz = this.baz.slice(0, 10) -`) - if err != nil { - b.Error(err.Error()) - } - } -} diff --git a/internal/bloblang/parser/query_arithmetic_parser.go b/internal/bloblang/parser/query_arithmetic_parser.go deleted file mode 100644 index 92803849e1..0000000000 --- a/internal/bloblang/parser/query_arithmetic_parser.go +++ /dev/null @@ -1,118 +0,0 @@ -package parser - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -var arithmeticOpPattern = OneOf( - Char('+'), - Char('-'), - Char('/'), - Char('*'), - Char('%'), - Term("&&"), - Term("||"), - Term("=="), - Term("!="), - Term(">="), - Term("<="), - Char('>'), - Char('<'), - Char('|'), -) - -func arithmeticOpParser(input []rune) Result[query.ArithmeticOperator] { - res := arithmeticOpPattern(input) - if res.Err != nil { - return Fail[query.ArithmeticOperator](res.Err, input) - } - - outRes := ResultInto[query.ArithmeticOperator](res) - switch res.Payload { - case "+": - outRes.Payload = query.ArithmeticAdd - case "-": - outRes.Payload = query.ArithmeticSub - case "/": - outRes.Payload = query.ArithmeticDiv - case "*": - outRes.Payload = query.ArithmeticMul - case "%": - outRes.Payload = query.ArithmeticMod - case "==": - outRes.Payload = query.ArithmeticEq - case "!=": - outRes.Payload = query.ArithmeticNeq - case "&&": - outRes.Payload = query.ArithmeticAnd - case "||": - outRes.Payload = query.ArithmeticOr - case ">": - outRes.Payload = query.ArithmeticGt - case "<": - outRes.Payload = query.ArithmeticLt - case ">=": - outRes.Payload = query.ArithmeticGte - case "<=": - outRes.Payload = query.ArithmeticLte - case "|": - outRes.Payload = query.ArithmeticPipe - default: - return Fail[query.ArithmeticOperator]( - NewFatalError(input, fmt.Errorf("operator not recognized: %v", res.Payload)), - input, - ) - } - return outRes -} - -func arithmeticParser(fnParser Func[query.Function]) Func[query.Function] { - p := Delimited( - Sequence( - FuncAsAny(Optional(JoinStringPayloads(Sequence(charMinus, DiscardedWhitespaceNewlineComments)))), - FuncAsAny(fnParser), - ), - TakeOnly(1, Sequence( - ZeroedFuncAs[string, query.ArithmeticOperator](DiscardAll(SpacesAndTabs)), - arithmeticOpParser, - ZeroedFuncAs[string, query.ArithmeticOperator](DiscardedWhitespaceNewlineComments), - )), - ) - - return func(input []rune) Result[query.Function] { - var fns []query.Function - - res := p(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - delimRes := res.Payload - for _, primaryRes := range delimRes.Primary { - fn := primaryRes[1].(query.Function) - if mStr, _ := primaryRes[0].(string); mStr == "-" { - var err error - if fn, err = query.NewArithmeticExpression( - []query.Function{ - query.NewLiteralFunction("", int64(0)), - fn, - }, - []query.ArithmeticOperator{ - query.ArithmeticSub, - }, - ); err != nil { - return Fail[query.Function](NewFatalError(input, err), input) - } - } - fns = append(fns, fn) - } - - fn, err := query.NewArithmeticExpression(fns, delimRes.Delimiter) - if err != nil { - return Fail[query.Function](NewFatalError(input, err), input) - } - return Success(fn, res.Remaining) - } -} diff --git a/internal/bloblang/parser/query_arithmetic_parser_test.go b/internal/bloblang/parser/query_arithmetic_parser_test.go deleted file mode 100644 index ac7107f001..0000000000 --- a/internal/bloblang/parser/query_arithmetic_parser_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestArithmeticParser(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - tests := map[string]struct { - input string - output string - messages []easyMsg - index int - }{ - "compare string to int": { - input: `"foo" != 5`, - output: `true`, - }, - "compare string to null": { - input: `"foo" != null`, - output: `true`, - }, - "compare string to int 2": { - input: `5 != "foo"`, - output: `true`, - }, - "compare string to null 2": { - input: `null != "foo"`, - output: `true`, - }, - "add strings": { - input: `"foo" + "bar" + "%v-%v".format(10, 20)`, - output: `foobar10-20`, - }, - "comparisons with not": { - input: `!true || false`, - output: `false`, - }, - "comparisons with not 2": { - input: `false || !false`, - output: `true`, - }, - "mod two ints": { - input: `5 % 2`, - output: `1`, - }, - "mod two strings": { - input: `"7".number() % "4".number()`, - output: `3`, - }, - "comparisons": { - input: `true && false || true && false`, - output: `false`, - }, - "comparisons 2": { - input: `false || true && true || false`, - output: `true`, - }, - "comparisons 3": { - input: `true || false && true`, - output: `true`, - }, - "and exit early": { - input: `false && ("not a number".number() > 0)`, - output: `false`, - }, - "and second exit early": { - input: `true && false && ("not a number".number() > 0)`, - output: `false`, - }, - "or exit early": { - input: `true || ("not a number".number() > 0)`, - output: `true`, - }, - "or second early": { - input: `false || true || ("not a number".number() > 0)`, - output: `true`, - }, - "add two ints": { - input: `json("foo") + json("bar")`, - output: `17`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "add two ints 2": { - input: `(json("foo")) + (json("bar"))`, - output: `17`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "add two ints 3": { - input: `json("foo") + 5`, - output: `10`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "subtract two ints": { - input: `json("foo") - 5`, - output: `0`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "subtract two ints 2": { - input: `json("foo") - 7`, - output: `-2`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "two ints and a string": { - input: `json("foo") + json("bar") + meta("baz").number(0)`, - output: `17`, - messages: []easyMsg{ - { - content: `{"foo":5,"bar":12}`, - meta: map[string]any{ - "baz": "this aint a number", - }, - }, - }, - }, - "two ints and a string 2": { - input: `json("foo") + json("bar") + "baz".number(0)`, - output: `17`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "add three ints": { - input: `json("foo") + json("bar") + meta("baz").number()`, - output: `20`, - messages: []easyMsg{ - { - content: `{"foo":5,"bar":12}`, - meta: map[string]any{ - "baz": "3", - }, - }, - }, - }, - "sub two ints": { - input: `json("foo") - json("bar")`, - output: `-7`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":12}`}, - }, - }, - "negate json int": { - input: `-json("foo")`, - output: `-5`, - messages: []easyMsg{ - {content: `{"foo":5}`}, - }, - }, - "sub and add two ints": { - input: `json("foo") + json("bar") - meta("foo").number() - meta("bar").number()`, - output: `6`, - messages: []easyMsg{ - { - content: `{"foo":5,"bar":12}`, - meta: map[string]any{ - "foo": "3", - "bar": "8", - }, - }, - }, - }, - "multiply two ints": { - input: `json("foo") * json("bar")`, - output: `10`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":2}`}, - }, - }, - "multiply three ints": { - input: `json("foo") * json("bar") * 2`, - output: `20`, - messages: []easyMsg{ - {content: `{"foo":5,"bar":2}`}, - }, - }, - "multiply and additions of ints": { - input: `2 + 3 * 2 + 1`, - output: `9`, - messages: []easyMsg{{}}, - }, - "multiply and additions of ints 2": { - input: `( 2 + 3 ) * (2 + 1)`, - output: `15`, - messages: []easyMsg{{}}, - }, - "multiply and additions of ints 3": { - input: `2 + 3 * 2 + 1 * 3`, - output: `11`, - messages: []easyMsg{{}}, - }, - "multiply and additions of ints 4": { - input: `(2 + 3 )* (2+1 ) * 3`, - output: `45`, - messages: []easyMsg{{}}, - }, - "division and subtractions of ints": { - input: `6 - 6 / 2 + 1`, - output: `4`, - messages: []easyMsg{{}}, - }, - "division and subtractions of ints 2": { - input: `(8 - 4) / (1 + 1)`, - output: `2`, - messages: []easyMsg{{}}, - }, - "compare ints": { - input: `8 == 2`, - output: `false`, - messages: []easyMsg{{}}, - }, - "compare ints 2": { - input: `8 != 2`, - output: `true`, - messages: []easyMsg{{}}, - }, - "compare ints 3": { - input: `8 == 8`, - output: `true`, - messages: []easyMsg{{}}, - }, - "compare ints 4": { - input: `8 > 7`, - output: `true`, - messages: []easyMsg{{}}, - }, - "compare ints 5": { - input: `8 > 2*6`, - output: `false`, - messages: []easyMsg{{}}, - }, - "compare ints 6": { - input: `8 >= 7+1`, - output: `true`, - messages: []easyMsg{{}}, - }, - "compare ints 7": { - input: `5 < 2*3`, - output: `true`, - messages: []easyMsg{{}}, - }, - "compare ints 8": { - input: `5 < 2*3`, - output: `true`, - messages: []easyMsg{{}}, - }, - "compare ints 9": { - input: `8 > 3 * 5`, - output: `false`, - messages: []easyMsg{{}}, - }, - "compare ints chained boolean": { - input: `8 > 3 && 1 < 4`, - output: `true`, - messages: []easyMsg{{}}, - }, - "coalesce json": { - input: `json("foo") | json("bar")`, - output: `from_bar`, - messages: []easyMsg{ - {content: `{"foo":null,"bar":"from_bar"}`}, - }, - }, - "coalesce json 2": { - input: `json("foo") | "notthis"`, - output: `from_foo`, - messages: []easyMsg{ - {content: `{"foo":"from_foo"}`}, - }, - }, - "coalesce deleted": { - input: `deleted() | "this"`, - output: `this`, - messages: []easyMsg{{}}, - }, - "coalesce nothing": { - input: `nothing() | "this"`, - output: `this`, - messages: []easyMsg{{}}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - e, pErr := tryParseQuery(test.input) - require.Nil(t, pErr) - - res, err := query.ExecToString(e, query.FunctionContext{ - Index: test.index, - MsgBatch: msg, - }) - require.NoError(t, err) - assert.Equal(t, test.output, res) - - bRes, err := query.ExecToBytes(e, query.FunctionContext{ - Index: test.index, - MsgBatch: msg, - }) - require.NoError(t, err) - res = string(bRes) - assert.Equal(t, test.output, res) - }) - } -} - -func TestArithmeticLiteralsParser(t *testing.T) { - tests := map[string]string{ - `2 == 3`: `false`, - `"2".number() == 2`: `true`, - `"2" == "2"`: `true`, - `"2" == "3"`: `false`, - `2.0 == 2`: `true`, - `true == true`: `true`, - `true == false`: `false`, - `2 == -2`: `false`, - `2 == 3-1`: `true`, - `2 == 2`: `true`, - `2 != 3`: `true`, - `2.5 == 3.2`: `false`, - `2.5 == 3.5-1`: `true`, - `2.3 == 2.3`: `true`, - `2.3 != 2.2`: `true`, - `3 != 3`: `false`, - `5 < 2*3`: `true`, - `5 > 2*3`: `false`, - `5 <= 2.5*2`: `true`, - `2 > -3`: `true`, - `-2 < 2`: `true`, - `-(2) < 2`: `true`, - `2 < -"2".number()`: `false`, - `2 > -"2".number()`: `true`, - `(2 == 2) && (1 != 2)`: `true`, - `(2 == 2) && (2 != 2)`: `false`, - `(2 == 2) || (2 != 2)`: `true`, - `(2 == 1) || (2 != 2)`: `false`, - `null == null`: `true`, - `{} == {}`: `true`, - `["foo"] == ["foo"]`: `true`, - `["bar"] == ["foo"]`: `false`, - `["bar"] != ["foo"]`: `true`, - `{} != null`: `true`, - } - - for k, v := range tests { - msg := message.QuickBatch(nil) - e, pErr := tryParseQuery(k) - require.Nil(t, pErr) - - res, err := query.ExecToString(e, query.FunctionContext{ - Index: 0, - MsgBatch: msg, - }) - require.NoError(t, err) - assert.Equal(t, v, res, k) - - bres, err := query.ExecToBytes(e, query.FunctionContext{MsgBatch: msg}) - require.NoError(t, err) - res = string(bres) - assert.Equal(t, v, res, k) - } -} diff --git a/internal/bloblang/parser/query_expression_parser.go b/internal/bloblang/parser/query_expression_parser.go deleted file mode 100644 index 7d5903fdfd..0000000000 --- a/internal/bloblang/parser/query_expression_parser.go +++ /dev/null @@ -1,265 +0,0 @@ -package parser - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" -) - -func matchCaseParser(pCtx Context) Func[query.MatchCase] { - ignoreToFn := ZeroedFuncAs[string, query.Function] - - p := Sequence( - OneOf( - ZeroedFuncAs[[]string, query.Function](Sequence( - Expect( - charUnderscore, - "match case", - ), - Optional(SpacesAndTabs), - Term("=>"), - )), - TakeOnly(0, Sequence( - Expect( - queryParser(pCtx), - "match case", - ), - ignoreToFn(Optional(SpacesAndTabs)), - ignoreToFn(Term("=>")), - )), - ), - ignoreToFn(Optional(SpacesAndTabs)), - queryParser(pCtx), - ) - - return func(input []rune) Result[query.MatchCase] { - res := p(input) - if res.Err != nil { - return Fail[query.MatchCase](res.Err, input) - } - - var caseFn query.Function - - if p := res.Payload[0]; p == nil { - caseFn = query.NewLiteralFunction("", true) - } else if lit, isLiteral := p.(*query.Literal); isLiteral { - caseFn = query.ClosureFunction("case statement", func(ctx query.FunctionContext) (any, error) { - v := ctx.Value() - if v == nil { - return false, nil - } - return value.ICompare(*v, lit.Value), nil - }, nil) - } else { - caseFn = p - } - - return Success( - query.NewMatchCase(caseFn, res.Payload[2]), - res.Remaining, - ) - } -} - -func matchExpressionParser(pCtx Context) Func[query.Function] { - return func(input []rune) Result[query.Function] { - res := Sequence( - FuncAsAny(Term("match")), - FuncAsAny(SpacesAndTabs), - FuncAsAny(Optional(queryParser(pCtx))), - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(MustBe( - DelimitedPattern( - Sequence( - charSquigOpen, - DiscardedWhitespaceNewlineComments, - ), - matchCaseParser(pCtx), - Sequence( - Discard(SpacesAndTabs), - OneOf( - charComma, - NewlineAllowComment, - ), - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquigClose, - ), - ), - )), - )(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - seqSlice := res.Payload - contextFn, _ := seqSlice[2].(query.Function) - - cases := seqSlice[4].([]query.MatchCase) - - return Success(query.NewMatchFunction(contextFn, cases...), res.Remaining) - } -} - -var strToQuery = ZeroedFuncAs[string, query.Function] - -func ifExpressionParser(pCtx Context) Func[query.Function] { - return func(input []rune) Result[query.Function] { - ifParser := Sequence( - strToQuery(Term("if")), - strToQuery(SpacesAndTabs), - MustBe(queryParser(pCtx)), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(charSquigOpen)), - strToQuery(DiscardedWhitespaceNewlineComments), - MustBe(queryParser(pCtx)), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(charSquigClose)), - ) - - elseIfParser := Optional(Sequence( - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(Term("else if")), - strToQuery(SpacesAndTabs), - MustBe(queryParser(pCtx)), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(charSquigOpen)), - strToQuery(DiscardedWhitespaceNewlineComments), - MustBe(queryParser(pCtx)), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(charSquigClose)), - )) - - elseParser := Optional(Sequence( - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(Term("else")), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(charSquigOpen)), - strToQuery(DiscardedWhitespaceNewlineComments), - MustBe(queryParser(pCtx)), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(charSquigClose)), - )) - - res := ifParser(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - seqSlice := res.Payload - queryFn := seqSlice[2] - ifFn := seqSlice[6] - - var elseIfs []query.ElseIf - for { - res = elseIfParser(res.Remaining) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - if res.Payload == nil { - break - } - seqSlice = res.Payload - elseIfs = append(elseIfs, query.ElseIf{ - QueryFn: seqSlice[3], - MapFn: seqSlice[7], - }) - } - - var elseFn query.Function - - res = elseParser(res.Remaining) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - if res.Payload != nil { - elseFn = res.Payload[5] - } - - return Success(query.NewIfFunction(queryFn, ifFn, elseIfs, elseFn), res.Remaining) - } -} - -func bracketsExpressionParser(pCtx Context) Func[query.Function] { - return func(input []rune) Result[query.Function] { - res := Sequence( - strToQuery(Expect( - charBracketOpen, - "function", - )), - strToQuery(DiscardedWhitespaceNewlineComments), - queryParser(pCtx), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(MustBe(Expect(charBracketClose, "closing bracket"))), - )(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - return Success(res.Payload[2], res.Remaining) - } -} - -var contextNameParser = Expect( - JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), - ), - "context name", -) - -func lambdaExpressionParser(pCtx Context) Func[query.Function] { - nameParser := Expect( - TakeOnly(0, Sequence( - contextNameParser, - SpacesAndTabs, - Term("->"), - SpacesAndTabs, - )), - "function", - ) - - return func(input []rune) Result[query.Function] { - res := nameParser(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - name := res.Payload - if name != "_" { - if pCtx.HasNamedContext(name) { - return Fail[query.Function]( - NewFatalError(input, fmt.Errorf("context label `%v` would shadow a parent context", name)), - input, - ) - } - if _, exists := map[string]struct{}{ - "root": {}, - "this": {}, - }[name]; exists { - return Fail[query.Function](NewFatalError(input, fmt.Errorf("context label `%v` is not allowed", name)), input) - } - pCtx = pCtx.WithNamedContext(name) - } - - queryRes := MustBe(queryParser(pCtx))(res.Remaining) - if queryRes.Err != nil { - return queryRes - } - - queryFn := queryRes.Payload - if chained, isChained := queryFn.(*query.NamedContextFunction); isChained { - err := fmt.Errorf("it would be in poor taste to capture the same context under both '%v' and '%v'", name, chained.Name()) - return Fail[query.Function](NewFatalError(input, err), input) - } - return Success(query.NewNamedContextFunction(name, queryFn), queryRes.Remaining) - } -} diff --git a/internal/bloblang/parser/query_expression_parser_test.go b/internal/bloblang/parser/query_expression_parser_test.go deleted file mode 100644 index 3d7e6b7c32..0000000000 --- a/internal/bloblang/parser/query_expression_parser_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestExpressionsParser(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - tests := map[string]struct { - input string - output string - messages []easyMsg - value *any - index int - }{ - "match literals": { - input: `match "string literal" { - this == "string literal" => "first" - this != "string literal" => "second" - _ => "third" - -} `, - output: `first`, - messages: []easyMsg{}, - }, - "match literals 2": { - input: `match "string literal" -{ - this != "string literal" => "first" - this == "string literal" => "second" - _ => "third" }`, - output: `second`, - messages: []easyMsg{}, - }, - "match literals 3": { - input: `match "string literal" { - this != "string literal" => "first" - this == "nope" => "second" - _ => "third" -}`, - output: `third`, - messages: []easyMsg{}, - }, - "match literals 4": { - input: `match "foo" { - "foo" => "first" - "bar" => "second" - _ => "third" -}`, - output: `first`, - messages: []easyMsg{}, - }, - "match literals 5": { - input: `match "bar" { - "foo" => "first" - "bar" => "second" - _ => "third" -}`, - output: `second`, - messages: []easyMsg{}, - }, - "match literals 6": { - input: `match "baz" { - "foo" => "first" - "bar" => "second" - _ => "third" -}`, - output: `third`, - messages: []easyMsg{}, - }, - "match function": { - input: `match json("foo") { - this > 10 => this + 1 - this > 5 => this + 2 - _ => this + 3 - }`, - output: `9`, - messages: []easyMsg{ - {content: `{"foo":7}`}, - }, - }, - "match function 2": { - input: `match json("foo") { - this > 10 => this + 1 - this > 5 => this + 2 - _ => this + 3 }`, - output: `16`, - messages: []easyMsg{ - {content: `{"foo":15}`}, - }, - }, - "match function 3": { - input: `match json("foo") { - this > 10 => this + 1 - this > 5 => this + 2 - _ => this + 3 -}`, - output: `5`, - messages: []easyMsg{ - {content: `{"foo":2}`}, - }, - }, - "match empty": { - input: `match "" { - json().foo > 5 => json().foo - json().bar > 5 => "bigbar" - _ => json().baz -}`, - output: `6`, - messages: []easyMsg{ - {content: `{"foo":6,"bar":3,"baz":"isbaz"}`}, - }, - }, - "match empty 2": { - input: `match "" { - json().foo > 5 => "bigfoo" - json().bar > 5 => "bigbar" - _ => json().baz }`, - output: `bigbar`, - messages: []easyMsg{ - {content: `{"foo":2,"bar":7,"baz":"isbaz"}`}, - }, - }, - "match empty 3": { - input: `match "" { - json().foo > 5 => "bigfoo" - json().bar.number().or(0) > 5 => "bigbar" - _ => json().baz }`, - output: `isbaz`, - messages: []easyMsg{ - {content: `{"foo":2,"bar":"not a number","baz":"isbaz"}`}, - }, - }, - "match function in braces": { - input: `(match json("foo") { - this > 10 => this + 1 - this > 5 => this + 2 - _ => this + 3 })`, - output: `9`, - messages: []easyMsg{ - {content: `{"foo":7}`}, - }, - }, - "match function in braces 2": { - input: `(match (json("foo")) { - (this > 10) => (this + 1) - (this > 5) => (this + 2) - _ => (this + 3) })`, - output: `9`, - messages: []easyMsg{ - {content: `{"foo":7}`}, - }, - }, - "no matches": { - input: `match "value" { - this == "not this value" => "yep" -}`, - output: `null`, - messages: []easyMsg{ - {content: `{"foo":6,"bar":3,"baz":"isbaz"}`}, - }, - }, - "match function no expression": { - input: `match { - json("foo") > 10 => json("foo") + 1 - json("foo") > 5 => json("foo") + 2 - _ => "nope" - }`, - output: `9`, - messages: []easyMsg{ - {content: `{"foo":7}`}, - }, - }, - "match with commas": { - input: `match "bar" { - "foo" => "first", - "bar" => "second", - _ => "third", -}`, - output: `second`, - messages: []easyMsg{}, - }, - "match with commas 2": { - input: `match "bar" { "foo" => "first", "bar" => "second", _ => "third" }`, - output: `second`, - messages: []easyMsg{}, - }, - "match with commas 3": { - input: `match "bar" {"foo"=>"first","bar"=>"second",_=>"third"}`, - output: `second`, - messages: []easyMsg{}, - }, - "if statement inline": { - input: `if "foo" == "foo" { "foo" } else { "bar" }`, - output: `foo`, - }, - "if statement with else ifs": { - input: `if json("type") == "foo" { - "was foo" -} else if json("type") == "bar" { - "was bar" -} else if json("type") == "baz" { - "was baz" -} else { - "was none" -}`, - output: `was foo`, - messages: []easyMsg{ - {content: `{"type":"foo"}`}, - }, - }, - "if statement with else ifs 2": { - input: `if json("type") == "foo"{"was foo"} else if json("type") == "bar"{"was bar"} else if json("type") == "baz"{"was baz"}else{"was none"}`, - output: `was baz`, - messages: []easyMsg{ - {content: `{"type":"baz"}`}, - }, - }, - "if statement with else ifs 3": { - input: `if json("type") == "foo" { - "was foo" -} else if json("type") == "bar" { - "was bar" -} else if json("type") == "baz" { - "was baz" -} else { - "was none" -}`, - output: `was none`, - messages: []easyMsg{ - {content: `{"type":"none of them"}`}, - }, - }, - "match array values": { - input: `match [ "foo" ] { - [ "foo" ] => "first", - "bar" => "second", - _ => "third", -}`, - output: `first`, - messages: []easyMsg{}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - e, pErr := tryParseQuery(test.input) - require.Nil(t, pErr) - - res, err := query.ExecToString(e, query.FunctionContext{ - Index: test.index, MsgBatch: msg, - }.WithValueFunc(func() *any { return test.value })) - require.NoError(t, err) - assert.Equal(t, test.output, res) - - bres, err := query.ExecToBytes(e, query.FunctionContext{ - Index: test.index, MsgBatch: msg, - }.WithValueFunc(func() *any { return test.value })) - require.NoError(t, err) - res = string(bres) - assert.Equal(t, test.output, res) - }) - } -} diff --git a/internal/bloblang/parser/query_function_parser.go b/internal/bloblang/parser/query_function_parser.go deleted file mode 100644 index 9d5db628ab..0000000000 --- a/internal/bloblang/parser/query_function_parser.go +++ /dev/null @@ -1,372 +0,0 @@ -package parser - -import ( - "errors" - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -func functionArgsParser(pCtx Context) Func[[]any] { - return func(input []rune) Result[[]any] { - return DelimitedPattern( - Expect(Sequence(charBracketOpen, DiscardedWhitespaceNewlineComments), "function arguments"), - MustBe(Expect( - OneOf( - FuncAsAny(namedArgParser(pCtx)), - FuncAsAny(queryParser(pCtx)), - ), "function argument"), - ), - MustBe(Expect(Sequence(Discard(SpacesAndTabs), charComma, DiscardedWhitespaceNewlineComments), "comma")), - MustBe(Expect(Sequence(DiscardedWhitespaceNewlineComments, charBracketClose), "closing bracket")), - )(input) - } -} - -type namedArg struct { - name string - value any -} - -func namedArgParser(pCtx Context) Func[namedArg] { - pattern := Sequence( - FuncAsAny(SnakeCase), - FuncAsAny(charColon), - FuncAsAny(Discard(SpacesAndTabs)), - FuncAsAny(MustBe(Expect(queryParser(pCtx), "argument value"))), - ) - - return func(input []rune) Result[namedArg] { - res := pattern(input) - if res.Err != nil { - return Fail[namedArg](res.Err, input) - } - - resSlice := res.Payload - return Success(namedArg{name: resSlice[0].(string), value: resSlice[3]}, res.Remaining) - } -} - -func parseMapExpression(fn query.Function, pCtx Context) Func[query.Function] { - // foo.(bar | baz) - // ^---------^ - pattern := TakeOnly(2, Sequence( - strToQuery(Expect(charBracketOpen, "method")), - strToQuery(DiscardedWhitespaceNewlineComments), - queryParser(pCtx), - strToQuery(DiscardedWhitespaceNewlineComments), - strToQuery(charBracketClose), - )) - - return func(input []rune) Result[query.Function] { - res := pattern(input) - if res.Err != nil { - return res - } - - method, err := query.NewMapMethod(fn, res.Payload) - if err != nil { - res.Err = NewFatalError(input, err) - res.Remaining = input - } else { - res.Payload = method - } - return res - } -} - -func parseFunctionTail(fn query.Function, pCtx Context) Func[query.Function] { - return OneOf( - // foo.(bar | baz) - // ^---------^ - parseMapExpression(fn, pCtx), - // foo.bar() - // ^---^ - methodParser(fn, pCtx), - // foo.bar - // ^-^ - fieldReferenceParser(fn), - ) -} - -func parseWithTails(fnParser Func[query.Function], pCtx Context) Func[query.Function] { - delimPattern := Sequence(charDot, DiscardedWhitespaceNewlineComments) - - mightNot := Sequence( - FuncAsAny(Optional(TakeOnly(0, Sequence( - Char('!'), - Discard(SpacesAndTabs), - )))), - FuncAsAny(fnParser), - ) - - return func(input []rune) Result[query.Function] { - var seq []any - var remaining []rune - - if res := mightNot(input); res.Err != nil { - return Fail[query.Function](res.Err, input) - } else { - seq = res.Payload - remaining = res.Remaining - } - - isNot := seq[0].(string) == "!" - fn := seq[1].(query.Function) - for { - if res := delimPattern(remaining); res.Err != nil { - if isNot { - fn = query.Not(fn) - } - return Success(fn, res.Remaining) - } else { - remaining = res.Remaining - } - - if res := MustBe(parseFunctionTail(fn, pCtx))(remaining); res.Err != nil { - return Fail[query.Function](res.Err, input) - } else { - fn = res.Payload - remaining = res.Remaining - } - } - } -} - -func quotedPathSegmentParser(input []rune) Result[string] { - res := QuotedString(input) - if res.Err != nil { - return res - } - - // Convert into a JSON pointer style path string. - rawSegment := strings.ReplaceAll(res.Payload, "~", "~0") - rawSegment = strings.ReplaceAll(rawSegment, ".", "~1") - - return Success(rawSegment, res.Remaining) -} - -var fieldReferencePattern = Expect( - OneOf( - JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), - ), - quotedPathSegmentParser, - ), - "field path", -) - -func fieldReferenceParser(ctxFn query.Function) Func[query.Function] { - return func(input []rune) Result[query.Function] { - res := fieldReferencePattern(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - fn, err := query.NewGetMethod(ctxFn, res.Payload) - if err != nil { - return Fail[query.Function](NewFatalError(input, err), input) - } - - return Success(fn, res.Remaining) - } -} - -var variableReferencePattern = Expect( - Sequence( - charDollar, - JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), - ), - ), - "variable path", -) - -func variableReferenceParser(input []rune) Result[query.Function] { - res := variableReferencePattern(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - return Success(query.NewVarFunction(res.Payload[1]), res.Remaining) -} - -var metadataReferencePattern = Expect( - Sequence( - Char('@'), - Optional(OneOf( - JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), - ), - QuotedString, - )), - ), - "metadata path", -) - -func metadataReferenceParser(input []rune) Result[query.Function] { - res := metadataReferencePattern(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - return Success(query.NewMetaFunction(res.Payload[1]), res.Remaining) -} - -var fieldReferenceRootPattern = Expect( - JoinStringPayloads( - UntilFail( - OneOf( - InRange('a', 'z'), - InRange('A', 'Z'), - InRange('0', '9'), - charUnderscore, - ), - ), - ), - "field path", -) - -func fieldReferenceRootParser(pCtx Context) Func[query.Function] { - return func(input []rune) Result[query.Function] { - res := fieldReferenceRootPattern(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - var fn query.Function - - path := res.Payload - if path == "this" { - fn = query.NewFieldFunction("") - } else if path == "root" { - fn = query.NewRootFieldFunction("") - } else { - if pCtx.HasNamedContext(path) { - fn = query.NewNamedContextFieldFunction(path, "") - } else { - // TODO V5: Remove this and force this, root, or named contexts - fn = query.NewFieldFunction(path) - } - } - - return Success(fn, res.Remaining) - } -} - -func extractArgsParserResult(paramsDef query.Params, args []any) (*query.ParsedParams, error) { - var namelessArgs []any - var namedArgs map[string]any - - for _, arg := range args { - namedArg, isNamed := arg.(namedArg) - if isNamed { - if namedArgs == nil { - namedArgs = map[string]any{} - } - if _, exists := namedArgs[namedArg.name]; exists { - return nil, fmt.Errorf("duplicate named arg: %v", namedArg.name) - } - namedArgs[namedArg.name] = namedArg.value - } else { - namelessArgs = append(namelessArgs, arg) - } - } - - if len(namelessArgs) > 0 && len(namedArgs) > 0 { - return nil, errors.New("cannot mix named and nameless arguments") - } - - var parsedParams *query.ParsedParams - var err error - if len(namedArgs) > 0 { - parsedParams, err = paramsDef.PopulateNamed(namedArgs) - } else { - parsedParams, err = paramsDef.PopulateNameless(namelessArgs...) - } - if err != nil { - return nil, err - } - - return parsedParams, nil -} - -func methodParser(fn query.Function, pCtx Context) Func[query.Function] { - p := Sequence(FuncAsAny(Expect(SnakeCase, "method")), FuncAsAny(functionArgsParser(pCtx))) - - return func(input []rune) Result[query.Function] { - res := p(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - seqSlice := res.Payload - - targetMethod := seqSlice[0].(string) - params, err := pCtx.Methods.Params(targetMethod) - if err != nil { - return Fail[query.Function](NewFatalError(res.Remaining, err), input) - } - - parsedParams, err := extractArgsParserResult(params, seqSlice[1].([]any)) - if err != nil { - return Fail[query.Function](NewFatalError(res.Remaining, err), input) - } - - method, err := pCtx.InitMethod(targetMethod, fn, parsedParams) - if err != nil { - return Fail[query.Function](NewFatalError(res.Remaining, err), input) - } - return Success(method, res.Remaining) - } -} - -func functionParser(pCtx Context) Func[query.Function] { - p := Sequence(FuncAsAny(Expect(SnakeCase, "function")), FuncAsAny(functionArgsParser(pCtx))) - - return func(input []rune) Result[query.Function] { - res := p(input) - if res.Err != nil { - return Fail[query.Function](res.Err, input) - } - - seqSlice := res.Payload - - targetFunc := seqSlice[0].(string) - params, err := pCtx.Functions.Params(targetFunc) - if err != nil { - return Fail[query.Function](NewFatalError(res.Remaining, err), input) - } - - parsedParams, err := extractArgsParserResult(params, seqSlice[1].([]any)) - if err != nil { - return Fail[query.Function](NewFatalError(res.Remaining, err), input) - } - - fn, err := pCtx.InitFunction(targetFunc, parsedParams) - if err != nil { - return Fail[query.Function](NewFatalError(res.Remaining, err), input) - } - return Success(fn, res.Remaining) - } -} diff --git a/internal/bloblang/parser/query_function_parser_test.go b/internal/bloblang/parser/query_function_parser_test.go deleted file mode 100644 index feac51ba77..0000000000 --- a/internal/bloblang/parser/query_function_parser_test.go +++ /dev/null @@ -1,782 +0,0 @@ -package parser - -import ( - "errors" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestFunctionQueries(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - err error - } - - tests := map[string]struct { - input string - output string - messages []easyMsg - value *any - index int - }{ - "without method": { - input: `this.without("bar","baz")`, - value: func() *any { - var v any = map[string]any{ - "foo": 1.0, - "bar": 2.0, - "baz": 3.0, - } - return &v - }(), - output: `{"foo":1}`, - }, - "without method trailing comma": { - input: `this.without("bar","baz",)`, - value: func() *any { - var v any = map[string]any{ - "foo": 1.0, - "bar": 2.0, - "baz": 3.0, - } - return &v - }(), - output: `{"foo":1}`, - }, - "literal function": { - input: `5`, - output: `5`, - messages: []easyMsg{{}}, - }, - "literal function 2": { - input: `"foo"`, - output: `foo`, - messages: []easyMsg{{}}, - }, - "literal function 3": { - input: `5 - 2`, - output: `3`, - messages: []easyMsg{{}}, - }, - "literal function 4": { - input: `false`, - output: `false`, - messages: []easyMsg{{}}, - }, - "literal function 5": { - input: `null`, - output: `null`, - messages: []easyMsg{{}}, - }, - "literal function 6": { - input: `null | "a string"`, - output: `a string`, - messages: []easyMsg{{}}, - }, - "json function": { - input: `json()`, - output: `{"foo":"bar"}`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - {content: `not json`}, - }, - }, - "json function 2": { - input: `json("foo")`, - output: `bar`, - messages: []easyMsg{ - {content: `{"foo":"bar"}`}, - }, - }, - "json function 3": { - input: `json("foo")`, - output: `bar`, - index: 1, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json function 4": { - input: `json("foo") && (json("bar") > 2)`, - output: `true`, - messages: []easyMsg{ - {content: `{"foo":true,"bar":3}`}, - }, - }, - "json function 5": { - input: `json("foo") && (json("bar") > 2)`, - output: `false`, - messages: []easyMsg{ - {content: `{"foo":true,"bar":1}`}, - }, - }, - "json function dynamic arg": { - input: `json(meta("path"))`, - output: `this`, - messages: []easyMsg{ - { - content: `{"foo":{"bar":"this"}}`, - meta: map[string]any{ - "path": "foo.bar", - }, - }, - }, - }, - "json function dynamic arg 2": { - input: `json(json("path"))`, - output: `this`, - messages: []easyMsg{ - {content: `{"path":"foo.bar","foo":{"bar":"this"}}`}, - }, - }, - "json function dynamic arg 3": { - input: `json().(json(path))`, - output: `this`, - messages: []easyMsg{ - {content: `{"path":"foo","foo":"this"}`}, - }, - }, - "json_from function": { - input: `json("foo").from(1)`, - output: `bar`, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "json_from function 3": { - input: `json("foo").from(-1)`, - output: `bar`, - messages: []easyMsg{ - {content: `not json`}, - {content: `{"foo":"bar"}`}, - }, - }, - "metadata triple quote string arg 1": { - input: `meta("""foo""")`, - output: `bar`, - index: 1, - messages: []easyMsg{ - {}, - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata triple quote string arg 2": { - input: `meta("""foo -bar""")`, - output: `bar`, - index: 1, - messages: []easyMsg{ - {}, - { - meta: map[string]any{ - "foo\nbar": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 1": { - input: `meta("foo")`, - output: `bar`, - index: 1, - messages: []easyMsg{ - {}, - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 2": { - input: `meta("bar")`, - output: "null", - messages: []easyMsg{ - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 3": { - input: `meta()`, - output: `{"baz":"qux","duck,1":"quack","foo":"bar"}`, - messages: []easyMsg{ - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 4": { - input: `meta("duck,1")`, - output: "quack", - messages: []easyMsg{ - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 5": { - input: `meta("foo").from(1)`, - output: "bar", - index: 0, - messages: []easyMsg{ - {}, - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 6": { - input: `meta("foo")`, - output: `null`, - index: 1, - messages: []easyMsg{ - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - {}, - }, - }, - "metadata 7": { - input: `meta().from(1)`, - output: `{}`, - messages: []easyMsg{ - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "metadata 8": { - input: `meta().from(1)`, - output: `{"baz":"qux","duck,1":"quack","foo":"bar"}`, - messages: []easyMsg{ - {}, - { - meta: map[string]any{ - "foo": "bar", - "baz": "qux", - "duck,1": "quack", - }, - }, - }, - }, - "error function": { - input: `error()`, - output: `test error`, - messages: []easyMsg{ - {err: errors.New("test error")}, - }, - }, - "error function 2": { - input: `error().from(1)`, - output: `null`, - messages: []easyMsg{ - {err: errors.New("test error")}, - }, - }, - "error function 3": { - input: `error().from(1)`, - output: `test error`, - messages: []easyMsg{ - {}, - {err: errors.New("test error")}, - }, - }, - "error function 4": { - input: `error()`, - output: `test error`, - index: 1, - messages: []easyMsg{ - {}, - {err: errors.New("test error")}, - }, - }, - "errored function": { - input: `errored()`, - output: `true`, - messages: []easyMsg{ - {err: errors.New("test error")}, - }, - }, - "errored function if": { - input: `if errored() { "failed" } else { "succeeded" }`, - output: `failed`, - index: 1, - messages: []easyMsg{ - {}, - {err: errors.New("test error")}, - }, - }, - "errored function else": { - input: `if errored() { "failed" } else { "succeeded" }`, - output: `succeeded`, - index: 0, - messages: []easyMsg{ - {}, - {err: errors.New("test error")}, - }, - }, - "errored function 2": { - input: `errored().from(1)`, - output: `false`, - messages: []easyMsg{ - {err: errors.New("test error")}, - }, - }, - "errored function 3": { - input: `errored().from(1)`, - output: `true`, - messages: []easyMsg{ - {}, - {err: errors.New("test error")}, - }, - }, - "errored function 4": { - input: `errored()`, - output: `true`, - index: 1, - messages: []easyMsg{ - {}, - {err: errors.New("test error")}, - }, - }, - "content function": { - input: `content()`, - output: `foobar`, - index: 0, - messages: []easyMsg{ - {content: `foobar`}, - {content: `barbaz`}, - }, - }, - "content function 2": { - input: `content().from(1)`, - output: `barbaz`, - index: 0, - messages: []easyMsg{ - {content: `foobar`}, - {content: `barbaz`}, - }, - }, - "content function 3": { - input: `content()`, - output: `barbaz`, - index: 1, - messages: []easyMsg{ - {content: `foobar`}, - {content: `barbaz`}, - }, - }, - "batch index": { - input: `batch_index()`, - output: `1`, - index: 1, - messages: []easyMsg{ - {}, {}, - }, - }, - "batch index 2": { - input: `batch_index()`, - output: `0`, - index: 0, - messages: []easyMsg{ - {}, {}, - }, - }, - "batch index 3": { - input: `batch_index().from(1)`, - output: `1`, - index: 0, - messages: []easyMsg{ - {}, {}, - }, - }, - "batch size": { - input: `batch_size()`, - output: `2`, - messages: []easyMsg{ - {}, {}, - }, - }, - "batch size 2": { - input: `batch_size()`, - output: `1`, - messages: []easyMsg{ - {}, - }, - }, - "field root": { - input: `this`, - output: `test`, - value: func() *any { - var v any = "test" - return &v - }(), - }, - "field root null": { - input: `this`, - output: `null`, - value: func() *any { - var v any - return &v - }(), - }, - "field map": { - input: `this.foo`, - output: `hello world`, - value: func() *any { - var v any = map[string]any{ - "foo": "hello world", - } - return &v - }(), - }, - "field literal": { - input: `this.foo.bar`, - output: `hello world`, - value: func() *any { - var v any = map[string]any{ - "foo": map[string]any{ - "bar": "hello world", - }, - } - return &v - }(), - }, - "field literal 2": { - input: `json().map(this.foo.bar)`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":{"bar":"hello world"}}`}, - }, - }, - "field literal 3": { - input: `json().map(this.foo.bar)`, - output: `null`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"hello world"}}`}, - }, - }, - "field literal 4": { - input: `json("foo").map(this.bar | this.baz)`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"hello world"}}`}, - }, - }, - "field literal 5": { - input: `json().(foo)`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":"hello world"}`}, - }, - }, - "field literal root": { - input: `this`, - output: `test`, - value: func() *any { - var v any = "test" - return &v - }(), - }, - "field literal root 2": { - input: `this.foo`, - output: `test`, - value: func() *any { - var v any = map[string]any{ - "foo": "test", - } - return &v - }(), - }, - "field quoted literal": { - input: `this."foo.bar"`, - output: `test`, - value: func() *any { - var v any = map[string]any{ - "foo.bar": "test", - } - return &v - }(), - }, - "field quoted literal extended": { - input: `this."foo.bar".baz`, - output: `test`, - value: func() *any { - var v any = map[string]any{ - "foo.bar": map[string]any{ - "baz": "test", - }, - } - return &v - }(), - }, - "map literal": { - input: `json().foo`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":"hello world"}`}, - }, - }, - "map literal 2": { - input: `json().foo.bar`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":{"bar":"hello world"}}`}, - }, - }, - "map literal 3": { - input: `json().foo.bar`, - output: `null`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"hello world"}}`}, - }, - }, - "map literal 4": { - input: `json("foo").(this.bar | this.baz)`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"hello world"}}`}, - }, - }, - "map literal 5": { - input: `json().foo.bar nah`, - output: `test`, - messages: []easyMsg{ - {content: `{"foo":{"bar":"test"}}`}, - }, - }, - "map literal 6": { - input: `json("foo").(bar | baz)`, - output: `hello world`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"hello world"}}`}, - }, - }, - "map literal 7": { - input: `json("foo").(bar | baz | quz).from_all()`, - output: `["from_baz","from_quz","from_bar"]`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"from_baz"},"quz":"not this"}`}, - {content: `{"foo":{"quz":"from_quz"}}`}, - {content: `{"foo":{"bar":"from_bar"},"baz":"and not this"}`}, - }, - }, - "map literal 8": { - input: `json().foo.(bar | baz | quz).from_all()`, - output: `["from_baz","from_quz","from_bar"]`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"from_baz"},"quz":"not this"}`}, - {content: `{"foo":{"quz":"from_quz"}}`}, - {content: `{"foo":{"bar":"from_bar"},"baz":"and not this"}`}, - }, - }, - "map literal 9": { - input: `json().(foo.bar | foo.baz | foo.quz).from_all()`, - output: `["from_baz","from_quz","from_bar"]`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"from_baz"},"quz":"not this"}`}, - {content: `{"foo":{"quz":"from_quz"}}`}, - {content: `{"foo":{"bar":"from_bar"},"baz":"and not this"}`}, - }, - }, - "map literal with comments": { - input: `json( - "foo" # Here's a thing -).( - bar | # And look at this thing - baz | - quz -).from_all() # And this`, - output: `["from_baz","from_quz","from_bar"]`, - messages: []easyMsg{ - {content: `{"foo":{"baz":"from_baz"},"quz":"not this"}`}, - {content: `{"foo":{"quz":"from_quz"}}`}, - {content: `{"foo":{"bar":"from_bar"},"baz":"and not this"}`}, - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - if m.err != nil { - part.ErrorSet(m.err) - } - msg = append(msg, part) - } - - e, perr := tryParseQuery(test.input) - require.Nil(t, perr) - - res, err := query.ExecToString(e, query.FunctionContext{ - Index: test.index, MsgBatch: msg, - }.WithValueFunc(func() *any { return test.value })) - require.NoError(t, err) - assert.Equal(t, test.output, res) - - bres, err := query.ExecToBytes(e, query.FunctionContext{ - Index: test.index, MsgBatch: msg, - }.WithValueFunc(func() *any { return test.value })) - require.NoError(t, err) - res = string(bres) - assert.Equal(t, test.output, res) - }) - } -} - -func TestCountersFunction(t *testing.T) { - tests := [][2]string{ - {`count("foo2")`, "1"}, - {`count("bar2")`, "1"}, - {`count("foo2")`, "2"}, - {`count("foo2")`, "3"}, - {`count("bar2")`, "2"}, - {`count("bar2")`, "3"}, - {`count("foo2")`, "4"}, - {`count("foo2")`, "5"}, - {`count("bar2")`, "4"}, - {`count("bar2")`, "5"}, - } - - for _, test := range tests { - e, perr := tryParseQuery(test[0]) - require.Nil(t, perr) - - res, err := query.ExecToString(e, query.FunctionContext{ - MsgBatch: message.QuickBatch(nil), - }) - require.NoError(t, err) - assert.Equal(t, test[1], res) - } -} - -func TestUUIDV4Function(t *testing.T) { - results := map[string]struct{}{} - - for i := 0; i < 100; i++ { - e, perr := tryParseQuery("uuid_v4()") - require.Nil(t, perr) - - res, err := query.ExecToString(e, query.FunctionContext{ - MsgBatch: message.QuickBatch(nil), - }) - if _, exists := results[res]; exists { - t.Errorf("Duplicate UUID generated: %v", res) - } - require.NoError(t, err) - results[res] = struct{}{} - } -} - -func TestTimestamps(t *testing.T) { - now := time.Now() - - e, perr := tryParseQuery("timestamp_unix_nano()") - require.Nil(t, perr) - - tStamp, err := query.ExecToString(e, query.FunctionContext{MsgBatch: message.QuickBatch(nil)}) - require.NoError(t, err) - - nanoseconds, err := strconv.ParseInt(tStamp, 10, 64) - if err != nil { - t.Fatal(err) - } - tThen := time.Unix(0, nanoseconds) - - if tThen.Sub(now).Seconds() > 5.0 { - t.Errorf("Timestamps too far out of sync: %v and %v", tThen, now) - } - - now = time.Now() - e, perr = tryParseQuery("timestamp_unix()") - if !assert.Nil(t, perr) { - require.NoError(t, perr.Err) - } - - tStamp, err = query.ExecToString(e, query.FunctionContext{MsgBatch: message.QuickBatch(nil)}) - require.NoError(t, err) - - seconds, err := strconv.ParseInt(tStamp, 10, 64) - if err != nil { - t.Fatal(err) - } - tThen = time.Unix(seconds, 0) - - if tThen.Sub(now).Seconds() > 5.0 { - t.Errorf("Timestamps too far out of sync: %v and %v", tThen, now) - } - - now = time.Now() - e, perr = tryParseQuery("timestamp_unix()") - if !assert.Nil(t, perr) { - require.NoError(t, perr.Err) - } - - tStamp, err = query.ExecToString(e, query.FunctionContext{MsgBatch: message.QuickBatch(nil)}) - require.NoError(t, err) - - var secondsF float64 - secondsF, err = strconv.ParseFloat(tStamp, 64) - if err != nil { - t.Fatal(err) - } - tThen = time.Unix(int64(secondsF), 0) - - if tThen.Sub(now).Seconds() > 5.0 { - t.Errorf("Timestamps too far out of sync: %v and %v", tThen, now) - } -} diff --git a/internal/bloblang/parser/query_literal_parser.go b/internal/bloblang/parser/query_literal_parser.go deleted file mode 100644 index 3634ecd61e..0000000000 --- a/internal/bloblang/parser/query_literal_parser.go +++ /dev/null @@ -1,101 +0,0 @@ -package parser - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -func dynamicArrayParser(pCtx Context) Func[any] { - return func(input []rune) Result[any] { - res := DelimitedPattern( - Expect(Sequence( - charSquareOpen, - DiscardedWhitespaceNewlineComments, - ), "array"), - Expect(queryParser(pCtx), "object"), - Sequence( - Discard(SpacesAndTabs), - charComma, - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquareClose, - ), - )(input) - if res.Err != nil { - return Fail[any](res.Err, input) - } - return Success[any](query.NewArrayLiteral(res.Payload...), res.Remaining) - } -} - -func dynamicObjectParser(pCtx Context) Func[any] { - return func(input []rune) Result[any] { - res := DelimitedPattern( - Expect(Sequence( - charSquigOpen, - DiscardedWhitespaceNewlineComments, - ), "object"), - Sequence( - OneOf( - FuncAsAny(QuotedString), - FuncAsAny(Expect(queryParser(pCtx), "object")), - ), - FuncAsAny(Discard(SpacesAndTabs)), - FuncAsAny(charColon), - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(Expect(queryParser(pCtx), "object")), - ), - Sequence( - Discard(SpacesAndTabs), - charComma, - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquigClose, - ), - )(input) - if res.Err != nil { - return Fail[any](res.Err, input) - } - - values := [][2]any{} - - for _, sequenceValue := range res.Payload { - values = append(values, [2]any{sequenceValue[0], sequenceValue[4]}) - } - - lit, err := query.NewMapLiteral(values) - if err != nil { - return Fail[any](NewFatalError(input, err), input) - } - - return Success(lit, res.Remaining) - } -} - -func literalValueParser(pCtx Context) Func[query.Function] { - p := OneOf( - FuncAsAny(Boolean), - FuncAsAny(Number), - FuncAsAny(TripleQuoteString), - FuncAsAny(QuotedString), - FuncAsAny(Null), - FuncAsAny(dynamicArrayParser(pCtx)), - FuncAsAny(dynamicObjectParser(pCtx)), - ) - - return func(input []rune) Result[query.Function] { - res := p(input) - if res.Err != nil { - return Fail[query.Function](res.Err, res.Remaining) - } - - if f, isFunction := res.Payload.(query.Function); isFunction { - return Success(f, res.Remaining) - } - - return Success[query.Function](query.NewLiteralFunction("", res.Payload), res.Remaining) - } -} diff --git a/internal/bloblang/parser/query_literal_parser_test.go b/internal/bloblang/parser/query_literal_parser_test.go deleted file mode 100644 index 348993d8d6..0000000000 --- a/internal/bloblang/parser/query_literal_parser_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestLiteralParserErrors(t *testing.T) { - tests := map[string]struct { - input string - err string - }{ - "bad object key": { - input: `{5:"foo"}`, - err: `line 1 char 1: object keys must be strings, received: int64`, - }, - "bad array element": { - input: `[5,null,"unterminated string]`, - err: `line 1 char 30: required: expected end quote`, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - _, err := tryParseQuery(test.input) - assert.Equal(t, test.err, err.ErrorAtPosition([]rune(test.input))) - }) - } -} - -func TestLiteralParser(t *testing.T) { - tests := map[string]struct { - mapping string - result any - parseErr string - err string - value *any - }{ - "basic map": { - mapping: `{"foo":"bar"}`, - result: map[string]any{ - "foo": "bar", - }, - }, - "basic map trailing comma": { - mapping: `{"foo":"bar",}`, - result: map[string]any{ - "foo": "bar", - }, - }, - "dynamic map": { - mapping: `{"foo":(5 + 5)}`, - result: map[string]any{ - "foo": int64(10), - }, - }, - "dynamic map trailing comma": { - mapping: `{"foo":(5 + 5),}`, - result: map[string]any{ - "foo": int64(10), - }, - }, - "dynamic map dynamic key": { - mapping: `{("foobar".uppercase()):5}`, - result: map[string]any{ - "FOOBAR": int64(5), - }, - }, - "dynamic map nested": { - mapping: `{"foo":{"bar":(5 + 5)}}`, - result: map[string]any{ - "foo": map[string]any{ - "bar": int64(10), - }, - }, - }, - "dynamic array": { - mapping: `["foo",(5 + 5),null]`, - result: []any{ - "foo", int64(10), nil, - }, - }, - "dynamic array trailing comma": { - mapping: `["foo",(5 + 5),null,]`, - result: []any{ - "foo", int64(10), nil, - }, - }, - "dynamic array nested": { - mapping: `["foo",[(5 + 5),"bar"],null]`, - result: []any{ - "foo", []any{int64(10), "bar"}, nil, - }, - }, - "bad array element": { - mapping: `["foo",(5 + "not a number"),"bar"]`, - parseErr: "cannot add types number (from number literal) and string (from string literal): 5 + \"", - }, - "bad object value": { - mapping: `{"foo":(5 + "not a number")}`, - parseErr: "cannot add types number (from number literal) and string (from string literal): 5 + \"", - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - res := queryParser(Context{ - Functions: query.AllFunctions, - Methods: query.AllMethods, - })([]rune(test.mapping)) - if test.parseErr != "" { - assert.Equal(t, test.parseErr, res.Err.Error()) - return - } - require.Nil(t, res.Err) - require.Implements(t, (*query.Function)(nil), res.Payload) - q := res.Payload - - result, err := q.Exec(query.FunctionContext{ - Index: 0, MsgBatch: message.QuickBatch(nil), - }.WithValueFunc(func() *any { return test.value })) - if test.err != "" { - assert.EqualError(t, err, test.err) - } else { - assert.Equal(t, test.result, result) - } - }) - } -} diff --git a/internal/bloblang/parser/query_method_parser_test.go b/internal/bloblang/parser/query_method_parser_test.go deleted file mode 100644 index 2f31c41594..0000000000 --- a/internal/bloblang/parser/query_method_parser_test.go +++ /dev/null @@ -1,580 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestMethodParser(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - tests := map[string]struct { - input string - output string - messages []easyMsg - index int - }{ - "literal function": { - input: `5.from(0)`, - output: `5`, - messages: []easyMsg{{}}, - }, - "json from": { - input: `json("foo").from(0)`, - output: `1`, - messages: []easyMsg{ - {content: `{"foo":1}`}, - {content: `{"foo":2}`}, - {content: `{"foo":3}`}, - {content: `{"foo":4}`}, - }, - }, - "json from 2": { - input: `json("foo").from(1)`, - output: `2`, - messages: []easyMsg{ - {content: `{"foo":1}`}, - {content: `{"foo":2}`}, - {content: `{"foo":3}`}, - {content: `{"foo":4}`}, - }, - }, - "json from 3": { - input: `json("foo").from(-1)`, - output: `4`, - messages: []easyMsg{ - {content: `{"foo":1}`}, - {content: `{"foo":2}`}, - {content: `{"foo":3}`}, - {content: `{"foo":4}`}, - }, - }, - "json from 4": { - input: `json("foo").from(-2)`, - output: `3`, - messages: []easyMsg{ - {content: `{"foo":1}`}, - {content: `{"foo":2}`}, - {content: `{"foo":3}`}, - {content: `{"foo":4}`}, - }, - }, - "json from all": { - input: `json("foo").from_all()`, - output: `["a","b","c"]`, - messages: []easyMsg{ - {content: `{"foo":"a"}`}, - {content: `{"foo":"b"}`}, - {content: `{"foo":"c"}`}, - }, - }, - "json from all/or": { - input: `json("foo").or("fallback").from_all()`, - output: `["a","fallback","c","fallback"]`, - messages: []easyMsg{ - {content: `{"foo":"a"}`}, - {content: `{}`}, - {content: `{"foo":"c"}`}, - {content: `not even json`}, - }, - }, - "json from all/or 2": { - input: `(json().foo | "fallback").from_all()`, - output: `["a","fallback","c","fallback"]`, - messages: []easyMsg{ - {content: `{"foo":"a"}`}, - {content: `{}`}, - {content: `{"foo":"c"}`}, - {content: `not even json`}, - }, - }, - "json from all/or 3": { - input: `json().foo.or("fallback").from_all()`, - output: `["a","fallback","c","fallback"]`, - messages: []easyMsg{ - {content: `{"foo":"a"}`}, - {content: `{}`}, - {content: `{"foo":"c"}`}, - {content: `not even json`}, - }, - }, - "deleted to or": { - input: `deleted().or("fallback")`, - output: `fallback`, - messages: []easyMsg{{}}, - }, - "nothing to or": { - input: `nothing().or("fallback")`, - output: `fallback`, - messages: []easyMsg{{}}, - }, - "json catch": { - input: `json().catch("nope")`, - output: `nope`, - messages: []easyMsg{ - {content: `this %$#% isnt json`}, - }, - }, - "json catch 2": { - input: `json().catch("nope")`, - output: `null`, - messages: []easyMsg{ - {content: `null`}, - }, - }, - "json catch 3": { - input: `json("foo").catch("nope")`, - output: `null`, - messages: []easyMsg{ - {content: `{"foo":null}`}, - }, - }, - "json catch 4": { - input: `json("foo").catch("nope")`, - output: `yep`, - messages: []easyMsg{ - {content: `{"foo":"yep"}`}, - }, - }, - "meta from all": { - input: `meta("foo").from_all()`, - output: `["bar",null,"baz"]`, - messages: []easyMsg{ - {meta: map[string]any{"foo": "bar"}}, - {}, - {meta: map[string]any{"foo": "baz"}}, - }, - }, - "or json null": { - input: `json("foo").or("backup")`, - output: `backup`, - messages: []easyMsg{ - {content: `{"foo":null}`}, - }, - }, - "or json null 2": { - input: `json("foo").or("backup")`, - output: `backup`, - messages: []easyMsg{ - {content: `{"bar":"nope"}`}, - }, - }, - "or json null 3": { - input: `json("foo").or(json("bar"))`, - output: `yep`, - messages: []easyMsg{ - {content: `{"bar":"yep"}`}, - }, - }, - "or boolean from all": { - input: `json("foo").or( json("bar") == "yep" ).from_all()`, - output: `["from foo",true,false,"from foo 2"]`, - messages: []easyMsg{ - {content: `{"foo":"from foo"}`}, - {content: `{"bar":"yep"}`}, - {content: `{"bar":"nope"}`}, - {content: `{"foo":"from foo 2","bar":"yep"}`}, - }, - }, - "or boolean from metadata": { - input: `meta("foo").or( meta("bar") == "yep" ).from_all()`, - output: `["from foo",true,false,"from foo 2"]`, - messages: []easyMsg{ - {meta: map[string]any{"foo": "from foo"}}, - {meta: map[string]any{"bar": "yep"}}, - {meta: map[string]any{"bar": "nope"}}, - {meta: map[string]any{"foo": "from foo 2", "bar": "yep"}}, - }, - }, - "map each": { - input: `json("foo").map_each(this + 10)`, - output: `[11,12,12]`, - messages: []easyMsg{ - {content: `{"foo":[1,2,2]}`}, - }, - }, - "map each inner map": { - input: `json("foo").map_each((this.bar + 10) | "woops")`, - output: `[11,"woops",12]`, - messages: []easyMsg{ - {content: `{"foo":[{"bar":1},2,{"bar":2}]}`}, - }, - }, - "map each some errors": { - input: `json("foo").map_each((this + 10) | "failed")`, - output: `[11,12,"failed",12]`, - messages: []easyMsg{ - {content: `{"foo":[1,2,"nope",2]}`}, - }, - }, - "map each uncaught errors": { - input: `json("foo").map_each(this.number(0) + 10)`, - output: `[11,12,10,12]`, - messages: []easyMsg{ - {content: `{"foo":[1,2,"nope",2]}`}, - }, - }, - "map each delete some elements": { - input: `json("foo").map_each( - match this { - this < 10 => deleted() - _ => this - 10 - } -)`, - output: `[1,2,3]`, - messages: []easyMsg{ - {content: `{"foo":[11,12,7,13]}`}, - }, - }, - "map each delete all elements for some reason": { - input: `json("foo").map_each(deleted())`, - output: `[]`, - messages: []easyMsg{ - {content: `{"foo":[11,12,7,13]}`}, - }, - }, - "map each object": { - input: `json("foo").map_each(value + 10)`, - output: `{"a":11,"b":12,"c":12}`, - messages: []easyMsg{ - {content: `{"foo":{"a":1,"b":2,"c":2}}`}, - }, - }, - "map each object delete some elements": { - input: `json("foo").map_each( - match { - value < 10 => deleted() - _ => value - 10 - } -)`, - output: `{"a":1,"b":2,"d":3}`, - messages: []easyMsg{ - {content: `{"foo":{"a":11,"b":12,"c":7,"d":13}}`}, - }, - }, - "map each object delete all elements": { - input: `json("foo").map_each(deleted())`, - output: `{}`, - messages: []easyMsg{ - {content: `{"foo":{"a":11,"b":12,"c":7,"d":13}}`}, - }, - }, - "test sum standard array": { - input: `json("foo").sum()`, - output: `5`, - messages: []easyMsg{ - {content: `{"foo":[1,2,2]}`}, - }, - }, - "test sum standard array 4": { - input: `json("foo").from_all().sum()`, - output: `16`, - messages: []easyMsg{ - {content: `{"foo":1}`}, - {content: `{"foo":3}`}, - {content: `{"foo":4}`}, - {content: `{"foo":8}`}, - }, - }, - "test map json": { - input: `json("foo").map(bar)`, - output: `yep`, - messages: []easyMsg{ - {content: `{"foo":{"bar":"yep"}}`}, - }, - }, - "test map json 2": { - input: `json("foo").map(bar.number() + 10)`, - output: `13`, - messages: []easyMsg{ - {content: `{"foo":{"bar":"3"}}`}, - }, - }, - "test map json 3": { - input: `json("foo").map(("static"))`, - output: `static`, - messages: []easyMsg{ - {content: `{"foo":{"bar":"3"}}`}, - }, - }, - "test string method": { - input: `5.string() == "5"`, - output: `true`, - messages: []easyMsg{{}}, - }, - "test number method": { - input: `"5".number() == 5`, - output: `true`, - messages: []easyMsg{{}}, - }, - "test uppercase method": { - input: `"foobar".uppercase()`, - output: `FOOBAR`, - messages: []easyMsg{{}}, - }, - "test lowercase method": { - input: `"FOOBAR".lowercase()`, - output: `foobar`, - messages: []easyMsg{{}}, - }, - "test format method": { - input: `"foo %v bar".format("test")`, - output: `foo test bar`, - messages: []easyMsg{{}}, - }, - "test format method 2": { - input: `"foo %v bar".format(meta("foo"))`, - output: `foo test bar`, - messages: []easyMsg{{ - meta: map[string]any{"foo": "test"}, - }}, - }, - "test format method 3": { - input: `json().("foo %v, %v, %v bar".format(value, meta("foo"), 3))`, - output: `foo yup, bar, 3 bar`, - messages: []easyMsg{{ - content: `{"value":"yup"}`, - meta: map[string]any{"foo": "bar"}, - }}, - }, - "test length string": { - input: `json("foo").length()`, - output: `5`, - messages: []easyMsg{{content: `{"foo":"hello"}`}}, - }, - "test length array": { - input: `json("foo").length()`, - output: `3`, - messages: []easyMsg{{content: `{"foo":["foo","bar","baz"]}`}}, - }, - "test length object": { - input: `json("foo").length()`, - output: `3`, - messages: []easyMsg{{content: `{"foo":{"foo":1,"bar":2,"baz":3}}`}}, - }, - "test get": { - input: `json().get("foo")`, - output: `bar`, - messages: []easyMsg{{content: `{"foo":"bar"}`}}, - }, - "test get 2": { - input: `json().get("foo")`, - output: `null`, - messages: []easyMsg{{content: `{"nope":"bar"}`}}, - }, - "test get 3": { - input: `json().get("foo.bar")`, - output: `baz`, - messages: []easyMsg{{content: `{"foo":{"bar":"baz"}}`}}, - }, - "test exists": { - input: `json().exists("foo")`, - output: `true`, - messages: []easyMsg{{content: `{"foo":"bar"}`}}, - }, - "test exists 2": { - input: `json().exists("foo")`, - output: `false`, - messages: []easyMsg{{content: `{"nope":"bar"}`}}, - }, - "test exists 3": { - input: `json().exists("foo.bar")`, - output: `true`, - messages: []easyMsg{{content: `{"foo":{"bar":"baz"}}`}}, - }, - "test exists 4": { - input: `json().exists("foo.bar")`, - output: `false`, - messages: []easyMsg{{content: `{"foo":{"nope":"baz"}}`}}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - e, perr := tryParseQuery(test.input) - require.Nil(t, perr) - res, err := query.ExecToString(e, query.FunctionContext{ - Index: test.index, - MsgBatch: msg, - }) - require.NoError(t, err) - assert.Equal(t, test.output, res) - }) - } -} - -func TestMethodErrors(t *testing.T) { - type easyMsg struct { - content string - meta map[string]string - } - - tests := map[string]struct { - input string - errStr string - messages []easyMsg - index int - }{ - "literal function": { - input: `"not a number".number()`, - errStr: "string literal: strconv.ParseFloat: parsing \"not a number\": invalid syntax", - messages: []easyMsg{{}}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - e, perr := tryParseQuery(test.input) - require.Nil(t, perr) - - _, err := e.Exec(query.FunctionContext{ - Index: test.index, - MsgBatch: msg, - }) - assert.EqualError(t, err, test.errStr) - }) - } -} - -func TestMethodMaps(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - tests := map[string]struct { - input string - output any - err string - maps map[string]query.Function - messages []easyMsg - index int - }{ - "no maps": { - input: `"foo".apply("nope")`, - err: "no maps were found", - messages: []easyMsg{{}}, - }, - "map not exist": { - input: `"foo".apply("nope")`, - err: "map nope was not found", - maps: map[string]query.Function{}, - messages: []easyMsg{{}}, - }, - "map static": { - input: `"foo".apply("foo")`, - output: "hello world", - maps: map[string]query.Function{ - "foo": query.NewLiteralFunction("", "hello world"), - }, - messages: []easyMsg{{}}, - }, - "map context": { - input: `json().apply("foo")`, - output: "this value", - maps: map[string]query.Function{ - "foo": query.NewFieldFunction("foo"), - }, - messages: []easyMsg{{ - content: `{"foo":"this value"}`, - }}, - }, - "map dynamic": { - input: `json().apply(meta("dyn_map"))`, - output: "this value", - maps: map[string]query.Function{ - "foo": query.NewFieldFunction("foo"), - "bar": query.NewFieldFunction("bar"), - }, - messages: []easyMsg{{ - content: `{"foo":"this value","bar":"and this value"}`, - meta: map[string]any{ - "dyn_map": "foo", - }, - }}, - }, - "map dynamic 2": { - input: `json().apply(meta("dyn_map"))`, - output: "and this value", - maps: map[string]query.Function{ - "foo": query.NewFieldFunction("foo"), - "bar": query.NewFieldFunction("bar"), - }, - messages: []easyMsg{{ - content: `{"foo":"this value","bar":"and this value"}`, - meta: map[string]any{ - "dyn_map": "bar", - }, - }}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - e, perr := tryParseQuery(test.input) - require.Nil(t, perr) - - res, err := e.Exec(query.FunctionContext{ - Maps: test.maps, - Index: test.index, - MsgBatch: msg, - }) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - } - assert.Equal(t, test.output, res) - }) - } -} diff --git a/internal/bloblang/parser/query_parser.go b/internal/bloblang/parser/query_parser.go deleted file mode 100644 index 264be26ba2..0000000000 --- a/internal/bloblang/parser/query_parser.go +++ /dev/null @@ -1,37 +0,0 @@ -package parser - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -func queryParser(pCtx Context) Func[query.Function] { - rootParser := parseWithTails(Expect( - OneOf( - matchExpressionParser(pCtx), - ifExpressionParser(pCtx), - lambdaExpressionParser(pCtx), - bracketsExpressionParser(pCtx), - literalValueParser(pCtx), - functionParser(pCtx), - metadataReferenceParser, - variableReferenceParser, - fieldReferenceRootParser(pCtx), - ), - "query", - ), pCtx) - return func(input []rune) Result[query.Function] { - res := SpacesAndTabs(input) - return arithmeticParser(rootParser)(res.Remaining) - } -} - -func tryParseQuery(expr string) (query.Function, *Error) { - res := queryParser(Context{ - Functions: query.AllFunctions, - Methods: query.AllMethods, - })([]rune(expr)) - if res.Err != nil { - return nil, res.Err - } - return res.Payload, nil -} diff --git a/internal/bloblang/parser/query_parser_test.go b/internal/bloblang/parser/query_parser_test.go deleted file mode 100644 index a231ce0114..0000000000 --- a/internal/bloblang/parser/query_parser_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -func TestFunctionParserErrors(t *testing.T) { - tests := map[string]struct { - input string - err string - }{ - "bad function 2": { - input: `not_a_function()`, - err: `line 1 char 17: unrecognised function 'not_a_function'`, - }, - "bad args 2": { - input: `json("foo`, - err: `line 1 char 10: required: expected end quote`, - }, - "bad args 3": { - input: `json(`, - err: `line 1 char 6: required: expected function argument`, - }, - "bad args 4": { - input: `json(0,`, - err: `line 1 char 8: required: expected function argument`, - }, - "bad args 7": { - input: `json(5)`, - err: `line 1 char 8: field path: wrong argument type, expected string, got number`, - }, - "bad args 8": { - input: `json(false)`, - err: `line 1 char 12: field path: wrong argument type, expected string, got bool`, - }, - "unfinished named arg first": { - input: `foo.parse_timestamp(format: "meow", tz:)`, - err: `line 1 char 40: required: expected argument value`, - }, - "unfinished named arg first with comma": { - input: `foo.parse_timestamp(format: "meow", tz:,)`, - err: `line 1 char 40: required: expected argument value`, - }, - "cannot mix args types": { - input: `foo.slice(0, high: 5)`, - err: `line 1 char 22: cannot mix named and nameless arguments`, - }, - "bad operators": { - input: `json("foo") + `, - err: `line 1 char 15: expected query`, - }, - "bad expression": { - input: `(json("foo") `, - err: `line 1 char 14: required: expected closing bracket`, - }, - "bad expression 2": { - input: `(json("foo") + `, - err: `line 1 char 16: expected query`, - }, - "bad expression 3": { - input: `(json("foo") + meta("bar") `, - err: `line 1 char 28: required: expected closing bracket`, - }, - "bad method": { - input: `json("foo").not_a_thing()`, - err: `line 1 char 26: unrecognised method 'not_a_thing'`, - }, - "bad method 2": { - input: `json("foo").not_a_thing()`, - err: `line 1 char 26: unrecognised method 'not_a_thing'`, - }, - "bad method args 2": { - input: `json("foo").from(`, - err: `line 1 char 18: required: expected function argument`, - }, - "bad method args 3": { - input: `json("foo").from()`, - err: `line 1 char 19: missing parameter: index`, - }, - "bad method args 4": { - input: `json("foo").from("nah")`, - err: `line 1 char 24: field index: expected number value, got string ("nah")`, - }, - "bad map args": { - input: `json("foo").map()`, - err: `line 1 char 18: missing parameter: query`, - }, - "gibberish": { - input: `json("foo").(=)`, - err: `line 1 char 14: required: expected query`, - }, - "gibberish 2": { - input: `json("foo").(1 + )`, - err: `line 1 char 18: required: expected query`, - }, - "bad match": { - input: `match json("foo")`, - err: `line 1 char 18: required: expected {`, - }, - "bad match 2": { - input: `match json("foo") what is this?`, - err: `line 1 char 19: required: expected {`, - }, - "shadowed context": { - input: `this.(foo -> foo.(foo -> foo.bar))`, - err: "line 1 char 19: context label `foo` would shadow a parent context", - }, - "shadowed this context": { - input: `this.(this -> this.foo)`, - err: "line 1 char 7: context label `this` is not allowed", - }, - "shadowed root context": { - input: `this.(root -> root.foo)`, - err: "line 1 char 7: context label `root` is not allowed", - }, - "chained captured context": { - input: `this.(foo -> bar -> baz -> baz.foo)`, - err: "line 1 char 14: it would be in poor taste to capture the same context under both 'bar' and 'baz'", - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - _, err := tryParseQuery(test.input) - require.NotNil(t, err) - assert.Equal(t, test.err, err.ErrorAtPosition([]rune(test.input))) - }) - } -} - -func TestFunctionParserLimits(t *testing.T) { - tests := map[string]struct { - input string - remaining string - }{ - "nothing": { - input: `json("foo") + meta("bar")`, - remaining: ``, - }, - "space before": { - input: ` json("foo") + meta("bar")`, - remaining: ``, - }, - "space before 2": { - input: ` json("foo") + meta("bar")`, - remaining: ``, - }, - "unfinished comment": { - input: `json("foo") + meta("bar") # Here's a comment`, - remaining: ` # Here's a comment`, - }, - "extra text": { - input: `json("foo") and this`, - remaining: ` and this`, - }, - "extra text 2": { - input: `json("foo") + meta("bar") and this`, - remaining: ` and this`, - }, - "extra text 3": { - input: `json("foo")+meta("bar")and this`, - remaining: `and this`, - }, - "extra text 4": { - input: `json("foo")+meta("bar") and this`, - remaining: ` and this`, - }, - "squiggly bracket": { - input: `json("foo")}`, - remaining: `}`, - }, - "normal bracket": { - input: `json("foo"))`, - remaining: `)`, - }, - "normal bracket 2": { - input: `json("foo"))))`, - remaining: `)))`, - }, - "normal bracket 3": { - input: `json("foo")) + json("bar")`, - remaining: `) + json("bar")`, - }, - "path literals": { - input: `this.foo bar baz`, - remaining: ` bar baz`, - }, - "path literals 2": { - input: `this.foo . bar baz`, - remaining: ` . bar baz`, - }, - "brackets at root": { - input: `(json().foo | "fallback").from_all()`, - remaining: ``, - }, - "brackets after root": { - input: `this.root.(json().foo | "fallback").from_all()`, - remaining: ``, - }, - "brackets after root 2": { - input: `this.root.(json().foo | "fallback").from_all().bar.baz`, - remaining: ``, - }, - "this at root": { - input: `this.foo.bar and then this`, - remaining: ` and then this`, - }, - "path literal at root": { - input: `foo.bar and then this`, - remaining: ` and then this`, - }, - "match expression": { - input: `match null { - "foo" == "bar" => "baz" - 5 > 10 => "or this" -} -not this`, - remaining: "\nnot this", - }, - "operators and line breaks": { - input: `(5 * 8) + - 6 - - 5 and also this`, - remaining: " and also this", - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - res := queryParser(Context{ - Functions: query.AllFunctions, - Methods: query.AllMethods, - })([]rune(test.input)) - require.Nil(t, res.Err) - assert.Equal(t, test.remaining, string(res.Remaining)) - }) - } -} diff --git a/internal/bloblang/parser/root_expression_parser.go b/internal/bloblang/parser/root_expression_parser.go deleted file mode 100644 index f521f2d0c5..0000000000 --- a/internal/bloblang/parser/root_expression_parser.go +++ /dev/null @@ -1,109 +0,0 @@ -package parser - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -func rootLevelIfExpressionParser(pCtx Context) Func[mapping.Statement] { - return func(input []rune) Result[mapping.Statement] { - ifParser := Sequence( - FuncAsAny(Expect(Term("if"), "assignment")), - FuncAsAny(SpacesAndTabs), - FuncAsAny(MustBe(queryParser(pCtx))), - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(DelimitedPattern( - Sequence( - charSquigOpen, - DiscardedWhitespaceNewlineComments, - ), - mappingStatement(pCtx, true, nil), - Sequence( - Discard(SpacesAndTabs), - NewlineAllowComment, - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquigClose, - ), - )), - ) - - elseIfParser := Optional(Sequence( - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(Term("else if")), - FuncAsAny(SpacesAndTabs), - FuncAsAny(MustBe(queryParser(pCtx))), - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(DelimitedPattern( - Sequence( - charSquigOpen, - DiscardedWhitespaceNewlineComments, - ), - mappingStatement(pCtx, true, nil), - Sequence( - Discard(SpacesAndTabs), - NewlineAllowComment, - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquigClose, - ), - )), - )) - - elseParser := Optional(Sequence( - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(Term("else")), - FuncAsAny(DiscardedWhitespaceNewlineComments), - FuncAsAny(DelimitedPattern( - Sequence( - charSquigOpen, - DiscardedWhitespaceNewlineComments, - ), - mappingStatement(pCtx, true, nil), - Sequence( - Discard(SpacesAndTabs), - NewlineAllowComment, - DiscardedWhitespaceNewlineComments, - ), - Sequence( - DiscardedWhitespaceNewlineComments, - charSquigClose, - ), - )), - )) - - res := ifParser(input) - if res.Err != nil { - return Fail[mapping.Statement](res.Err, input) - } - - seqSlice := res.Payload - stmt := mapping.NewRootLevelIfStatement(input) - stmt.Add(seqSlice[2].(query.Function), seqSlice[4].([]mapping.Statement)...) - - for { - res = elseIfParser(res.Remaining) - if res.Err != nil { - return Fail[mapping.Statement](res.Err, input) - } - if res.Payload == nil { - break - } - seqSlice = res.Payload - stmt.Add(seqSlice[3].(query.Function), seqSlice[5].([]mapping.Statement)...) - } - - res = elseParser(res.Remaining) - if res.Err != nil { - return Fail[mapping.Statement](res.Err, input) - } - if seqSlice = res.Payload; seqSlice != nil { - stmt.Add(nil, seqSlice[3].([]mapping.Statement)...) - } - return Success[mapping.Statement](stmt, res.Remaining) - } -} diff --git a/internal/bloblang/parser/root_expression_parser_test.go b/internal/bloblang/parser/root_expression_parser_test.go deleted file mode 100644 index be2fcdb250..0000000000 --- a/internal/bloblang/parser/root_expression_parser_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestRootExpressionMappings(t *testing.T) { - type part struct { - Content string - Meta map[string]any - } - - tests := map[string]struct { - index int - mapping string - io [][2]part - }{ - "root level if statement": { - mapping: ` -if this.foo > this.bar { - root.a = "foo was bigger than bar" - root.b = "yep, agreed" -} -root.c = "idk" -`, - io: [][2]part{ - { - {Content: `{"foo":5,"bar":3}`}, - {Content: `{"a":"foo was bigger than bar","b":"yep, agreed","c":"idk"}`}, - }, - { - {Content: `{"foo":2,"bar":3}`}, - {Content: `{"c":"idk"}`}, - }, - }, - }, - "root level if/else statement": { - mapping: ` -if this.foo > this.bar { - -root.a = "foo was bigger than bar" - -root.b = "yep, agreed" - -} else { root.c = "idk" } -`, - io: [][2]part{ - { - {Content: `{"foo":5,"bar":3}`}, - {Content: `{"a":"foo was bigger than bar","b":"yep, agreed"}`}, - }, - { - {Content: `{"foo":2,"bar":3}`}, - {Content: `{"c":"idk"}`}, - }, - }, - }, - "root level if/elseif/else statement": { - mapping: ` -if this.foo > this.bar { - root.a = "foo was bigger than bar" - root.b = "yep, agreed" -} else if this.foo == this.bar { - root.c = "idk" -} else { - root.d = "heh, nice" -} -`, - io: [][2]part{ - { - {Content: `{"foo":5,"bar":3}`}, - {Content: `{"a":"foo was bigger than bar","b":"yep, agreed"}`}, - }, - { - {Content: `{"foo":2,"bar":2}`}, - {Content: `{"c":"idk"}`}, - }, - { - {Content: `{"foo":2,"bar":3}`}, - {Content: `{"d":"heh, nice"}`}, - }, - }, - }, - "root level meta assignments": { - mapping: ` -root = "" -if this.foo > this.bar { - meta a = "foo was bigger than bar" - meta b = "yep, agreed" -} else if this.foo == this.bar { - meta c = "idk" -} else { - meta = {"d": "heh, nice"} -} -`, - io: [][2]part{ - { - {Content: `{"foo":5,"bar":3}`}, - {Meta: map[string]any{"a": "foo was bigger than bar", "b": "yep, agreed"}}, - }, - { - {Content: `{"foo":2,"bar":2}`}, - {Meta: map[string]any{"c": "idk"}}, - }, - { - {Content: `{"foo":2,"bar":3}`}, - {Meta: map[string]any{"d": "heh, nice"}}, - }, - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - for _, io := range test.io { - inPart := message.NewPart([]byte(io[0].Content)) - for k, v := range io[0].Meta { - inPart.MetaSetMut(k, v) - } - - if io[1].Meta == nil { - io[1].Meta = map[string]any{} - } - - exec, perr := ParseMapping(GlobalContext(), test.mapping) - require.Nil(t, perr) - - resPart, err := exec.MapPart(test.index, message.Batch{inPart}) - require.NoError(t, err) - - outPart := part{ - Content: string(resPart.AsBytes()), - Meta: map[string]any{}, - } - _ = resPart.MetaIterMut(func(k string, v any) error { - outPart.Meta[k] = v - return nil - }) - assert.Equal(t, io[1], outPart) - } - }) - } -} diff --git a/internal/bloblang/plugins/bloblang.go b/internal/bloblang/plugins/bloblang.go deleted file mode 100644 index bb49df93c1..0000000000 --- a/internal/bloblang/plugins/bloblang.go +++ /dev/null @@ -1,53 +0,0 @@ -package plugins - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Register adds any native Bloblang methods and functions to the global sets -// that aren't defined within the query package. -func Register() error { - dynamicBloblangParserContext := parser.Context{ - Functions: query.AllFunctions.OnlyPure().NoMessage(), - Methods: query.AllMethods.OnlyPure(), - }.DisabledImports() - - return query.AllMethods.Add( - query.NewMethodSpec( - "bloblang", "Executes an argument Bloblang mapping on the target. This method can be used in order to execute dynamic mappings. Imports and functions that interact with the environment, such as `file` and `env`, or that access message information directly, such as `content` or `json`, are not enabled for dynamic Bloblang mappings.", - ).InCategory( - query.MethodCategoryParsing, "", - query.NewExampleSpec( - "", - "root.body = this.body.bloblang(this.mapping)", - `{"body":{"foo":"hello world"},"mapping":"root.foo = this.foo.uppercase()"}`, - `{"body":{"foo":"HELLO WORLD"}}`, - `{"body":{"foo":"hello world 2"},"mapping":"root.foo = this.foo.capitalize()"}`, - `{"body":{"foo":"Hello World 2"}}`, - ), - ).Beta().Param(query.ParamString("mapping", "The mapping to execute.")), - func(target query.Function, args *query.ParsedParams) (query.Function, error) { - mappingStr, err := args.FieldString("mapping") - if err != nil { - return nil, err - } - exec, parserErr := parser.ParseMapping(dynamicBloblangParserContext, mappingStr) - if parserErr != nil { - return nil, parserErr - } - return query.ClosureFunction("method bloblang", func(ctx query.FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - return exec.Exec(query.FunctionContext{ - Vars: map[string]any{}, - Maps: exec.Maps(), - MsgBatch: message.QuickBatch(nil), - }.WithValue(v)) - }, target.QueryTargets), nil - }, - ) -} diff --git a/internal/bloblang/query/arithmetic.go b/internal/bloblang/query/arithmetic.go deleted file mode 100644 index 176aa6b7b3..0000000000 --- a/internal/bloblang/query/arithmetic.go +++ /dev/null @@ -1,543 +0,0 @@ -package query - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -//------------------------------------------------------------------------------ - -// ArithmeticOperator represents an arithmetic operation that combines the -// results of two query functions. -type ArithmeticOperator int - -// All arithmetic operators. -const ( - ArithmeticAdd ArithmeticOperator = iota - ArithmeticSub - ArithmeticDiv - ArithmeticMul - ArithmeticMod - ArithmeticEq - ArithmeticNeq - ArithmeticGt - ArithmeticLt - ArithmeticGte - ArithmeticLte - ArithmeticAnd - ArithmeticOr - ArithmeticPipe -) - -func (o ArithmeticOperator) String() string { - switch o { - case ArithmeticAdd: - return "add" - case ArithmeticSub: - return "subtract" - case ArithmeticDiv: - return "divide" - case ArithmeticMul: - return "multiply" - case ArithmeticMod: - return "modulo" - case ArithmeticEq, ArithmeticNeq, ArithmeticGt, ArithmeticLt, ArithmeticGte, ArithmeticLte: - return "compare" - case ArithmeticAnd: - return "boolean and" - case ArithmeticOr: - return "boolean or" - case ArithmeticPipe: - return "coalesce" - } - return "" -} - -type arithmeticOpFunc[T any] func(lhs, rhs Function, l, r any) (T, error) - -func arithmeticFunc[T any](lhs, rhs Function, op arithmeticOpFunc[T]) (Function, error) { - annotation := rhs.Annotation() - - var litL, litR *Literal - var isLit bool - if litL, isLit = lhs.(*Literal); isLit { - if litR, isLit = rhs.(*Literal); isLit { - res, err := op(lhs, rhs, litL.Value, litR.Value) - if err != nil { - return nil, err - } - return NewLiteralFunction(annotation, res), nil - } - } - - return ClosureFunction(annotation, func(ctx FunctionContext) (any, error) { - var err error - var leftV, rightV any - if leftV, err = lhs.Exec(ctx); err == nil { - rightV, err = rhs.Exec(ctx) - } - if err != nil { - return nil, err - } - return op(lhs, rhs, leftV, rightV) - }, aggregateTargetPaths(lhs, rhs)), nil -} - -//------------------------------------------------------------------------------ - -// ErrDivideByZero occurs when an arithmetic operator is prevented from dividing -// a value by zero. -var ErrDivideByZero = errors.New("attempted to divide by zero") - -type ( - intArithmeticFunc[T any] func(left, right int64) (T, error) - uintArithmeticFunc[T any] func(left, right uint64) (T, error) - floatArithmeticFunc[T any] func(left, right float64) (T, error) -) - -// Takes two arithmetic funcs, one for integer values and one for float values -// and returns a generic arithmetic func. If both values can be represented as -// integers the integer func is called, otherwise the float func is called. -func numberDegradationFunc[T any]( - op ArithmeticOperator, - uiFn uintArithmeticFunc[T], - iFn intArithmeticFunc[T], - fFn floatArithmeticFunc[T], -) arithmeticOpFunc[T] { - return func(lhs, rhs Function, left, right any) (t T, err error) { - left = value.ISanitize(left) - right = value.ISanitize(right) - - // If either value is a float then we degrade into a float calculation. - if leftFloat, leftIsFloat := left.(float64); leftIsFloat { - rightFloat, err := value.IGetNumber(right) - if err != nil { - return t, NewTypeMismatch(op.String(), lhs, rhs, left, right) - } - return fFn(leftFloat, rightFloat) - } - if rightFloat, rightIsFloat := right.(float64); rightIsFloat { - leftFloat, err := value.IGetNumber(left) - if err != nil { - return t, NewTypeMismatch(op.String(), lhs, rhs, left, right) - } - return fFn(leftFloat, rightFloat) - } - - // If either value is a signed integer then we degrade into a signed int - // calculation. - if leftInt, leftIsInt := left.(int64); leftIsInt { - rightInt, err := value.IGetInt(right) - if err != nil { - return t, NewTypeMismatch(op.String(), lhs, rhs, left, right) - } - return iFn(leftInt, rightInt) - } - if rightInt, rightIsInt := right.(int64); rightIsInt { - leftInt, err := value.IGetInt(left) - if err != nil { - return t, NewTypeMismatch(op.String(), lhs, rhs, left, right) - } - return iFn(leftInt, rightInt) - } - - // Finally, if we can obtain an unsigned integer from both values we can - // calculate based on those values. - leftUInt, err := value.IGetUInt(left) - if err != nil { - return t, NewTypeMismatch(op.String(), lhs, rhs, left, right) - } - rightUInt, err := value.IGetUInt(right) - if err != nil { - return t, NewTypeMismatch(op.String(), lhs, rhs, left, right) - } - return uiFn(leftUInt, rightUInt) - } -} - -func prodOp(op ArithmeticOperator) (arithmeticOpFunc[any], bool) { - switch op { - case ArithmeticMul: - return numberDegradationFunc(op, - func(lhs, rhs uint64) (any, error) { - return lhs * rhs, nil - }, - func(lhs, rhs int64) (any, error) { - return lhs * rhs, nil - }, - func(lhs, rhs float64) (any, error) { - return lhs * rhs, nil - }, - ), true - case ArithmeticDiv: - // Only executes on float values. - return func(lFn, rFn Function, left, right any) (any, error) { - lhs, err := value.IGetNumber(left) - if err != nil { - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - rhs, err := value.IGetNumber(right) - if err != nil { - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - if rhs == 0 { - return nil, ErrFrom(ErrDivideByZero, rFn) - } - return lhs / rhs, nil - }, true - case ArithmeticMod: - // Only executes on integer values. - return func(lFn, rFn Function, left, right any) (any, error) { - lhs, err := value.IGetInt(left) - if err != nil { - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - rhs, err := value.IGetInt(right) - if err != nil { - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - if rhs == 0 { - return nil, ErrFrom(ErrDivideByZero, rFn) - } - return lhs % rhs, nil - }, true - } - return nil, false -} - -func sumOp(op ArithmeticOperator) (arithmeticOpFunc[any], bool) { - switch op { - case ArithmeticAdd: - numberAdd := numberDegradationFunc(op, - func(lhs, rhs uint64) (any, error) { - return lhs + rhs, nil - }, - func(left, right int64) (any, error) { - return left + right, nil - }, - func(left, right float64) (any, error) { - return left + right, nil - }, - ) - return func(lFn, rFn Function, left, right any) (any, error) { - switch left.(type) { - case float64, int, int64, uint64, json.Number: - return numberAdd(lFn, rFn, left, right) - case string, []byte: - lhs, err := value.IGetString(left) - if err != nil { - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - rhs, err := value.IGetString(right) - if err != nil { - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - return lhs + rhs, nil - } - return nil, NewTypeMismatch(op.String(), lFn, rFn, left, right) - }, true - case ArithmeticSub: - return numberDegradationFunc(op, - func(lhs, rhs uint64) (any, error) { - return lhs - rhs, nil - }, - func(lhs, rhs int64) (any, error) { - return lhs - rhs, nil - }, - func(lhs, rhs float64) (any, error) { - return lhs - rhs, nil - }, - ), true - } - return nil, false -} - -//------------------------------------------------------------------------------ - -func compareTFn[T int64 | uint64 | float64 | string](op ArithmeticOperator) func(lhs, rhs T) bool { - switch op { - case ArithmeticEq: - return func(lhs, rhs T) bool { - return lhs == rhs - } - case ArithmeticNeq: - return func(lhs, rhs T) bool { - return lhs != rhs - } - case ArithmeticGt: - return func(lhs, rhs T) bool { - return lhs > rhs - } - case ArithmeticGte: - return func(lhs, rhs T) bool { - return lhs >= rhs - } - case ArithmeticLt: - return func(lhs, rhs T) bool { - return lhs < rhs - } - case ArithmeticLte: - return func(lhs, rhs T) bool { - return lhs <= rhs - } - } - return nil -} - -func compareBoolFn(op ArithmeticOperator) func(lhs, rhs bool) bool { - switch op { - case ArithmeticEq: - return func(lhs, rhs bool) bool { - return lhs == rhs - } - case ArithmeticNeq: - return func(lhs, rhs bool) bool { - return lhs != rhs - } - } - return nil -} - -func compareGenericFn(op ArithmeticOperator) func(lhs, rhs any) bool { - switch op { - case ArithmeticEq: - return value.ICompare - case ArithmeticNeq: - return func(lhs, rhs any) bool { - return !value.ICompare(lhs, rhs) - } - } - return nil -} - -func compareOp(op ArithmeticOperator) (arithmeticOpFunc[bool], bool) { - if _, exists := map[ArithmeticOperator]struct{}{ - ArithmeticEq: {}, - ArithmeticNeq: {}, - ArithmeticGt: {}, - ArithmeticGte: {}, - ArithmeticLt: {}, - ArithmeticLte: {}, - }[op]; !exists { - return nil, false - } - - strOpFn := compareTFn[string](op) - - floatCompareFn := compareTFn[float64](op) - intCompareFn := compareTFn[int64](op) - uintCompareFn := compareTFn[uint64](op) - numOpFn := numberDegradationFunc(op, func(left, right uint64) (bool, error) { - return uintCompareFn(left, right), nil - }, func(left, right int64) (bool, error) { - return intCompareFn(left, right), nil - }, func(left, right float64) (bool, error) { - return floatCompareFn(left, right), nil - }) - - boolOpFn := compareBoolFn(op) - genericOpFn := compareGenericFn(op) - - return func(lFn, rFn Function, left, right any) (bool, error) { - switch lhs := value.RestrictForComparison(left).(type) { - case string: - if strOpFn == nil { - return false, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - rhs, err := value.IGetString(right) - if err != nil { - if genericOpFn == nil { - return false, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - return genericOpFn(lhs, value.RestrictForComparison(right)), nil - } - return strOpFn(lhs, rhs), nil - - case float64, int64, uint64: - if numOpFn == nil { - return false, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - b, err := numOpFn(lFn, rFn, left, right) - if err != nil { - if genericOpFn == nil { - return false, err - } - return genericOpFn(lhs, value.RestrictForComparison(right)), nil - } - return b, nil - - case bool: - if boolOpFn == nil { - return false, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - rhs, err := value.IGetBool(right) - if err != nil { - if genericOpFn == nil { - return false, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - return genericOpFn(lhs, value.RestrictForComparison(right)), nil - } - return boolOpFn(lhs, rhs), nil - - default: - if genericOpFn == nil { - return false, NewTypeMismatch(op.String(), lFn, rFn, left, right) - } - return genericOpFn(left, right), nil - } - }, true -} - -func boolOr(lhs, rhs Function) Function { - return ClosureFunction(rhs.Annotation(), func(ctx FunctionContext) (any, error) { - lhsV, err := lhs.Exec(ctx) - if err != nil { - return nil, err - } - b, err := value.IGetBool(lhsV) - if err != nil { - return nil, err - } - if b { - return true, nil - } - rhsV, err := rhs.Exec(ctx) - if err != nil { - return nil, err - } - if b, err = value.IGetBool(rhsV); err != nil { - return nil, err - } - return b, nil - }, aggregateTargetPaths(lhs, rhs)) -} - -func boolAnd(lhs, rhs Function) Function { - return ClosureFunction(rhs.Annotation(), func(ctx FunctionContext) (any, error) { - lhsV, err := lhs.Exec(ctx) - if err != nil { - return nil, err - } - b, err := value.IGetBool(lhsV) - if err != nil { - return nil, err - } - if !b { - return false, nil - } - rhsV, err := rhs.Exec(ctx) - if err != nil { - return nil, err - } - if b, err = value.IGetBool(rhsV); err != nil { - return nil, err - } - return b, nil - }, aggregateTargetPaths(lhs, rhs)) -} - -func coalesce(lhs, rhs Function) Function { - return ClosureFunction(rhs.Annotation(), func(ctx FunctionContext) (any, error) { - lhsV, err := lhs.Exec(ctx) - if err == nil && !value.IIsNull(lhsV) { - return lhsV, nil - } - return rhs.Exec(ctx) - }, aggregateTargetPaths(lhs, rhs)) -} - -// NewArithmeticExpression creates a single query function from a list of child -// functions and the arithmetic operator types that chain them together. The -// length of functions must be exactly one fewer than the length of operators. -func NewArithmeticExpression(fns []Function, ops []ArithmeticOperator) (Function, error) { - if len(fns) == 1 && len(ops) == 0 { - return fns[0], nil - } - if len(fns) != (len(ops) + 1) { - return nil, fmt.Errorf("mismatch of functions (%v) to arithmetic operators (%v)", len(fns), len(ops)) - } - - var err error - - // First pass to resolve division, multiplication and coalesce - fnsNew, opsNew := []Function{fns[0]}, []ArithmeticOperator{} - for i, op := range ops { - leftFn, rightFn := fnsNew[len(fnsNew)-1], fns[i+1] - if opFunc, isProd := prodOp(op); isProd { - if fnsNew[len(fnsNew)-1], err = arithmeticFunc(leftFn, rightFn, opFunc); err != nil { - return nil, err - } - } else if op == ArithmeticPipe { - fnsNew[len(fnsNew)-1] = coalesce(leftFn, rightFn) - } else { - fnsNew = append(fnsNew, rightFn) - opsNew = append(opsNew, op) - } - } - fns, ops = fnsNew, opsNew - if len(fns) == 1 { - return fns[0], nil - } - - // Second pass to resolve addition and subtraction - fnsNew, opsNew = []Function{fns[0]}, []ArithmeticOperator{} - for i, op := range ops { - leftFn, rightFn := fnsNew[len(fnsNew)-1], fns[i+1] - if opFunc, isSum := sumOp(op); isSum { - if fnsNew[len(fnsNew)-1], err = arithmeticFunc(leftFn, rightFn, opFunc); err != nil { - return nil, err - } - } else { - fnsNew = append(fnsNew, rightFn) - opsNew = append(opsNew, op) - } - } - fns, ops = fnsNew, opsNew - if len(fns) == 1 { - return fns[0], nil - } - - // Third pass for numerical comparison - fnsNew, opsNew = []Function{fns[0]}, []ArithmeticOperator{} - for i, op := range ops { - leftFn, rightFn := fnsNew[len(fnsNew)-1], fns[i+1] - if opFunc, isCompare := compareOp(op); isCompare { - if fnsNew[len(fnsNew)-1], err = arithmeticFunc(leftFn, rightFn, opFunc); err != nil { - return nil, err - } - } else { - fnsNew = append(fnsNew, rightFn) - opsNew = append(opsNew, op) - } - } - fns, ops = fnsNew, opsNew - if len(fns) == 1 { - return fns[0], nil - } - - // Fourth pass for boolean operators - fnsNew, opsNew = []Function{fns[0]}, []ArithmeticOperator{} - for i, op := range ops { - leftFn, rightFn := fnsNew[len(fnsNew)-1], fns[i+1] - switch op { - case ArithmeticAnd: - fnsNew[len(fnsNew)-1] = boolAnd(leftFn, rightFn) - case ArithmeticOr: - fnsNew[len(fnsNew)-1] = boolOr(leftFn, rightFn) - default: - fnsNew = append(fnsNew, rightFn) - opsNew = append(opsNew, op) - } - } - fns, ops = fnsNew, opsNew - if len(fns) == 1 { - return fns[0], nil - } - - return nil, fmt.Errorf("unresolved arithmetic operators (%v)", ops) -} - -//------------------------------------------------------------------------------ diff --git a/internal/bloblang/query/arithmetic_test.go b/internal/bloblang/query/arithmetic_test.go deleted file mode 100644 index 84131b98e1..0000000000 --- a/internal/bloblang/query/arithmetic_test.go +++ /dev/null @@ -1,921 +0,0 @@ -package query - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestArithmeticNumberDegradation(t *testing.T) { - fn := numberDegradationFunc(ArithmeticAdd, - func(left, right uint64) (any, error) { - return left / right, nil - }, - func(left, right int64) (any, error) { - return left / right, nil - }, - func(left, right float64) (any, error) { - return left / right, nil - }, - ) - - testCases := []struct { - name string - left any - right any - result any - err string - }{ - { - name: "two ints", - left: int64(12), - right: uint32(3), - result: int64(4), - }, - { - name: "two floats", - left: 8.0, - right: 3.2, - result: 2.5, - }, - { - name: "left is float", - left: 12.0, - right: uint32(3), - result: 4.0, - }, - { - name: "right is float", - left: int32(12), - right: 3.0, - result: 4.0, - }, - { - name: "both are int json", - left: json.Number("12"), - right: json.Number("3"), - result: int64(4), - }, - { - name: "both are float json", - left: json.Number("8.0"), - right: json.Number("3.2"), - result: 2.5, - }, - { - name: "left is int json", - left: json.Number("12"), - right: json.Number("3.0"), - result: 4.0, - }, - { - name: "right is int json", - left: json.Number("12.0"), - right: json.Number("3"), - result: 4.0, - }, - { - name: "left is invalid int", - left: "not a number", - right: 3, - err: "cannot add types string (from left) and number (from right)", - }, - { - name: "right is invalid int", - left: 3, - right: "not a number", - err: "cannot add types number (from left) and string (from right)", - }, - { - name: "left is invalid float", - left: "not a number", - right: 3.0, - err: "cannot add types string (from left) and number (from right)", - }, - { - name: "right is invalid float", - left: 3.0, - right: "not a number", - err: "cannot add types number (from left) and string (from right)", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - res, err := fn(NewLiteralFunction("left", test.left), NewLiteralFunction("right", test.right), test.left, test.right) - if test.err != "" { - assert.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - assert.Equal(t, test.result, res) - } - }) - } -} - -func TestArithmeticComparisons(t *testing.T) { - testCases := []struct { - name string - left any - right any - op ArithmeticOperator - result any - errContains string - }{ - { - name: "left int64 to right int64", - left: int64(1780921717355446273), - right: int64(1780921717355446272), - op: ArithmeticGt, - result: true, - }, - { - name: "left uint64 to right uint64", - left: uint64(18446744073709551613), - right: uint64(18446744073709551612), - op: ArithmeticGt, - result: true, - }, - { - name: "right null equal to int", - left: int64(12), - right: nil, - op: ArithmeticEq, - result: false, - }, - { - name: "right null not equal to int", - left: int64(12), - right: nil, - op: ArithmeticNeq, - result: true, - }, - { - name: "left null equal to int", - left: nil, - right: int64(10), - op: ArithmeticEq, - result: false, - }, - { - name: "left null not equal to int", - left: nil, - right: int64(12), - op: ArithmeticNeq, - result: true, - }, - { - name: "null equal to null", - left: nil, - right: nil, - op: ArithmeticEq, - result: true, - }, - { - name: "right null equal to string", - left: "foo", - right: nil, - op: ArithmeticEq, - result: false, - }, - { - name: "right null not equal to string", - left: "foo", - right: nil, - op: ArithmeticNeq, - result: true, - }, - { - name: "left null equal to string", - left: nil, - right: "foo", - op: ArithmeticEq, - result: false, - }, - { - name: "left null not equal to string", - left: nil, - right: "foo", - op: ArithmeticNeq, - result: true, - }, - { - name: "right null equal to bool", - left: true, - right: nil, - op: ArithmeticEq, - result: false, - }, - { - name: "right null not equal to bool", - left: true, - right: nil, - op: ArithmeticNeq, - result: true, - }, - { - name: "left null equal to bool", - left: nil, - right: true, - op: ArithmeticEq, - result: false, - }, - { - name: "left null not equal to bool", - left: nil, - right: true, - op: ArithmeticNeq, - result: true, - }, - { - name: "false equal true", - left: false, - right: true, - op: ArithmeticEq, - result: false, - }, - { - name: "false equal false", - left: false, - right: false, - op: ArithmeticEq, - result: true, - }, - { - name: "false not equal true", - left: false, - right: true, - op: ArithmeticNeq, - result: true, - }, - { - name: "false not equal false", - left: false, - right: false, - op: ArithmeticNeq, - result: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - fn, err := NewArithmeticExpression( - []Function{ - NewLiteralFunction("left", test.left), - NewLiteralFunction("right", test.right), - }, - []ArithmeticOperator{test.op}, - ) - require.NoError(t, err) - - res, err := fn.Exec(FunctionContext{}) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.result, res) - } - }) - } -} - -func TestArithmetic(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - arithmetic := func(fns []Function, ops []ArithmeticOperator) Function { - t.Helper() - fn, err := NewArithmeticExpression(fns, ops) - require.NoError(t, err) - return fn - } - function := func(name string, args ...any) Function { - t.Helper() - fn, err := InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - opaqueLit := func(v any) Function { - return ClosureFunction("foobar", func(ctx FunctionContext) (any, error) { - return v, nil - }, nil) - } - - tests := map[string]struct { - input Function - output any - err error - messages []easyMsg - index int - }{ - "compare string to int": { - input: arithmetic( - []Function{ - NewLiteralFunction("", "foo"), - NewLiteralFunction("", int64(5)), - }, - []ArithmeticOperator{ - ArithmeticNeq, - }, - ), - output: true, - }, - "dont divide by zero": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(5)), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticDiv, - }, - ), - err: errors.New("foobar: attempted to divide by zero"), - }, - "dont divide by zero 2": { - input: arithmetic( - []Function{ - NewLiteralFunction("left thing", int64(5)), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticMod, - }, - ), - err: errors.New("foobar: attempted to divide by zero"), - }, - "compare string to null": { - input: arithmetic( - []Function{ - NewLiteralFunction("", "foo"), - NewLiteralFunction("", nil), - }, - []ArithmeticOperator{ - ArithmeticNeq, - }, - ), - output: true, - }, - "compare string to int 2": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(5)), - NewLiteralFunction("", "foo"), - }, - []ArithmeticOperator{ - ArithmeticNeq, - }, - ), - output: true, - }, - "compare string to null 2": { - input: arithmetic( - []Function{ - NewLiteralFunction("", nil), - NewLiteralFunction("", "foo"), - }, - []ArithmeticOperator{ - ArithmeticNeq, - }, - ), - output: true, - }, - "add strings": { - input: arithmetic( - []Function{ - NewLiteralFunction("", "foo"), - NewLiteralFunction("", "bar"), - NewLiteralFunction("", "baz"), - }, - []ArithmeticOperator{ - ArithmeticAdd, - ArithmeticAdd, - }, - ), - output: `foobarbaz`, - }, - "comparisons with not": { - input: arithmetic( - []Function{ - Not(NewLiteralFunction("", true)), - NewLiteralFunction("", false), - }, - []ArithmeticOperator{ - ArithmeticOr, - }, - ), - output: false, - }, - "comparisons with not 2": { - input: arithmetic( - []Function{ - NewLiteralFunction("", false), - Not(NewLiteralFunction("", false)), - }, - []ArithmeticOperator{ - ArithmeticOr, - }, - ), - output: true, - }, - "mod two ints": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(5)), - NewLiteralFunction("", int64(2)), - }, - []ArithmeticOperator{ - ArithmeticMod, - }, - ), - output: int64(1), - }, - "number comparisons": { - input: arithmetic( - []Function{ - NewLiteralFunction("", 5.0), - NewLiteralFunction("", 5.0), - }, - []ArithmeticOperator{ - ArithmeticNeq, - }, - ), - output: false, - }, - "comparisons": { - input: arithmetic( - []Function{ - NewLiteralFunction("", true), - NewLiteralFunction("", false), - NewLiteralFunction("", true), - NewLiteralFunction("", false), - }, - []ArithmeticOperator{ - ArithmeticAnd, - ArithmeticOr, - ArithmeticAnd, - }, - ), - output: false, - }, - "comparisons 2": { - input: arithmetic( - []Function{ - NewLiteralFunction("", false), - NewLiteralFunction("", true), - NewLiteralFunction("", true), - NewLiteralFunction("", false), - }, - []ArithmeticOperator{ - ArithmeticOr, - ArithmeticAnd, - ArithmeticOr, - }, - ), - output: true, - }, - "comparisons 3": { - input: arithmetic( - []Function{ - NewLiteralFunction("", true), - NewLiteralFunction("", false), - NewLiteralFunction("", true), - }, - []ArithmeticOperator{ - ArithmeticOr, - ArithmeticAnd, - }, - ), - output: true, - }, - "err comparison": { - input: arithmetic( - []Function{ - NewLiteralFunction("", "not a number"), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - err: errors.New("cannot compare types string (from string literal) and number (from foobar)"), - }, - "numbers comparison": { - input: arithmetic( - []Function{ - NewLiteralFunction("", float64(15)), - NewLiteralFunction("", uint64(0)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - output: true, - }, - "numbers comparison 2": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(0)), - NewLiteralFunction("", float64(15)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - output: false, - }, - "numbers comparison 3": { - input: arithmetic( - []Function{ - NewLiteralFunction("", uint64(15)), - NewLiteralFunction("", int64(15)), - }, - []ArithmeticOperator{ - ArithmeticGte, - }, - ), - output: true, - }, - "numbers comparison 4": { - input: arithmetic( - []Function{ - NewLiteralFunction("", uint64(15)), - NewLiteralFunction("", float64(15)), - }, - []ArithmeticOperator{ - ArithmeticLte, - }, - ), - output: true, - }, - "numbers comparison 5": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(15)), - NewLiteralFunction("", float64(15)), - }, - []ArithmeticOperator{ - ArithmeticLt, - }, - ), - output: false, - }, - "and exit early": { - input: arithmetic( - []Function{ - NewLiteralFunction("", false), - arithmetic( - []Function{ - NewLiteralFunction("", "not a number"), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - }, - []ArithmeticOperator{ - ArithmeticAnd, - }, - ), - output: false, - }, - "and second exit early": { - input: arithmetic( - []Function{ - NewLiteralFunction("", true), - NewLiteralFunction("", false), - arithmetic( - []Function{ - NewLiteralFunction("", "not a number"), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - }, - []ArithmeticOperator{ - ArithmeticAnd, - ArithmeticAnd, - }, - ), - output: false, - }, - "or exit early": { - input: arithmetic( - []Function{ - NewLiteralFunction("", true), - arithmetic( - []Function{ - NewLiteralFunction("", "not a number"), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - }, - []ArithmeticOperator{ - ArithmeticOr, - }, - ), - output: true, - }, - "or second exit early": { - input: arithmetic( - []Function{ - NewLiteralFunction("", false), - NewLiteralFunction("", true), - arithmetic( - []Function{ - NewLiteralFunction("", "not a number"), - opaqueLit(int64(0)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - ), - }, - []ArithmeticOperator{ - ArithmeticOr, - ArithmeticOr, - }, - ), - output: true, - }, - "multiply and additions of ints 3": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(2)), - NewLiteralFunction("", int64(3)), - NewLiteralFunction("", float64(2)), - NewLiteralFunction("", uint64(1)), - NewLiteralFunction("", uint64(3)), - }, - []ArithmeticOperator{ - ArithmeticAdd, - ArithmeticMul, - ArithmeticAdd, - ArithmeticMul, - }, - ), - output: float64(11), - }, - "division and subtractions of ints": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(6)), - NewLiteralFunction("", int64(6)), - NewLiteralFunction("", float64(2)), - NewLiteralFunction("", uint64(1)), - }, - []ArithmeticOperator{ - ArithmeticSub, - ArithmeticDiv, - ArithmeticAdd, - }, - ), - output: float64(4), - }, - "coalesce json": { - input: arithmetic( - []Function{ - function("json", "foo"), - function("json", "bar"), - }, - []ArithmeticOperator{ - ArithmeticPipe, - }, - ), - output: `from_bar`, - messages: []easyMsg{ - {content: `{"foo":null,"bar":"from_bar"}`}, - }, - }, - "coalesce json 2": { - input: arithmetic( - []Function{ - function("json", "foo"), - NewLiteralFunction("", "not this"), - }, - []ArithmeticOperator{ - ArithmeticPipe, - }, - ), - output: `from_foo`, - messages: []easyMsg{ - {content: `{"foo":"from_foo"}`}, - }, - }, - "coalesce delete unmapped": { - input: arithmetic( - []Function{ - NewLiteralFunction("", value.Delete(nil)), - NewLiteralFunction("", value.Nothing(nil)), - NewLiteralFunction("", "this"), - }, - []ArithmeticOperator{ - ArithmeticPipe, - ArithmeticPipe, - }, - ), - output: `this`, - }, - "compare maps": { - input: arithmetic( - []Function{ - NewLiteralFunction("", map[string]any{ - "foo": "bar", - }), - NewLiteralFunction("", map[string]any{ - "foo": "bar", - }), - }, - []ArithmeticOperator{ - ArithmeticEq, - }, - ), - output: true, - }, - "compare maps neg": { - input: arithmetic( - []Function{ - NewLiteralFunction("", map[string]any{ - "foo": "bar", - }), - NewLiteralFunction("", map[string]any{ - "foo": "baz", - }), - }, - []ArithmeticOperator{ - ArithmeticNeq, - }, - ), - output: true, - }, - "compare slices": { - input: arithmetic( - []Function{ - NewLiteralFunction("", []any{"foo", 10}), - NewLiteralFunction("", []any{"foo", 10}), - }, - []ArithmeticOperator{ - ArithmeticEq, - }, - ), - output: true, - }, - "compare slices different lens": { - input: arithmetic( - []Function{ - NewLiteralFunction("", []any{"foo", 10, false}), - NewLiteralFunction("", []any{"foo", 10}), - }, - []ArithmeticOperator{ - ArithmeticEq, - }, - ), - output: false, - }, - "compare slices different values": { - input: arithmetic( - []Function{ - NewLiteralFunction("", []any{"foo", 11}), - NewLiteralFunction("", []any{"foo", 10}), - }, - []ArithmeticOperator{ - ArithmeticEq, - }, - ), - output: false, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - res, err := test.input.Exec(FunctionContext{ - Index: test.index, - MsgBatch: msg, - }) - if test.err != nil { - require.EqualError(t, err, test.err.Error()) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } - }) - } -} - -func TestArithmeticTargets(t *testing.T) { - arithmetic := func(fns []Function, ops []ArithmeticOperator) Function { - t.Helper() - fn, err := NewArithmeticExpression(fns, ops) - require.NoError(t, err) - return fn - } - function := func(name string, args ...any) Function { - t.Helper() - fn, err := InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - opaqueLit := func(v any) Function { - return ClosureFunction("", func(ctx FunctionContext) (any, error) { - return v, nil - }, nil) - } - - tests := map[string]struct { - input Function - output []TargetPath - }{ - "no targets": { - input: arithmetic( - []Function{ - NewLiteralFunction("", int64(5)), - opaqueLit("bar"), - }, - []ArithmeticOperator{ - ArithmeticAdd, - }, - ), - output: nil, - }, - "coalesced targets": { - input: arithmetic( - []Function{ - function("meta", "foo"), - function("var", "bar"), - }, - []ArithmeticOperator{ - ArithmeticPipe, - }, - ), - output: []TargetPath{ - NewTargetPath(TargetMetadata, "foo"), - NewTargetPath(TargetVariable, "bar"), - }, - }, - "mix of function types": { - input: arithmetic( - []Function{ - function("meta", "buz"), - NewLiteralFunction("", int64(5)), - function("json", "foo.bar"), - NewLiteralFunction("", "bar"), - NewFieldFunction("qux.quz"), - }, - []ArithmeticOperator{ - ArithmeticEq, - ArithmeticAdd, - ArithmeticMul, - ArithmeticGt, - }, - ), - output: []TargetPath{ - NewTargetPath(TargetMetadata, "buz"), - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "qux", "quz"), - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - _, res := test.input.QueryTargets(TargetsContext{ - Maps: map[string]Function{}, - }) - assert.Equal(t, test.output, res) - }) - } -} diff --git a/internal/bloblang/query/docs.go b/internal/bloblang/query/docs.go deleted file mode 100644 index 19d60080f6..0000000000 --- a/internal/bloblang/query/docs.go +++ /dev/null @@ -1,302 +0,0 @@ -package query - -// ExampleSpec provides a mapping example and some input/output results to -// display. -type ExampleSpec struct { - Mapping string `json:"mapping"` - Summary string `json:"summary"` - Results [][2]string `json:"results"` - SkipTesting bool `json:"skip_testing"` -} - -// NewExampleSpec creates a new example spec. -func NewExampleSpec(summary, mapping string, results ...string) ExampleSpec { - structuredResults := make([][2]string, 0, len(results)/2) - for i, res := range results { - if i%2 == 1 { - structuredResults = append(structuredResults, [2]string{results[i-1], res}) - } - } - return ExampleSpec{ - Mapping: mapping, - Summary: summary, - Results: structuredResults, - } -} - -// NewNotTestedExampleSpec creates a new not tested example spec. -func NewNotTestedExampleSpec(summary, mapping string, results ...string) ExampleSpec { - structuredResults := make([][2]string, 0, len(results)/2) - for i, res := range results { - if i%2 == 1 { - structuredResults = append(structuredResults, [2]string{results[i-1], res}) - } - } - return ExampleSpec{ - Mapping: mapping, - Summary: summary, - Results: structuredResults, - SkipTesting: true, - } -} - -//------------------------------------------------------------------------------ - -// Status of a function or method. -type Status string - -// Component statuses. -var ( - StatusStable Status = "stable" - StatusBeta Status = "beta" - StatusExperimental Status = "experimental" - StatusDeprecated Status = "deprecated" - StatusHidden Status = "hidden" -) - -//------------------------------------------------------------------------------ - -// Function categories. -var ( - FunctionCategoryGeneral = "General" - FunctionCategoryMessage = "Message Info" - FunctionCategoryEnvironment = "Environment" - FunctionCategoryDeprecated = "Deprecated" - FunctionCategoryPlugin = "Plugin" - FunctionCategoryFakeData = "Fake Data Generation" -) - -// FunctionSpec describes a Bloblang function. -type FunctionSpec struct { - // The release status of the function. - Status Status `json:"status"` - - // A category to place the function within. - Category string `json:"category"` - - // Name of the function (as it appears in config). - Name string `json:"name"` - - // Description of the functions purpose (in markdown). - Description string `json:"description,omitempty"` - - // Params defines the expected arguments of the function. - Params Params `json:"params"` - - // Examples shows general usage for the function. - Examples []ExampleSpec `json:"examples,omitempty"` - - // Impure indicates that a function accesses or interacts with the outer - // environment, and is therefore unsafe to execute in shared environments. - Impure bool `json:"impure"` - - // Version is the Benthos version this component was introduced. - Version string `json:"version,omitempty"` -} - -// NewFunctionSpec creates a new function spec. -func NewFunctionSpec(category, name, description string, examples ...ExampleSpec) FunctionSpec { - return FunctionSpec{ - Status: StatusStable, - Category: category, - Name: name, - Description: description, - Examples: examples, - Params: NewParams(), - } -} - -// Experimental flags the function as an experimental component. -func (s FunctionSpec) Experimental() FunctionSpec { - s.Status = StatusExperimental - return s -} - -// Beta flags the function as a beta component. -func (s FunctionSpec) Beta() FunctionSpec { - s.Status = StatusBeta - return s -} - -// AtVersion sets the Benthos version this component was introduced. -func (s FunctionSpec) AtVersion(v string) FunctionSpec { - s.Version = v - return s -} - -// MarkImpure flags the function as being impure, meaning it access or interacts -// with the environment. -func (s FunctionSpec) MarkImpure() FunctionSpec { - s.Impure = true - return s -} - -// Param adds a parameter to the function. -func (s FunctionSpec) Param(def ParamDefinition) FunctionSpec { - s.Params = s.Params.Add(def) - return s -} - -// NewDeprecatedFunctionSpec creates a new function spec that is deprecated. -func NewDeprecatedFunctionSpec(name, description string, examples ...ExampleSpec) FunctionSpec { - return FunctionSpec{ - Status: StatusDeprecated, - Category: FunctionCategoryDeprecated, - Name: name, - Description: description, - Examples: examples, - Params: NewParams(), - } -} - -// NewHiddenFunctionSpec creates a new function spec that is hidden from the docs. -func NewHiddenFunctionSpec(name string) FunctionSpec { - return FunctionSpec{ - Status: StatusHidden, - Name: name, - Params: NewParams(), - } -} - -//------------------------------------------------------------------------------ - -// Method categories. -var ( - MethodCategoryStrings = "String Manipulation" - MethodCategoryNumbers = "Number Manipulation" - MethodCategoryTime = "Timestamp Manipulation" - MethodCategoryRegexp = "Regular Expressions" - MethodCategoryEncoding = "Encoding and Encryption" - MethodCategoryCoercion = "Type Coercion" - MethodCategoryParsing = "Parsing" - MethodCategoryObjectAndArray = "Object & Array Manipulation" - MethodCategoryJWT = "JSON Web Tokens" - MethodCategoryGeoIP = "GeoIP" - MethodCategoryDeprecated = "Deprecated" - MethodCategoryPlugin = "Plugin" -) - -// MethodCatSpec describes how a method behaves in the context of a given -// category. -type MethodCatSpec struct { - Category string - Description string - Examples []ExampleSpec -} - -// MethodSpec describes a Bloblang method. -type MethodSpec struct { - // The release status of the function. - Status Status `json:"status"` - - // Name of the method (as it appears in config). - Name string `json:"name"` - - // Description of the method purpose (in markdown). - Description string `json:"description,omitempty"` - - // Params defines the expected arguments of the method. - Params Params `json:"params"` - - // Examples shows general usage for the method. - Examples []ExampleSpec `json:"examples,omitempty"` - - // Categories that this method fits within. - Categories []MethodCatSpec `json:"categories,omitempty"` - - // Impure indicates that a method accesses or interacts with the outer - // environment, and is therefore unsafe to execute in shared environments. - Impure bool `json:"impure"` - - // Version is the Benthos version this component was introduced. - Version string `json:"version,omitempty"` -} - -// NewMethodSpec creates a new method spec. -func NewMethodSpec(name, description string, examples ...ExampleSpec) MethodSpec { - return MethodSpec{ - Name: name, - Status: StatusStable, - Description: description, - Examples: examples, - Params: NewParams(), - } -} - -// NewDeprecatedMethodSpec creates a new method spec that is deprecated. The -// method will not appear in docs or searches but will still be usable in -// mappings. -func NewDeprecatedMethodSpec(name, description string, examples ...ExampleSpec) MethodSpec { - return MethodSpec{ - Name: name, - Status: StatusDeprecated, - Examples: examples, - Params: NewParams(), - } -} - -// NewHiddenMethodSpec creates a new method spec that is hidden from docs. -func NewHiddenMethodSpec(name string) MethodSpec { - return MethodSpec{ - Name: name, - Status: StatusHidden, - Params: NewParams(), - } -} - -// Experimental flags the method as an experimental component. -func (m MethodSpec) Experimental() MethodSpec { - m.Status = StatusExperimental - return m -} - -// Beta flags the function as a beta component. -func (m MethodSpec) Beta() MethodSpec { - m.Status = StatusBeta - return m -} - -// AtVersion sets the Benthos version this component was introduced. -func (m MethodSpec) AtVersion(v string) MethodSpec { - m.Version = v - return m -} - -// MarkImpure flags the method as being impure, meaning it access or interacts -// with the environment. -func (m MethodSpec) MarkImpure() MethodSpec { - m.Impure = true - return m -} - -// Param adds a parameter to the function. -func (m MethodSpec) Param(def ParamDefinition) MethodSpec { - m.Params = m.Params.Add(def) - return m -} - -// VariadicParams configures the method spec to allow variadic parameters. -func (m MethodSpec) VariadicParams() MethodSpec { - m.Params = VariadicParams() - return m -} - -// InCategory describes the methods behaviour in the context of a given -// category, methods can belong to multiple categories. For example, the -// `contains` method behaves differently in the object and array category versus -// the strings one, but belongs in both. -func (m MethodSpec) InCategory(category, description string, examples ...ExampleSpec) MethodSpec { - if m.Status == StatusDeprecated { - category = MethodCategoryDeprecated - } - - cats := make([]MethodCatSpec, 0, len(m.Categories)+1) - cats = append(cats, m.Categories...) - cats = append(cats, MethodCatSpec{ - Category: category, - Description: description, - Examples: examples, - }) - m.Categories = cats - return m -} diff --git a/internal/bloblang/query/errors.go b/internal/bloblang/query/errors.go deleted file mode 100644 index 375fa689d9..0000000000 --- a/internal/bloblang/query/errors.go +++ /dev/null @@ -1,86 +0,0 @@ -package query - -import ( - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// ErrNoContext is a common query error where a query attempts to reference a -// structured field when there is no context. -type ErrNoContext struct { - FieldName string -} - -// Error returns an attempt at a useful error message. -func (e ErrNoContext) Error() string { - if e.FieldName != "" { - return fmt.Sprintf("context was undefined, unable to reference `%v`", e.FieldName) - } - return "context was undefined" -} - -//------------------------------------------------------------------------------ - -type errFrom struct { - from Function - err error -} - -func (e *errFrom) Error() string { - return fmt.Sprintf("%v: %v", e.from.Annotation(), e.err) -} - -func (e *errFrom) Unwrap() error { - return e.err -} - -// ErrFrom wraps an error with the annotation of a function. -func ErrFrom(err error, from Function) error { - if err == nil { - return nil - } - if tErr, isTypeErr := err.(*value.TypeError); isTypeErr { - if tErr.From == "" { - tErr.From = from.Annotation() - } - return err - } - if _, isTypeMismatchErr := err.(*TypeMismatch); isTypeMismatchErr { - return err - } - var fErr *errFrom - if errors.As(err, &fErr) { - return err - } - return &errFrom{from: from, err: err} -} - -//------------------------------------------------------------------------------ - -// TypeMismatch represents an error where two values should be a comparable type -// but are not. -type TypeMismatch struct { - Lfn Function - Rfn Function - Left value.Type - Right value.Type - Operation string -} - -// Error implements the standard error interface. -func (t *TypeMismatch) Error() string { - return fmt.Sprintf("cannot %v types %v (from %v) and %v (from %v)", t.Operation, t.Left, t.Lfn.Annotation(), t.Right, t.Rfn.Annotation()) -} - -// NewTypeMismatch creates a new type mismatch error. -func NewTypeMismatch(operation string, lfn, rfn Function, left, right any) *TypeMismatch { - return &TypeMismatch{ - Lfn: lfn, - Rfn: rfn, - Left: value.ITypeOf(left), - Right: value.ITypeOf(right), - Operation: operation, - } -} diff --git a/internal/bloblang/query/errors_test.go b/internal/bloblang/query/errors_test.go deleted file mode 100644 index 2f6b662ad1..0000000000 --- a/internal/bloblang/query/errors_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package query - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestTypeError(t *testing.T) { - tests := map[string]struct { - from string - actual any - types []value.Type - exp string - }{ - "want nothing get str": { - actual: "hello world", - types: []value.Type{}, - exp: `unexpected value, got string ("hello world")`, - }, - "want num get str": { - actual: "hello world", - types: []value.Type{value.TNumber}, - exp: `expected number value, got string ("hello world")`, - }, - "want num get str from": { - from: "method foo", - actual: "hello world", - types: []value.Type{value.TNumber}, - exp: `expected number value, got string from method foo ("hello world")`, - }, - "want num or bool get str": { - actual: "hello world", - types: []value.Type{value.TNumber, value.TBool}, - exp: `expected number or bool value, got string ("hello world")`, - }, - "want num, bool or array get str": { - actual: "hello world", - types: []value.Type{value.TNumber, value.TBool, value.TArray}, - exp: `expected number, bool or array value, got string ("hello world")`, - }, - "want num get bytes": { - actual: []byte("foo"), - types: []value.Type{value.TNumber}, - exp: `expected number value, got bytes`, - }, - "want num get bool": { - actual: false, - types: []value.Type{value.TNumber}, - exp: `expected number value, got bool (false)`, - }, - "want num get array": { - actual: []any{"foo"}, - types: []value.Type{value.TNumber}, - exp: `expected number value, got array`, - }, - "want num get object": { - actual: map[string]any{"foo": "bar"}, - types: []value.Type{value.TNumber}, - exp: `expected number value, got object`, - }, - "want num get null": { - actual: nil, - types: []value.Type{value.TNumber}, - exp: `expected number value, got null`, - }, - "want num get delete": { - actual: value.Delete(nil), - types: []value.Type{value.TNumber}, - exp: `expected number value, got delete`, - }, - "want num get nothing": { - actual: value.Nothing(nil), - types: []value.Type{value.TNumber}, - exp: `expected number value, got nothing`, - }, - "want num get unknown": { - actual: []string{"unknown"}, - types: []value.Type{value.TNumber}, - exp: `expected number value, got unknown`, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - assert.Equal(t, test.exp, value.NewTypeErrorFrom(test.from, test.actual, test.types...).Error()) - }) - } -} - -func TestErrorFromError(t *testing.T) { - err := ErrFrom(errors.New("foo"), NewLiteralFunction("bar", nil)) - assert.EqualError(t, err, "bar: foo") - - err = ErrFrom(err, NewLiteralFunction("baz", nil)) - assert.EqualError(t, err, "bar: foo") - - err = ErrFrom(fmt.Errorf("wat: %w", err), NewLiteralFunction("baz", nil)) - assert.EqualError(t, err, "wat: bar: foo") - - err = ErrFrom(fmt.Errorf("wat: %w", value.NewTypeError("hello", value.TBool)), NewLiteralFunction("baz", nil)) - assert.EqualError(t, err, "baz: wat: expected bool value, got string (\"hello\")") -} - -func TestTypeMismatchError(t *testing.T) { - tests := map[string]struct { - operator string - left any - right any - exp string - }{ - "string to number": { - operator: "compare", - left: "foo", - right: 10.0, - exp: `cannot compare types string (from left thing) and number (from right thing)`, - }, - "bool to array": { - operator: "compare", - left: false, - right: []any{"foo"}, - exp: `cannot compare types bool (from left thing) and array (from right thing)`, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - assert.Equal(t, test.exp, NewTypeMismatch( - test.operator, - NewLiteralFunction("left thing", nil), - NewLiteralFunction("right thing", nil), - test.left, test.right, - ).Error()) - }) - } -} diff --git a/internal/bloblang/query/expression.go b/internal/bloblang/query/expression.go deleted file mode 100644 index 9a4266df85..0000000000 --- a/internal/bloblang/query/expression.go +++ /dev/null @@ -1,184 +0,0 @@ -package query - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// MatchCase represents a single match case of a match expression, where a case -// query is checked and, if true, the underlying query is executed and returned. -type MatchCase struct { - caseFn Function - queryFn Function -} - -// NewMatchCase creates a single match case of a match expression, where a case -// query is checked and, if true, the underlying query is executed and returned. -func NewMatchCase(caseFn, queryFn Function) MatchCase { - return MatchCase{ - caseFn: caseFn, - queryFn: queryFn, - } -} - -// NewMatchFunction takes a contextual mapping and a list of MatchCases, when -// the function is executed. -func NewMatchFunction(contextFn Function, cases ...MatchCase) Function { - if contextFn == nil { - contextFn = ClosureFunction("this", func(ctx FunctionContext) (any, error) { - var value any - if v := ctx.Value(); v != nil { - value = *v - } - return value, nil - }, nil) - } - return ClosureFunction("match expression", func(ctx FunctionContext) (any, error) { - ctxVal, err := contextFn.Exec(ctx) - if err != nil { - return nil, err - } - for i, c := range cases { - caseCtx := ctx.WithValue(ctxVal) - var caseVal any - if caseVal, err = c.caseFn.Exec(caseCtx); err != nil { - return nil, fmt.Errorf("failed to check match case %v: %w", i, err) - } - if matched, _ := caseVal.(bool); matched { - return c.queryFn.Exec(caseCtx) - } - } - return value.Nothing(nil), nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - contextCtx, contextTargets := contextFn.QueryTargets(ctx) - contextCtx = contextCtx.WithValues(contextTargets).WithValuesAsContext() - - var targets []TargetPath - for _, c := range cases { - _, caseTargets := c.caseFn.QueryTargets(contextCtx) - targets = append(targets, caseTargets...) - - // TODO: Include new current targets in returned context - _, queryTargets := c.queryFn.QueryTargets(contextCtx) - targets = append(targets, queryTargets...) - } - - targets = append(targets, contextTargets...) - return ctx, targets - }) -} - -// ElseIf represents an else-if block in an if expression. -type ElseIf struct { - QueryFn Function - MapFn Function -} - -// NewIfFunction creates a logical if expression from a query which should -// return a boolean value. If the returned boolean is true then the ifFn is -// executed and returned, otherwise elseFn is executed and returned. -func NewIfFunction(queryFn, ifFn Function, elseIfs []ElseIf, elseFn Function) Function { - allFns := []Function{ - queryFn, ifFn, elseFn, - } - for _, eIf := range elseIfs { - allFns = append(allFns, eIf.QueryFn, eIf.MapFn) - } - - return ClosureFunction("if expression", func(ctx FunctionContext) (any, error) { - queryVal, err := queryFn.Exec(ctx) - if err != nil { - return nil, fmt.Errorf("failed to check if condition: %w", err) - } - - queryRes, isBool := queryVal.(bool) - if !isBool { - if queryVal == nil { - // TODO V5: Remove this, we want to enforce only explicit - // boolean true/false. However, we realised that users had - // `if foo { ... }` in places where `foo` could potentially be a - // boolean or `null` and so we're allowing `null` to be an - // honorary `false` until v5. - queryRes = false - } else { - return nil, fmt.Errorf("%v resolved to a non-boolean value %v (%T)", queryFn.Annotation(), queryVal, queryVal) - } - } - if queryRes { - return ifFn.Exec(ctx) - } - - for i, eFn := range elseIfs { - queryVal, err = eFn.QueryFn.Exec(ctx) - if err != nil { - return nil, fmt.Errorf("failed to check if condition %v: %w", i+1, err) - } - queryRes, isBool := queryVal.(bool) - if !isBool { - if queryVal == nil { - // TODO V5: Remove this, as above - queryRes = false - } else { - return nil, fmt.Errorf("%v resolved to a non-boolean value %v (%T)", eFn.QueryFn.Annotation(), queryVal, queryVal) - } - } - if queryRes { - return eFn.MapFn.Exec(ctx) - } - } - - if elseFn != nil { - return elseFn.Exec(ctx) - } - return value.Nothing(nil), nil - }, aggregateTargetPaths(allFns...)) -} - -// NewNamedContextFunction wraps a function and ensures that when the function -// is executed with a new context the context is captured under a new name, with -// the "main" context left intact. -func NewNamedContextFunction(name string, fn Function) Function { - return &NamedContextFunction{name: name, fn: fn} -} - -// NamedContextFunction wraps a query function in a mechanism that captures the -// current context under an alias. -type NamedContextFunction struct { - name string - fn Function -} - -// Name returns the alias under which the context will be captured. -func (n *NamedContextFunction) Name() string { - return n.name -} - -// Annotation returns the annotation of the underlying function. -func (n *NamedContextFunction) Annotation() string { - return n.fn.Annotation() -} - -// Exec executes the wrapped query function with the context captured under an -// alias. -func (n *NamedContextFunction) Exec(ctx FunctionContext) (any, error) { - v, nextCtx := ctx.PopValue() - if v == nil { - return nil, fmt.Errorf("failed to capture context %v: %w", n.name, ErrNoContext{}) - } - if n.name != "_" { - nextCtx = nextCtx.WithNamedValue(n.name, *v) - } - return n.fn.Exec(nextCtx) -} - -// QueryTargets provides a summary of which fields the underlying query function -// targets. -func (n *NamedContextFunction) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - if n.name == "_" { - ctx = ctx.PopContext() - } else { - ctx = ctx.WithContextAsNamed(n.name) - } - return n.fn.QueryTargets(ctx) -} diff --git a/internal/bloblang/query/expression_test.go b/internal/bloblang/query/expression_test.go deleted file mode 100644 index 37dce680c3..0000000000 --- a/internal/bloblang/query/expression_test.go +++ /dev/null @@ -1,445 +0,0 @@ -package query - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestExpressions(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - mustFunc := func(fn Function, err error) Function { - t.Helper() - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - input Function - value *any - output any - errContains string - messages []easyMsg - index int - }{ - "if false": { - input: NewIfFunction( - mustFunc(NewArithmeticExpression( - []Function{ - NewLiteralFunction("", int64(10)), - NewLiteralFunction("", int64(20)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - )), - NewLiteralFunction("", "foo"), - nil, - nil, - ), - output: value.Nothing(nil), - }, - "if false else": { - input: NewIfFunction( - mustFunc(NewArithmeticExpression( - []Function{ - NewLiteralFunction("", int64(10)), - NewLiteralFunction("", int64(20)), - }, - []ArithmeticOperator{ - ArithmeticGt, - }, - )), - NewLiteralFunction("", "foo"), - nil, - NewLiteralFunction("", "bar"), - ), - output: "bar", - }, - "if else if": { - input: NewIfFunction( - NewLiteralFunction("", false), - NewLiteralFunction("", "foo"), - []ElseIf{ - { - QueryFn: NewLiteralFunction("", false), - MapFn: NewLiteralFunction("", "bar"), - }, - { - QueryFn: NewLiteralFunction("", true), - MapFn: NewLiteralFunction("", "baz"), - }, - }, - NewLiteralFunction("", "buz"), - ), - output: "baz", - }, - "if true": { - input: NewIfFunction( - mustFunc(NewArithmeticExpression( - []Function{ - NewLiteralFunction("", int64(10)), - NewLiteralFunction("", int64(20)), - }, - []ArithmeticOperator{ - ArithmeticLt, - }, - )), - NewLiteralFunction("", "foo"), - nil, - NewLiteralFunction("", value.Nothing(nil)), - ), - output: "foo", - }, - "if query fails": { - input: NewIfFunction( - NewVarFunction("doesnt exist"), - NewLiteralFunction("", "foo"), - nil, - NewLiteralFunction("", "bar"), - ), - errContains: "failed to check if condition: variables were undefined", - }, - "match context fails": { - input: NewMatchFunction( - NewVarFunction("doesnt exist"), - NewMatchCase(NewLiteralFunction("", true), NewLiteralFunction("", "foo")), - ), - errContains: "variables were undefined", - }, - "match first case fails": { - input: NewMatchFunction( - NewLiteralFunction("", "context"), - NewMatchCase(NewVarFunction("doesnt exist"), NewLiteralFunction("", "foo")), - NewMatchCase(NewLiteralFunction("", true), NewLiteralFunction("", "bar")), - ), - errContains: "failed to check match case 0: variables were undefined", - }, - "match second case fails": { - input: NewMatchFunction( - NewLiteralFunction("", "context"), - NewMatchCase(NewLiteralFunction("", true), NewLiteralFunction("", "bar")), - NewMatchCase(NewVarFunction("doesnt exist"), NewLiteralFunction("", "foo")), - ), - output: "bar", - }, - "match context empty": { - input: NewMatchFunction( - nil, - NewMatchCase(NewLiteralFunction("", true), NewFieldFunction("")), - ), - value: func() *any { - var v any = "context" - return &v - }(), - output: "context", - }, - "match context": { - input: NewMatchFunction( - NewLiteralFunction("", "context"), - NewMatchCase(NewLiteralFunction("", true), NewFieldFunction("")), - ), - output: "context", - }, - "match context all fail": { - input: NewMatchFunction( - NewLiteralFunction("", "context"), - NewMatchCase(NewLiteralFunction("", false), NewLiteralFunction("", "foo")), - NewMatchCase(NewLiteralFunction("", false), NewLiteralFunction("", "bar")), - ), - output: value.Nothing(nil), - }, - "named context map arithmetic": { - input: mustFunc(NewArithmeticExpression( - []Function{ - mustFunc(NewMapMethod( - NewFieldFunction("foo"), - NewNamedContextFunction("next", mustFunc(NewArithmeticExpression( - []Function{ - NewNamedContextFieldFunction("next", "bar"), - NewFieldFunction("baz"), - }, - []ArithmeticOperator{ - ArithmeticAdd, - }, - ))), - )), - NewFieldFunction("foo.bar"), - }, - []ArithmeticOperator{ - ArithmeticAdd, - }, - )), - value: func() *any { - var v any = map[string]any{ - "foo": map[string]any{ - "bar": 7, - }, - "baz": 23, - } - return &v - }(), - output: int64(37), - }, - "named context map to literal": { - input: mustFunc(NewMapMethod( - NewFieldFunction("foo"), - NewNamedContextFunction("next", NewArrayLiteral( - NewNamedContextFieldFunction("next", "bar"), - NewFieldFunction("baz"), - ).(Function)), - )), - value: func() *any { - var v any = map[string]any{ - "foo": map[string]any{ - "bar": 7, - }, - "baz": 23, - } - return &v - }(), - output: []any{7, 23}, - }, - "dropped context map to literal": { - input: mustFunc(NewMapMethod( - NewFieldFunction("foo"), - NewNamedContextFunction("_", NewArrayLiteral( - NewFieldFunction("baz"), - ).(Function)), - )), - value: func() *any { - var v any = map[string]any{ - "foo": map[string]any{ - "bar": 7, - }, - "baz": 23, - } - return &v - }(), - output: []any{23}, - }, - "non-boolean literal": { - input: NewIfFunction( - NewLiteralFunction("", "hello world"), - NewLiteralFunction("", "foo"), - nil, - NewLiteralFunction("", "buz"), - ), - errContains: "string literal resolved to a non-boolean value hello world", - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - for i := 0; i < 10; i++ { - res, err := test.input.Exec(FunctionContext{ - Maps: map[string]Function{}, - Index: test.index, - MsgBatch: msg, - }.WithValueFunc(func() *any { return test.value })) - if test.errContains != "" { - require.Error(t, err) - require.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - } - require.Equal(t, test.output, res) - } - - // Ensure nothing changed - for i, m := range test.messages { - doc, err := msg.Get(i).AsStructuredMut() - if err == nil { - msg.Get(i).SetStructured(doc) - } - assert.Equal(t, m.content, string(msg.Get(i).AsBytes())) - } - }) - } -} - -func TestExpressionTargets(t *testing.T) { - mustFunc := func(fn Function, err error) Function { - t.Helper() - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - input Function - output []TargetPath - }{ - "named context map arithmetic": { - input: mustFunc(NewMapMethod( - NewFieldFunction("foo"), - NewNamedContextFunction("next", mustFunc(NewArithmeticExpression( - []Function{ - NewNamedContextFieldFunction("next", "bar"), - NewFieldFunction("baz"), - }, - []ArithmeticOperator{ - ArithmeticAdd, - }, - ))), - )), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo"), - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "baz"), - }, - }, - "named context map to literal": { - input: mustFunc(NewMapMethod( - NewFieldFunction("foo"), - NewNamedContextFunction("next", NewArrayLiteral( - NewNamedContextFieldFunction("next", "bar"), - NewFieldFunction("baz"), - ).(Function)), - )), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo"), - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "baz"), - }, - }, - "dropped context map to literal": { - input: mustFunc(NewMapMethod( - NewFieldFunction("foo"), - NewNamedContextFunction("_", NewArrayLiteral( - NewFieldFunction("baz"), - ).(Function)), - )), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo"), - NewTargetPath(TargetValue, "baz"), - }, - }, - "if query path": { - input: NewIfFunction( - mustFunc(InitFunctionHelper("json", "foo.bar")), - NewLiteralFunction("", "foo"), - nil, - mustFunc(InitFunctionHelper("var", "baz")), - ), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetVariable, "baz"), - }, - }, - "if else if query path": { - input: NewIfFunction( - mustFunc(InitFunctionHelper("json", "foo.bar")), - NewLiteralFunction("", "foo"), - []ElseIf{ - { - QueryFn: mustFunc(InitFunctionHelper("json", "foo.baz")), - MapFn: NewLiteralFunction("", "bar"), - }, - { - QueryFn: mustFunc(InitFunctionHelper("meta", "buz")), - MapFn: mustFunc(InitFunctionHelper("meta", "quz")), - }, - }, - mustFunc(InitFunctionHelper("var", "baz")), - ), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetVariable, "baz"), - NewTargetPath(TargetValue, "foo", "baz"), - NewTargetPath(TargetMetadata, "buz"), - NewTargetPath(TargetMetadata, "quz"), - }, - }, - "match empty context": { - input: NewMatchFunction( - nil, - NewMatchCase( - NewFieldFunction("foo"), - NewFieldFunction("bar"), - ), - NewMatchCase( - NewFieldFunction("baz"), - NewFieldFunction("buz"), - ), - ), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo"), - NewTargetPath(TargetValue, "bar"), - NewTargetPath(TargetValue, "baz"), - NewTargetPath(TargetValue, "buz"), - }, - }, - "match meta context": { - input: NewMatchFunction( - mustFunc(InitFunctionHelper("meta", "foo")), - NewMatchCase( - mustFunc(InitFunctionHelper("meta", "bar")), - NewFieldFunction("baz"), - ), - NewMatchCase( - NewFieldFunction("buz"), - NewLiteralFunction("", "qux"), - ), - ), - output: []TargetPath{ - NewTargetPath(TargetMetadata, "bar"), - NewTargetPath(TargetMetadata, "foo", "baz"), - NewTargetPath(TargetMetadata, "foo", "buz"), - NewTargetPath(TargetMetadata, "foo"), - }, - }, - "match value context": { - input: NewMatchFunction( - NewFieldFunction("foo.bar"), - NewMatchCase( - mustFunc(InitFunctionHelper("meta", "bar")), - NewFieldFunction("baz.buz"), - ), - NewMatchCase( - NewFieldFunction("qux.quz"), - NewLiteralFunction("", "quack"), - ), - ), - output: []TargetPath{ - NewTargetPath(TargetMetadata, "bar"), - NewTargetPath(TargetValue, "foo", "bar", "baz", "buz"), - NewTargetPath(TargetValue, "foo", "bar", "qux", "quz"), - NewTargetPath(TargetValue, "foo", "bar"), - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - _, res := test.input.QueryTargets(TargetsContext{ - Maps: map[string]Function{}, - }) - assert.Equal(t, test.output, res) - }) - } -} diff --git a/internal/bloblang/query/function_ctor.go b/internal/bloblang/query/function_ctor.go deleted file mode 100644 index 3de64c0c00..0000000000 --- a/internal/bloblang/query/function_ctor.go +++ /dev/null @@ -1,67 +0,0 @@ -package query - -// Function takes a set of contextual arguments and returns the result of the -// query. -type Function interface { - // Execute this function for a message of a batch. - Exec(ctx FunctionContext) (any, error) - - // Annotation returns a string token to identify the function within error - // messages. The returned token is not valid Bloblang and cannot be used to - // recreate the function. - Annotation() string - - // MarshalString returns a string representation of the function that could - // be parsed back into the exact equivalent function. The result will be - // normalized, which means the representation may not match the original - // input from the user. - // MarshalString() string - - // Returns a list of targets that this function attempts (or may attempt) to - // access. A context must be provided that describes the current execution - // context that this function will be executed upon, which is how it is able - // to determine the full path and origin of values that it targets. - // - // A new context is returned which should be provided to methods that act - // upon this function when querying their own targets. - QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) -} - -// FunctionCtor constructs a new function from input arguments. -type FunctionCtor func(args *ParsedParams) (Function, error) - -//------------------------------------------------------------------------------ - -// ClosureFunction allows you to define a Function using closures, this is a -// convenient constructor for function implementations that don't manage complex -// state. -func ClosureFunction( - annotation string, - exec func(ctx FunctionContext) (any, error), - queryTargets func(ctx TargetsContext) (TargetsContext, []TargetPath), -) Function { - if queryTargets == nil { - queryTargets = func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil } - } - return closureFunction{annotation: annotation, exec: exec, queryTargets: queryTargets} -} - -type closureFunction struct { - annotation string - exec func(ctx FunctionContext) (any, error) - queryTargets func(ctx TargetsContext) (TargetsContext, []TargetPath) -} - -func (f closureFunction) Annotation() string { - return f.annotation -} - -// Exec the underlying closure. -func (f closureFunction) Exec(ctx FunctionContext) (any, error) { - return f.exec(ctx) -} - -// QueryTargets returns nothing. -func (f closureFunction) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - return f.queryTargets(ctx) -} diff --git a/internal/bloblang/query/function_set.go b/internal/bloblang/query/function_set.go deleted file mode 100644 index 399923bb02..0000000000 --- a/internal/bloblang/query/function_set.go +++ /dev/null @@ -1,203 +0,0 @@ -package query - -import ( - "errors" - "fmt" - "regexp" - "sort" -) - -type functionDetails struct { - ctor FunctionCtor - spec FunctionSpec -} - -// FunctionSet contains an explicit set of functions to be available in a -// Bloblang query. -type FunctionSet struct { - disableCtors bool - functions map[string]functionDetails -} - -// NewFunctionSet creates a function set without any functions in it. -func NewFunctionSet() *FunctionSet { - return &FunctionSet{ - functions: map[string]functionDetails{}, - } -} - -var ( - nameRegexpRaw = `^[a-z0-9]+(_[a-z0-9]+)*$` - nameRegexp = regexp.MustCompile(nameRegexpRaw) -) - -// Add a new function to this set by providing a spec (name and documentation), -// a constructor to be called for each instantiation of the function, and -// information regarding the arguments of the function. -func (f *FunctionSet) Add(spec FunctionSpec, ctor FunctionCtor) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("function name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if err := spec.Params.validate(); err != nil { - return err - } - f.functions[spec.Name] = functionDetails{ctor: ctor, spec: spec} - return nil -} - -// Docs returns a slice of function specs, which document each function. -func (f *FunctionSet) Docs() []FunctionSpec { - specSlice := make([]FunctionSpec, 0, len(f.functions)) - for _, v := range f.functions { - specSlice = append(specSlice, v.spec) - } - sort.Slice(specSlice, func(i, j int) bool { - return specSlice[i].Name < specSlice[j].Name - }) - return specSlice -} - -// Params attempts to obtain an argument specification for a given function. -func (f *FunctionSet) Params(name string) (Params, error) { - details, exists := f.functions[name] - if !exists { - return VariadicParams(), badFunctionErr(name) - } - return details.spec.Params, nil -} - -// Init attempts to initialize a function of the set by name and zero or more -// arguments. -func (f *FunctionSet) Init(name string, args *ParsedParams) (Function, error) { - details, exists := f.functions[name] - if !exists { - return nil, badFunctionErr(name) - } - if f.disableCtors { - return disabledFunction(name), nil - } - return wrapCtorWithDynamicArgs(name, args, details.ctor) -} - -// Without creates a clone of the function set that can be mutated in isolation, -// where a variadic list of functions will be excluded from the set. -func (f *FunctionSet) Without(functions ...string) *FunctionSet { - excludeMap := make(map[string]struct{}, len(functions)) - for _, k := range functions { - excludeMap[k] = struct{}{} - } - - details := make(map[string]functionDetails, len(f.functions)) - for k, v := range f.functions { - if _, exists := excludeMap[k]; !exists { - details[k] = v - } - } - return &FunctionSet{disableCtors: f.disableCtors, functions: details} -} - -// OnlyPure creates a clone of the function set that can be mutated in -// isolation, where all impure functions are removed. -func (f *FunctionSet) OnlyPure() *FunctionSet { - var excludes []string - for _, v := range f.functions { - if v.spec.Impure { - excludes = append(excludes, v.spec.Name) - } - } - return f.Without(excludes...) -} - -// NoMessage creates a clone of the function set that can be mutated in -// isolation, where all message access functions are removed. -func (f *FunctionSet) NoMessage() *FunctionSet { - var excludes []string - for _, v := range f.functions { - if v.spec.Category == FunctionCategoryMessage { - excludes = append(excludes, v.spec.Name) - } - } - return f.Without(excludes...) -} - -// Deactivated returns a version of the function set where constructors are -// disabled, allowing mappings to be parsed and validated but not executed. -// -// The underlying register of functions is shared with the target set, and -// therefore functions added to this set will also be added to the still -// activated set. Use the Without method (with empty args if applicable) in -// order to create a deep copy of the set that is independent of the source. -func (f *FunctionSet) Deactivated() *FunctionSet { - newSet := *f - newSet.disableCtors = true - return &newSet -} - -//------------------------------------------------------------------------------ - -// AllFunctions is a set containing every single function declared by this -// package, and any globally declared plugin methods. -var AllFunctions = NewFunctionSet() - -func registerFunction(spec FunctionSpec, ctor FunctionCtor) struct{} { - if err := AllFunctions.Add(spec, func(args *ParsedParams) (Function, error) { - return ctor(args) - }); err != nil { - panic(err) - } - return struct{}{} -} - -func registerSimpleFunction(spec FunctionSpec, fn func(ctx FunctionContext) (any, error)) struct{} { - if err := AllFunctions.Add(spec, func(*ParsedParams) (Function, error) { - return ClosureFunction("function "+spec.Name, fn, nil), nil - }); err != nil { - panic(err) - } - return struct{}{} -} - -// InitFunctionHelper attempts to initialise a function by its name and a list -// of arguments, this is convenient for writing tests. -func InitFunctionHelper(name string, args ...any) (Function, error) { - details, ok := AllFunctions.functions[name] - if !ok { - return nil, badFunctionErr(name) - } - parsedArgs, err := details.spec.Params.PopulateNameless(args...) - if err != nil { - return nil, err - } - return AllFunctions.Init(name, parsedArgs) -} - -// FunctionDocs returns a slice of specs, one for each function. -func FunctionDocs() []FunctionSpec { - return AllFunctions.Docs() -} - -//------------------------------------------------------------------------------ - -func disabledFunction(name string) Function { - return ClosureFunction("function "+name, func(ctx FunctionContext) (any, error) { - return nil, errors.New("this function has been disabled") - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }) -} - -func wrapCtorWithDynamicArgs(name string, args *ParsedParams, fn FunctionCtor) (Function, error) { - fns := args.dynamic() - if len(fns) == 0 { - return fn(args) - } - return ClosureFunction("function "+name, func(ctx FunctionContext) (any, error) { - newArgs, err := args.ResolveDynamic(ctx) - if err != nil { - return nil, fmt.Errorf("function '%s': %w", name, err) - } - dynFunc, err := fn(newArgs) - if err != nil { - return nil, err - } - return dynFunc.Exec(ctx) - }, aggregateTargetPaths(fns...)), nil -} diff --git a/internal/bloblang/query/function_set_test.go b/internal/bloblang/query/function_set_test.go deleted file mode 100644 index efb66ac2d3..0000000000 --- a/internal/bloblang/query/function_set_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package query - -import ( - "errors" - "sort" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func listFunctions(f *FunctionSet) []string { - functionNames := make([]string, 0, len(f.functions)) - for k := range f.functions { - functionNames = append(functionNames, k) - } - sort.Strings(functionNames) - return functionNames -} - -func TestFunctionSetWithout(t *testing.T) { - setOne := AllFunctions - setTwo := setOne.Without("uuid_v4") - - assert.Contains(t, listFunctions(setOne), "uuid_v4") - assert.NotContains(t, listFunctions(setTwo), "uuid_v4") - - _, err := setOne.Init("uuid_v4", nil) - assert.NoError(t, err) - - _, err = setTwo.Init("uuid_v4", nil) - assert.EqualError(t, err, "unrecognised function 'uuid_v4'") - - _, err = setTwo.Init("timestamp_unix", nil) - assert.NoError(t, err) -} - -func TestFunctionSetOnlyPure(t *testing.T) { - setOne := AllFunctions - require.NoError(t, setOne.Add(NewFunctionSpec("meower", "meow", "Does impure meows.").MarkImpure(), func(args *ParsedParams) (Function, error) { - return nil, errors.New("not implemented") - })) - setTwo := setOne.OnlyPure() - - assert.Contains(t, listFunctions(setOne), "meow") - assert.NotContains(t, listFunctions(setTwo), "meow") -} - -func TestFunctionSetDeactivated(t *testing.T) { - setOne := AllFunctions.Without() - setTwo := setOne.Deactivated() - - customErr := errors.New("custom error") - - spec := NewFunctionSpec(FunctionCategoryGeneral, "meow", "").Param(ParamString("val1", "")) - require.NoError(t, setOne.Add(spec, func(args *ParsedParams) (Function, error) { - return ClosureFunction("", func(ctx FunctionContext) (any, error) { - return nil, customErr - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }), nil - })) - - assert.Contains(t, listFunctions(setOne), "meow") - assert.Contains(t, listFunctions(setTwo), "meow") - - goodArgs, err := spec.Params.PopulateNameless("hello") - require.NoError(t, err) - - fnOne, err := setOne.Init("meow", goodArgs) - require.NoError(t, err) - - fnTwo, err := setTwo.Init("meow", goodArgs) - require.NoError(t, err) - - _, err = fnOne.Exec(FunctionContext{}) - assert.Equal(t, customErr, err) - - _, err = fnTwo.Exec(FunctionContext{}) - assert.EqualError(t, err, "this function has been disabled") -} - -func TestFunctionResolveParamError(t *testing.T) { - setOne := AllFunctions.Without() - - spec := NewFunctionSpec(FunctionCategoryGeneral, "meow", "").Param(ParamString("val1", "")) - require.NoError(t, setOne.Add(spec, func(args *ParsedParams) (Function, error) { - return ClosureFunction("", func(ctx FunctionContext) (any, error) { - return "ok", nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }), nil - })) - - assert.Contains(t, listFunctions(setOne), "meow") - - badArgs, err := spec.Params.PopulateNameless(NewFieldFunction("doc.foo")) - require.NoError(t, err) - - fnOne, err := setOne.Init("meow", badArgs) - require.NoError(t, err) - - _, err = fnOne.Exec(FunctionContext{}) - assert.EqualError(t, err, "function 'meow': failed to extract input arg 'val1': context was undefined, unable to reference `doc.foo`") -} - -func TestFunctionBadName(t *testing.T) { - testCases := map[string]string{ - "!no": "function name '!no' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo__bar": "function name 'foo__bar' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "-foo-bar": "function name '-foo-bar' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo-bar-": "function name 'foo-bar-' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "": "function name '' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo-bar": "function name 'foo-bar' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo-bar_baz": "function name 'foo-bar_baz' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "FOO": "function name 'FOO' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foobarbaz": "", - "foobarbaz89": "", - "foo_bar_baz": "", - "fo1_ba2_ba3": "", - } - - for k, v := range testCases { - t.Run(k, func(t *testing.T) { - setOne := AllFunctions.Without() - err := setOne.Add(NewFunctionSpec(FunctionCategoryGeneral, k, ""), nil) - if v != "" { - assert.EqualError(t, err, v) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/internal/bloblang/query/functions.go b/internal/bloblang/query/functions.go deleted file mode 100644 index 7100915bce..0000000000 --- a/internal/bloblang/query/functions.go +++ /dev/null @@ -1,952 +0,0 @@ -package query - -import ( - "context" - "errors" - "fmt" - "math" - "math/rand" - "sync" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/gofrs/uuid" - gonanoid "github.com/matoous/go-nanoid/v2" - "github.com/segmentio/ksuid" - - "github.com/benthosdev/benthos/v4/internal/tracing" - "github.com/benthosdev/benthos/v4/internal/value" -) - -type fieldFunction struct { - namedContext string - fromRoot bool - path []string -} - -func (f *fieldFunction) expand(path ...string) *fieldFunction { - newFn := *f - newPath := make([]string, 0, len(f.path)+len(path)) - newPath = append(newPath, f.path...) - newPath = append(newPath, path...) - newFn.path = newPath - return &newFn -} - -func (f *fieldFunction) Annotation() string { - path := f.namedContext - if f.fromRoot { - path = "root" - } else if path == "" { - path = "this" - } - if len(f.path) > 0 { - path = path + "." + SliceToDotPath(f.path...) - } - return "field `" + path + "`" -} - -func (f *fieldFunction) Exec(ctx FunctionContext) (any, error) { - var target any - if f.fromRoot { - if ctx.NewValue == nil { - return nil, errors.New("unable to reference `root` from this context") - } - target = *ctx.NewValue - } else if f.namedContext == "" { - v := ctx.Value() - if v == nil { - var fieldName string - if len(f.path) > 0 { - fieldName = SliceToDotPath(f.path...) - } - return nil, ErrNoContext{ - FieldName: fieldName, - } - } - target = *v - } else { - var ok bool - if target, ok = ctx.NamedValue(f.namedContext); !ok { - return ctx, fmt.Errorf("named context %v was not found", f.namedContext) - } - } - if len(f.path) == 0 { - return target, nil - } - return gabs.Wrap(target).S(f.path...).Data(), nil -} - -func (f *fieldFunction) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - var basePaths []TargetPath - if f.fromRoot { - basePaths = []TargetPath{NewTargetPath(TargetRoot)} - } else if f.namedContext == "" { - if basePaths = ctx.MainContext(); len(basePaths) == 0 { - basePaths = []TargetPath{NewTargetPath(TargetValue)} - } - } else { - basePaths = ctx.NamedContext(f.namedContext) - } - paths := make([]TargetPath, len(basePaths)) - for i, p := range basePaths { - paths[i] = p - paths[i].Path = append(paths[i].Path, f.path...) - } - ctx = ctx.WithValues(paths) - return ctx, paths -} - -func (f *fieldFunction) Close(ctx context.Context) error { - return nil -} - -// NewNamedContextFieldFunction creates a query function that attempts to -// return a field from a named context. -func NewNamedContextFieldFunction(namedContext, pathStr string) Function { - var path []string - if pathStr != "" { - path = gabs.DotPathToSlice(pathStr) - } - return &fieldFunction{namedContext: namedContext, fromRoot: false, path: path} -} - -// NewFieldFunction creates a query function that returns a field from the -// current context. -func NewFieldFunction(pathStr string) Function { - var path []string - if pathStr != "" { - path = gabs.DotPathToSlice(pathStr) - } - return &fieldFunction{ - path: path, - } -} - -// NewRootFieldFunction creates a query function that returns a field from the -// root context. -func NewRootFieldFunction(pathStr string) Function { - var path []string - if pathStr != "" { - path = gabs.DotPathToSlice(pathStr) - } - return &fieldFunction{ - fromRoot: true, - path: path, - } -} - -//------------------------------------------------------------------------------ - -// Literal wraps a static value and returns it for each invocation of the -// function. -type Literal struct { - annotation string - Value any -} - -// Annotation returns a token identifier of the function. -func (l *Literal) Annotation() string { - if l.annotation == "" { - return string(value.ITypeOf(l.Value)) + " literal" - } - return l.annotation -} - -// Exec returns a literal value. -func (l *Literal) Exec(ctx FunctionContext) (any, error) { - return l.Value, nil -} - -// QueryTargets returns nothing. -func (l *Literal) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - return ctx, nil -} - -// Close does nothing. -func (l *Literal) Close(ctx context.Context) error { - return nil -} - -// String returns a string representation of the literal function. -func (l *Literal) String() string { - return fmt.Sprintf("%v", l.Value) -} - -// NewLiteralFunction creates a query function that returns a static, literal -// value. -func NewLiteralFunction(annotation string, v any) *Literal { - return &Literal{annotation: annotation, Value: v} -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "batch_index", - "Returns the index of the mapped message within a batch. This is useful for applying maps only on certain messages of a batch.", - NewExampleSpec("", - `root = if batch_index() > 0 { deleted() }`, - ), - ), - func(ctx FunctionContext) (any, error) { - return int64(ctx.Index), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "batch_size", - "Returns the size of the message batch.", - NewExampleSpec("", - `root.foo = batch_size()`, - ), - ), - func(ctx FunctionContext) (any, error) { - return int64(ctx.MsgBatch.Len()), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "content", - "Returns the full raw contents of the mapping target message as a byte array. When mapping to a JSON field the value should be encoded using the method xref:guides:bloblang/methods.adoc#encode[`encode`], or cast to a string directly using the method xref:guides:bloblang/methods.adoc#string[`string`], otherwise it will be base64 encoded by default.", - NewExampleSpec("", - `root.doc = content().string()`, - `{"foo":"bar"}`, - `{"doc":"{\"foo\":\"bar\"}"}`, - ), - ), - func(ctx FunctionContext) (any, error) { - return ctx.MsgBatch.Get(ctx.Index).AsBytes(), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "tracing_span", - "Provides the message tracing span xref:components:tracers/about.adoc[(created via Open Telemetry APIs)] as an object serialized via text map formatting. The returned value will be `null` if the message does not have a span.", - NewExampleSpec("", - `root.headers.traceparent = tracing_span().traceparent`, - `{"some_stuff":"just can't be explained by science"}`, - `{"headers":{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}}`, - ), - ).Experimental(), - func(fCtx FunctionContext) (any, error) { - span := tracing.GetSpan(fCtx.MsgBatch.Get(fCtx.Index)) - if span == nil { - return nil, nil - } - return span.TextMap() - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "tracing_id", - "Provides the message trace id. The returned value will be zeroed if the message does not contain a span.", - NewExampleSpec("", - `meta trace_id = tracing_id()`, - ), - ).Experimental(), - func(fCtx FunctionContext) (any, error) { - traceID := tracing.GetTraceID(fCtx.MsgBatch.Get(fCtx.Index)) - return traceID, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewDeprecatedFunctionSpec( - "count", - "The `count` function is a counter starting at 1 which increments after each time it is called. Count takes an argument which is an identifier for the counter, allowing you to specify multiple unique counters in your configuration.", - NewExampleSpec("", - `root = this -root.id = count("bloblang_function_example")`, - `{"message":"foo"}`, - `{"id":1,"message":"foo"}`, - `{"message":"bar"}`, - `{"id":2,"message":"bar"}`, - ), - ).Param(ParamString("name", "An identifier for the counter.")).MarkImpure(), - countFunction, -) - -var ( - counters = map[string]int64{} - countersMux = &sync.Mutex{} -) - -func countFunction(args *ParsedParams) (Function, error) { - name, err := args.FieldString("name") - if err != nil { - return nil, err - } - return ClosureFunction("function count", func(ctx FunctionContext) (any, error) { - countersMux.Lock() - defer countersMux.Unlock() - - var count int64 - var exists bool - - if count, exists = counters[name]; exists { - count++ - } else { - count = 1 - } - counters[name] = count - - return count, nil - }, nil), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "deleted", - "A function that returns a result indicating that the mapping target should be deleted. Deleting, also known as dropping, messages will result in them being acknowledged as successfully processed to inputs in a Benthos pipeline. For more information about error handling patterns read xref:configuration:error_handling.adoc[].", - NewExampleSpec("", - `root = this -root.bar = deleted()`, - `{"bar":"bar_value","baz":"baz_value","foo":"foo value"}`, - `{"baz":"baz_value","foo":"foo value"}`, - ), - NewExampleSpec( - "Since the result is a value it can be used to do things like remove elements of an array within `map_each`.", - `root.new_nums = this.nums.map_each(num -> if num < 10 { deleted() } else { num - 10 })`, - `{"nums":[3,11,4,17]}`, - `{"new_nums":[1,7]}`, - ), - ), - func(*ParsedParams) (Function, error) { - return NewLiteralFunction("delete", value.Delete(nil)), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "error", - "If an error has occurred during the processing of a message this function returns the reported cause of the error as a string, otherwise `null`. For more information about error handling patterns read xref:configuration:error_handling.adoc[].", - NewExampleSpec("", - `root.doc.error = error()`, - ), - ), - func(ctx FunctionContext) (any, error) { - v := ctx.MsgBatch.Get(ctx.Index).ErrorGet() - if v != nil { - return v.Error(), nil - } - return nil, nil - }, -) - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryMessage, "errored", - "Returns a boolean value indicating whether an error has occurred during the processing of a message. For more information about error handling patterns read xref:configuration:error_handling.adoc[].", - NewExampleSpec("", - `root.doc.status = if errored() { 400 } else { 200 }`, - ), - ), - func(ctx FunctionContext) (any, error) { - return ctx.MsgBatch.Get(ctx.Index).ErrorGet() != nil, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "range", - "The `range` function creates an array of integers following a range between a start, stop and optional step integer argument. If the step argument is omitted then it defaults to 1. A negative step can be provided as long as stop < start.", - NewExampleSpec("", - `root.a = range(0, 10) -root.b = range(start: 0, stop: this.max, step: 2) # Using named params -root.c = range(0, -this.max, -2)`, - `{"max":10}`, - `{"a":[0,1,2,3,4,5,6,7,8,9],"b":[0,2,4,6,8],"c":[0,-2,-4,-6,-8]}`, - ), - ). - Param(ParamInt64("start", "The start value.")). - Param(ParamInt64("stop", "The stop value.")). - Param(ParamInt64("step", "The step value.").Default(1)), - rangeFunction, -) - -func rangeFunction(args *ParsedParams) (Function, error) { - start, err := args.FieldInt64("start") - if err != nil { - return nil, err - } - stop, err := args.FieldInt64("stop") - if err != nil { - return nil, err - } - step, err := args.FieldInt64("step") - if err != nil { - return nil, err - } - if step == 0 { - return nil, errors.New("step must be greater than or less than 0") - } - if step < 0 { - if stop > start { - return nil, fmt.Errorf("with negative step arg stop (%v) must be <= start (%v)", stop, start) - } - } else if start >= stop { - return nil, fmt.Errorf("with positive step arg start (%v) must be < stop (%v)", start, stop) - } - r := make([]any, (stop-start)/step) - for i := 0; i < len(r); i++ { - r[i] = start + step*int64(i) - } - return ClosureFunction("function range", func(ctx FunctionContext) (any, error) { - return r, nil - }, nil), nil -} - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryMessage, "json", - "Returns the value of a field within a JSON message located by a [dot path][field_paths] argument. This function always targets the entire source JSON document regardless of the mapping context.", - NewExampleSpec("", - `root.mapped = json("foo.bar")`, - `{"foo":{"bar":"hello world"}}`, - `{"mapped":"hello world"}`, - ), - NewExampleSpec( - "The path argument is optional and if omitted the entire JSON payload is returned.", - `root.doc = json()`, - `{"foo":{"bar":"hello world"}}`, - `{"doc":{"foo":{"bar":"hello world"}}}`, - ), - ).Param(ParamString("path", "An optional [dot path][field_paths] identifying a field to obtain.").Default("")), - jsonFunction, -) - -func jsonFunction(args *ParsedParams) (Function, error) { - path, err := args.FieldString("path") - if err != nil { - return nil, err - } - var argPath []string - if path != "" { - argPath = gabs.DotPathToSlice(path) - } - return ClosureFunction("json path `"+SliceToDotPath(argPath...)+"`", func(ctx FunctionContext) (any, error) { - jPart, err := ctx.MsgBatch.Get(ctx.Index).AsStructured() - if err != nil { - return nil, err - } - gPart := gabs.Wrap(jPart) - if len(argPath) > 0 { - gPart = gPart.Search(argPath...) - } - return value.ISanitize(gPart.Data()), nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetValue, argPath...), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil -} - -//------------------------------------------------------------------------------ - -// NewMetaFunction creates a new function for obtaining a metadata value. -func NewMetaFunction(key string) Function { - if key != "" { - return ClosureFunction("meta field "+key, func(ctx FunctionContext) (any, error) { - if ctx.NewMeta == nil { - return nil, errors.New("metadata cannot be queried in this context") - } - v, exists := ctx.NewMeta.MetaGetMut(key) - if !exists { - return nil, nil - } - return v, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata, key), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }) - } - return ClosureFunction("meta object", func(ctx FunctionContext) (any, error) { - if ctx.NewMeta == nil { - return nil, errors.New("metadata cannot be queried in this context") - } - kvs := map[string]any{} - _ = ctx.NewMeta.MetaIterMut(func(k string, v any) error { - kvs[k] = v - return nil - }) - return kvs, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }) -} - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryMessage, "metadata", - "Returns the value of a metadata key from the input message, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map, in order to query metadata mutations made within a mapping use the xref:guides:bloblang/about.adoc#metadata[`@` operator]. This function supports extracting metadata from other messages of a batch with the `from` method.", - NewExampleSpec("", `root.topic = metadata("kafka_topic")`), - NewExampleSpec( - "The key parameter is optional and if omitted the entire metadata contents are returned as an object.", - `root.all_metadata = metadata()`, - ), - ).Param(ParamString("key", "An optional key of a metadata value to obtain.").Default("")), - func(args *ParsedParams) (Function, error) { - key, err := args.FieldString("key") - if err != nil { - return nil, err - } - if key != "" { - return ClosureFunction("metadata field "+key, func(ctx FunctionContext) (any, error) { - v, exists := ctx.MsgBatch.Get(ctx.Index).MetaGetMut(key) - if !exists { - return nil, nil - } - return v, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata, key), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil - } - return ClosureFunction("metadata object", func(ctx FunctionContext) (any, error) { - kvs := map[string]any{} - _ = ctx.MsgBatch.Get(ctx.Index).MetaIterMut(func(k string, v any) error { - kvs[k] = v - return nil - }) - return kvs, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil - }, -) - -var _ = registerFunction( - NewDeprecatedFunctionSpec( - "meta", - "Returns the value of a metadata key from the input message as a string, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map. In order to query metadata mutations made within a mapping use the <>. This function supports extracting metadata from other messages of a batch with the `from` method.", - NewExampleSpec("", - `root.topic = meta("kafka_topic")`, - `root.topic = meta("nope") | meta("also nope") | "default"`, - ), - NewExampleSpec( - "The key parameter is optional and if omitted the entire metadata contents are returned as an object.", - `root.all_metadata = meta()`, - ), - ).Param(ParamString("key", "An optional key of a metadata value to obtain.").Default("")), - func(args *ParsedParams) (Function, error) { - key, err := args.FieldString("key") - if err != nil { - return nil, err - } - if key != "" { - return ClosureFunction("meta field "+key, func(ctx FunctionContext) (any, error) { - v := ctx.MsgBatch.Get(ctx.Index).MetaGetStr(key) - if v == "" { - return nil, nil - } - return v, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata, key), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil - } - return ClosureFunction("meta object", func(ctx FunctionContext) (any, error) { - kvs := map[string]any{} - _ = ctx.MsgBatch.Get(ctx.Index).MetaIterStr(func(k, v string) error { - kvs[k] = v - return nil - }) - return kvs, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewDeprecatedFunctionSpec( - "root_meta", - "Returns the value of a metadata key from the new message being created as a string, or `null` if the key does not exist. Changes made to metadata during a mapping will be reflected by this function.", - NewExampleSpec("", - `root.topic = root_meta("kafka_topic")`, - `root.topic = root_meta("nope") | root_meta("also nope") | "default"`, - ), - NewExampleSpec( - "The key parameter is optional and if omitted the entire metadata contents are returned as an object.", - `root.all_metadata = root_meta()`, - ), - ).Param(ParamString("key", "An optional key of a metadata value to obtain.").Default("")), - func(args *ParsedParams) (Function, error) { - key, err := args.FieldString("key") - if err != nil { - return nil, err - } - if key != "" { - return ClosureFunction("root_meta field "+key, func(ctx FunctionContext) (any, error) { - if ctx.NewMeta == nil { - return nil, errors.New("root metadata cannot be queried in this context") - } - v := ctx.NewMeta.MetaGetStr(key) - if v == "" { - return nil, nil - } - return v, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata, key), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil - } - return ClosureFunction("root_meta object", func(ctx FunctionContext) (any, error) { - if ctx.NewMeta == nil { - return nil, errors.New("root metadata cannot be queried in this context") - } - kvs := map[string]any{} - _ = ctx.NewMeta.MetaIterStr(func(k, v string) error { - kvs[k] = v - return nil - }) - return kvs, nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetMetadata), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewHiddenFunctionSpec("nothing"), - func(*ParsedParams) (Function, error) { - return NewLiteralFunction("nothing", value.Nothing(nil)), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "random_int", ` -Generates a non-negative pseudo-random 64-bit integer. An optional integer argument can be provided in order to seed the random number generator. - -Optional `+"`min` and `max`"+` arguments can be provided in order to only generate numbers within a range. Neither of these parameters can be set via a dynamic expression (i.e. from values taken from mapped data). Instead, for dynamic ranges extract a min and max manually using a modulo operator (`+"`random_int() % a + b`"+`).`, - NewExampleSpec("", - `root.first = random_int() -root.second = random_int(1) -root.third = random_int(max:20) -root.fourth = random_int(min:10, max:20) -root.fifth = random_int(timestamp_unix_nano(), 5, 20) -root.sixth = random_int(seed:timestamp_unix_nano(), max:20) -`, - ), - NewExampleSpec("It is possible to specify a dynamic seed argument, in which case the argument will only be resolved once during the lifetime of the mapping.", - `root.first = random_int(timestamp_unix_nano())`, - `root.second = random_int(timestamp_unix_nano(), 5, 20)`, - ), - ). - Param(ParamQuery( - "seed", - "A seed to use, if a query is provided it will only be resolved once during the lifetime of the mapping.", - true, - ).Default(NewLiteralFunction("", 0))). - Param(ParamInt64("min", "The minimum value the random generated number will have. The default value is 0.").Default(0).DisableDynamic()). - Param(ParamInt64("max", fmt.Sprintf("The maximum value the random generated number will have. The default value is %d (math.MaxInt64 - 1).", uint64(math.MaxInt64-1))).Default(int64(math.MaxInt64-1)).DisableDynamic()), - randomIntFunction, -) - -func randomIntFunction(args *ParsedParams) (Function, error) { - seedFn, err := args.FieldQuery("seed") - if err != nil { - return nil, err - } - min, err := args.FieldInt64("min") - if err != nil { - return nil, err - } - max, err := args.FieldInt64("max") - if err != nil { - return nil, err - } - if min < 0 { - return nil, fmt.Errorf("min (%d) must be a positive number", min) - } - if max < min { - return nil, fmt.Errorf("min (%d) must be smaller or equal than max (%d)", min, max) - } - if max == math.MaxInt64 { - return nil, fmt.Errorf("max must be smaller than the max allowed for an int64 (%d)", uint64(math.MaxInt64)) - } - var randMut sync.Mutex - var r *rand.Rand - - return ClosureFunction("function random_int", func(ctx FunctionContext) (any, error) { - randMut.Lock() - defer randMut.Unlock() - - if r == nil { - seedI, err := seedFn.Exec(ctx) - if err != nil { - return nil, fmt.Errorf("failed to seed random number generator: %v", err) - } - - seed, err := value.IToInt(seedI) - if err != nil { - return nil, fmt.Errorf("failed to seed random number generator: %v", err) - } - - r = rand.New(rand.NewSource(seed)) - } - // Int63n generates a random number within a half-open interval [0,n) - v := r.Int63n(max-min+1) + min - return v, nil - }, nil), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryEnvironment, "now", - "Returns the current timestamp as a string in RFC 3339 format with the local timezone. Use the method `ts_format` in order to change the format and timezone.", - NewExampleSpec("", - `root.received_at = now()`, - ), - NewExampleSpec("", - `root.received_at = now().ts_format("Mon Jan 2 15:04:05 -0700 MST 2006", "UTC")`, - ), - ), - func(args *ParsedParams) (Function, error) { - return ClosureFunction("function now", func(_ FunctionContext) (any, error) { - return time.Now().Format(time.RFC3339Nano), nil - }, nil), nil - }, -) - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryEnvironment, "timestamp_unix", - "Returns the current unix timestamp in seconds.", - NewExampleSpec("", - `root.received_at = timestamp_unix()`, - ), - ), - func(_ FunctionContext) (any, error) { - return time.Now().Unix(), nil - }, -) - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryEnvironment, "timestamp_unix_milli", - "Returns the current unix timestamp in milliseconds.", - NewExampleSpec("", - `root.received_at = timestamp_unix_milli()`, - ), - ), - func(_ FunctionContext) (any, error) { - return time.Now().UnixMilli(), nil - }, -) - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryEnvironment, "timestamp_unix_micro", - "Returns the current unix timestamp in microseconds.", - NewExampleSpec("", - `root.received_at = timestamp_unix_micro()`, - ), - ), - func(_ FunctionContext) (any, error) { - return time.Now().UnixMicro(), nil - }, -) - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryEnvironment, "timestamp_unix_nano", - "Returns the current unix timestamp in nanoseconds.", - NewExampleSpec("", - `root.received_at = timestamp_unix_nano()`, - ), - ), - func(_ FunctionContext) (any, error) { - return time.Now().UnixNano(), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "throw", - "Throws an error similar to a regular mapping error. This is useful for abandoning a mapping entirely given certain conditions.", - NewExampleSpec("", - `root.doc.type = match { - this.exists("header.id") => "foo" - this.exists("body.data") => "bar" - _ => throw("unknown type") -} -root.doc.contents = (this.body.content | this.thing.body)`, - `{"header":{"id":"first"},"thing":{"body":"hello world"}}`, - `{"doc":{"contents":"hello world","type":"foo"}}`, - `{"nothing":"matches"}`, - `Error("failed assignment (line 1): unknown type")`, - ), - ).Param(ParamString("why", "A string explanation for why an error was thrown, this will be added to the resulting error message.")), - func(args *ParsedParams) (Function, error) { - msg, err := args.FieldString("why") - if err != nil { - return nil, err - } - return ClosureFunction("function throw", func(_ FunctionContext) (any, error) { - return nil, errors.New(msg) - }, nil), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "uuid_v4", - "Generates a new RFC-4122 UUID each time it is invoked and prints a string representation.", - NewExampleSpec("", `root.id = uuid_v4()`), - ), - func(_ FunctionContext) (any, error) { - u4, err := uuid.NewV4() - if err != nil { - panic(err) - } - return u4.String(), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "nanoid", - "Generates a new nanoid each time it is invoked and prints a string representation.", - NewExampleSpec("", `root.id = nanoid()`), - NewExampleSpec("It is possible to specify an optional length parameter.", `root.id = nanoid(54)`), - NewExampleSpec("It is also possible to specify an optional custom alphabet after the length parameter.", `root.id = nanoid(54, "abcde")`), - ). - Param(ParamInt64("length", "An optional length.").Optional()). - Param(ParamString("alphabet", "An optional custom alphabet to use for generating IDs. When specified the field `length` must also be present.").Optional()), - nanoidFunction, -) - -func nanoidFunction(args *ParsedParams) (Function, error) { - lenArg, err := args.FieldOptionalInt64("length") - if err != nil { - return nil, err - } - alphabetArg, err := args.FieldOptionalString("alphabet") - if err != nil { - return nil, err - } - if alphabetArg != nil && lenArg == nil { - return nil, errors.New("field length must be specified when an alphabet is specified") - } - return ClosureFunction("function nanoid", func(ctx FunctionContext) (any, error) { - if alphabetArg != nil { - return gonanoid.Generate(*alphabetArg, int(*lenArg)) - } - if lenArg != nil { - return gonanoid.New(int(*lenArg)) - } - return gonanoid.New() - }, nil), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleFunction( - NewFunctionSpec( - FunctionCategoryGeneral, "ksuid", - "Generates a new ksuid each time it is invoked and prints a string representation.", - NewExampleSpec("", `root.id = ksuid()`), - ), - func(_ FunctionContext) (any, error) { - return ksuid.New().String(), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerFunction( - NewHiddenFunctionSpec("var").Param(ParamString("name", "The name of the target variable.")), - func(args *ParsedParams) (Function, error) { - name, err := args.FieldString("name") - if err != nil { - return nil, err - } - return NewVarFunction(name), nil - }, -) - -// NewVarFunction creates a new variable function. -func NewVarFunction(name string) Function { - return ClosureFunction("variable "+name, func(ctx FunctionContext) (any, error) { - if ctx.Vars == nil { - return nil, errors.New("variables were undefined") - } - if res, ok := ctx.Vars[name]; ok { - return res, nil - } - return nil, fmt.Errorf("variable '%v' undefined", name) - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - paths := []TargetPath{ - NewTargetPath(TargetVariable, name), - } - ctx = ctx.WithValues(paths) - return ctx, paths - }) -} diff --git a/internal/bloblang/query/functions_test.go b/internal/bloblang/query/functions_test.go deleted file mode 100644 index 2647e5c9db..0000000000 --- a/internal/bloblang/query/functions_test.go +++ /dev/null @@ -1,493 +0,0 @@ -package query - -import ( - "fmt" - "math" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestFunctions(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - mustFunc := func(name string, args ...any) Function { - t.Helper() - fn, err := InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - - mustMethod := func(fn Function, name string, args ...any) Function { - t.Helper() - fn, err := InitMethodHelper(name, fn, args...) - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - input Function - output any - err string - messages []easyMsg - vars map[string]any - index int - }{ - "check throw function 1": { - input: mustFunc("throw", "foo"), - err: "foo", - }, - "check throw function 2": { - input: mustMethod( - mustFunc("throw", "foo"), - "catch", "bar", - ), - output: "bar", - }, - "check var function": { - input: mustMethod( - mustFunc("var", "foo"), - "uppercase", - ), - output: "FOOBAR", - vars: map[string]any{ - "foo": "foobar", - }, - }, - "check var function object": { - input: mustMethod( - mustMethod( - mustFunc("var", "foo"), - "get", "bar", - ), - "uppercase", - ), - output: "FOOBAR", - vars: map[string]any{ - "foo": map[string]any{ - "bar": "foobar", - }, - }, - }, - "check var function error": { - input: mustFunc("var", "foo"), - vars: map[string]any{}, - err: `variable 'foo' undefined`, - }, - "check meta function object": { - input: mustFunc("meta", "foo"), - output: "foobar", - messages: []easyMsg{ - {content: "", meta: map[string]any{ - "foo": "foobar", - }}, - }, - }, - "check meta function error": { - input: mustFunc("meta", "foo"), - vars: map[string]any{}, - output: nil, - }, - "check metadata function object": { - input: mustFunc("meta", "foo"), - output: "foobar", - messages: []easyMsg{ - {content: "", meta: map[string]any{ - "foo": "foobar", - }}, - }, - }, - "check source_metadata function object": { - input: mustFunc("meta", "foo"), - output: "foobar", - messages: []easyMsg{ - {content: "", meta: map[string]any{ - "foo": "foobar", - }}, - }, - }, - "check range start > end": { - input: mustFunc("range", mustFunc("var", "start"), 0, 1), - vars: map[string]any{ - "start": 10, - }, - err: `with positive step arg start (10) must be < stop (0)`, - }, - "check range start >= end": { - input: mustFunc("range", mustFunc("var", "start"), 10, 1), - vars: map[string]any{ - "start": 10, - }, - err: `with positive step arg start (10) must be < stop (10)`, - }, - "check range zero step": { - input: mustFunc("range", mustFunc("var", "start"), 100, 0), - vars: map[string]any{ - "start": 10, - }, - err: `step must be greater than or less than 0`, - }, - "check range start < end neg step": { - input: mustFunc("range", mustFunc("var", "start"), 100, -1), - vars: map[string]any{ - "start": 10, - }, - err: `with negative step arg stop (100) must be <= start (10)`, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - for i := 0; i < 10; i++ { - res, err := test.input.Exec(FunctionContext{ - Vars: test.vars, - Maps: map[string]Function{}, - Index: test.index, - MsgBatch: msg, - NewMeta: msg.Get(test.index), - }) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - } - assert.Equal(t, test.output, res) - } - - // Ensure nothing changed - for i, m := range test.messages { - doc, err := msg.Get(i).AsStructuredMut() - if err == nil { - msg.Get(i).SetStructured(doc) - } - assert.Equal(t, m.content, string(msg.Get(i).AsBytes())) - } - }) - } -} - -func TestFunctionTargets(t *testing.T) { - function := func(name string, args ...any) Function { - t.Helper() - fn, err := InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - - tests := []struct { - input Function - output []TargetPath - }{ - { - input: function("throw", "foo"), - }, - { - input: function("json", "foo.bar.baz"), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar", "baz"), - }, - }, - { - input: NewFieldFunction("foo.bar.baz"), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar", "baz"), - }, - }, - { - input: function("meta", "foo"), - output: []TargetPath{ - NewTargetPath(TargetMetadata, "foo"), - }, - }, - { - input: function("var", "foo"), - output: []TargetPath{ - NewTargetPath(TargetVariable, "foo"), - }, - }, - } - - for i, test := range tests { - test := test - t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { - t.Parallel() - - _, res := test.input.QueryTargets(TargetsContext{ - Maps: map[string]Function{}, - }) - assert.Equal(t, test.output, res) - }) - } -} - -func TestNanoidFunction(t *testing.T) { - e, err := InitFunctionHelper("nanoid") - require.NoError(t, err) - - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - assert.NotEmpty(t, res) -} - -func TestNanoidFunctionLength(t *testing.T) { - e, err := InitFunctionHelper("nanoid", int64(54)) - require.NoError(t, err) - - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - assert.Len(t, res, 54) -} - -func TestNanoidFunctionAlphabet(t *testing.T) { - e, err := InitFunctionHelper("nanoid", int64(1), "a") - require.NoError(t, err) - - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "a", res) -} - -func TestKsuidFunction(t *testing.T) { - e, err := InitFunctionHelper("ksuid") - require.NoError(t, err) - - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - assert.NotEmpty(t, res) -} - -func TestRandomInt(t *testing.T) { - e, err := InitFunctionHelper("random_int") - require.NoError(t, err) - - tallies := map[int64]int64{} - - for i := 0; i < 100; i++ { - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - require.IsType(t, int64(0), res) - tallies[res.(int64)]++ - } - - // Can't prove it ain't random, but I can kick up a fuss if something - // stinks. - assert.GreaterOrEqual(t, len(tallies), 20) - for _, v := range tallies { - assert.LessOrEqual(t, v, int64(10)) - } - - // Create a new random_int function with a different seed - e, err = InitFunctionHelper("random_int", 10) - require.NoError(t, err) - - secondTallies := map[int64]int64{} - - for i := 0; i < 100; i++ { - res, err := e.Exec(FunctionContext{}.WithValue(i)) - require.NoError(t, err) - require.IsType(t, int64(0), res) - secondTallies[res.(int64)]++ - } - - assert.NotEqual(t, tallies, secondTallies) - assert.GreaterOrEqual(t, len(secondTallies), 20) - for _, v := range secondTallies { - assert.LessOrEqual(t, v, int64(10)) - } -} - -func TestRandomIntDynamic(t *testing.T) { - idFn := NewFieldFunction("") - - e, err := InitFunctionHelper("random_int", idFn) - require.NoError(t, err) - - tallies := map[int64]int64{} - - for i := 0; i < 100; i++ { - res, err := e.Exec(FunctionContext{}.WithValue(i)) - require.NoError(t, err) - require.IsType(t, int64(0), res) - tallies[res.(int64)]++ - } - - // Can't prove it ain't random, but I can kick up a fuss if something - // stinks. - assert.GreaterOrEqual(t, len(tallies), 20) - for _, v := range tallies { - assert.LessOrEqual(t, v, int64(10)) - } - - // Create a new random_int function and feed the same values in - e, err = InitFunctionHelper("random_int", idFn) - require.NoError(t, err) - - secondTallies := map[int64]int64{} - - for i := 0; i < 100; i++ { - res, err := e.Exec(FunctionContext{}.WithValue(i)) - require.NoError(t, err) - require.IsType(t, int64(0), res) - secondTallies[res.(int64)]++ - } - - assert.Equal(t, tallies, secondTallies) - - // Create a new random_int function and feed the first value in the same, - // but following values are different. - e, err = InitFunctionHelper("random_int", idFn) - require.NoError(t, err) - - thirdTallies := map[int64]int64{} - - for i := 0; i < 100; i++ { - input := i - if input > 0 { - input += 10 - } - res, err := e.Exec(FunctionContext{}.WithValue(input)) - require.NoError(t, err) - require.IsType(t, int64(0), res) - thirdTallies[res.(int64)]++ - } - - assert.Equal(t, tallies, thirdTallies) -} - -func TestRandomIntMilliDynamicParallel(t *testing.T) { - tsFn, err := InitFunctionHelper("timestamp_unix_milli") - require.NoError(t, err) - - e, err := InitFunctionHelper("random_int", tsFn) - require.NoError(t, err) - - startChan := make(chan struct{}) - wg := sync.WaitGroup{} - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-startChan - for j := 0; j < 100; j++ { - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - require.IsType(t, int64(0), res) - } - }() - } - - close(startChan) - wg.Wait() -} - -func TestRandomIntMicroDynamicParallel(t *testing.T) { - tsFn, err := InitFunctionHelper("timestamp_unix_micro") - require.NoError(t, err) - - e, err := InitFunctionHelper("random_int", tsFn) - require.NoError(t, err) - - startChan := make(chan struct{}) - wg := sync.WaitGroup{} - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-startChan - for j := 0; j < 100; j++ { - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - require.IsType(t, int64(0), res) - } - }() - } - - close(startChan) - wg.Wait() -} - -func TestRandomIntDynamicParallel(t *testing.T) { - tsFn, err := InitFunctionHelper("timestamp_unix_nano") - require.NoError(t, err) - - e, err := InitFunctionHelper("random_int", tsFn) - require.NoError(t, err) - - startChan := make(chan struct{}) - wg := sync.WaitGroup{} - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-startChan - for j := 0; j < 100; j++ { - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - require.IsType(t, int64(0), res) - } - }() - } - - close(startChan) - wg.Wait() -} - -func TestRandomIntWithinRange(t *testing.T) { - tsFn, err := InitFunctionHelper("timestamp_unix_nano") - require.NoError(t, err) - var min, max int64 = 10, 20 - e, err := InitFunctionHelper("random_int", tsFn, min, max) - require.NoError(t, err) - - for i := 0; i < 1000; i++ { - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - require.IsType(t, int64(0), res) - assert.GreaterOrEqual(t, res.(int64), min) - assert.LessOrEqual(t, res.(int64), max) - } - - // Create a new random_int function with one single possible value - e, err = InitFunctionHelper("random_int", tsFn, 10, 10) - require.NoError(t, err) - - for i := 0; i < 1000; i++ { - res, err := e.Exec(FunctionContext{}) - require.NoError(t, err) - require.IsType(t, int64(0), res) - assert.Equal(t, int64(10), res.(int64)) - } - - // Create a new random_int function with an invalid range - _, err = InitFunctionHelper("random_int", tsFn, 11, 10) - require.Error(t, err) - - // Create a new random_int function with a negative nin value - _, err = InitFunctionHelper("random_int", tsFn, -1, 10) - require.Error(t, err) - - // Create a new random_int function with a max that will overflow - _, err = InitFunctionHelper("random_int", tsFn, 0, math.MaxInt64) - require.Error(t, err) -} diff --git a/internal/bloblang/query/iterator.go b/internal/bloblang/query/iterator.go deleted file mode 100644 index e701514104..0000000000 --- a/internal/bloblang/query/iterator.go +++ /dev/null @@ -1,287 +0,0 @@ -package query - -import ( - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -var errEndOfIter = errors.New("iterator reached the end") - -// Iterator allows traversal of a Bloblang function result in iterations. -type Iterator interface { - // Next provides the next element of the iterator, or an error. When the - // iterator has reached the end ErrEndOfIter is returned. - Next() (any, error) - - // Len provides a static length of the iterator when possible. - Len() (int, bool) -} - -// Iterable is an interface implemented by Bloblang functions that are able to -// expose their results as an interator, allowing for more efficient chaining of -// array based methods. -type Iterable interface { - // TryIterate attempts to create an iterator that walks the function result. - // Some functions will be unable to provide an iterator due to either the - // context or function arguments provided, therefore it's possible that a - // static value will be returned instead. - TryIterate(ctx FunctionContext) (Iterator, any, error) -} - -//------------------------------------------------------------------------------ - -func execTryIter(iFn Iterable, fn Function, ctx FunctionContext) (iter Iterator, v any, err error) { - if iFn != nil { - if iter, v, err = iFn.TryIterate(ctx); err != nil || iter != nil { - return - } - } else if v, err = fn.Exec(ctx); err != nil { - return - } - if arr, ok := v.([]any); ok { - return arrayIterator(arr), nil, nil - } - return -} - -func arrayIterator(arr []any) Iterator { - return closureIterator{ - next: func() (any, error) { - if len(arr) == 0 { - return nil, errEndOfIter - } - v := arr[0] - arr = arr[1:] - return v, nil - }, - len: func() (int, bool) { - return len(arr), true - }, - } -} - -func drainIter(iter Iterator) ([]any, error) { - var arr []any - if l, ok := iter.Len(); ok { - arr = make([]any, 0, l) - } - for { - v, err := iter.Next() - if err != nil { - if errors.Is(err, errEndOfIter) { - return arr, nil - } - return nil, err - } - arr = append(arr, v) - } -} - -type closureIterator struct { - next func() (any, error) - len func() (int, bool) -} - -func (c closureIterator) Next() (any, error) { - return c.next() -} - -func (c closureIterator) Len() (int, bool) { - if c.len == nil { - return 0, false - } - return c.len() -} - -//------------------------------------------------------------------------------ - -type filterMethod struct { - target Function - iterTarget Iterable - mapFn Function -} - -func newFilterMethod(target Function, args ...any) (Function, error) { - mapFn, ok := args[0].(Function) - if !ok { - return nil, fmt.Errorf("expected query argument, received %T", args[0]) - } - iterTarget, _ := target.(Iterable) - return &filterMethod{ - target: target, - iterTarget: iterTarget, - mapFn: mapFn, - }, nil -} - -func (f *filterMethod) Annotation() string { - return "method filter" -} - -func (f *filterMethod) TryIterate(ctx FunctionContext) (Iterator, any, error) { - iter, res, err := execTryIter(f.iterTarget, f.target, ctx) - if err != nil { - return nil, nil, err - } - if iter == nil { - res, err = f.execFallback(ctx, res) - return nil, res, err - } - return closureIterator{ - next: func() (any, error) { - for { - v, err := iter.Next() - if err != nil { - if err != errEndOfIter { - err = ErrFrom(err, f.target) - } - return nil, err - } - f, err := f.mapFn.Exec(ctx.WithValue(v)) - if err != nil { - return nil, err - } - if b, _ := f.(bool); b { - return v, nil - } - } - }, - }, nil, nil -} - -// We also support filtering objects, so when we're unable to spawn an iterator -// we attempt to process a map. -func (f *filterMethod) execFallback(ctx FunctionContext, res any) (any, error) { - m, ok := res.(map[string]any) - if !ok { - return nil, ErrFrom(value.NewTypeError(res, value.TArray, value.TObject), f.target) - } - newMap := make(map[string]any, len(m)) - for k, v := range m { - var ctxMap any = map[string]any{ - "key": k, - "value": v, - } - f, err := f.mapFn.Exec(ctx.WithValue(ctxMap)) - if err != nil { - return nil, err - } - if b, _ := f.(bool); b { - newMap[k] = v - } - } - return newMap, nil -} - -func (f *filterMethod) Exec(ctx FunctionContext) (any, error) { - iter, res, err := f.TryIterate(ctx) - if err != nil || res != nil { - return res, err - } - return drainIter(iter) -} - -func (f *filterMethod) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - return f.target.QueryTargets(ctx) -} - -//------------------------------------------------------------------------------ - -type mapEachMethod struct { - target Function - iterTarget Iterable - mapFn Function -} - -func newMapEachMethod(target Function, args ...any) (Function, error) { - mapFn, ok := args[0].(Function) - if !ok { - return nil, fmt.Errorf("expected query argument, received %T", args[0]) - } - iterTarget, _ := target.(Iterable) - return &mapEachMethod{ - target: target, - iterTarget: iterTarget, - mapFn: mapFn, - }, nil -} - -func (m *mapEachMethod) Annotation() string { - return "method map_each" -} - -func (m *mapEachMethod) TryIterate(ctx FunctionContext) (Iterator, any, error) { - iter, res, err := execTryIter(m.iterTarget, m.target, ctx) - if err != nil { - return nil, nil, err - } - if iter == nil { - res, err = m.execFallback(ctx, res) - return nil, res, err - } - return closureIterator{ - next: func() (any, error) { - for { - v, err := iter.Next() - if err != nil { - if err != errEndOfIter { - err = ErrFrom(err, m.target) - } - return nil, err - } - - newV, err := m.mapFn.Exec(ctx.WithValue(v)) - if err != nil { - return nil, ErrFrom(err, m.mapFn) - } - switch newV.(type) { - case value.Delete: - case value.Nothing: - return v, nil - default: - return newV, nil - } - } - }, - }, nil, nil -} - -func (m *mapEachMethod) execFallback(ctx FunctionContext, res any) (any, error) { - resMap, ok := res.(map[string]any) - if !ok { - return nil, ErrFrom(value.NewTypeError(res, value.TArray, value.TObject), m.target) - } - newMap := make(map[string]any, len(resMap)) - for k, v := range resMap { - var ctxMap any = map[string]any{ - "key": k, - "value": v, - } - newV, mapErr := m.mapFn.Exec(ctx.WithValue(ctxMap)) - if mapErr != nil { - return nil, fmt.Errorf("failed to process element %v: %w", k, ErrFrom(mapErr, m.mapFn)) - } - switch newV.(type) { - case value.Delete: - case value.Nothing: - newMap[k] = v - default: - newMap[k] = newV - } - } - return newMap, nil -} - -func (m *mapEachMethod) Exec(ctx FunctionContext) (any, error) { - iter, res, err := m.TryIterate(ctx) - if err != nil || res != nil { - return res, err - } - return drainIter(iter) -} - -func (m *mapEachMethod) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - return m.target.QueryTargets(ctx) -} diff --git a/internal/bloblang/query/iterator_test.go b/internal/bloblang/query/iterator_test.go deleted file mode 100644 index 885d331685..0000000000 --- a/internal/bloblang/query/iterator_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package query - -import ( - "fmt" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/require" -) - -func TestIteratorMethods(t *testing.T) { - literalFn := func(val any) Function { - fn := NewLiteralFunction("", val) - return fn - } - jsonFn := func(json string) Function { - t.Helper() - gObj, err := gabs.ParseJSON([]byte(json)) - require.NoError(t, err) - fn := NewLiteralFunction("", gObj.Data()) - return fn - } - arithmetic := func(left, right Function, op ArithmeticOperator) Function { - t.Helper() - fn, err := NewArithmeticExpression( - []Function{left, right}, - []ArithmeticOperator{op}, - ) - require.NoError(t, err) - return fn - } - type easyMethod struct { - name string - args []any - } - methods := func(fn Function, methods ...easyMethod) Function { - t.Helper() - for _, m := range methods { - var err error - fn, err = InitMethodHelper(m.name, fn, m.args...) - require.NoError(t, err) - } - return fn - } - method := func(name string, args ...any) easyMethod { - return easyMethod{name: name, args: args} - } - - tests := map[string]struct { - input Function - value *any - output any - err string - }{ - "check map each": { - input: func() Function { - fn, err := newMapEachMethod(jsonFn(`["foo","bar"]`), methods( - NewFieldFunction(""), - method("uppercase"), - )) - require.NoError(t, err) - return fn - }(), - output: []any{"FOO", "BAR"}, - }, - "check map each 2": { - input: func() Function { - fn, err := newMapEachMethod( - jsonFn(`["foo","bar"]`), - methods( - literalFn("(%v)"), - method("format", NewFieldFunction("")), - method("uppercase"), - )) - require.NoError(t, err) - return fn - }(), - output: []any{"(FOO)", "(BAR)"}, - }, - "check map each object": { - input: func() Function { - fn, err := newMapEachMethod( - jsonFn(`{"foo":"hello world","bar":"this is ash"}`), - methods( - NewFieldFunction("value"), - method("uppercase"), - )) - require.NoError(t, err) - return fn - }(), - output: map[string]any{ - "foo": "HELLO WORLD", - "bar": "THIS IS ASH", - }, - }, - "check filter array": { - input: func() Function { - fn, err := newFilterMethod( - jsonFn(`[2,14,4,11,7]`), - arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", 10.0), - ArithmeticGt, - )) - require.NoError(t, err) - return fn - }(), - output: []any{14.0, 11.0}, - }, - "check filter object": { - input: func() Function { - fn, err := newFilterMethod( - jsonFn(`{"foo":"hello ! world","bar":"this is ash","baz":"im cool!"}`), - methods( - NewFieldFunction("value"), - method("contains", "!"), - )) - require.NoError(t, err) - return fn - }(), - output: map[string]any{ - "foo": "hello ! world", - "baz": "im cool!", - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - res, err := test.input.Exec(FunctionContext{ - Maps: map[string]Function{}, - }.WithValueFunc(func() *any { return test.value })) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - } - require.Equal(t, test.output, res) - }) - } -} - -func filterFunction() Function { - i := 0 - return ClosureFunction("", func(ctx FunctionContext) (any, error) { - i++ - return i%100 == 0, nil - }, nil) -} - -func benchFilterIter(b *testing.B, n, m int) { - startingArray := make([]any, n) - for i := range startingArray { - startingArray[i] = fmt.Sprintf("foo%v", i) - } - - var err error - var fn Function = NewLiteralFunction("", startingArray) - filter := filterFunction() - for i := 0; i < m; i++ { - fn, err = newFilterMethod(fn, filter) - require.NoError(b, err) - } - - startingContext := FunctionContext{}.WithValue(startingArray) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - _, err := fn.Exec(startingContext) - require.NoError(b, err) - } -} - -func benchFilterNoIter(b *testing.B, n, m int) { - startingArray := make([]any, n) - for i := range startingArray { - startingArray[i] = fmt.Sprintf("foo%v", i) - } - - var err error - var fn Function = NewLiteralFunction("", startingArray) - filter := filterFunction() - for i := 0; i < m; i++ { - fn, err = InitMethodHelper("filter", fn, filter) - require.NoError(b, err) - } - - startingContext := FunctionContext{}.WithValue(startingArray) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - _, err := fn.Exec(startingContext) - require.NoError(b, err) - } -} - -func BenchmarkFilterIter(b *testing.B) { - for _, n := range []int{10, 100, 1000} { - for _, m := range []int{1, 10, 100} { - n, m := n, m - b.Run(fmt.Sprintf("OldN%vM%v", n, m), func(b *testing.B) { - benchFilterNoIter(b, n, m) - }) - b.Run(fmt.Sprintf("NewN%vM%v", n, m), func(b *testing.B) { - benchFilterIter(b, n, m) - }) - } - } -} diff --git a/internal/bloblang/query/literals.go b/internal/bloblang/query/literals.go deleted file mode 100644 index 27296be036..0000000000 --- a/internal/bloblang/query/literals.go +++ /dev/null @@ -1,196 +0,0 @@ -package query - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -var _ Function = &mapLiteral{} - -type mapLiteral struct { - keyValues [][2]any -} - -// NewMapLiteral creates a map literal from a slice of key/value pairs. If all -// keys and values are static then a static map[string]interface{} value is -// returned. However, if any keys or values are dynamic a Function is returned. -func NewMapLiteral(values [][2]any) (any, error) { - isDynamic := false - staticValues := make(map[string]any, len(values)) - for i, kv := range values { - var key string - switch t := kv[0].(type) { - case string: - key = t - case *Literal: - var isStr bool - if key, isStr = t.Value.(string); !isStr { - return nil, fmt.Errorf("object keys must be strings, received: %T", t.Value) - } - values[i][0] = key - case Function: - isDynamic = true - default: - return nil, fmt.Errorf("object keys must be strings, received: %T", t) - } - switch t := kv[1].(type) { - case *Literal: - values[i][1] = t.Value - if !isDynamic { - switch t.Value.(type) { - case value.Delete, value.Nothing: - default: - staticValues[key] = t.Value - } - } - case Function: - isDynamic = true - default: - if !isDynamic { - switch kv[1].(type) { - case value.Delete, value.Nothing: - default: - staticValues[key] = kv[1] - } - } - } - } - if isDynamic { - return &mapLiteral{keyValues: values}, nil - } - return staticValues, nil -} - -func (m *mapLiteral) Annotation() string { - return "object literal" -} - -func (m *mapLiteral) Exec(ctx FunctionContext) (any, error) { - dynMap := make(map[string]any, len(m.keyValues)) - for _, kv := range m.keyValues { - var key string - var val any - - var err error - switch t := kv[0].(type) { - case string: - key = t - case Function: - var keyI any - if keyI, err = t.Exec(ctx); err != nil { - return nil, fmt.Errorf("failed to resolve key: %w", err) - } - switch t2 := keyI.(type) { - case string: - key = t2 - case []byte: - key = string(t2) - default: - return nil, fmt.Errorf("mapping returned invalid key type: %T", keyI) - } - default: - return nil, fmt.Errorf("invalid key type: %T", kv[0]) - } - - if fn, isFunction := kv[1].(Function); isFunction { - if val, err = fn.Exec(ctx); err != nil { - return nil, fmt.Errorf("failed to resolve '%v' value: %w", key, err) - } - } else { - val = kv[1] - } - - switch val.(type) { - case value.Delete, value.Nothing: - default: - dynMap[key] = val - } - } - return dynMap, nil -} - -func (m *mapLiteral) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - var targetPaths []TargetPath - for _, kv := range m.keyValues { - if fn, ok := kv[0].(Function); ok { - _, paths := fn.QueryTargets(ctx) - targetPaths = append(targetPaths, paths...) - } - if fn, ok := kv[1].(Function); ok { - _, paths := fn.QueryTargets(ctx) - targetPaths = append(targetPaths, paths...) - } - } - // TODO: Mark next context with aliases? - return ctx, targetPaths -} - -//------------------------------------------------------------------------------ - -var _ Function = &arrayLiteral{} - -type arrayLiteral struct { - values []any -} - -// NewArrayLiteral creates an array literal from a slice of values. If all -// values are static then a static []interface{} value is returned. However, if -// any values are dynamic a Function is returned. -func NewArrayLiteral(values ...Function) any { - var expandedValues []any - isDynamic := false - for _, v := range values { - switch t := v.(type) { - case *Literal: - switch t.Value.(type) { - case value.Delete, value.Nothing: - default: - expandedValues = append(expandedValues, t.Value) - } - case Function: - isDynamic = true - expandedValues = append(expandedValues, v) - default: - expandedValues = append(expandedValues, v) - } - } - if !isDynamic { - return expandedValues - } - return &arrayLiteral{expandedValues} -} - -func (a *arrayLiteral) Annotation() string { - return "array literal" -} - -func (a *arrayLiteral) Exec(ctx FunctionContext) (any, error) { - dynArray := make([]any, 0, len(a.values)) - for _, v := range a.values { - if fn, isFunction := v.(Function); isFunction { - var err error - if v, err = fn.Exec(ctx); err != nil { - return nil, err - } - } - switch v.(type) { - case value.Delete, value.Nothing: - default: - dynArray = append(dynArray, v) - } - } - return dynArray, nil -} - -func (a *arrayLiteral) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - var targetPaths []TargetPath - for _, v := range a.values { - if fn, ok := v.(Function); ok { - _, paths := fn.QueryTargets(ctx) - targetPaths = append(targetPaths, paths...) - } - } - // TODO: Mark next context with aliases? - return ctx, targetPaths -} diff --git a/internal/bloblang/query/literals_test.go b/internal/bloblang/query/literals_test.go deleted file mode 100644 index d678b54af1..0000000000 --- a/internal/bloblang/query/literals_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package query - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestLiterals(t *testing.T) { - mustVal := func(i any, err error) any { - t.Helper() - require.NoError(t, err) - return i - } - - tests := map[string]struct { - input any - value any - output any - err error - targets []TargetPath - }{ - "dynamic object values": { - input: mustVal(NewMapLiteral( - [][2]any{ - {"test1", NewFieldFunction("first")}, - {"test2", NewFieldFunction("second")}, - {"deleteme", value.Delete(nil)}, - {"donotmapme", value.Nothing(nil)}, - {"test3", "static"}, - }, - )), - value: map[string]any{ - "first": "foo", - "second": "bar", - }, - output: map[string]any{ - "test1": "foo", - "test2": "bar", - "test3": "static", - }, - targets: []TargetPath{ - NewTargetPath(TargetValue, "first"), - NewTargetPath(TargetValue, "second"), - }, - }, - "dynamic object keys and values": { - input: mustVal(NewMapLiteral( - [][2]any{ - {NewFieldFunction("first"), NewFieldFunction("second")}, - {"test2", "static"}, - }, - )), - value: map[string]any{ - "first": "foo", - "second": "bar", - }, - output: map[string]any{ - "foo": "bar", - "test2": "static", - }, - targets: []TargetPath{ - NewTargetPath(TargetValue, "first"), - NewTargetPath(TargetValue, "second"), - }, - }, - "object literal function keys and values": { - input: mustVal(NewMapLiteral( - [][2]any{ - {NewLiteralFunction("", "first"), NewLiteralFunction("", "second")}, - {NewLiteralFunction("", "third"), NewLiteralFunction("", "fourth")}, - }, - )), - value: map[string]any{}, - output: map[string]any{ - "first": "second", - "third": "fourth", - }, - }, - "static object": { - input: mustVal(NewMapLiteral( - [][2]any{ - {"test1", "static1"}, - {"test2", "static2"}, - {"deleteme", value.Delete(nil)}, - {"donotmapme", value.Nothing(nil)}, - {"test3", "static3"}, - }, - )), - output: map[string]any{ - "test1": "static1", - "test2": "static2", - "test3": "static3", - }, - }, - "dynamic array values": { - input: NewArrayLiteral( - NewFieldFunction("first"), - NewFieldFunction("second"), - NewLiteralFunction("delete", value.Delete(nil)), - NewLiteralFunction("meow", "static"), - NewLiteralFunction("woof", value.Nothing(nil)), - NewLiteralFunction("", "static literal"), - ), - value: map[string]any{ - "first": "foo", - "second": "bar", - }, - output: []any{ - "foo", - "bar", - "static", - "static literal", - }, - targets: []TargetPath{ - NewTargetPath(TargetValue, "first"), - NewTargetPath(TargetValue, "second"), - }, - }, - "static array values": { - input: NewArrayLiteral( - NewLiteralFunction("", "static1"), - NewLiteralFunction("", value.Delete(nil)), - NewLiteralFunction("", "static2"), - NewLiteralFunction("", value.Nothing(nil)), - NewLiteralFunction("", "static3"), - ), - output: []any{ - "static1", - "static2", - "static3", - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - var err error - var targets []TargetPath - - res := test.input - if fn, ok := test.input.(Function); ok { - res, err = fn.Exec(FunctionContext{ - Maps: map[string]Function{}, - }.WithValue(test.value)) - _, targets = fn.QueryTargets(TargetsContext{ - Maps: map[string]Function{}, - }) - } - - if test.err != nil { - require.EqualError(t, err, test.err.Error()) - } else { - require.NoError(t, err) - } - - require.Equal(t, test.output, res) - require.Equal(t, test.targets, targets) - }) - } -} diff --git a/internal/bloblang/query/method_ctor.go b/internal/bloblang/query/method_ctor.go deleted file mode 100644 index e00e732668..0000000000 --- a/internal/bloblang/query/method_ctor.go +++ /dev/null @@ -1,72 +0,0 @@ -package query - -import ( - "encoding/json" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// MethodCtor constructs a new method from a target function and input args. -type MethodCtor func(target Function, args *ParsedParams) (Function, error) - -type simpleMethodConstructor func(args *ParsedParams) (simpleMethod, error) - -func registerSimpleMethod(spec MethodSpec, ctor simpleMethodConstructor) struct{} { - return registerMethod(spec, func(target Function, args *ParsedParams) (Function, error) { - fn, err := ctor(args) - if err != nil { - return nil, err - } - return ClosureFunction("method "+spec.Name, func(ctx FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - res, err := fn(v, ctx) - if err != nil { - return nil, ErrFrom(err, target) - } - return res, nil - }, target.QueryTargets), nil - }) -} - -type simpleMethod func(v any, ctx FunctionContext) (any, error) - -func stringMethod(fn func(v string) (any, error)) simpleMethod { - return func(v any, ctx FunctionContext) (any, error) { - s, err := value.IGetString(v) - if err != nil { - return nil, err - } - return fn(s) - } -} - -func numberMethod(fn func(f *float64, i *int64, ui *uint64) (any, error)) simpleMethod { - return func(v any, ctx FunctionContext) (any, error) { - var f *float64 - var i *int64 - var ui *uint64 - switch t := v.(type) { - case float64: - f = &t - case int64: - i = &t - case uint64: - ui = &t - case json.Number: - if ji, err := t.Int64(); err == nil { - i = &ji - } else if jf, err := t.Float64(); err == nil { - f = &jf - } else { - return nil, fmt.Errorf("failed to parse number: %v", err) - } - default: - return nil, value.NewTypeError(v, value.TNumber) - } - return fn(f, i, ui) - } -} diff --git a/internal/bloblang/query/method_set.go b/internal/bloblang/query/method_set.go deleted file mode 100644 index 61a3fb633d..0000000000 --- a/internal/bloblang/query/method_set.go +++ /dev/null @@ -1,176 +0,0 @@ -package query - -import ( - "errors" - "fmt" - "sort" -) - -type methodDetails struct { - ctor MethodCtor - spec MethodSpec -} - -// MethodSet contains an explicit set of methods to be available in a Bloblang -// query. -type MethodSet struct { - disableCtors bool - methods map[string]methodDetails -} - -// NewMethodSet creates a method set without any methods in it. -func NewMethodSet() *MethodSet { - return &MethodSet{ - methods: map[string]methodDetails{}, - } -} - -// Add a new method to this set by providing a spec (name and documentation), -// a constructor to be called for each instantiation of the method, and -// information regarding the arguments of the method. -func (m *MethodSet) Add(spec MethodSpec, ctor MethodCtor) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("method name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if err := spec.Params.validate(); err != nil { - return err - } - m.methods[spec.Name] = methodDetails{ctor: ctor, spec: spec} - return nil -} - -// Docs returns a slice of method specs, which document each method. -func (m *MethodSet) Docs() []MethodSpec { - specSlice := make([]MethodSpec, 0, len(m.methods)) - for _, v := range m.methods { - specSlice = append(specSlice, v.spec) - } - sort.Slice(specSlice, func(i, j int) bool { - return specSlice[i].Name < specSlice[j].Name - }) - return specSlice -} - -// Params attempts to obtain an argument specification for a given method type. -func (m *MethodSet) Params(name string) (Params, error) { - details, exists := m.methods[name] - if !exists { - return VariadicParams(), badMethodErr(name) - } - return details.spec.Params, nil -} - -// Init attempts to initialize a method of the set by name from a target -// function and zero or more arguments. -func (m *MethodSet) Init(name string, target Function, args *ParsedParams) (Function, error) { - details, exists := m.methods[name] - if !exists { - return nil, badMethodErr(name) - } - if m.disableCtors { - return disabledMethod(name), nil - } - return wrapMethodCtorWithDynamicArgs(name, target, args, details.ctor) -} - -// Without creates a clone of the method set that can be mutated in isolation, -// where a variadic list of methods will be excluded from the set. -func (m *MethodSet) Without(methods ...string) *MethodSet { - excludeMap := make(map[string]struct{}, len(methods)) - for _, k := range methods { - excludeMap[k] = struct{}{} - } - - details := make(map[string]methodDetails, len(m.methods)) - for k, v := range m.methods { - if _, exists := excludeMap[k]; !exists { - details[k] = v - } - } - return &MethodSet{disableCtors: m.disableCtors, methods: details} -} - -// OnlyPure creates a clone of the methods set that can be mutated in isolation, -// where all impure methods are removed. -func (m *MethodSet) OnlyPure() *MethodSet { - var excludes []string - for _, v := range m.methods { - if v.spec.Impure { - excludes = append(excludes, v.spec.Name) - } - } - return m.Without(excludes...) -} - -// Deactivated returns a version of the method set where constructors are -// disabled, allowing mappings to be parsed and validated but not executed. -// -// The underlying register of methods is shared with the target set, and -// therefore methods added to this set will also be added to the still activated -// set. Use the Without method (with empty args if applicable) in order to -// create a deep copy of the set that is independent of the source. -func (m *MethodSet) Deactivated() *MethodSet { - newSet := *m - newSet.disableCtors = true - return &newSet -} - -//------------------------------------------------------------------------------ - -// AllMethods is a set containing every single method declared by this package, -// and any globally declared plugin methods. -var AllMethods = NewMethodSet() - -func registerMethod(spec MethodSpec, ctor MethodCtor) struct{} { - if err := AllMethods.Add(spec, func(target Function, args *ParsedParams) (Function, error) { - return ctor(target, args) - }); err != nil { - panic(err) - } - return struct{}{} -} - -// InitMethodHelper attempts to initialise a method by its name, target function -// and arguments, this is convenient for writing tests. -func InitMethodHelper(name string, target Function, args ...any) (Function, error) { - details, ok := AllMethods.methods[name] - if !ok { - return nil, badMethodErr(name) - } - parsedArgs, err := details.spec.Params.PopulateNameless(args...) - if err != nil { - return nil, err - } - return AllMethods.Init(name, target, parsedArgs) -} - -// MethodDocs returns a slice of specs, one for each method. -func MethodDocs() []MethodSpec { - return AllMethods.Docs() -} - -//------------------------------------------------------------------------------ - -func disabledMethod(name string) Function { - return ClosureFunction("method "+name, func(ctx FunctionContext) (any, error) { - return nil, errors.New("this method has been disabled") - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }) -} - -func wrapMethodCtorWithDynamicArgs(name string, target Function, args *ParsedParams, fn MethodCtor) (Function, error) { - fns := args.dynamic() - if len(fns) == 0 { - return fn(target, args) - } - return ClosureFunction("method "+name, func(ctx FunctionContext) (any, error) { - newArgs, err := args.ResolveDynamic(ctx) - if err != nil { - return nil, fmt.Errorf("method '%s': %w", name, err) - } - dynFunc, err := fn(target, newArgs) - if err != nil { - return nil, err - } - return dynFunc.Exec(ctx) - }, aggregateTargetPaths(fns...)), nil -} diff --git a/internal/bloblang/query/method_set_test.go b/internal/bloblang/query/method_set_test.go deleted file mode 100644 index a1851059bb..0000000000 --- a/internal/bloblang/query/method_set_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package query - -import ( - "errors" - "sort" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func listMethods(m *MethodSet) []string { - methodNames := make([]string, 0, len(m.methods)) - for k := range m.methods { - methodNames = append(methodNames, k) - } - sort.Strings(methodNames) - return methodNames -} - -func TestMethodSetWithout(t *testing.T) { - setOne := AllMethods - setTwo := setOne.Without("explode") - - assert.Contains(t, listMethods(setOne), "explode") - assert.NotContains(t, listMethods(setTwo), "explode") - - explodeParamSpec, _ := setOne.Params("explode") - explodeParams, err := explodeParamSpec.PopulateNameless("foo.bar") - require.NoError(t, err) - - _, err = setOne.Init("explode", NewLiteralFunction("", nil), explodeParams) - assert.NoError(t, err) - - _, err = setTwo.Init("explode", NewLiteralFunction("", nil), explodeParams) - assert.EqualError(t, err, "unrecognised method 'explode'") - - mapEachParamSpec, _ := setTwo.Params("map_each") - mapEachParams, err := mapEachParamSpec.PopulateNameless(NewFieldFunction("foo")) - require.NoError(t, err) - - _, err = setTwo.Init("map_each", NewLiteralFunction("", nil), mapEachParams) - assert.NoError(t, err) -} - -func TestMethodSetDeactivated(t *testing.T) { - setOne := AllMethods.Without() - setTwo := setOne.Deactivated() - - customErr := errors.New("custom error") - - spec := NewMethodSpec("meow", "").Param(ParamString("val1", "")) - require.NoError(t, setOne.Add(spec, func(target Function, args *ParsedParams) (Function, error) { - return ClosureFunction("", func(ctx FunctionContext) (any, error) { - return nil, customErr - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }), nil - })) - - assert.Contains(t, listMethods(setOne), "meow") - assert.Contains(t, listMethods(setTwo), "meow") - - goodArgs, err := spec.Params.PopulateNameless("hello") - require.NoError(t, err) - - fnOne, err := setOne.Init("meow", NewLiteralFunction("", nil), goodArgs) - require.NoError(t, err) - - fnTwo, err := setTwo.Init("meow", NewLiteralFunction("", nil), goodArgs) - require.NoError(t, err) - - _, err = fnOne.Exec(FunctionContext{}) - assert.Equal(t, customErr, err) - - _, err = fnTwo.Exec(FunctionContext{}) - assert.EqualError(t, err, "this method has been disabled") -} - -func TestMethodResolveParamError(t *testing.T) { - setOne := AllMethods.Without() - - spec := NewMethodSpec("meow", "").Param(ParamString("val1", "")) - require.NoError(t, setOne.Add(spec, func(target Function, args *ParsedParams) (Function, error) { - return ClosureFunction("", func(ctx FunctionContext) (any, error) { - return "ok", nil - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { return ctx, nil }), nil - })) - - assert.Contains(t, listMethods(setOne), "meow") - - badArgs, err := spec.Params.PopulateNameless(NewFieldFunction("doc.foo")) - require.NoError(t, err) - - fnOne, err := setOne.Init("meow", NewLiteralFunction("", nil), badArgs) - require.NoError(t, err) - - _, err = fnOne.Exec(FunctionContext{}) - assert.EqualError(t, err, "method 'meow': failed to extract input arg 'val1': context was undefined, unable to reference `doc.foo`") -} - -func TestMethodBadName(t *testing.T) { - testCases := map[string]string{ - "!no": "method name '!no' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo__bar": "method name 'foo__bar' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "-foo-bar": "method name '-foo-bar' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo-bar-": "method name 'foo-bar-' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "": "method name '' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo-bar": "method name 'foo-bar' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foo-bar_baz": "method name 'foo-bar_baz' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "FOO": "method name 'FOO' does not match the required regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/", - "foobarbaz": "", - "foobarbaz89": "", - "foo_bar_baz": "", - "fo1_ba2_ba3": "", - } - - for k, v := range testCases { - t.Run(k, func(t *testing.T) { - setOne := AllMethods.Without() - err := setOne.Add(NewMethodSpec(k, ""), nil) - if v != "" { - assert.EqualError(t, err, v) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/internal/bloblang/query/methods.go b/internal/bloblang/query/methods.go deleted file mode 100644 index 1b3d20c947..0000000000 --- a/internal/bloblang/query/methods.go +++ /dev/null @@ -1,512 +0,0 @@ -package query - -import ( - "errors" - "fmt" - "strconv" - - "github.com/Jeffail/gabs/v2" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -var _ = registerMethod( - NewMethodSpec( - "apply", - "Apply a declared mapping to a target value.", - NewExampleSpec("", - `map thing { - root.inner = this.first -} - -root.foo = this.doc.apply("thing")`, - `{"doc":{"first":"hello world"}}`, - `{"foo":{"inner":"hello world"}}`, - ), - NewExampleSpec("", - `map create_foo { - root.name = "a foo" - root.purpose = "to be a foo" -} - -root = this -root.foo = null.apply("create_foo")`, - `{"id":"1234"}`, - `{"foo":{"name":"a foo","purpose":"to be a foo"},"id":"1234"}`, - ), - ).Param(ParamString("mapping", "The mapping to apply.")), - applyMethod, -) - -func applyMethod(target Function, args *ParsedParams) (Function, error) { - targetMap, err := args.FieldString("mapping") - if err != nil { - return nil, err - } - - return ClosureFunction("map "+targetMap, func(ctx FunctionContext) (any, error) { - res, err := target.Exec(ctx) - if err != nil { - return nil, err - } - ctx = ctx.WithValue(res) - - if ctx.Maps == nil { - return nil, errors.New("no maps were found") - } - m, ok := ctx.Maps[targetMap] - if !ok { - return nil, fmt.Errorf("map %v was not found", targetMap) - } - - // ISOLATED VARIABLES - ctx.Vars = map[string]any{} - return m.Exec(ctx) - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - mapFn, ok := ctx.Maps[targetMap] - if !ok { - return target.QueryTargets(ctx) - } - - mapCtx, targets := target.QueryTargets(ctx) - mapCtx = mapCtx.WithValues(targets).WithValuesAsContext() - - returnCtx, mapTargets := mapFn.QueryTargets(mapCtx) - return returnCtx, append(targets, mapTargets...) - }), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec("bool", "").InCategory( - MethodCategoryCoercion, - "Attempt to parse a value into a boolean. An optional argument can be provided, in which case if the value cannot be parsed the argument will be returned instead. If the value is a number then any non-zero value will resolve to `true`, if the value is a string then any of the following values are considered valid: `1, t, T, TRUE, true, True, 0, f, F, FALSE`.", - NewExampleSpec("", - `root.foo = this.thing.bool() -root.bar = this.thing.bool(true)`, - ), - ).Param(ParamBool("default", "An optional value to yield if the target cannot be parsed as a boolean.").Optional()), - boolMethod, -) - -func boolMethod(target Function, args *ParsedParams) (Function, error) { - defaultBool, err := args.FieldOptionalBool("default") - if err != nil { - return nil, err - } - return ClosureFunction("method bool", func(ctx FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - if defaultBool != nil { - return *defaultBool, nil - } - return nil, err - } - f, err := value.IToBool(v) - if err != nil { - if defaultBool != nil { - return *defaultBool, nil - } - return nil, ErrFrom(err, target) - } - return f, nil - }, target.QueryTargets), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "catch", - "If the result of a target query fails (due to incorrect types, failed parsing, etc) the argument is returned instead.", - NewExampleSpec("", - `root.doc.id = this.thing.id.string().catch(uuid_v4())`, - ), - NewExampleSpec("The fallback argument can be a mapping, allowing you to capture the error string and yield structured data back.", - `root.url = this.url.parse_url().catch(err -> {"error":err,"input":this.url})`, - `{"url":"invalid %&# url"}`, - `{"url":{"error":"field `+"`this.url`"+`: parse \"invalid %&\": invalid URL escape \"%&\"","input":"invalid %&# url"}}`, - ), - NewExampleSpec("When the input document is not structured attempting to reference structured fields with `this` will result in an error. Therefore, a convenient way to delete non-structured data is with a catch.", - `root = this.catch(deleted())`, - `{"doc":{"foo":"bar"}}`, - `{"doc":{"foo":"bar"}}`, - `not structured data`, - ``, - ), - ).Param(ParamQuery("fallback", "A value to yield, or query to execute, if the target query fails.", true)), - catchMethod, -) - -func catchMethod(fn Function, args *ParsedParams) (Function, error) { - catchFn, err := args.FieldQuery("fallback") - if err != nil { - return nil, err - } - return ClosureFunction("method catch", func(ctx FunctionContext) (any, error) { - res, err := fn.Exec(ctx) - if err != nil { - return catchFn.Exec(ctx.WithValue(err.Error())) - } - return res, err - }, aggregateTargetPaths(fn, catchFn)), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "from", - "Modifies a target query such that certain functions are executed from the perspective of another message in the batch. This allows you to mutate events based on the contents of other messages. Functions that support this behavior are `content`, `json` and `meta`.", - NewExampleSpec("For example, the following map extracts the contents of the JSON field `foo` specifically from message index `1` of a batch, effectively overriding the field `foo` for all messages of a batch to that of message 1:", - `root = this -root.foo = json("foo").from(1)`, - ), - ).Param(ParamInt64("index", "The message index to use as a perspective.")), - func(target Function, args *ParsedParams) (Function, error) { - i64, err := args.FieldInt64("index") - if err != nil { - return nil, err - } - return &fromMethod{ - index: int(i64), - target: target, - }, nil - }, -) - -type fromMethod struct { - index int - target Function -} - -func (f *fromMethod) Annotation() string { - return f.target.Annotation() + " from " + strconv.Itoa(f.index) -} - -func (f *fromMethod) Exec(ctx FunctionContext) (any, error) { - ctx.Index = f.index - return f.target.Exec(ctx) -} - -func (f *fromMethod) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - // TODO: Modify context to represent new index. - return f.target.QueryTargets(ctx) -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "from_all", - "Modifies a target query such that certain functions are executed from the perspective of each message in the batch, and returns the set of results as an array. Functions that support this behavior are `content`, `json` and `meta`.", - NewExampleSpec("", - `root = this -root.foo_summed = json("foo").from_all().sum()`, - ), - ), - fromAllMethod, -) - -func fromAllMethod(target Function, _ *ParsedParams) (Function, error) { - return ClosureFunction("method from_all", func(ctx FunctionContext) (any, error) { - values := make([]any, ctx.MsgBatch.Len()) - for i := 0; i < ctx.MsgBatch.Len(); i++ { - subCtx := ctx - subCtx.Index = i - var err error - if values[i], err = target.Exec(subCtx); err != nil { - return nil, err - } - } - return values, nil - }, target.QueryTargets), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "get", - "Extract a field value, identified via a xref:configuration:field_paths.adoc[dot path], from an object.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.result = this.foo.get(this.target)`, - `{"foo":{"bar":"from bar","baz":"from baz"},"target":"bar"}`, - `{"result":"from bar"}`, - `{"foo":{"bar":"from bar","baz":"from baz"},"target":"baz"}`, - `{"result":"from baz"}`, - ), - ).Param(ParamString("path", "A xref:configuration:field_paths.adoc[dot path] identifying a field to obtain.")), - getMethodCtor, -) - -type getMethod struct { - fn Function - path []string -} - -func (g *getMethod) Annotation() string { - return "path `" + SliceToDotPath(g.path...) + "`" -} - -func (g *getMethod) Exec(ctx FunctionContext) (any, error) { - v, err := g.fn.Exec(ctx) - if err != nil { - return nil, err - } - return gabs.Wrap(v).S(g.path...).Data(), nil -} - -func (g *getMethod) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - ctx, fnPaths := g.fn.QueryTargets(ctx) - - basePaths := ctx.Value() - paths := make([]TargetPath, len(basePaths)) - for i, p := range basePaths { - paths[i] = p - paths[i].Path = append(paths[i].Path, g.path...) - } - ctx = ctx.WithValues(paths) - - return ctx, append(fnPaths, paths...) -} - -// NewGetMethod creates a new get method. -func NewGetMethod(target Function, pathStr string) (Function, error) { - path := gabs.DotPathToSlice(pathStr) - switch t := target.(type) { - case *getMethod: - newPath := append([]string{}, t.path...) - newPath = append(newPath, path...) - return &getMethod{ - fn: t.fn, - path: newPath, - }, nil - case *fieldFunction: - return t.expand(path...), nil - } - return &getMethod{ - fn: target, - path: path, - }, nil -} - -func getMethodCtor(target Function, args *ParsedParams) (Function, error) { - pathStr, err := args.FieldString("path") - if err != nil { - return nil, err - } - return NewGetMethod(target, pathStr) -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewHiddenMethodSpec("map"). - Param(ParamQuery("query", "A query to execute on the target.", false)), - mapMethod, -) - -// NewMapMethod attempts to create a map method. -func NewMapMethod(target, mapFn Function) (Function, error) { - return ClosureFunction(mapFn.Annotation(), func(ctx FunctionContext) (any, error) { - res, err := target.Exec(ctx) - if err != nil { - return nil, err - } - return mapFn.Exec(ctx.WithValue(res)) - }, func(ctx TargetsContext) (TargetsContext, []TargetPath) { - mapCtx, targets := target.QueryTargets(ctx) - mapCtx = mapCtx.WithValues(targets).WithValuesAsContext() - - returnCtx, mapTargets := mapFn.QueryTargets(mapCtx) - return returnCtx, append(targets, mapTargets...) - }), nil -} - -func mapMethod(target Function, args *ParsedParams) (Function, error) { - mapFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - return NewMapMethod(target, mapFn) -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod(NewHiddenMethodSpec("not"), notMethodCtor) - -type notMethod struct { - fn Function -} - -// Not returns a logical NOT of a child function. -func Not(fn Function) Function { - return ¬Method{ - fn: fn, - } -} - -func (n *notMethod) Annotation() string { - return "not " + n.fn.Annotation() -} - -func (n *notMethod) Exec(ctx FunctionContext) (any, error) { - v, err := n.fn.Exec(ctx) - if err != nil { - return nil, err - } - b, ok := v.(bool) - if !ok { - return nil, value.NewTypeErrorFrom(n.fn.Annotation(), v, value.TBool) - } - return !b, nil -} - -func (n *notMethod) QueryTargets(ctx TargetsContext) (TargetsContext, []TargetPath) { - return n.fn.QueryTargets(ctx) -} - -func notMethodCtor(target Function, _ *ParsedParams) (Function, error) { - return ¬Method{fn: target}, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "not_null", "", - ).InCategory( - MethodCategoryCoercion, - "Ensures that the given value is not `null`, and if so returns it, otherwise an error is returned.", - NewExampleSpec("", - `root.a = this.a.not_null()`, - `{"a":"foobar","b":"barbaz"}`, - `{"a":"foobar"}`, - `{"b":"barbaz"}`, - `Error("failed assignment (line 1): field `+"`this.a`"+`: value is null")`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - if v == nil { - return nil, errors.New("value is null") - } - return v, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "number", "", - ).InCategory( - MethodCategoryCoercion, - "Attempt to parse a value into a number. An optional argument can be provided, in which case if the value cannot be parsed into a number the argument will be returned instead.", - NewExampleSpec("", - `root.foo = this.thing.number() + 10 -root.bar = this.thing.number(5) * 10`, - ), - ).Param(ParamFloat("default", "An optional value to yield if the target cannot be parsed as a number.").Optional()), - numberCoerceMethod, -) - -func numberCoerceMethod(target Function, args *ParsedParams) (Function, error) { - defaultNum, err := args.FieldOptionalFloat("default") - if err != nil { - return nil, err - } - return ClosureFunction("method number", func(ctx FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - if defaultNum != nil { - return *defaultNum, nil - } - return nil, err - } - f, err := value.IToNumber(v) - if err != nil { - if defaultNum != nil { - return *defaultNum, nil - } - return nil, ErrFrom(err, target) - } - return f, nil - }, target.QueryTargets), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "or", "If the result of the target query fails or resolves to `null`, returns the argument instead. This is an explicit method alternative to the coalesce pipe operator `|`.", - NewExampleSpec("", `root.doc.id = this.thing.id.or(uuid_v4())`), - ).Param(ParamQuery("fallback", "A value to yield, or query to execute, if the target query fails or resolves to `null`.", true)), - orMethod, -) - -func orMethod(fn Function, args *ParsedParams) (Function, error) { - orFn, err := args.FieldQuery("fallback") - if err != nil { - return nil, err - } - return ClosureFunction("method or", func(ctx FunctionContext) (any, error) { - res, err := fn.Exec(ctx) - if err != nil || value.IIsNull(res) { - return orFn.Exec(ctx) - } - return res, err - }, aggregateTargetPaths(fn, orFn)), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "type", "", - ).InCategory( - MethodCategoryCoercion, - "Returns the type of a value as a string, providing one of the following values: `string`, `bytes`, `number`, `bool`, `timestamp`, `array`, `object` or `null`.", - NewExampleSpec("", - `root.bar_type = this.bar.type() -root.foo_type = this.foo.type()`, - `{"bar":10,"foo":"is a string"}`, - `{"bar_type":"number","foo_type":"string"}`, - ), - NewExampleSpec("", - `root.type = this.type()`, - `"foobar"`, - `{"type":"string"}`, - `666`, - `{"type":"number"}`, - `false`, - `{"type":"bool"}`, - `["foo", "bar"]`, - `{"type":"array"}`, - `{"foo": "bar"}`, - `{"type":"object"}`, - `null`, - `{"type":"null"}`, - ), - NewExampleSpec("", - `root.type = content().type()`, - `foobar`, - `{"type":"bytes"}`, - ), - NewExampleSpec("", - `root.type = this.ts_parse("2006-01-02").type()`, - `"2022-06-06"`, - `{"type":"timestamp"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - return string(value.ITypeOf(v)), nil - }, nil - }, -) diff --git a/internal/bloblang/query/methods_numbers.go b/internal/bloblang/query/methods_numbers.go deleted file mode 100644 index 345c1357bb..0000000000 --- a/internal/bloblang/query/methods_numbers.go +++ /dev/null @@ -1,235 +0,0 @@ -package query - -import ( - "errors" - "fmt" - "math" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -var _ = registerSimpleMethod( - NewMethodSpec("ceil", "Returns the least integer value greater than or equal to a number. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned.").InCategory( - MethodCategoryNumbers, "", - NewExampleSpec("", - `root.new_value = this.value.ceil()`, - `{"value":5.3}`, - `{"new_value":6}`, - `{"value":-5.9}`, - `{"new_value":-5}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return numberMethod(func(f *float64, i *int64, ui *uint64) (any, error) { - if f != nil { - ceiled := math.Ceil(*f) - if i, err := value.IToInt(ceiled); err == nil { - return i, nil - } - return ceiled, nil - } - if i != nil { - return *i, nil - } - return *ui, nil - }), nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "floor", "Returns the greatest integer value less than or equal to the target number. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned.", - ).InCategory( - MethodCategoryNumbers, - "", - NewExampleSpec("", - `root.new_value = this.value.floor()`, - `{"value":5.7}`, - `{"new_value":5}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return numberMethod(func(f *float64, i *int64, ui *uint64) (any, error) { - if f != nil { - floored := math.Floor(*f) - if i, err := value.IToInt(floored); err == nil { - return i, nil - } - return floored, nil - } - if i != nil { - return *i, nil - } - return *ui, nil - }), nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec("log", "Returns the natural logarithm of a number.").InCategory( - MethodCategoryNumbers, "", - NewExampleSpec("", - `root.new_value = this.value.log().round()`, - `{"value":1}`, - `{"new_value":0}`, - `{"value":2.7183}`, - `{"new_value":1}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return numberMethod(func(f *float64, i *int64, ui *uint64) (any, error) { - var v float64 - if f != nil { - v = *f - } else if i != nil { - v = float64(*i) - } else { - v = float64(*ui) - } - return math.Log(v), nil - }), nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec("log10", "Returns the decimal logarithm of a number.").InCategory( - MethodCategoryNumbers, "", - NewExampleSpec("", - `root.new_value = this.value.log10()`, - `{"value":100}`, - `{"new_value":2}`, - `{"value":1000}`, - `{"new_value":3}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return numberMethod(func(f *float64, i *int64, ui *uint64) (any, error) { - var v float64 - if f != nil { - v = *f - } else if i != nil { - v = float64(*i) - } else { - v = float64(*ui) - } - return math.Log10(v), nil - }), nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "max", - "Returns the largest numerical value found within an array. All values must be numerical and the array must not be empty, otherwise an error is returned.", - ).InCategory( - MethodCategoryNumbers, "", - NewExampleSpec("", - `root.biggest = this.values.max()`, - `{"values":[0,3,2.5,7,5]}`, - `{"biggest":7}`, - ), - NewExampleSpec("", - `root.new_value = [0,this.value].max()`, - `{"value":-1}`, - `{"new_value":0}`, - `{"value":7}`, - `{"new_value":7}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - arr, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - if len(arr) == 0 { - return nil, errors.New("the array was empty") - } - var max float64 - for i, n := range arr { - f, err := value.IGetNumber(n) - if err != nil { - return nil, fmt.Errorf("index %v of array: %w", i, err) - } - if i == 0 || f > max { - max = f - } - } - return max, nil - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "min", - "Returns the smallest numerical value found within an array. All values must be numerical and the array must not be empty, otherwise an error is returned.", - ).InCategory( - MethodCategoryNumbers, "", - NewExampleSpec("", - `root.smallest = this.values.min()`, - `{"values":[0,3,-2.5,7,5]}`, - `{"smallest":-2.5}`, - ), - NewExampleSpec("", - `root.new_value = [10,this.value].min()`, - `{"value":2}`, - `{"new_value":2}`, - `{"value":23}`, - `{"new_value":10}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - arr, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - if len(arr) == 0 { - return nil, errors.New("the array was empty") - } - var max float64 - for i, n := range arr { - f, err := value.IGetNumber(n) - if err != nil { - return nil, fmt.Errorf("index %v of array: %w", i, err) - } - if i == 0 || f < max { - max = f - } - } - return max, nil - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "round", "Rounds numbers to the nearest integer, rounding half away from zero. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned.", - ).InCategory( - MethodCategoryNumbers, - "", - NewExampleSpec("", - `root.new_value = this.value.round()`, - `{"value":5.3}`, - `{"new_value":5}`, - `{"value":5.9}`, - `{"new_value":6}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return numberMethod(func(f *float64, i *int64, ui *uint64) (any, error) { - if f != nil { - rounded := math.Round(*f) - if i, err := value.IToInt(rounded); err == nil { - return i, nil - } - return rounded, nil - } - if i != nil { - return *i, nil - } - return *ui, nil - }), nil - }, -) diff --git a/internal/bloblang/query/methods_strings.go b/internal/bloblang/query/methods_strings.go deleted file mode 100644 index e34bd8c873..0000000000 --- a/internal/bloblang/query/methods_strings.go +++ /dev/null @@ -1,2025 +0,0 @@ -package query - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/ascii85" - "encoding/base64" - "encoding/csv" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "hash" - "hash/crc32" - "html" - "io" - "net/url" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/OneOfOne/xxhash" - "github.com/microcosm-cc/bluemonday" - "github.com/tilinna/z85" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "bytes", "", - ).InCategory( - MethodCategoryCoercion, - "Marshal a value into a byte array. If the value is already a byte array it is unchanged.", - NewExampleSpec("", - `root.first_byte = this.name.bytes().index(0)`, - `{"name":"foobar bazson"}`, - `{"first_byte":102}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - return value.IToBytes(v), nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "capitalize", "", - ).InCategory( - MethodCategoryStrings, - "Takes a string value and returns a copy with all Unicode letters that begin words mapped to their Unicode title case.", - NewExampleSpec("", - `root.title = this.title.capitalize()`, - `{"title":"the foo bar"}`, - `{"title":"The Foo Bar"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return cases.Title(language.English).String(t), nil - case []byte: - return cases.Title(language.English).Bytes(t), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "encode", "", - ).InCategory( - MethodCategoryEncoding, - "Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`.", - // NOTE: z85 has been removed from the list until we can support - // misaligned data automatically. It'll still be supported for backwards - // compatibility, but given it behaves differently to `ascii85` I think - // it's a poor user experience to expose it. - NewExampleSpec("", - `root.encoded = this.value.encode("hex")`, - `{"value":"hello world"}`, - `{"encoded":"68656c6c6f20776f726c64"}`, - ), - NewExampleSpec("", - `root.encoded = content().encode("ascii85")`, - `this is totally unstructured data`, - "{\"encoded\":\"FD,B0+DGm>FDl80Ci\\\"A>F`)8BEckl6F`M&(+Cno&@/\"}", - ), - ).Param(ParamString("scheme", "The encoding scheme to use.")), - func(args *ParsedParams) (simpleMethod, error) { - schemeStr, err := args.FieldString("scheme") - if err != nil { - return nil, err - } - - var schemeFn func([]byte) (string, error) - switch schemeStr { - case "base64": - schemeFn = func(b []byte) (string, error) { - var buf bytes.Buffer - e := base64.NewEncoder(base64.StdEncoding, &buf) - _, _ = e.Write(b) - e.Close() - return buf.String(), nil - } - case "base64url": - schemeFn = func(b []byte) (string, error) { - var buf bytes.Buffer - e := base64.NewEncoder(base64.URLEncoding, &buf) - _, _ = e.Write(b) - e.Close() - return buf.String(), nil - } - case "base64rawurl": - schemeFn = func(b []byte) (string, error) { - var buf bytes.Buffer - e := base64.NewEncoder(base64.RawURLEncoding, &buf) - _, _ = e.Write(b) - e.Close() - return buf.String(), nil - } - case "hex": - schemeFn = func(b []byte) (string, error) { - var buf bytes.Buffer - e := hex.NewEncoder(&buf) - if _, err := e.Write(b); err != nil { - return "", err - } - return buf.String(), nil - } - case "ascii85": - schemeFn = func(b []byte) (string, error) { - var buf bytes.Buffer - e := ascii85.NewEncoder(&buf) - if _, err := e.Write(b); err != nil { - return "", err - } - if err := e.Close(); err != nil { - return "", err - } - return buf.String(), nil - } - case "z85": - schemeFn = func(b []byte) (string, error) { - // TODO: Update this to support misaligned input data similar to the - // ascii85 encoder. - enc := make([]byte, z85.EncodedLen(len(b))) - if _, err := z85.Encode(enc, b); err != nil { - return "", err - } - return string(enc), nil - } - default: - return nil, fmt.Errorf("unrecognized encoding type: %v", schemeStr) - } - - return func(v any, ctx FunctionContext) (any, error) { - var res string - var err error - switch t := v.(type) { - case string: - res, err = schemeFn([]byte(t)) - case []byte: - res, err = schemeFn(t) - default: - err = value.NewTypeError(v, value.TString) - } - return res, err - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "decode", "", - ).InCategory( - MethodCategoryEncoding, - "Decodes an encoded string target according to a chosen scheme and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `string`, or encoded using the method `encode`, otherwise it will be base64 encoded by default.\n\nAvailable schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`.", - // NOTE: z85 has been removed from the list until we can support - // misaligned data automatically. It'll still be supported for backwards - // compatibility, but given it behaves differently to `ascii85` I think - // it's a poor user experience to expose it. - NewExampleSpec("", - `root.decoded = this.value.decode("hex").string()`, - `{"value":"68656c6c6f20776f726c64"}`, - `{"decoded":"hello world"}`, - ), - NewExampleSpec("", - `root = this.encoded.decode("ascii85")`, - "{\"encoded\":\"FD,B0+DGm>FDl80Ci\\\"A>F`)8BEckl6F`M&(+Cno&@/\"}", - `this is totally unstructured data`, - ), - ).Param(ParamString("scheme", "The decoding scheme to use.")), - func(args *ParsedParams) (simpleMethod, error) { - schemeStr, err := args.FieldString("scheme") - if err != nil { - return nil, err - } - - var schemeFn func([]byte) ([]byte, error) - switch schemeStr { - case "base64": - schemeFn = func(b []byte) ([]byte, error) { - e := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(b)) - return io.ReadAll(e) - } - case "base64url": - schemeFn = func(b []byte) ([]byte, error) { - e := base64.NewDecoder(base64.URLEncoding, bytes.NewReader(b)) - return io.ReadAll(e) - } - case "base64rawurl": - schemeFn = func(b []byte) ([]byte, error) { - e := base64.NewDecoder(base64.RawURLEncoding, bytes.NewReader(b)) - return io.ReadAll(e) - } - case "hex": - schemeFn = func(b []byte) ([]byte, error) { - e := hex.NewDecoder(bytes.NewReader(b)) - return io.ReadAll(e) - } - case "ascii85": - schemeFn = func(b []byte) ([]byte, error) { - e := ascii85.NewDecoder(bytes.NewReader(b)) - return io.ReadAll(e) - } - case "z85": - schemeFn = func(b []byte) ([]byte, error) { - // TODO: Update this to support misaligned input data similar to the - // ascii85 decoder. - dec := make([]byte, z85.DecodedLen(len(b))) - if _, err := z85.Decode(dec, b); err != nil { - return nil, err - } - return dec, nil - } - default: - return nil, fmt.Errorf("unrecognized encoding type: %v", schemeStr) - } - - return func(v any, ctx FunctionContext) (any, error) { - var res []byte - var err error - switch t := v.(type) { - case string: - res, err = schemeFn([]byte(t)) - case []byte: - res, err = schemeFn(t) - default: - err = value.NewTypeError(v, value.TString) - } - return res, err - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "encrypt_aes", "", - ).InCategory( - MethodCategoryEncoding, - "Encrypts a string or byte array target according to a chosen AES encryption method and returns a string result. The algorithms require a key and an initialization vector / nonce. Available schemes are: `ctr`, `ofb`, `cbc`.", - NewExampleSpec("", - `let key = "2b7e151628aed2a6abf7158809cf4f3c".decode("hex") -let vector = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff".decode("hex") -root.encrypted = this.value.encrypt_aes("ctr", $key, $vector).encode("hex")`, - `{"value":"hello world!"}`, - `{"encrypted":"84e9b31ff7400bdf80be7254"}`, - ), - ). - Param(ParamString("scheme", "The scheme to use for encryption, one of `ctr`, `ofb`, `cbc`.")). - Param(ParamString("key", "A key to encrypt with.")). - Param(ParamString("iv", "An initialization vector / nonce.")), - func(args *ParsedParams) (simpleMethod, error) { - schemeStr, err := args.FieldString("scheme") - if err != nil { - return nil, err - } - keyStr, err := args.FieldString("key") - if err != nil { - return nil, err - } - block, err := aes.NewCipher([]byte(keyStr)) - if err != nil { - return nil, err - } - - ivStr, err := args.FieldString("iv") - if err != nil { - return nil, err - } - iv := []byte(ivStr) - if len(iv) != block.BlockSize() { - return nil, errors.New("the key must match the initialisation vector size") - } - - var schemeFn func([]byte) (string, error) - switch schemeStr { - case "ctr": - schemeFn = func(b []byte) (string, error) { - ciphertext := make([]byte, len(b)) - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(ciphertext, b) - return string(ciphertext), nil - } - case "ofb": - schemeFn = func(b []byte) (string, error) { - ciphertext := make([]byte, len(b)) - stream := cipher.NewOFB(block, iv) - stream.XORKeyStream(ciphertext, b) - return string(ciphertext), nil - } - case "cbc": - schemeFn = func(b []byte) (string, error) { - if len(b)%aes.BlockSize != 0 { - return "", errors.New("plaintext is not a multiple of the block size") - } - - ciphertext := make([]byte, len(b)) - stream := cipher.NewCBCEncrypter(block, iv) - stream.CryptBlocks(ciphertext, b) - return string(ciphertext), nil - } - default: - return nil, fmt.Errorf("unrecognized encryption type: %v", schemeStr) - } - return func(v any, ctx FunctionContext) (any, error) { - var res string - var err error - switch t := v.(type) { - case string: - res, err = schemeFn([]byte(t)) - case []byte: - res, err = schemeFn(t) - default: - err = value.NewTypeError(v, value.TString) - } - return res, err - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "decrypt_aes", "", - ).InCategory( - MethodCategoryEncoding, - "Decrypts an encrypted string or byte array target according to a chosen AES encryption method and returns the result as a byte array. The algorithms require a key and an initialization vector / nonce. Available schemes are: `ctr`, `ofb`, `cbc`.", - NewExampleSpec("", - `let key = "2b7e151628aed2a6abf7158809cf4f3c".decode("hex") -let vector = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff".decode("hex") -root.decrypted = this.value.decode("hex").decrypt_aes("ctr", $key, $vector).string()`, - `{"value":"84e9b31ff7400bdf80be7254"}`, - `{"decrypted":"hello world!"}`, - ), - ). - Param(ParamString("scheme", "The scheme to use for decryption, one of `ctr`, `ofb`, `cbc`.")). - Param(ParamString("key", "A key to decrypt with.")). - Param(ParamString("iv", "An initialization vector / nonce.")), - func(args *ParsedParams) (simpleMethod, error) { - schemeStr, err := args.FieldString("scheme") - if err != nil { - return nil, err - } - - keyStr, err := args.FieldString("key") - if err != nil { - return nil, err - } - block, err := aes.NewCipher([]byte(keyStr)) - if err != nil { - return nil, err - } - - ivStr, err := args.FieldString("iv") - if err != nil { - return nil, err - } - iv := []byte(ivStr) - if len(iv) != block.BlockSize() { - return nil, errors.New("the key must match the initialisation vector size") - } - - var schemeFn func([]byte) ([]byte, error) - switch schemeStr { - case "ctr": - schemeFn = func(b []byte) ([]byte, error) { - plaintext := make([]byte, len(b)) - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(plaintext, b) - return plaintext, nil - } - case "ofb": - schemeFn = func(b []byte) ([]byte, error) { - plaintext := make([]byte, len(b)) - stream := cipher.NewOFB(block, iv) - stream.XORKeyStream(plaintext, b) - return plaintext, nil - } - case "cbc": - schemeFn = func(b []byte) ([]byte, error) { - if len(b)%aes.BlockSize != 0 { - return nil, errors.New("ciphertext is not a multiple of the block size") - } - stream := cipher.NewCBCDecrypter(block, iv) - stream.CryptBlocks(b, b) - return b, nil - } - default: - return nil, fmt.Errorf("unrecognized decryption type: %v", schemeStr) - } - return func(v any, ctx FunctionContext) (any, error) { - var res []byte - var err error - switch t := v.(type) { - case string: - res, err = schemeFn([]byte(t)) - case []byte: - res, err = schemeFn(t) - default: - err = value.NewTypeError(v, value.TString) - } - return res, err - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "escape_html", "", - ).InCategory( - MethodCategoryStrings, - "Escapes a string so that special characters like `<` to become `<`. It escapes only five such characters: `<`, `>`, `&`, `'` and `\"` so that it can be safely placed within an HTML entity.", - NewExampleSpec("", - `root.escaped = this.value.escape_html()`, - `{"value":"foo & bar"}`, - `{"escaped":"foo & bar"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return html.EscapeString(s), nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "index_of", "", - ).InCategory( - MethodCategoryStrings, - "Returns the starting index of the argument substring in a string target, or `-1` if the target doesn't contain the argument.", - NewExampleSpec("", - `root.index = this.thing.index_of("bar")`, - `{"thing":"foobar"}`, - `{"index":3}`, - ), - NewExampleSpec("", - `root.index = content().index_of("meow")`, - `the cat meowed, the dog woofed`, - `{"index":8}`, - ), - ).Param(ParamString("value", "A string to search for.")), - func(args *ParsedParams) (simpleMethod, error) { - substring, err := args.FieldString("value") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return int64(strings.Index(t, substring)), nil - case []byte: - return int64(bytes.Index(t, []byte(substring))), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "unescape_html", "", - ).InCategory( - MethodCategoryStrings, - "Unescapes a string so that entities like `<` become `<`. It unescapes a larger range of entities than `escape_html` escapes. For example, `á` unescapes to `á`, as does `á` and `&xE1;`.", - NewExampleSpec("", - `root.unescaped = this.value.unescape_html()`, - `{"value":"foo & bar"}`, - `{"unescaped":"foo & bar"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return html.UnescapeString(s), nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "escape_url_query", "", - ).InCategory( - MethodCategoryStrings, - "Escapes a string so that it can be safely placed within a URL query.", - NewExampleSpec("", - `root.escaped = this.value.escape_url_query()`, - `{"value":"foo & bar"}`, - `{"escaped":"foo+%26+bar"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return url.QueryEscape(s), nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "unescape_url_query", "", - ).InCategory( - MethodCategoryStrings, - "Expands escape sequences from a URL query string.", - NewExampleSpec("", - `root.unescaped = this.value.unescape_url_query()`, - `{"value":"foo+%26+bar"}`, - `{"unescaped":"foo & bar"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return url.QueryUnescape(s) - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "filepath_join", "", - ).InCategory( - MethodCategoryStrings, - "Joins an array of path elements into a single file path. The separator depends on the operating system of the machine.", - NewExampleSpec("", - `root.path = this.path_elements.filepath_join()`, - strings.ReplaceAll(`{"path_elements":["/foo/","bar.txt"]}`, "/", string(filepath.Separator)), - strings.ReplaceAll(`{"path":"/foo/bar.txt"}`, "/", string(filepath.Separator)), - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - arr, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - strs := make([]string, 0, len(arr)) - for i, ele := range arr { - str, err := value.IGetString(ele) - if err != nil { - return nil, fmt.Errorf("path element %v: %w", i, err) - } - strs = append(strs, str) - } - return filepath.Join(strs...), nil - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "filepath_split", "", - ).InCategory( - MethodCategoryStrings, - "Splits a file path immediately following the final Separator, separating it into a directory and file name component returned as a two element array of strings. If there is no Separator in the path, the first element will be empty and the second will contain the path. The separator depends on the operating system of the machine.", - NewExampleSpec("", - `root.path_sep = this.path.filepath_split()`, - strings.ReplaceAll(`{"path":"/foo/bar.txt"}`, "/", string(filepath.Separator)), - strings.ReplaceAll(`{"path_sep":["/foo/","bar.txt"]}`, "/", string(filepath.Separator)), - `{"path":"baz.txt"}`, - `{"path_sep":["","baz.txt"]}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - dir, file := filepath.Split(s) - return []any{dir, file}, nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "format", "", - ).InCategory( - MethodCategoryStrings, - "Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go https://pkg.go.dev/fmt[`fmt` package documentation] for the list of valid format verbs.", - NewExampleSpec("", - `root.foo = "%s(%v): %v".format(this.name, this.age, this.fingers)`, - `{"name":"lance","age":37,"fingers":13}`, - `{"foo":"lance(37): 13"}`, - ), - ).VariadicParams(), - func(args *ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return fmt.Sprintf(s, args.Raw()...), nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "has_prefix", "", - ).InCategory( - MethodCategoryStrings, - "Checks whether a string has a prefix argument and returns a bool.", - NewExampleSpec("", - `root.t1 = this.v1.has_prefix("foo") -root.t2 = this.v2.has_prefix("foo")`, - `{"v1":"foobar","v2":"barfoo"}`, - `{"t1":true,"t2":false}`, - ), - ).Param(ParamString("value", "The string to test.")), - func(args *ParsedParams) (simpleMethod, error) { - prefix, err := args.FieldString("value") - if err != nil { - return nil, err - } - prefixB := []byte(prefix) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.HasPrefix(t, prefix), nil - case []byte: - return bytes.HasPrefix(t, prefixB), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "has_suffix", "", - ).InCategory( - MethodCategoryStrings, - "Checks whether a string has a suffix argument and returns a bool.", - NewExampleSpec("", - `root.t1 = this.v1.has_suffix("foo") -root.t2 = this.v2.has_suffix("foo")`, - `{"v1":"foobar","v2":"barfoo"}`, - `{"t1":false,"t2":true}`, - ), - ).Param(ParamString("value", "The string to test.")), - func(args *ParsedParams) (simpleMethod, error) { - suffix, err := args.FieldString("value") - if err != nil { - return nil, err - } - suffixB := []byte(suffix) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.HasSuffix(t, suffix), nil - case []byte: - return bytes.HasSuffix(t, suffixB), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "hash", "", - ).InCategory( - MethodCategoryEncoding, - ` -Hashes a string or byte array according to a chosen algorithm and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `+"xref:guides:bloblang/methods.adoc#string[`string`], or encoded using the method xref:guides:bloblang/methods.adoc#encode[`encode`]"+`, otherwise it will be base64 encoded by default. - -Available algorithms are: `+"`hmac_sha1`, `hmac_sha256`, `hmac_sha512`, `md5`, `sha1`, `sha256`, `sha512`, `xxhash64`, `crc32`"+`. - -The following algorithms require a key, which is specified as a second argument: `+"`hmac_sha1`, `hmac_sha256`, `hmac_sha512`"+`.`, - NewExampleSpec("", - `root.h1 = this.value.hash("sha1").encode("hex") -root.h2 = this.value.hash("hmac_sha1","static-key").encode("hex")`, - `{"value":"hello world"}`, - `{"h1":"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed","h2":"d87e5f068fa08fe90bb95bc7c8344cb809179d76"}`, - ), - NewExampleSpec("The `crc32` algorithm supports options for the polynomial.", - `root.h1 = this.value.hash(algorithm: "crc32", polynomial: "Castagnoli").encode("hex") -root.h2 = this.value.hash(algorithm: "crc32", polynomial: "Koopman").encode("hex")`, - `{"value":"hello world"}`, - `{"h1":"c99465aa","h2":"df373d3c"}`, - ), - ). - Param(ParamString("algorithm", "The hasing algorithm to use.")). - Param(ParamString("key", "An optional key to use.").Optional()). - Param(ParamString("polynomial", "An optional polynomial key to use when selecting the `crc32` algorithm, otherwise ignored. Options are `IEEE` (default), `Castagnoli` and `Koopman`").Default("IEEE")), - func(args *ParsedParams) (simpleMethod, error) { - algorithmStr, err := args.FieldString("algorithm") - if err != nil { - return nil, err - } - var key []byte - keyParam, err := args.FieldOptionalString("key") - if err != nil { - return nil, err - } - if keyParam != nil { - key = []byte(*keyParam) - } - poly, err := args.FieldString("polynomial") - if err != nil { - return nil, err - } - var hashFn func([]byte) ([]byte, error) - switch algorithmStr { - case "hmac_sha1", "hmac-sha1": - if len(key) == 0 { - return nil, fmt.Errorf("hash algorithm %v requires a key argument", algorithmStr) - } - hashFn = func(b []byte) ([]byte, error) { - hasher := hmac.New(sha1.New, key) - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "hmac_sha256", "hmac-sha256": - if len(key) == 0 { - return nil, fmt.Errorf("hash algorithm %v requires a key argument", algorithmStr) - } - hashFn = func(b []byte) ([]byte, error) { - hasher := hmac.New(sha256.New, key) - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "hmac_sha512", "hmac-sha512": - if len(key) == 0 { - return nil, fmt.Errorf("hash algorithm %v requires a key argument", algorithmStr) - } - hashFn = func(b []byte) ([]byte, error) { - hasher := hmac.New(sha512.New, key) - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "md5": - hashFn = func(b []byte) ([]byte, error) { - hasher := md5.New() - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "sha1": - hashFn = func(b []byte) ([]byte, error) { - hasher := sha1.New() - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "sha256": - hashFn = func(b []byte) ([]byte, error) { - hasher := sha256.New() - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "sha512": - hashFn = func(b []byte) ([]byte, error) { - hasher := sha512.New() - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - case "xxhash64": - hashFn = func(b []byte) ([]byte, error) { - h := xxhash.New64() - _, _ = h.Write(b) - return []byte(strconv.FormatUint(h.Sum64(), 10)), nil - } - case "crc32": - hashFn = func(b []byte) ([]byte, error) { - var hasher hash.Hash - switch poly { - case "IEEE": - hasher = crc32.NewIEEE() - case "Castagnoli": - hasher = crc32.New(crc32.MakeTable(crc32.Castagnoli)) - case "Koopman": - hasher = crc32.New(crc32.MakeTable(crc32.Koopman)) - default: - return nil, fmt.Errorf("unsupported crc32 hash key %q", poly) - } - _, _ = hasher.Write(b) - return hasher.Sum(nil), nil - } - default: - return nil, fmt.Errorf("unrecognized hash type: %v", algorithmStr) - } - return func(v any, ctx FunctionContext) (any, error) { - var res []byte - var err error - switch t := v.(type) { - case string: - res, err = hashFn([]byte(t)) - case []byte: - res, err = hashFn(t) - default: - err = value.NewTypeError(v, value.TString) - } - return res, err - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "join", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Join an array of strings with an optional delimiter into a single string.", - NewExampleSpec("", - `root.joined_words = this.words.join() -root.joined_numbers = this.numbers.map_each(this.string()).join(",")`, - `{"words":["hello","world"],"numbers":[3,8,11]}`, - `{"joined_numbers":"3,8,11","joined_words":"helloworld"}`, - ), - ).Param(ParamString("delimiter", "An optional delimiter to add between each string.").Optional()), - func(args *ParsedParams) (simpleMethod, error) { - delimArg, err := args.FieldOptionalString("delimiter") - if err != nil { - return nil, err - } - delim := "" - if delimArg != nil { - delim = *delimArg - } - return func(v any, ctx FunctionContext) (any, error) { - slice, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - - var buf bytes.Buffer - for i, sv := range slice { - if i > 0 { - _, _ = buf.WriteString(delim) - } - switch t := sv.(type) { - case string: - _, _ = buf.WriteString(t) - case []byte: - _, _ = buf.Write(t) - default: - return nil, fmt.Errorf("failed to join element %v: %w", i, value.NewTypeError(sv, value.TString)) - } - } - return buf.String(), nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "uppercase", "", - ).InCategory( - MethodCategoryStrings, - "Convert a string value into uppercase.", - NewExampleSpec("", - `root.foo = this.foo.uppercase()`, - `{"foo":"hello world"}`, - `{"foo":"HELLO WORLD"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.ToUpper(t), nil - case []byte: - return bytes.ToUpper(t), nil - default: - return nil, value.NewTypeError(v, value.TString) - } - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "lowercase", "", - ).InCategory( - MethodCategoryStrings, - "Convert a string value into lowercase.", - NewExampleSpec("", - `root.foo = this.foo.lowercase()`, - `{"foo":"HELLO WORLD"}`, - `{"foo":"hello world"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.ToLower(t), nil - case []byte: - return bytes.ToLower(t), nil - default: - return nil, value.NewTypeError(v, value.TString) - } - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "parse_csv", "", - ).InCategory( - MethodCategoryParsing, - "Attempts to parse a string into an array of objects by following the CSV format described in RFC 4180.", - NewExampleSpec("Parses CSV data with a header row", - `root.orders = this.orders.parse_csv()`, - `{"orders":"foo,bar\nfoo 1,bar 1\nfoo 2,bar 2"}`, - `{"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]}`, - ), - NewExampleSpec("Parses CSV data without a header row", - `root.orders = this.orders.parse_csv(false)`, - `{"orders":"foo 1,bar 1\nfoo 2,bar 2"}`, - `{"orders":[["foo 1","bar 1"],["foo 2","bar 2"]]}`, - ), - NewExampleSpec("Parses CSV data delimited by dots", - `root.orders = this.orders.parse_csv(delimiter:".")`, - `{"orders":"foo.bar\nfoo 1.bar 1\nfoo 2.bar 2"}`, - `{"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]}`, - ), - NewExampleSpec("Parses CSV data containing a quote in an unquoted field", - `root.orders = this.orders.parse_csv(lazy_quotes:true)`, - `{"orders":"foo,bar\nfoo 1,bar 1\nfoo\" \"2,bar\" \"2"}`, - `{"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar\" \"2","foo":"foo\" \"2"}]}`, - )). - Param(ParamBool("parse_header_row", "Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, the output will be an array of row arrays.").Default(true)). - Param(ParamString("delimiter", "The delimiter to use for splitting values in each record. It must be a single character.").Default(",")). - Param(ParamBool("lazy_quotes", "If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field.").Default(false)), - parseCSVMethod, -) - -func parseCSVMethod(args *ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - var parseHeaderRow bool - var optBool *bool - var err error - if optBool, err = args.FieldOptionalBool("parse_header_row"); err != nil { - return nil, err - } - parseHeaderRow = *optBool - - var delimiter rune - var optString *string - if optString, err = args.FieldOptionalString("delimiter"); err != nil { - return nil, err - } - delimRunes := []rune(*optString) - if len(delimRunes) != 1 { - return nil, errors.New("delimiter value must be exactly one character") - } - delimiter = delimRunes[0] - - var lazyQuotes bool - if optBool, err = args.FieldOptionalBool("lazy_quotes"); err != nil { - return nil, err - } - lazyQuotes = *optBool - - var csvBytes []byte - switch t := v.(type) { - case string: - csvBytes = []byte(t) - case []byte: - csvBytes = t - default: - return nil, value.NewTypeError(v, value.TString) - } - - r := csv.NewReader(bytes.NewReader(csvBytes)) - r.Comma = delimiter - r.LazyQuotes = lazyQuotes - strRecords, err := r.ReadAll() - if err != nil { - return nil, err - } - if len(strRecords) == 0 { - return nil, errors.New("zero records were parsed") - } - - var records []any - if parseHeaderRow { - records = make([]any, 0, len(strRecords)-1) - headers := strRecords[0] - if len(headers) == 0 { - return nil, errors.New("no headers found on first row") - } - for j, strRecord := range strRecords[1:] { - if len(headers) != len(strRecord) { - return nil, fmt.Errorf("record on line %v: record mismatch with headers", j) - } - obj := make(map[string]any, len(strRecord)) - for i, r := range strRecord { - obj[headers[i]] = r - } - records = append(records, obj) - } - } else { - records = make([]any, 0, len(strRecords)) - for _, rec := range strRecords { - genericSlice := make([]any, len(rec)) - for i, v := range rec { - genericSlice[i] = v - } - records = append(records, genericSlice) - } - } - - return records, nil - }, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "parse_json", "", - ).Param( - ParamBool("use_number", "An optional flag that when set makes parsing numbers as json.Number instead of the default float64.").Optional(), - ).InCategory( - MethodCategoryParsing, - "Attempts to parse a string as a JSON document and returns the result.", - NewExampleSpec("", - `root.doc = this.doc.parse_json()`, - `{"doc":"{\"foo\":\"bar\"}"}`, - `{"doc":{"foo":"bar"}}`, - ), - NewExampleSpec("", - `root.doc = this.doc.parse_json(use_number: true)`, - `{"doc":"{\"foo\":\"11380878173205700000000000000000000000000000000\"}"}`, - `{"doc":{"foo":"11380878173205700000000000000000000000000000000"}}`, - ), - ), - func(args *ParsedParams) (simpleMethod, error) { - useNumber, err := args.FieldOptionalBool("use_number") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - var jsonBytes []byte - switch t := v.(type) { - case string: - jsonBytes = []byte(t) - case []byte: - jsonBytes = t - default: - return nil, value.NewTypeError(v, value.TString) - } - var jObj any - decoder := json.NewDecoder(bytes.NewReader(jsonBytes)) - if useNumber != nil && *useNumber { - decoder.UseNumber() - } - if err := decoder.Decode(&jObj); err != nil { - return nil, fmt.Errorf("failed to parse value as JSON: %w", err) - } - return jObj, nil - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "parse_yaml", "", - ).InCategory( - MethodCategoryParsing, - "Attempts to parse a string as a single YAML document and returns the result.", - NewExampleSpec("", - `root.doc = this.doc.parse_yaml()`, - `{"doc":"foo: bar"}`, - `{"doc":{"foo":"bar"}}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - var yamlBytes []byte - switch t := v.(type) { - case string: - yamlBytes = []byte(t) - case []byte: - yamlBytes = t - default: - return nil, value.NewTypeError(v, value.TString) - } - var sObj any - if err := yaml.Unmarshal(yamlBytes, &sObj); err != nil { - return nil, fmt.Errorf("failed to parse value as YAML: %w", err) - } - return sObj, nil - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "format_yaml", "", - ).InCategory( - MethodCategoryParsing, - "Serializes a target value into a YAML byte array.", - NewExampleSpec("", - `root = this.doc.format_yaml()`, - `{"doc":{"foo":"bar"}}`, - `foo: bar -`, - ), - NewExampleSpec("Use the `.string()` method in order to coerce the result into a string.", - `root.doc = this.doc.format_yaml().string()`, - `{"doc":{"foo":"bar"}}`, - `{"doc":"foo: bar\n"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - return yaml.Marshal(v) - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "format_json", "", - ).InCategory( - MethodCategoryParsing, - "Serializes a target value into a pretty-printed JSON byte array (with 4 space indentation by default).", - NewExampleSpec("", - `root = this.doc.format_json()`, - `{"doc":{"foo":"bar"}}`, - `{ - "foo": "bar" -}`, - ), - NewExampleSpec("Pass a string to the `indent` parameter in order to customise the indentation.", - `root = this.format_json(" ")`, - `{"doc":{"foo":"bar"}}`, - `{ - "doc": { - "foo": "bar" - } -}`, - ), - NewExampleSpec("Use the `.string()` method in order to coerce the result into a string.", - `root.doc = this.doc.format_json().string()`, - `{"doc":{"foo":"bar"}}`, - `{"doc":"{\n \"foo\": \"bar\"\n}"}`, - ), - NewExampleSpec("Set the `no_indent` parameter to true to disable indentation. The result is equivalent to calling `bytes()`.", - `root = this.doc.format_json(no_indent: true)`, - `{"doc":{"foo":"bar"}}`, - `{"foo":"bar"}`, - ), - ). - Beta(). - Param(ParamString( - "indent", - "Indentation string. Each element in a JSON object or array will begin on a new, indented line followed by one or more copies of indent according to the indentation nesting.", - ).Default(strings.Repeat(" ", 4))). - Param(ParamBool( - "no_indent", - "Disable indentation.", - ).Default(false)), - func(args *ParsedParams) (simpleMethod, error) { - indentOpt, err := args.FieldOptionalString("indent") - if err != nil { - return nil, err - } - indent := "" - if indentOpt != nil { - indent = *indentOpt - } - noIndentOpt, err := args.FieldOptionalBool("no_indent") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - if *noIndentOpt { - return json.Marshal(v) - } - return json.MarshalIndent(v, "", indent) - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "parse_url", "Attempts to parse a URL from a string value, returning a structured result that describes the various facets of the URL. The fields returned within the structured result roughly follow https://pkg.go.dev/net/url#URL, and may be expanded in future in order to present more information.", - ).InCategory( - MethodCategoryParsing, "", - NewExampleSpec("", - `root.foo_url = this.foo_url.parse_url()`, - `{"foo_url":"https://www.docs.redpanda.com/redpanda-connect/guides/bloblang/about/"}`, - `{"foo_url":{"fragment":"","host":"www.docs.redpanda.com","opaque":"","path":"/redpanda-connect/guides/bloblang/about/","raw_fragment":"","raw_path":"","raw_query":"","scheme":"https"}}`, - ), - NewExampleSpec("", - `root.username = this.url.parse_url().user.name | "unknown"`, - `{"url":"amqp://foo:bar@127.0.0.1:5672/"}`, - `{"username":"foo"}`, - `{"url":"redis://localhost:6379"}`, - `{"username":"unknown"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(data string) (any, error) { - urlParsed, err := url.Parse(data) - if err != nil { - return nil, err - } - values := map[string]any{ - "scheme": urlParsed.Scheme, - "opaque": urlParsed.Opaque, - "host": urlParsed.Host, - "path": urlParsed.Path, - "raw_path": urlParsed.RawPath, - "raw_query": urlParsed.RawQuery, - "fragment": urlParsed.Fragment, - "raw_fragment": urlParsed.RawFragment, - } - if urlParsed.User != nil { - userObj := map[string]any{ - "name": urlParsed.User.Username(), - } - if pass, exists := urlParsed.User.Password(); exists { - userObj["password"] = pass - } - values["user"] = userObj - } - return values, nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "reverse", "", - ).InCategory( - MethodCategoryStrings, - "Returns the target string in reverse order.", - NewExampleSpec("", - `root.reversed = this.thing.reverse()`, - `{"thing":"backwards"}`, - `{"reversed":"sdrawkcab"}`, - ), - NewExampleSpec("", - `root = content().reverse()`, - `{"thing":"backwards"}`, - `}"sdrawkcab":"gniht"{`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - runes := []rune(t) - for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { - runes[i], runes[j] = runes[j], runes[i] - } - return string(runes), nil - case []byte: - result := make([]byte, len(t)) - for i, b := range t { - result[len(t)-i-1] = b - } - return result, nil - } - - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "quote", "", - ).InCategory( - MethodCategoryStrings, - "Quotes a target string using escape sequences (`\\t`, `\\n`, `\\xFF`, `\\u0100`) for control characters and non-printable characters.", - NewExampleSpec("", - `root.quoted = this.thing.quote()`, - `{"thing":"foo\nbar"}`, - `{"quoted":"\"foo\\nbar\""}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return strconv.Quote(s), nil - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "unquote", "", - ).InCategory( - MethodCategoryStrings, - "Unquotes a target string, expanding any escape sequences (`\\t`, `\\n`, `\\xFF`, `\\u0100`) for control characters and non-printable characters.", - NewExampleSpec("", - `root.unquoted = this.thing.unquote()`, - `{"thing":"\"foo\\nbar\""}`, - `{"unquoted":"foo\nbar"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return stringMethod(func(s string) (any, error) { - return strconv.Unquote(s) - }), nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewHiddenMethodSpec("replace"). - Param(ParamString("old", "A string to match against.")). - Param(ParamString("new", "A string to replace with.")), - replaceAllImpl, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "replace_all", "", - ).InCategory( - MethodCategoryStrings, - "Replaces all occurrences of the first argument in a target string with the second argument.", - NewExampleSpec("", - `root.new_value = this.value.replace_all("foo","dog")`, - `{"value":"The foo ate my homework"}`, - `{"new_value":"The dog ate my homework"}`, - ), - ). - Param(ParamString("old", "A string to match against.")). - Param(ParamString("new", "A string to replace with.")), - replaceAllImpl, -) - -func replaceAllImpl(args *ParsedParams) (simpleMethod, error) { - oldStr, err := args.FieldString("old") - if err != nil { - return nil, err - } - newStr, err := args.FieldString("new") - if err != nil { - return nil, err - } - oldB, newB := []byte(oldStr), []byte(newStr) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.ReplaceAll(t, oldStr, newStr), nil - case []byte: - return bytes.ReplaceAll(t, oldB, newB), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewHiddenMethodSpec("replace_many"). - Param(ParamArray("values", "An array of values, each even value will be replaced with the following odd value.")), - replaceAllManyImpl, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "replace_all_many", "", - ).InCategory( - MethodCategoryStrings, - "For each pair of strings in an argument array, replaces all occurrences of the first item of the pair with the second. This is a more compact way of chaining a series of `replace_all` methods.", - NewExampleSpec("", - `root.new_value = this.value.replace_all_many([ - "", "<b>", - "", "</b>", - "", "<i>", - "", "</i>", -])`, - `{"value":"Hello World"}`, - `{"new_value":"<i>Hello</i> <b>World</b>"}`, - ), - ).Param(ParamArray("values", "An array of values, each even value will be replaced with the following odd value.")), - replaceAllManyImpl, -) - -func replaceAllManyImpl(args *ParsedParams) (simpleMethod, error) { - items, err := args.FieldArray("values") - if err != nil { - return nil, err - } - if len(items)%2 != 0 { - return nil, fmt.Errorf("invalid arg, replacements should be in pairs and must therefore be even: %v", items) - } - - var replacePairs [][2]string - var replacePairsBytes [][2][]byte - - for i := 0; i < len(items); i += 2 { - from, err := value.IGetString(items[i]) - if err != nil { - return nil, fmt.Errorf("invalid replacement value at index %v: %w", i, err) - } - to, err := value.IGetString(items[i+1]) - if err != nil { - return nil, fmt.Errorf("invalid replacement value at index %v: %w", i+1, err) - } - replacePairs = append(replacePairs, [2]string{from, to}) - replacePairsBytes = append(replacePairsBytes, [2][]byte{[]byte(from), []byte(to)}) - } - - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - for _, pair := range replacePairs { - t = strings.ReplaceAll(t, pair[0], pair[1]) - } - return t, nil - case []byte: - for _, pair := range replacePairsBytes { - t = bytes.ReplaceAll(t, pair[0], pair[1]) - } - return t, nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "re_find_all", "", - ).InCategory( - MethodCategoryRegexp, - "Returns an array containing all successive matches of a regular expression in a string.", - NewExampleSpec("", - `root.matches = this.value.re_find_all("a.")`, - `{"value":"paranormal"}`, - `{"matches":["ar","an","al"]}`, - ), - ).Param(ParamString("pattern", "The pattern to match against.")), - func(args *ParsedParams) (simpleMethod, error) { - reStr, err := args.FieldString("pattern") - if err != nil { - return nil, err - } - re, err := regexp.Compile(reStr) - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - var result []any - switch t := v.(type) { - case string: - matches := re.FindAllString(t, -1) - result = make([]any, 0, len(matches)) - for _, str := range matches { - result = append(result, str) - } - case []byte: - matches := re.FindAll(t, -1) - result = make([]any, 0, len(matches)) - for _, str := range matches { - result = append(result, string(str)) - } - default: - return nil, value.NewTypeError(v, value.TString) - } - return result, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "re_find_all_submatch", "", - ).InCategory( - MethodCategoryRegexp, - "Returns an array of arrays containing all successive matches of the regular expression in a string and the matches, if any, of its subexpressions.", - NewExampleSpec("", - `root.matches = this.value.re_find_all_submatch("a(x*)b")`, - `{"value":"-axxb-ab-"}`, - `{"matches":[["axxb","xx"],["ab",""]]}`, - ), - ).Param(ParamString("pattern", "The pattern to match against.")), - func(args *ParsedParams) (simpleMethod, error) { - reStr, err := args.FieldString("pattern") - if err != nil { - return nil, err - } - re, err := regexp.Compile(reStr) - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - var result []any - switch t := v.(type) { - case string: - groupMatches := re.FindAllStringSubmatch(t, -1) - result = make([]any, 0, len(groupMatches)) - for _, matches := range groupMatches { - r := make([]any, 0, len(matches)) - for _, str := range matches { - r = append(r, str) - } - result = append(result, r) - } - case []byte: - groupMatches := re.FindAllSubmatch(t, -1) - result = make([]any, 0, len(groupMatches)) - for _, matches := range groupMatches { - r := make([]any, 0, len(matches)) - for _, str := range matches { - r = append(r, string(str)) - } - result = append(result, r) - } - default: - return nil, value.NewTypeError(v, value.TString) - } - return result, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "re_find_object", "", - ).InCategory( - MethodCategoryRegexp, - "Returns an object containing the first match of the regular expression and the matches of its subexpressions. The key of each match value is the name of the group when specified, otherwise it is the index of the matching group, starting with the expression as a whole at 0.", - NewExampleSpec("", - `root.matches = this.value.re_find_object("a(?Px*)b")`, - `{"value":"-axxb-ab-"}`, - `{"matches":{"0":"axxb","foo":"xx"}}`, - ), - NewExampleSpec("", - `root.matches = this.value.re_find_object("(?P\\w+):\\s+(?P\\w+)")`, - `{"value":"option1: value1"}`, - `{"matches":{"0":"option1: value1","key":"option1","value":"value1"}}`, - ), - ).Param(ParamString("pattern", "The pattern to match against.")), - func(args *ParsedParams) (simpleMethod, error) { - reStr, err := args.FieldString("pattern") - if err != nil { - return nil, err - } - re, err := regexp.Compile(reStr) - if err != nil { - return nil, err - } - groups := re.SubexpNames() - for i, k := range groups { - if k == "" { - groups[i] = strconv.Itoa(i) - } - } - return func(v any, ctx FunctionContext) (any, error) { - result := make(map[string]any, len(groups)) - switch t := v.(type) { - case string: - groupMatches := re.FindStringSubmatch(t) - for i, match := range groupMatches { - key := groups[i] - result[key] = match - } - case []byte: - groupMatches := re.FindSubmatch(t) - for i, match := range groupMatches { - key := groups[i] - result[key] = match - } - default: - return nil, value.NewTypeError(v, value.TString) - } - return result, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "re_find_all_object", "", - ).InCategory( - MethodCategoryRegexp, - "Returns an array of objects containing all matches of the regular expression and the matches of its subexpressions. The key of each match value is the name of the group when specified, otherwise it is the index of the matching group, starting with the expression as a whole at 0.", - NewExampleSpec("", - `root.matches = this.value.re_find_all_object("a(?Px*)b")`, - `{"value":"-axxb-ab-"}`, - `{"matches":[{"0":"axxb","foo":"xx"},{"0":"ab","foo":""}]}`, - ), - NewExampleSpec("", - `root.matches = this.value.re_find_all_object("(?m)(?P\\w+):\\s+(?P\\w+)$")`, - `{"value":"option1: value1\noption2: value2\noption3: value3"}`, - `{"matches":[{"0":"option1: value1","key":"option1","value":"value1"},{"0":"option2: value2","key":"option2","value":"value2"},{"0":"option3: value3","key":"option3","value":"value3"}]}`, - ), - ).Param(ParamString("pattern", "The pattern to match against.")), - func(args *ParsedParams) (simpleMethod, error) { - reStr, err := args.FieldString("pattern") - if err != nil { - return nil, err - } - re, err := regexp.Compile(reStr) - if err != nil { - return nil, err - } - groups := re.SubexpNames() - for i, k := range groups { - if k == "" { - groups[i] = strconv.Itoa(i) - } - } - return func(v any, ctx FunctionContext) (any, error) { - var result []any - switch t := v.(type) { - case string: - reMatches := re.FindAllStringSubmatch(t, -1) - result = make([]any, 0, len(reMatches)) - for _, matches := range reMatches { - obj := make(map[string]any, len(groups)) - for i, match := range matches { - key := groups[i] - obj[key] = match - } - result = append(result, obj) - } - case []byte: - reMatches := re.FindAllSubmatch(t, -1) - result = make([]any, 0, len(reMatches)) - for _, matches := range reMatches { - obj := make(map[string]any, len(groups)) - for i, match := range matches { - key := groups[i] - obj[key] = match - } - result = append(result, obj) - } - default: - return nil, value.NewTypeError(v, value.TString) - } - return result, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "re_match", "", - ).InCategory( - MethodCategoryRegexp, - "Checks whether a regular expression matches against any part of a string and returns a boolean.", - NewExampleSpec("", - `root.matches = this.value.re_match("[0-9]")`, - `{"value":"there are 10 puppies"}`, - `{"matches":true}`, - `{"value":"there are ten puppies"}`, - `{"matches":false}`, - ), - ).Param(ParamString("pattern", "The pattern to match against.")), - func(args *ParsedParams) (simpleMethod, error) { - reStr, err := args.FieldString("pattern") - if err != nil { - return nil, err - } - re, err := regexp.Compile(reStr) - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - var result bool - switch t := v.(type) { - case string: - result = re.MatchString(t) - case []byte: - result = re.Match(t) - default: - return nil, value.NewTypeError(v, value.TString) - } - return result, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewHiddenMethodSpec("re_replace"). - Param(ParamString("pattern", "The pattern to match against.")). - Param(ParamString("value", "The value to replace with.")), - reReplaceAllImpl, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "re_replace_all", "", - ).InCategory( - MethodCategoryRegexp, - "Replaces all occurrences of the argument regular expression in a string with a value. Inside the value $ signs are interpreted as submatch expansions, e.g. `$1` represents the text of the first submatch.", - NewExampleSpec("", - `root.new_value = this.value.re_replace_all("ADD ([0-9]+)","+($1)")`, - `{"value":"foo ADD 70"}`, - `{"new_value":"foo +(70)"}`, - ), - ). - Param(ParamString("pattern", "The pattern to match against.")). - Param(ParamString("value", "The value to replace with.")), - reReplaceAllImpl, -) - -func reReplaceAllImpl(args *ParsedParams) (simpleMethod, error) { - reStr, err := args.FieldString("pattern") - if err != nil { - return nil, err - } - re, err := regexp.Compile(reStr) - if err != nil { - return nil, err - } - with, err := args.FieldString("value") - if err != nil { - return nil, err - } - withBytes := []byte(with) - return func(v any, ctx FunctionContext) (any, error) { - var result string - switch t := v.(type) { - case string: - result = re.ReplaceAllString(t, with) - case []byte: - result = string(re.ReplaceAll(t, withBytes)) - default: - return nil, value.NewTypeError(v, value.TString) - } - return result, nil - }, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "split", "", - ).InCategory( - MethodCategoryStrings, - "Split a string value into an array of strings by splitting it on a string separator.", - NewExampleSpec("", - `root.new_value = this.value.split(",")`, - `{"value":"foo,bar,baz"}`, - `{"new_value":["foo","bar","baz"]}`, - ), - ).Param(ParamString("delimiter", "The delimiter to split with.")), - func(args *ParsedParams) (simpleMethod, error) { - delim, err := args.FieldString("delimiter") - if err != nil { - return nil, err - } - delimB := []byte(delim) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - bits := strings.Split(t, delim) - vals := make([]any, 0, len(bits)) - for _, b := range bits { - vals = append(vals, b) - } - return vals, nil - case []byte: - bits := bytes.Split(t, delimB) - vals := make([]any, 0, len(bits)) - for _, b := range bits { - vals = append(vals, b) - } - return vals, nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "string", "", - ).InCategory( - MethodCategoryCoercion, - "Marshal a value into a string. If the value is already a string it is unchanged.", - NewExampleSpec("", - `root.nested_json = this.string()`, - `{"foo":"bar"}`, - `{"nested_json":"{\"foo\":\"bar\"}"}`, - ), - NewExampleSpec("", - `root.id = this.id.string()`, - `{"id":228930314431312345}`, - `{"id":"228930314431312345"}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - return value.IToString(v), nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -// TODO: V5 remove this -var _ = registerSimpleMethod( - NewMethodSpec( - "strip_html", "", - ).InCategory( - MethodCategoryStrings, - "Attempts to remove all HTML tags from a target string.", - NewExampleSpec("", - `root.stripped = this.value.strip_html()`, - `{"value":"

the plain old text

"}`, - `{"stripped":"the plain old text"}`, - ), - NewExampleSpec("It's also possible to provide an explicit list of element types to preserve in the output.", - `root.stripped = this.value.strip_html(["article"])`, - `{"value":"

the plain old text

"}`, - `{"stripped":"
the plain old text
"}`, - ), - ).Param(ParamArray("preserve", "An optional array of element types to preserve in the output.").Optional()), - func(args *ParsedParams) (simpleMethod, error) { - p := bluemonday.NewPolicy() - tags, err := args.FieldOptionalArray("preserve") - if err != nil { - return nil, err - } - if tags != nil { - tagStrs := make([]string, len(*tags)) - for i, ele := range *tags { - var ok bool - if tagStrs[i], ok = ele.(string); !ok { - return nil, fmt.Errorf("invalid arg at index %v: %w", i, value.NewTypeError(ele, value.TString)) - } - } - p = p.AllowElements(tagStrs...) - } - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return p.Sanitize(t), nil - case []byte: - return p.SanitizeBytes(t), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "trim", "", - ).InCategory( - MethodCategoryStrings, - "Remove all leading and trailing characters from a string that are contained within an argument cutset. If no arguments are provided then whitespace is removed.", - NewExampleSpec("", - `root.title = this.title.trim("!?") -root.description = this.description.trim()`, - `{"description":" something happened and its amazing! ","title":"!!!watch out!?"}`, - `{"description":"something happened and its amazing!","title":"watch out"}`, - ), - ).Param(ParamString("cutset", "An optional string of characters to trim from the target value.").Optional()), - func(args *ParsedParams) (simpleMethod, error) { - cutset, err := args.FieldOptionalString("cutset") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - if cutset == nil { - return strings.TrimSpace(t), nil - } - return strings.Trim(t, *cutset), nil - case []byte: - if cutset == nil { - return bytes.TrimSpace(t), nil - } - return bytes.Trim(t, *cutset), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "trim_prefix", "", - ).InCategory( - MethodCategoryStrings, - "Remove the provided leading prefix substring from a string. If the string does not have the prefix substring, it is returned unchanged.", - NewExampleSpec("", - `root.name = this.name.trim_prefix("foobar_") -root.description = this.description.trim_prefix("foobar_")`, - `{"description":"unchanged","name":"foobar_blobton"}`, - `{"description":"unchanged","name":"blobton"}`, - ), - ).Param(ParamString("prefix", "The leading prefix substring to trim from the string.")). - AtVersion("4.12.0"), - func(args *ParsedParams) (simpleMethod, error) { - prefix, err := args.FieldString("prefix") - if err != nil { - return nil, err - } - bytesPrefix := []byte(prefix) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.TrimPrefix(t, prefix), nil - case []byte: - return bytes.TrimPrefix(t, bytesPrefix), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "trim_suffix", "", - ).InCategory( - MethodCategoryStrings, - "Remove the provided trailing suffix substring from a string. If the string does not have the suffix substring, it is returned unchanged.", - NewExampleSpec("", - `root.name = this.name.trim_suffix("_foobar") -root.description = this.description.trim_suffix("_foobar")`, - `{"description":"unchanged","name":"blobton_foobar"}`, - `{"description":"unchanged","name":"blobton"}`, - ), - ).Param(ParamString("suffix", "The trailing suffix substring to trim from the string.")). - AtVersion("4.12.0"), - func(args *ParsedParams) (simpleMethod, error) { - suffix, err := args.FieldString("suffix") - if err != nil { - return nil, err - } - bytesSuffix := []byte(suffix) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.TrimSuffix(t, suffix), nil - case []byte: - return bytes.TrimSuffix(t, bytesSuffix), nil - } - return nil, value.NewTypeError(v, value.TString) - }, nil - }, -) diff --git a/internal/bloblang/query/methods_structured.go b/internal/bloblang/query/methods_structured.go deleted file mode 100644 index 5e24b6b5c7..0000000000 --- a/internal/bloblang/query/methods_structured.go +++ /dev/null @@ -1,1735 +0,0 @@ -package query - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "sort" - "strings" - - "github.com/Jeffail/gabs/v2" - jsonschema "github.com/xeipuuv/gojsonschema" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "all", - "Checks each element of an array against a query and returns true if all elements passed. An error occurs if the target is not an array, or if any element results in the provided query returning a non-boolean result. Returns false if the target array is empty.", - ).InCategory( - MethodCategoryObjectAndArray, - "", - NewExampleSpec("", - `root.all_over_21 = this.patrons.all(patron -> patron.age >= 21)`, - `{"patrons":[{"id":"1","age":18},{"id":"2","age":23}]}`, - `{"all_over_21":false}`, - `{"patrons":[{"id":"1","age":45},{"id":"2","age":23}]}`, - `{"all_over_21":true}`, - ), - ).Param(ParamQuery("test", "A test query to apply to each element.", false)), - func(args *ParsedParams) (simpleMethod, error) { - queryFn, err := args.FieldQuery("test") - if err != nil { - return nil, err - } - return func(res any, ctx FunctionContext) (any, error) { - arr, ok := res.([]any) - if !ok { - return nil, value.NewTypeError(res, value.TArray) - } - if len(arr) == 0 { - return false, nil - } - for i, v := range arr { - res, err := queryFn.Exec(ctx.WithValue(v)) - if err != nil { - return nil, fmt.Errorf("element %v: %w", i, err) - } - b, ok := res.(bool) - if !ok { - return nil, fmt.Errorf("element %v: %w", i, value.NewTypeError(res, value.TBool)) - } - if !b { - return false, nil - } - } - return true, nil - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "any", - "Checks the elements of an array against a query and returns true if any element passes. An error occurs if the target is not an array, or if an element results in the provided query returning a non-boolean result. Returns false if the target array is empty.", - ).InCategory( - MethodCategoryObjectAndArray, - "", - NewExampleSpec("", - `root.any_over_21 = this.patrons.any(patron -> patron.age >= 21)`, - `{"patrons":[{"id":"1","age":18},{"id":"2","age":23}]}`, - `{"any_over_21":true}`, - `{"patrons":[{"id":"1","age":10},{"id":"2","age":12}]}`, - `{"any_over_21":false}`, - ), - ).Param(ParamQuery("test", "A test query to apply to each element.", false)), - func(args *ParsedParams) (simpleMethod, error) { - queryFn, err := args.FieldQuery("test") - if err != nil { - return nil, err - } - return func(res any, ctx FunctionContext) (any, error) { - arr, ok := res.([]any) - if !ok { - return nil, value.NewTypeError(res, value.TArray) - } - - if len(arr) == 0 { - return false, nil - } - - for i, v := range arr { - res, err := queryFn.Exec(ctx.WithValue(v)) - if err != nil { - return nil, fmt.Errorf("element %v: %w", i, err) - } - b, ok := res.(bool) - if !ok { - return nil, fmt.Errorf("element %v: %w", i, value.NewTypeError(res, value.TBool)) - } - if b { - return true, nil - } - } - - return false, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "append", - "Returns an array with new elements appended to the end.", - ).InCategory( - MethodCategoryObjectAndArray, - "", - NewExampleSpec("", - `root.foo = this.foo.append("and", "this")`, - `{"foo":["bar","baz"]}`, - `{"foo":["bar","baz","and","this"]}`, - ), - ).VariadicParams(), - func(args *ParsedParams) (simpleMethod, error) { - argsList := args.Raw() - return func(res any, ctx FunctionContext) (any, error) { - arr, ok := res.([]any) - if !ok { - return nil, value.NewTypeError(res, value.TArray) - } - copied := make([]any, 0, len(arr)+len(argsList)) - copied = append(copied, arr...) - return append(copied, argsList...), nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "collapse", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Collapse an array or object into an object of key/value pairs for each field, where the key is the full path of the structured field in dot path notation. Empty arrays an objects are ignored by default.", - NewExampleSpec("", - `root.result = this.collapse()`, - `{"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]}`, - `{"result":{"foo.0.bar":"1","foo.2.bar":"2"}}`, - ), - NewExampleSpec( - "An optional boolean parameter can be set to true in order to include empty objects and arrays.", - `root.result = this.collapse(include_empty: true)`, - `{"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]}`, - `{"result":{"foo.0.bar":"1","foo.1.bar":{},"foo.2.bar":"2","foo.3.bar":[]}}`, - ), - ).Param(ParamBool("include_empty", "Whether to include empty objects and arrays in the resulting object.").Default(false)), - func(args *ParsedParams) (simpleMethod, error) { - includeEmpty, err := args.FieldBool("include_empty") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - gObj := gabs.Wrap(v) - if includeEmpty { - return gObj.FlattenIncludeEmpty() - } - return gObj.Flatten() - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "contains", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Checks whether an array contains an element matching the argument, or an object contains a value matching the argument, and returns a boolean result. Numerical comparisons are made irrespective of the representation type (float versus integer).", - NewExampleSpec("", - `root.has_foo = this.thing.contains("foo")`, - `{"thing":["this","foo","that"]}`, - `{"has_foo":true}`, - `{"thing":["this","bar","that"]}`, - `{"has_foo":false}`, - ), - NewExampleSpec("", - `root.has_bar = this.thing.contains(20)`, - `{"thing":[10.3,20.0,"huh",3]}`, - `{"has_bar":true}`, - `{"thing":[2,3,40,67]}`, - `{"has_bar":false}`, - ), - ).InCategory( - MethodCategoryStrings, - "Checks whether a string contains a substring and returns a boolean result.", - NewExampleSpec("", - `root.has_foo = this.thing.contains("foo")`, - `{"thing":"this foo that"}`, - `{"has_foo":true}`, - `{"thing":"this bar that"}`, - `{"has_foo":false}`, - ), - ).Param(ParamAny("value", "A value to test against elements of the target.")), - func(args *ParsedParams) (simpleMethod, error) { - compareRight, err := args.Field("value") - if err != nil { - return nil, err - } - sub := value.IToString(compareRight) - bsub := value.IToBytes(compareRight) - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - return strings.Contains(t, sub), nil - case []byte: - return bytes.Contains(t, bsub), nil - case []any: - for _, compareLeft := range t { - if value.ICompare(compareRight, compareLeft) { - return true, nil - } - } - case map[string]any: - for _, compareLeft := range t { - if value.ICompare(compareRight, compareLeft) { - return true, nil - } - } - default: - return nil, value.NewTypeError(v, value.TString, value.TArray, value.TObject) - } - return false, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "enumerated", - "Converts an array into a new array of objects, where each object has a field index containing the `index` of the element and a field `value` containing the original value of the element.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.foo = this.foo.enumerated()`, - `{"foo":["bar","baz"]}`, - `{"foo":[{"index":0,"value":"bar"},{"index":1,"value":"baz"}]}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - arr, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - enumerated := make([]any, 0, len(arr)) - for i, ele := range arr { - enumerated = append(enumerated, map[string]any{ - "index": int64(i), - "value": ele, - }) - } - return enumerated, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "exists", - "Checks that a field, identified via a xref:configuration:field_paths.adoc[dot path], exists in an object.", - NewExampleSpec("", - `root.result = this.foo.exists("bar.baz")`, - `{"foo":{"bar":{"baz":"yep, I exist"}}}`, - `{"result":true}`, - `{"foo":{"bar":{}}}`, - `{"result":false}`, - `{"foo":{}}`, - `{"result":false}`, - ), - ).Param(ParamString("path", "A xref:configuration:field_paths.adoc[dot path] to a field.")), - func(args *ParsedParams) (simpleMethod, error) { - pathStr, err := args.FieldString("path") - if err != nil { - return nil, err - } - path := gabs.DotPathToSlice(pathStr) - return func(v any, ctx FunctionContext) (any, error) { - return gabs.Wrap(v).Exists(path...), nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "explode", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Explodes an array or object at a xref:configuration:field_paths.adoc[field path].", - NewExampleSpec(`##### On arrays - -Exploding arrays results in an array containing elements matching the original document, where the target field of each element is an element of the exploded array:`, - `root = this.explode("value")`, - `{"id":1,"value":["foo","bar","baz"]}`, - `[{"id":1,"value":"foo"},{"id":1,"value":"bar"},{"id":1,"value":"baz"}]`, - ), - NewExampleSpec(`##### On objects - -Exploding objects results in an object where the keys match the target object, and the values match the original document but with the target field replaced by the exploded value:`, - `root = this.explode("value")`, - `{"id":1,"value":{"foo":2,"bar":[3,4],"baz":{"bev":5}}}`, - `{"bar":{"id":1,"value":[3,4]},"baz":{"id":1,"value":{"bev":5}},"foo":{"id":1,"value":2}}`, - ), - ).Param(ParamString("path", "A xref:configuration:field_paths.adoc[dot path] to a field to explode.")), - func(args *ParsedParams) (simpleMethod, error) { - pathRaw, err := args.FieldString("path") - if err != nil { - return nil, err - } - path := gabs.DotPathToSlice(pathRaw) - return func(v any, ctx FunctionContext) (any, error) { - rootMap, ok := v.(map[string]any) - if !ok { - return nil, value.NewTypeError(v, value.TObject) - } - - target := gabs.Wrap(v).Search(path...) - copyFrom := mapWithout(rootMap, [][]string{path}) - - switch t := target.Data().(type) { - case []any: - result := make([]any, len(t)) - for i, ele := range t { - gExploded := gabs.Wrap(value.IClone(copyFrom)) - _, _ = gExploded.Set(ele, path...) - result[i] = gExploded.Data() - } - return result, nil - case map[string]any: - result := make(map[string]any, len(t)) - for key, ele := range t { - gExploded := gabs.Wrap(value.IClone(copyFrom)) - _, _ = gExploded.Set(ele, path...) - result[key] = gExploded.Data() - } - return result, nil - } - - return nil, fmt.Errorf("expected array or object value at path '%v', found: %v", pathRaw, value.ITypeOf(target.Data())) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "filter", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Executes a mapping query argument for each element of an array or key/value pair of an object. If the query returns `false` the item is removed from the resulting array or object. The item will also be removed if the query returns any non-boolean value.", - NewExampleSpec(``, - `root.new_nums = this.nums.filter(num -> num > 10)`, - `{"nums":[3,11,4,17]}`, - `{"new_nums":[11,17]}`, - ), - NewExampleSpec(`##### On objects - -When filtering objects the mapping query argument is provided a context with a field `+"`key`"+` containing the value key, and a field `+"`value`"+` containing the value.`, - `root.new_dict = this.dict.filter(item -> item.value.contains("foo"))`, - `{"dict":{"first":"hello foo","second":"world","third":"this foo is great"}}`, - `{"new_dict":{"first":"hello foo","third":"this foo is great"}}`, - ), - ).Param(ParamQuery("test", "A query to apply to each element, if this query resolves to any value other than a boolean `true` the element will be removed from the result.", false)), - func(args *ParsedParams) (simpleMethod, error) { - mapFn, err := args.FieldQuery("test") - if err != nil { - return nil, err - } - return func(res any, ctx FunctionContext) (any, error) { - var resValue any - switch t := res.(type) { - case []any: - newSlice := make([]any, 0, len(t)) - for _, v := range t { - f, err := mapFn.Exec(ctx.WithValue(v)) - if err != nil { - return nil, err - } - if b, _ := f.(bool); b { - newSlice = append(newSlice, v) - } - } - resValue = newSlice - case map[string]any: - newMap := make(map[string]any, len(t)) - for k, v := range t { - var ctxMap any = map[string]any{ - "key": k, - "value": v, - } - f, err := mapFn.Exec(ctx.WithValue(ctxMap)) - if err != nil { - return nil, err - } - if b, _ := f.(bool); b { - newMap[k] = v - } - } - resValue = newMap - default: - return nil, value.NewTypeError(res, value.TArray, value.TObject) - } - return resValue, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "find", - "Returns the index of the first occurrence of a value in an array. `-1` is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer).", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.index = this.find("bar")`, - `["foo", "bar", "baz"]`, - `{"index":1}`, - ), - NewExampleSpec("", - `root.index = this.things.find(this.goal)`, - `{"goal":"bar","things":["foo", "bar", "baz"]}`, - `{"index":1}`, - ), - ).Beta().Param(ParamAny("value", "A value to find.")), - func(args *ParsedParams) (simpleMethod, error) { - val, err := args.Field("value") - if err != nil { - return nil, err - } - - return func(v any, ctx FunctionContext) (any, error) { - array, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - - for i, elem := range array { - if value.ICompare(val, elem) { - return i, nil - } - } - return -1, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "find_all", - "Returns an array containing the indexes of all occurrences of a value in an array. An empty array is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer).", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.index = this.find_all("bar")`, - `["foo", "bar", "baz", "bar"]`, - `{"index":[1,3]}`, - ), - NewExampleSpec("", - `root.indexes = this.things.find_all(this.goal)`, - `{"goal":"bar","things":["foo", "bar", "baz", "bar", "buz"]}`, - `{"indexes":[1,3]}`, - ), - ).Beta().Param(ParamAny("value", "A value to find.")), - func(args *ParsedParams) (simpleMethod, error) { - val, err := args.Field("value") - if err != nil { - return nil, err - } - - return func(v any, ctx FunctionContext) (any, error) { - array, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - - output := []any{} - for i, elem := range array { - if value.ICompare(val, elem) { - output = append(output, i) - } - } - - return output, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "find_by", - "Returns the index of the first occurrence of an array where the provided query resolves to a boolean `true`. `-1` is returned if there are no matches.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.index = this.find_by(v -> v != "bar")`, - `["foo", "bar", "baz"]`, - `{"index":0}`, - ), - ).Beta().Param(ParamQuery("query", "A query to execute for each element.", false)), - func(args *ParsedParams) (simpleMethod, error) { - queryFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - - return func(v any, ctx FunctionContext) (any, error) { - array, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - - for i, elem := range array { - iIsMatch, err := queryFn.Exec(ctx.WithValue(elem)) - if err != nil { - return nil, fmt.Errorf("query returned an error for index %v: %w", i, err) - } - isMatch, ok := iIsMatch.(bool) - if !ok { - return nil, fmt.Errorf("query returned a non-boolean value for index %v: %w", i, value.NewTypeError(iIsMatch, value.TBool)) - } - if isMatch { - return i, nil - } - } - return -1, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "find_all_by", - "Returns an array containing the indexes of all occurrences of an array where the provided query resolves to a boolean `true`. An empty array is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer).", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.index = this.find_all_by(v -> v != "bar")`, - `["foo", "bar", "baz"]`, - `{"index":[0,2]}`, - ), - ).Beta().Param(ParamQuery("query", "A query to execute for each element.", false)), - func(args *ParsedParams) (simpleMethod, error) { - queryFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - - return func(v any, ctx FunctionContext) (any, error) { - array, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - - output := []any{} - for i, elem := range array { - iIsMatch, err := queryFn.Exec(ctx.WithValue(elem)) - if err != nil { - return nil, fmt.Errorf("query returned an error for index %v: %w", i, err) - } - isMatch, ok := iIsMatch.(bool) - if !ok { - return nil, fmt.Errorf("query returned a non-boolean value for index %v: %w", i, value.NewTypeError(iIsMatch, value.TBool)) - } - if isMatch { - output = append(output, i) - } - } - - return output, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "flatten", - "Iterates an array and any element that is itself an array is removed and has its elements inserted directly in the resulting array.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec(``, - `root.result = this.flatten()`, - `["foo",["bar","baz"],"buz"]`, - `{"result":["foo","bar","baz","buz"]}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - array, isArray := v.([]any) - if !isArray { - return nil, value.NewTypeError(v, value.TArray) - } - result := make([]any, 0, len(array)) - for _, child := range array { - switch t := child.(type) { - case []any: - result = append(result, t...) - default: - result = append(result, t) - } - } - return result, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "fold", - "Takes two arguments: an initial value, and a mapping query. For each element of an array the mapping context is an object with two fields `tally` and `value`, where `tally` contains the current accumulated value and `value` is the value of the current element. The mapping must return the result of adding the value to the tally.\n\nThe first argument is the value that `tally` will have on the first call.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec(``, - `root.sum = this.foo.fold(0, item -> item.tally + item.value)`, - `{"foo":[3,8,11]}`, - `{"sum":22}`, - ), - NewExampleSpec(``, - `root.result = this.foo.fold("", item -> "%v%v".format(item.tally, item.value))`, - `{"foo":["hello ", "world"]}`, - `{"result":"hello world"}`, - ), - NewExampleSpec(`You can use fold to merge an array of objects together:`, - `root.smoothie = this.fruits.fold({}, item -> item.tally.merge(item.value))`, - `{"fruits":[{"apple":5},{"banana":3},{"orange":8}]}`, - `{"smoothie":{"apple":5,"banana":3,"orange":8}}`, - ), - ). - Param(ParamAny("initial", "The initial value to start the fold with. For example, an empty object `{}`, a zero count `0`, or an empty string `\"\"`.")). - Param(ParamQuery("query", "A query to apply for each element. The query is provided an object with two fields; `tally` containing the current tally, and `value` containing the value of the current element. The query should result in a new tally to be passed to the next element query.", false)), - func(args *ParsedParams) (simpleMethod, error) { - foldTallyStart, err := args.Field("initial") - if err != nil { - return nil, err - } - foldFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - return func(res any, ctx FunctionContext) (any, error) { - resArray, ok := res.([]any) - if !ok { - return nil, value.NewTypeError(res, value.TArray) - } - - tally := value.IClone(foldTallyStart) - for _, v := range resArray { - newV, mapErr := foldFn.Exec(ctx.WithValue(map[string]any{ - "tally": tally, - "value": v, - })) - if mapErr != nil { - return nil, mapErr - } - tally = newV - } - return tally, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "index", - "Extract an element from an array by an index. The index can be negative, and if so the element will be selected from the end counting backwards starting from -1. E.g. an index of -1 returns the last element, an index of -2 returns the element before the last, and so on.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.last_name = this.names.index(-1)`, - `{"names":["rachel","stevens"]}`, - `{"last_name":"stevens"}`, - ), - NewExampleSpec("It is also possible to use this method on byte arrays, in which case the selected element will be returned as an integer.", - `root.last_byte = this.name.bytes().index(-1)`, - `{"name":"foobar bazson"}`, - `{"last_byte":110}`, - ), - ).Param(ParamInt64("index", "The index to obtain from an array.")), - func(args *ParsedParams) (simpleMethod, error) { - index, err := args.FieldInt64("index") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - switch array := v.(type) { - case []any: - i := int(index) - if i < 0 { - i = len(array) + i - } - if i < 0 || i >= len(array) { - return nil, fmt.Errorf("index '%v' was out of bounds for array size: %v", i, len(array)) - } - return array[i], nil - case []byte: - i := int(index) - if i < 0 { - i = len(array) + i - } - if i < 0 || i >= len(array) { - return nil, fmt.Errorf("index '%v' was out of bounds for array size: %v", i, len(array)) - } - return int64(array[i]), nil - default: - return nil, value.NewTypeError(v, value.TArray) - } - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "json_schema", - "Checks a https://json-schema.org/[JSON schema] against a value and returns the value if it matches or throws and error if it does not.", - ).InCategory( - MethodCategoryObjectAndArray, - "", - NewExampleSpec("", - `root = this.json_schema("""{ - "type":"object", - "properties":{ - "foo":{ - "type":"string" - } - } -}""")`, - `{"foo":"bar"}`, - `{"foo":"bar"}`, - `{"foo":5}`, - `Error("failed assignment (line 1): field `+"`this`"+`: foo invalid type. expected: string, given: integer")`, - ), - NewExampleSpec( - "In order to load a schema from a file use the `file` function.", - `root = this.json_schema(file(env("BENTHOS_TEST_BLOBLANG_SCHEMA_FILE")))`, - ), - ).Beta().Param(ParamString("schema", "The schema to check values against.")), - func(args *ParsedParams) (simpleMethod, error) { - schemaStr, err := args.FieldString("schema") - if err != nil { - return nil, err - } - schema, err := jsonschema.NewSchema(jsonschema.NewStringLoader(schemaStr)) - if err != nil { - return nil, fmt.Errorf("failed to parse json schema definition: %w", err) - } - return func(res any, ctx FunctionContext) (any, error) { - result, err := schema.Validate(jsonschema.NewGoLoader(res)) - if err != nil { - return nil, err - } - if !result.Valid() { - var errStr string - for i, desc := range result.Errors() { - if i > 0 { - errStr += "\n" - } - description := strings.ToLower(desc.Description()) - if property := desc.Details()["property"]; property != nil { - description = property.(string) + strings.TrimPrefix(description, strings.ToLower(property.(string))) - } - errStr = errStr + desc.Field() + " " + description - } - return nil, errors.New(errStr) - } - return res, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "keys", - "Returns the keys of an object as an array.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.foo_keys = this.foo.keys()`, - `{"foo":{"bar":1,"baz":2}}`, - `{"foo_keys":["bar","baz"]}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - if m, ok := v.(map[string]any); ok { - keys := make([]any, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Slice(keys, func(i, j int) bool { - return keys[i].(string) < keys[j].(string) - }) - return keys, nil - } - return nil, value.NewTypeError(v, value.TObject) - }, nil - }, -) - -var _ = registerSimpleMethod( - NewMethodSpec( - "key_values", - "Returns the key/value pairs of an object as an array, where each element is an object with a `key` field and a `value` field. The order of the resulting array will be random.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec("", - `root.foo_key_values = this.foo.key_values().sort_by(pair -> pair.key)`, - - `{"foo":{"bar":1,"baz":2}}`, - `{"foo_key_values":[{"key":"bar","value":1},{"key":"baz","value":2}]}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - if m, ok := v.(map[string]any); ok { - keyValues := make([]any, 0, len(m)) - for k, v := range m { - keyValues = append(keyValues, map[string]any{ - "key": k, - "value": v, - }) - } - return keyValues, nil - } - return nil, value.NewTypeError(v, value.TObject) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "length", "", - ).InCategory( - MethodCategoryStrings, "Returns the length of a string.", - NewExampleSpec("", - `root.foo_len = this.foo.length()`, - `{"foo":"hello world"}`, - `{"foo_len":11}`, - ), - ).InCategory( - MethodCategoryObjectAndArray, "Returns the length of an array or object (number of keys).", - NewExampleSpec("", - `root.foo_len = this.foo.length()`, - `{"foo":["first","second"]}`, - `{"foo_len":2}`, - `{"foo":{"first":"bar","second":"baz"}}`, - `{"foo_len":2}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - var length int64 - switch t := v.(type) { - case string: - length = int64(len(t)) - case []byte: - length = int64(len(t)) - case []any: - length = int64(len(t)) - case map[string]any: - length = int64(len(t)) - default: - return nil, value.NewTypeError(v, value.TString, value.TArray, value.TObject) - } - return length, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "map_each", "", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec(`##### On arrays - -Apply a mapping to each element of an array and replace the element with the result. Within the argument mapping the context is the value of the element being mapped.`, - `root.new_nums = this.nums.map_each(num -> if num < 10 { - deleted() -} else { - num - 10 -})`, - `{"nums":[3,11,4,17]}`, - `{"new_nums":[1,7]}`, - ), - NewExampleSpec(`##### On objects - -Apply a mapping to each value of an object and replace the value with the result. Within the argument mapping the context is an object with a field `+"`key`"+` containing the value key, and a field `+"`value`"+`.`, - `root.new_dict = this.dict.map_each(item -> item.value.uppercase())`, - `{"dict":{"foo":"hello","bar":"world"}}`, - `{"new_dict":{"bar":"WORLD","foo":"HELLO"}}`, - ), - ).Param(ParamQuery("query", "A query that will be used to map each element.", false)), - func(args *ParsedParams) (simpleMethod, error) { - mapFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - return func(res any, ctx FunctionContext) (any, error) { - var resValue any - var err error - switch t := res.(type) { - case []any: - newSlice := make([]any, 0, len(t)) - for i, v := range t { - newV, mapErr := mapFn.Exec(ctx.WithValue(v)) - if mapErr != nil { - return nil, fmt.Errorf("failed to process element %v: %w", i, ErrFrom(mapErr, mapFn)) - } - switch newV.(type) { - case value.Delete: - case value.Nothing: - newSlice = append(newSlice, v) - default: - newSlice = append(newSlice, newV) - } - } - resValue = newSlice - case map[string]any: - newMap := make(map[string]any, len(t)) - for k, v := range t { - var ctxMap any = map[string]any{ - "key": k, - "value": v, - } - newV, mapErr := mapFn.Exec(ctx.WithValue(ctxMap)) - if mapErr != nil { - return nil, fmt.Errorf("failed to process element %v: %w", k, ErrFrom(mapErr, mapFn)) - } - switch newV.(type) { - case value.Delete: - case value.Nothing: - newMap[k] = v - default: - newMap[k] = newV - } - } - resValue = newMap - default: - return nil, value.NewTypeError(res, value.TArray) - } - if err != nil { - return nil, err - } - return resValue, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "map_each_key", "", - ).InCategory( - MethodCategoryObjectAndArray, `Apply a mapping to each key of an object, and replace the key with the result, which must be a string.`, - NewExampleSpec(``, - `root.new_dict = this.dict.map_each_key(key -> key.uppercase())`, - `{"dict":{"keya":"hello","keyb":"world"}}`, - `{"new_dict":{"KEYA":"hello","KEYB":"world"}}`, - ), - NewExampleSpec(``, - `root = this.map_each_key(key -> if key.contains("kafka") { "_" + key })`, - `{"amqp_key":"foo","kafka_key":"bar","kafka_topic":"baz"}`, - `{"_kafka_key":"bar","_kafka_topic":"baz","amqp_key":"foo"}`, - ), - ).Param(ParamQuery("query", "A query that will be used to map each key.", false)), - func(args *ParsedParams) (simpleMethod, error) { - mapFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - return func(res any, ctx FunctionContext) (any, error) { - obj, ok := res.(map[string]any) - if !ok { - return nil, value.NewTypeError(res, value.TObject) - } - - newMap := make(map[string]any, len(obj)) - for k, v := range obj { - var ctxVal any = k - newKey, mapErr := mapFn.Exec(ctx.WithValue(ctxVal)) - if mapErr != nil { - return nil, mapErr - } - - switch t := newKey.(type) { - // TODO: Revise whether we want this. - // case Delete: - case value.Nothing: - newMap[k] = v - case string: - newMap[t] = v - default: - return nil, fmt.Errorf("unexpected result from key mapping: %w", value.NewTypeError(newKey, value.TString)) - } - } - return newMap, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "merge", "Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the result will be an array containing both values, where values that are already arrays will be expanded into the resulting array. In order to simply override destination fields on collision use the <> method.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec(``, - `root = this.foo.merge(this.bar)`, - `{"foo":{"first_name":"fooer","likes":"bars"},"bar":{"second_name":"barer","likes":"foos"}}`, - `{"first_name":"fooer","likes":["bars","foos"],"second_name":"barer"}`, - ), - ).Param(ParamAny("with", "A value to merge the target value with.")), - mergeMethod, -) - -func mergeMethod(target Function, args *ParsedParams) (Function, error) { - mergeFromSource, err := args.Field("with") - if err != nil { - return nil, err - } - return ClosureFunction("method merge", func(ctx FunctionContext) (any, error) { - mergeInto, err := target.Exec(ctx) - if err != nil { - return nil, err - } - - mergeFrom := value.IClone(mergeFromSource) - if root, isArray := mergeInto.([]any); isArray { - if rhs, isAlsoArray := mergeFrom.([]any); isAlsoArray { - return append(root, rhs...), nil - } - return append(root, mergeFrom), nil - } - - if _, isObject := mergeInto.(map[string]any); !isObject { - return nil, value.NewTypeErrorFrom(target.Annotation(), mergeInto, value.TObject, value.TArray) - } - - root := gabs.New() - if err = root.Merge(gabs.Wrap(mergeInto)); err == nil { - err = root.Merge(gabs.Wrap(mergeFrom)) - } - if err != nil { - return nil, err - } - return root.Data(), nil - }, target.QueryTargets), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "assign", "Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the value in the destination object will be overwritten by that of source object. In order to preserve both values on collision use the <> method.", - ).InCategory( - MethodCategoryObjectAndArray, "", - NewExampleSpec(``, - `root = this.foo.assign(this.bar)`, - `{"foo":{"first_name":"fooer","likes":"bars"},"bar":{"second_name":"barer","likes":"foos"}}`, - `{"first_name":"fooer","likes":"foos","second_name":"barer"}`, - ), - ).Param(ParamAny("with", "A value to merge the target value with.")), - assignMethod, -) - -func assignMethod(target Function, args *ParsedParams) (Function, error) { - assignFromSource, err := args.Field("with") - if err != nil { - return nil, err - } - return ClosureFunction("method assign", func(ctx FunctionContext) (any, error) { - assignInto, err := target.Exec(ctx) - if err != nil { - return nil, err - } - - assignFrom := value.IClone(assignFromSource) - if root, isArray := assignInto.([]any); isArray { - if rhs, isAlsoArray := assignFrom.([]any); isAlsoArray { - return append(root, rhs...), nil - } - return append(root, assignFrom), nil - } - - if _, isObject := assignInto.(map[string]any); !isObject { - return nil, value.NewTypeErrorFrom(target.Annotation(), assignInto, value.TObject, value.TArray) - } - - root := gabs.New() - if err = root.MergeFn(gabs.Wrap(assignInto), assigner); err == nil { - err = root.MergeFn(gabs.Wrap(assignFrom), assigner) - } - if err != nil { - return nil, err - } - return root.Data(), nil - }, target.QueryTargets), nil -} - -func assigner(destination, source any) any { - return source -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "not_empty", "", - ).InCategory( - MethodCategoryCoercion, - "Ensures that the given string, array or object value is not empty, and if so returns it, otherwise an error is returned.", - NewExampleSpec("", - `root.a = this.a.not_empty()`, - `{"a":"foo"}`, - `{"a":"foo"}`, - - `{"a":""}`, - `Error("failed assignment (line 1): field `+"`this.a`"+`: string value is empty")`, - - `{"a":["foo","bar"]}`, - `{"a":["foo","bar"]}`, - - `{"a":[]}`, - `Error("failed assignment (line 1): field `+"`this.a`"+`: array value is empty")`, - - `{"a":{"b":"foo","c":"bar"}}`, - `{"a":{"b":"foo","c":"bar"}}`, - - `{"a":{}}`, - `Error("failed assignment (line 1): field `+"`this.a`"+`: object value is empty")`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - if t == "" { - return nil, errors.New("string value is empty") - } - case []any: - if len(t) == 0 { - return nil, errors.New("array value is empty") - } - case map[string]any: - if len(t) == 0 { - return nil, errors.New("object value is empty") - } - default: - return nil, value.NewTypeError(v, value.TString, value.TArray, value.TObject) - } - return v, nil - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "sort", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Attempts to sort the values of an array in increasing order. The type of all values must match in order for the ordering to succeed. Supports string and number values.", - NewExampleSpec("", - `root.sorted = this.foo.sort()`, - `{"foo":["bbb","ccc","aaa"]}`, - `{"sorted":["aaa","bbb","ccc"]}`, - ), - NewExampleSpec("It's also possible to specify a mapping argument, which is provided an object context with fields `left` and `right`, the mapping must return a boolean indicating whether the `left` value is less than `right`. This allows you to sort arrays containing non-string or non-number values.", - `root.sorted = this.foo.sort(item -> item.left.v < item.right.v)`, - `{"foo":[{"id":"foo","v":"bbb"},{"id":"bar","v":"ccc"},{"id":"baz","v":"aaa"}]}`, - `{"sorted":[{"id":"baz","v":"aaa"},{"id":"foo","v":"bbb"},{"id":"bar","v":"ccc"}]}`, - ), - ). - Param(ParamQuery( - "compare", - "An optional query that should explicitly compare elements `left` and `right` and provide a boolean result.", - false, - ).Optional()), - sortMethod, -) - -func sortMethod(target Function, args *ParsedParams) (Function, error) { - compareFn := func(ctx FunctionContext, values []any, i, j int) (bool, error) { - switch values[i].(type) { - case float64, int, int64, uint64, json.Number: - lhs, err := value.IGetNumber(values[i]) - if err != nil { - return false, fmt.Errorf("sort element %v: %w", i, err) - } - rhs, err := value.IGetNumber(values[j]) - if err != nil { - return false, fmt.Errorf("sort element %v: %w", j, err) - } - return lhs < rhs, nil - case string, []byte: - lhs, err := value.IGetString(values[i]) - if err != nil { - return false, fmt.Errorf("sort element %v: %w", i, err) - } - rhs, err := value.IGetString(values[j]) - if err != nil { - return false, fmt.Errorf("sort element %v: %w", j, err) - } - return lhs < rhs, nil - } - return false, fmt.Errorf("sort element %v: %w", i, value.NewTypeError(values[i], value.TNumber, value.TString)) - } - - mapFn, err := args.FieldOptionalQuery("compare") - if err != nil { - return nil, err - } - - if mapFn != nil { - compareFn = func(ctx FunctionContext, values []any, i, j int) (bool, error) { - var ctxValue any = map[string]any{ - "left": values[i], - "right": values[j], - } - v, err := mapFn.Exec(ctx.WithValue(ctxValue)) - if err != nil { - return false, err - } - b, ok := v.(bool) - if !ok { - return false, value.NewTypeErrorFrom("sort argument", v, value.TBool) - } - return b, nil - } - } - - targets := target.QueryTargets - if mapFn != nil { - targets = aggregateTargetPaths(target, mapFn) - } - - return ClosureFunction("method sort", func(ctx FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - if m, ok := v.([]any); ok { - values := make([]any, 0, len(m)) - values = append(values, m...) - - sort.Slice(values, func(i, j int) bool { - if err == nil { - var b bool - b, err = compareFn(ctx, values, i, j) - return b - } - return false - }) - if err != nil { - return nil, err - } - return values, nil - } - return nil, value.NewTypeErrorFrom(target.Annotation(), v, value.TArray) - }, targets), nil -} - -var _ = registerMethod( - NewMethodSpec( - "sort_by", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Attempts to sort the elements of an array, in increasing order, by a value emitted by an argument query applied to each element. The type of all values must match in order for the ordering to succeed. Supports string and number values.", - NewExampleSpec("", - `root.sorted = this.foo.sort_by(ele -> ele.id)`, - `{"foo":[{"id":"bbb","message":"bar"},{"id":"aaa","message":"foo"},{"id":"ccc","message":"baz"}]}`, - `{"sorted":[{"id":"aaa","message":"foo"},{"id":"bbb","message":"bar"},{"id":"ccc","message":"baz"}]}`, - ), - ).Param(ParamQuery("query", "A query to apply to each element that yields a value used for sorting.", false)), - sortByMethod, -) - -func sortByMethod(target Function, args *ParsedParams) (Function, error) { - mapFn, err := args.FieldQuery("query") - if err != nil { - return nil, err - } - - compareFn := func(ctx FunctionContext, values []any, i, j int) (bool, error) { - var leftValue, rightValue any - var err error - - if leftValue, err = mapFn.Exec(ctx.WithValue(values[i])); err != nil { - return false, err - } - if rightValue, err = mapFn.Exec(ctx.WithValue(values[j])); err != nil { - return false, err - } - - switch leftValue.(type) { - case float64, int, int64, uint64, json.Number: - lhs, err := value.IGetNumber(leftValue) - if err != nil { - return false, fmt.Errorf("sort_by element %v: %w", i, ErrFrom(err, mapFn)) - } - rhs, err := value.IGetNumber(rightValue) - if err != nil { - return false, fmt.Errorf("sort_by element %v: %w", j, ErrFrom(err, mapFn)) - } - return lhs < rhs, nil - case string, []byte: - lhs, err := value.IGetString(leftValue) - if err != nil { - return false, fmt.Errorf("sort_by element %v: %w", i, ErrFrom(err, mapFn)) - } - rhs, err := value.IGetString(rightValue) - if err != nil { - return false, fmt.Errorf("sort_by element %v: %w", j, ErrFrom(err, mapFn)) - } - return lhs < rhs, nil - } - return false, fmt.Errorf("sort_by element %v: %w", i, ErrFrom(value.NewTypeError(leftValue, value.TNumber, value.TString), mapFn)) - } - - return ClosureFunction("method sort_by", func(ctx FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - if m, ok := v.([]any); ok { - values := make([]any, 0, len(m)) - values = append(values, m...) - - sort.Slice(values, func(i, j int) bool { - if err == nil { - var b bool - b, err = compareFn(ctx, values, i, j) - return b - } - return false - }) - if err != nil { - return nil, err - } - return values, nil - } - return nil, value.NewTypeErrorFrom(target.Annotation(), v, value.TArray) - }, aggregateTargetPaths(target, mapFn)), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "slice", "", - ).InCategory( - MethodCategoryStrings, - "Extract a slice from a string by specifying two indices, a low and high bound, which selects a half-open range that includes the first character, but excludes the last one. If the second index is omitted then it defaults to the length of the input sequence.", - NewExampleSpec("", - `root.beginning = this.value.slice(0, 2) -root.end = this.value.slice(4)`, - `{"value":"foo bar"}`, - `{"beginning":"fo","end":"bar"}`, - ), - NewExampleSpec(`A negative low index can be used, indicating an offset from the end of the sequence. If the low index is greater than the length of the sequence then an empty result is returned.`, - `root.last_chunk = this.value.slice(-4) -root.the_rest = this.value.slice(0, -4)`, - `{"value":"foo bar"}`, - `{"last_chunk":" bar","the_rest":"foo"}`, - ), - ).InCategory( - MethodCategoryObjectAndArray, - "Extract a slice from an array by specifying two indices, a low and high bound, which selects a half-open range that includes the first element, but excludes the last one. If the second index is omitted then it defaults to the length of the input sequence.", - NewExampleSpec("", - `root.beginning = this.value.slice(0, 2) -root.end = this.value.slice(4)`, - `{"value":["foo","bar","baz","buz","bev"]}`, - `{"beginning":["foo","bar"],"end":["bev"]}`, - ), - NewExampleSpec( - `A negative low index can be used, indicating an offset from the end of the sequence. If the low index is greater than the length of the sequence then an empty result is returned.`, - `root.last_chunk = this.value.slice(-2) -root.the_rest = this.value.slice(0, -2)`, - `{"value":["foo","bar","baz","buz","bev"]}`, - `{"last_chunk":["buz","bev"],"the_rest":["foo","bar","baz"]}`, - ), - ). - Param(ParamInt64("low", "The low bound, which is the first element of the selection, or if negative selects from the end.")). - Param(ParamInt64("high", "An optional high bound.").Optional()), - sliceMethod, -) - -func sliceMethod(args *ParsedParams) (simpleMethod, error) { - low, err := args.FieldInt64("low") - if err != nil { - return nil, err - } - high, err := args.FieldOptionalInt64("high") - if err != nil { - return nil, err - } - if high != nil && *high > 0 && low >= *high { - return nil, fmt.Errorf("lower slice bound %v must be lower than upper (%v)", low, *high) - } - getBounds := func(l int64) (lowV, highV int64, err error) { - highV = l - if high != nil { - if *high < 0 { - highV += *high - } else { - highV = *high - } - } - if highV > l { - highV = l - } - if highV < 0 { - highV = 0 - } - lowV = low - if lowV < 0 { - lowV = l + lowV - if lowV < 0 { - lowV = 0 - } - } - if lowV > highV { - err = fmt.Errorf("lower slice bound %v must be lower than or equal to upper bound (%v) and target length (%v)", lowV, highV, l) - } - return - } - return func(v any, ctx FunctionContext) (any, error) { - switch t := v.(type) { - case string: - start, end, err := getBounds(int64(len(t))) - if err != nil { - return nil, err - } - return t[start:end], nil - case []byte: - start, end, err := getBounds(int64(len(t))) - if err != nil { - return nil, err - } - return t[start:end], nil - case []any: - start, end, err := getBounds(int64(len(t))) - if err != nil { - return nil, err - } - return t[start:end], nil - } - return nil, value.NewTypeError(v, value.TArray, value.TString) - }, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerMethod( - NewMethodSpec( - "sum", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Sum the numerical values of an array.", - NewExampleSpec("", - `root.sum = this.foo.sum()`, - `{"foo":[3,8,4]}`, - `{"sum":15}`, - ), - ), - sumMethod, -) - -func sumMethod(target Function, _ *ParsedParams) (Function, error) { - return ClosureFunction("method sum", func(ctx FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - switch t := value.ISanitize(v).(type) { - case float64, int64, uint64, json.Number: - return v, nil - case []any: - var total float64 - for i, v := range t { - n, nErr := value.IGetNumber(v) - if nErr != nil { - err = fmt.Errorf("index %v: %w", i, nErr) - } else { - total += n - } - } - if err != nil { - return nil, err - } - return total, nil - } - return nil, value.NewTypeErrorFrom(target.Annotation(), v, value.TArray) - }, target.QueryTargets), nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "unique", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Attempts to remove duplicate values from an array. The array may contain a combination of different value types, but numbers and strings are checked separately (`\"5\"` is a different element to `5`).", - NewExampleSpec("", - `root.uniques = this.foo.unique()`, - `{"foo":["a","b","a","c"]}`, - `{"uniques":["a","b","c"]}`, - ), - ). - Param(ParamQuery( - "emit", - "An optional query that can be used in order to yield a value for each element to determine uniqueness.", - false, - ).Optional()), - uniqueMethod, -) - -func uniqueMethod(args *ParsedParams) (simpleMethod, error) { - emitFn, err := args.FieldOptionalQuery("emit") - if err != nil { - return nil, err - } - return func(v any, ctx FunctionContext) (any, error) { - slice, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - - var strCompares map[string]struct{} - var numCompares map[float64]struct{} - - checkStr := func(str string) bool { - if strCompares == nil { - strCompares = make(map[string]struct{}, len(slice)) - } - _, exists := strCompares[str] - if !exists { - strCompares[str] = struct{}{} - } - return !exists - } - - checkNum := func(num float64) bool { - if numCompares == nil { - numCompares = make(map[float64]struct{}, len(slice)) - } - _, exists := numCompares[num] - if !exists { - numCompares[num] = struct{}{} - } - return !exists - } - - uniqueSlice := make([]any, 0, len(slice)) - for i, v := range slice { - check := v - if emitFn != nil { - var err error - if check, err = emitFn.Exec(ctx.WithValue(v)); err != nil { - return nil, fmt.Errorf("index %v: %w", i, err) - } - } - var unique bool - switch t := value.ISanitize(check).(type) { - case string: - unique = checkStr(t) - case []byte: - unique = checkStr(string(t)) - case json.Number: - f, err := t.Float64() - if err != nil { - var i int64 - if i, err = t.Int64(); err == nil { - f = float64(i) - } - } - if err != nil { - return nil, fmt.Errorf("index %v: failed to parse number: %w", i, err) - } - unique = checkNum(f) - case int64: - unique = checkNum(float64(t)) - case uint64: - unique = checkNum(float64(t)) - case float64: - unique = checkNum(t) - default: - return nil, fmt.Errorf("index %v: %w", i, value.NewTypeError(check, value.TString, value.TNumber)) - } - if unique { - uniqueSlice = append(uniqueSlice, v) - } - } - return uniqueSlice, nil - }, nil -} - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "values", "", - ).InCategory( - MethodCategoryObjectAndArray, - "Returns the values of an object as an array. The order of the resulting array will be random.", - NewExampleSpec("", - `root.foo_vals = this.foo.values().sort()`, - `{"foo":{"bar":1,"baz":2}}`, - `{"foo_vals":[1,2]}`, - ), - ), - func(*ParsedParams) (simpleMethod, error) { - return func(v any, ctx FunctionContext) (any, error) { - if m, ok := v.(map[string]any); ok { - values := make([]any, 0, len(m)) - for _, e := range m { - values = append(values, e) - } - return values, nil - } - return nil, value.NewTypeError(v, value.TObject) - }, nil - }, -) - -//------------------------------------------------------------------------------ - -var _ = registerSimpleMethod( - NewMethodSpec( - "without", "", - ).InCategory( - MethodCategoryObjectAndArray, - `Returns an object where one or more xref:configuration:field_paths.adoc[field path] arguments are removed. Each path specifies a specific field to be deleted from the input object, allowing for nested fields. - -If a key within a nested path does not exist or is not an object then it is not removed.`, - NewExampleSpec("", - `root = this.without("inner.a","inner.c","d")`, - `{"inner":{"a":"first","b":"second","c":"third"},"d":"fourth","e":"fifth"}`, - `{"e":"fifth","inner":{"b":"second"}}`, - ), - ).VariadicParams(), - func(args *ParsedParams) (simpleMethod, error) { - excludeList := make([][]string, 0, len(args.Raw())) - for i, argVal := range args.Raw() { - argStr, err := value.IGetString(argVal) - if err != nil { - return nil, fmt.Errorf("argument %v: %w", i, err) - } - excludeList = append(excludeList, gabs.DotPathToSlice(argStr)) - } - return func(v any, ctx FunctionContext) (any, error) { - m, ok := v.(map[string]any) - if !ok { - return nil, value.NewTypeError(v, value.TObject) - } - return mapWithout(m, excludeList), nil - }, nil - }, -) - -func mapWithout(m map[string]any, paths [][]string) map[string]any { - newMap := make(map[string]any, len(m)) - for k, v := range m { - excluded := false - var nestedExclude [][]string - for _, p := range paths { - if p[0] == k { - if len(p) > 1 { - nestedExclude = append(nestedExclude, p[1:]) - } else { - excluded = true - } - } - } - if !excluded { - if len(nestedExclude) > 0 { - vMap, ok := v.(map[string]any) - if ok { - newMap[k] = mapWithout(vMap, nestedExclude) - } else { - newMap[k] = v - } - } else { - newMap[k] = v - } - } - } - return newMap -} diff --git a/internal/bloblang/query/methods_structured_test.go b/internal/bloblang/query/methods_structured_test.go deleted file mode 100644 index 8bcf048d02..0000000000 --- a/internal/bloblang/query/methods_structured_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package query - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestMethodImmutability(t *testing.T) { - testCases := []struct { - name string - method string - target any - args []any - exp any - }{ - { - name: "merge arrays", - method: "merge", - target: []any{"foo", "bar"}, - args: []any{ - []any{"baz", "buz"}, - }, - exp: []any{"foo", "bar", "baz", "buz"}, - }, - { - name: "merge into an array", - method: "merge", - target: []any{"foo", "bar"}, - args: []any{ - map[string]any{"baz": "buz"}, - }, - exp: []any{"foo", "bar", map[string]any{"baz": "buz"}}, - }, - { - name: "merge objects", - method: "merge", - target: map[string]any{"foo": "bar"}, - args: []any{ - map[string]any{"baz": "buz"}, - }, - exp: map[string]any{ - "foo": "bar", - "baz": "buz", - }, - }, - { - name: "merge collision", - method: "merge", - target: map[string]any{"foo": "bar", "baz": "buz"}, - args: []any{ - map[string]any{"foo": "qux"}, - }, - exp: map[string]any{ - "foo": []any{"bar", "qux"}, - "baz": "buz", - }, - }, - - { - name: "assign arrays", - method: "assign", - target: []any{"foo", "bar"}, - args: []any{ - []any{"baz", "buz"}, - }, - exp: []any{"foo", "bar", "baz", "buz"}, - }, - { - name: "assign into an array", - method: "assign", - target: []any{"foo", "bar"}, - args: []any{ - map[string]any{"baz": "buz"}, - }, - exp: []any{"foo", "bar", map[string]any{"baz": "buz"}}, - }, - { - name: "assign objects", - method: "assign", - target: map[string]any{"foo": "bar"}, - args: []any{ - map[string]any{"baz": "buz"}, - }, - exp: map[string]any{ - "foo": "bar", - "baz": "buz", - }, - }, - { - name: "assign collision", - method: "assign", - target: map[string]any{"foo": "bar", "baz": "buz"}, - args: []any{ - map[string]any{"foo": "qux"}, - }, - exp: map[string]any{ - "foo": "qux", - "baz": "buz", - }, - }, - - { - name: "contains object positive", - method: "contains", - target: []any{ - map[string]any{"foo": "bar"}, - }, - args: []any{ - map[string]any{"foo": "bar"}, - }, - exp: true, - }, - { - name: "contains object negative", - method: "contains", - target: []any{ - map[string]any{"foo": "bar"}, - }, - args: []any{ - map[string]any{"baz": "buz"}, - }, - exp: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - targetClone := value.IClone(test.target) - argsClone := value.IClone(test.args).([]any) - - fn, err := InitMethodHelper(test.method, NewLiteralFunction("", targetClone), argsClone...) - require.NoError(t, err) - - res, err := fn.Exec(FunctionContext{ - Maps: map[string]Function{}, - Index: 0, - MsgBatch: nil, - }) - require.NoError(t, err) - - assert.Equal(t, test.exp, res) - assert.Equal(t, test.target, targetClone) - assert.Equal(t, test.args, argsClone) - }) - } -} diff --git a/internal/bloblang/query/methods_test.go b/internal/bloblang/query/methods_test.go deleted file mode 100644 index 9b734dc898..0000000000 --- a/internal/bloblang/query/methods_test.go +++ /dev/null @@ -1,2109 +0,0 @@ -package query - -import ( - "encoding/json" - "strconv" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -var linebreakStr = `foo -bar -baz` - -func TestMethods(t *testing.T) { - type easyMsg struct { - content string - meta map[string]any - } - - literalFn := func(val any) Function { - fn := NewLiteralFunction("", val) - return fn - } - jsonFn := func(json string) Function { - t.Helper() - gObj, err := gabs.ParseJSON([]byte(json)) - require.NoError(t, err) - fn := NewLiteralFunction("", gObj.Data()) - return fn - } - function := func(name string, args ...any) Function { - t.Helper() - fn, err := InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - arithmetic := func(left, right Function, op ArithmeticOperator) Function { - t.Helper() - fn, err := NewArithmeticExpression( - []Function{left, right}, - []ArithmeticOperator{op}, - ) - require.NoError(t, err) - return fn - } - jn := func(i int) json.Number { - return json.Number(strconv.Itoa(i)) - } - - type easyMethod struct { - name string - args []any - } - methods := func(fn Function, methods ...easyMethod) Function { - t.Helper() - for _, m := range methods { - var err error - fn, err = InitMethodHelper(m.name, fn, m.args...) - require.NoError(t, err) - } - return fn - } - method := func(name string, args ...any) easyMethod { - return easyMethod{name: name, args: args} - } - - tests := map[string]struct { - input Function - value *any - output any - err string - messages []easyMsg - index int - }{ - "check format_json with default indentation": { - input: methods( - jsonFn(`{"doc":{"foo":"bar"}}`), - method("format_json"), - ), - output: []byte(`{ - "doc": { - "foo": "bar" - } -}`), - }, - "check format_json with two spaces indentation": { - input: methods( - jsonFn(`{"doc":{"foo":"bar"}}`), - method("format_json", " "), - ), - output: []byte(`{ - "doc": { - "foo": "bar" - } -}`), - }, - "check format_json with one tab indentation": { - input: methods( - jsonFn(`{"doc":{"foo":"bar"}}`), - method("format_json", "\t"), - ), - output: []byte(`{ - "doc": { - "foo": "bar" - } -}`), - }, - "check format_json with empty indentation": { - input: methods( - jsonFn(`{"doc":{"foo":"bar"}}`), - method("format_json", ""), - ), - output: []byte(`{ -"doc": { -"foo": "bar" -} -}`), - }, - "check format_yaml": { - input: methods( - jsonFn(`{"doc":{"foo":"bar"}}`), - method("format_yaml"), - ), - output: []byte(`doc: - foo: bar -`), - }, - "check parse csv 1": { - input: methods( - literalFn("foo,bar,baz\n1,2,3\n4,5,6"), - method("parse_csv"), - ), - output: []any{ - map[string]any{ - "foo": "1", - "bar": "2", - "baz": "3", - }, - map[string]any{ - "foo": "4", - "bar": "5", - "baz": "6", - }, - }, - }, - "check parse csv 2": { - input: methods( - literalFn("foo,bar,baz"), - method("parse_csv"), - ), - output: []any{}, - }, - "check parse csv 3": { - input: methods( - literalFn("foo,bar\nfoo 1,bar 1\nfoo 2,bar 2"), - method("parse_csv"), - method("string"), - ), - output: `[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]`, - }, - "check parse csv error 1": { - input: methods( - literalFn("foo,bar,baz\n1,2,3,4"), - method("parse_csv"), - ), - err: "string literal: record on line 2: wrong number of fields", - }, - "check explode 1": { - input: methods( - jsonFn(`{"foo":[1,2,3],"id":"bar"}`), - method("explode", "foo"), - method("string"), - ), - output: `[{"foo":1,"id":"bar"},{"foo":2,"id":"bar"},{"foo":3,"id":"bar"}]`, - }, - "check explode 2": { - input: methods( - jsonFn(`{"foo":{"also":"this","bar":[{"key":"value1"},{"key":"value2"},{"key":"value3"}]},"id":"baz"}`), - method("explode", "foo.bar"), - method("string"), - ), - output: `[{"foo":{"also":"this","bar":{"key":"value1"}},"id":"baz"},{"foo":{"also":"this","bar":{"key":"value2"}},"id":"baz"},{"foo":{"also":"this","bar":{"key":"value3"}},"id":"baz"}]`, - }, - "check explode 3": { - input: methods( - jsonFn(`{"foo":{"a":1,"b":2,"c":3},"id":"bar"}`), - method("explode", "foo"), - method("string"), - ), - output: `{"a":{"foo":1,"id":"bar"},"b":{"foo":2,"id":"bar"},"c":{"foo":3,"id":"bar"}}`, - }, - "check explode 4": { - input: methods( - jsonFn(`{"foo":{"also":"this","bar":{"key1":["a","b"],"key2":{"c":3,"d":4}}},"id":"baz"}`), - method("explode", "foo.bar"), - method("string"), - ), - output: `{"key1":{"foo":{"also":"this","bar":["a","b"]},"id":"baz"},"key2":{"foo":{"also":"this","bar":{"c":3,"d":4}},"id":"baz"}}`, - }, - "check explode error 1": { - input: methods( - jsonFn(`{"foo":"not an array","id":"bar"}`), - method("explode", "foo"), - method("string"), - ), - err: "object literal: expected array or object value at path 'foo', found: string", - }, - "check explode error 2": { - input: methods( - jsonFn(`{"id":"bar"}`), - method("explode", "foo"), - method("string"), - ), - err: "object literal: expected array or object value at path 'foo', found: null", - }, - "check without single": { - input: methods( - jsonFn(`{"a":"first","b":"second"}`), - method("without", "a"), - ), - output: map[string]any{"b": "second"}, - }, - "check without double": { - input: methods( - jsonFn(`{"a":"first","b":"second","c":"third"}`), - method("without", "a", "c"), - ), - output: map[string]any{"b": "second"}, - }, - "check without nested": { - input: methods( - jsonFn(`{"inner":{"a":"first","b":"second","c":"third"}}`), - method("without", "inner.a", "inner.c", "thisdoesntexist"), - ), - output: map[string]any{ - "inner": map[string]any{"b": "second"}, - }, - }, - "check without combination": { - input: methods( - jsonFn(`{"d":"fourth","e":"fifth","inner":{"a":"first","b":"second","c":"third"}}`), - method("without", "d", "inner.a", "inner.c"), - ), - output: map[string]any{ - "e": "fifth", - "inner": map[string]any{"b": "second"}, - }, - }, - "check without nested not object": { - input: methods( - jsonFn(`{"a":"first","b":"second","c":"third"}`), - method("without", "a", "c.foo"), - ), - output: map[string]any{ - "b": "second", - "c": "third", - }, - }, - "check unique custom": { - input: methods( - jsonFn(`[{"v":"a"},{"v":"b"},{"v":"c"},{"v":"b"},{"v":"d"},{"v":"a"}]`), - method("unique", NewFieldFunction("v")), - ), - output: []any{ - map[string]any{"v": "a"}, - map[string]any{"v": "b"}, - map[string]any{"v": "c"}, - map[string]any{"v": "d"}, - }, - }, - "check unique bad": { - input: methods( - jsonFn(`[{"v":"a"},{"v":"b"},{"v":"c"},{"v":"b"},{"v":"d"},{"v":"a"}]`), - method("unique"), - ), - err: "array literal: index 0: expected string or number value, got object", - }, - "check unique not array": { - input: methods( - literalFn("foo"), - method("unique"), - ), - err: "expected array value, got string from string literal (\"foo\")", - }, - "check unique": { - input: methods( - jsonFn(`[3.0,5,3,4,5.1,5]`), - method("unique"), - ), - output: []any{3.0, 5.0, 4.0, 5.1}, - }, - "check unique strings": { - input: methods( - jsonFn(`["a","b","c","b","d","a"]`), - method("unique"), - ), - output: []any{"a", "b", "c", "d"}, - }, - "check unique mixed": { - input: methods( - jsonFn(`[3.0,"a","5",3,"b",5,"c","b",5.0,"d","a"]`), - method("unique"), - ), - output: []any{3.0, "a", "5", "b", 5.0, "c", "d"}, - }, - "check html escape query": { - input: methods( - literalFn("foo & bar"), - method("escape_html"), - ), - output: "foo & bar", - }, - "check html escape query bytes": { - input: methods( - function("content"), - method("escape_html"), - ), - messages: []easyMsg{ - {content: `foo & bar`}, - }, - output: "foo & bar", - }, - "check html unescape query": { - input: methods( - literalFn("foo & bar"), - method("unescape_html"), - ), - output: "foo & bar", - }, - "check html unescape query bytes": { - input: methods( - function(`content`), - method("unescape_html"), - ), - messages: []easyMsg{ - {content: `foo & bar`}, - }, - output: "foo & bar", - }, - "check sort custom": { - input: methods( - jsonFn(`[3,22,13,7,30]`), - method("sort", arithmetic(NewFieldFunction("left"), NewFieldFunction("right"), ArithmeticGt)), - ), - output: []any{30.0, 22.0, 13.0, 7.0, 3.0}, - }, - "check sort error": { - input: methods( - jsonFn(`[3,22,{"foo":"bar"},7,null]`), - method("sort"), - ), - err: "sort element 2: expected number or string value, got object", - }, - "check sort strings custom": { - input: methods( - jsonFn(`["c","a","f","z"]`), - method("sort", arithmetic(NewFieldFunction("left"), NewFieldFunction("right"), ArithmeticGt)), - ), - output: []any{"z", "f", "c", "a"}, - }, - "check join": { - input: methods( - jsonFn(`["foo","bar"]`), - method("join", ","), - ), - output: "foo,bar", - }, - "check join 2": { - input: methods( - jsonFn(`["foo"]`), - method("join", ","), - ), - output: "foo", - }, - "check join 3": { - input: methods( - jsonFn(`[]`), - method("join", ","), - ), - output: "", - }, - "check join no delim": { - input: methods( - jsonFn(`["foo","bar"]`), - method("join"), - ), - output: "foobar", - }, - "check join fail not array": { - input: methods( - literalFn("foo"), - method("join", ","), - ), - err: "expected array value, got string from string literal (\"foo\")", - }, - "check join fail number": { - input: methods( - jsonFn(`["foo",10,"bar"]`), - method("join", ","), - ), - err: "array literal: failed to join element 1: expected string value, got number (10)", - }, - "check regexp find all submatch": { - input: methods( - literalFn("-axxb-ab-"), - method("re_find_all_submatch", "a(x*)b"), - ), - output: []any{ - []any{"axxb", "xx"}, - []any{"ab", ""}, - }, - }, - "check regexp find all submatch bytes": { - input: methods( - function(`content`), - method("re_find_all_submatch", "a(x*)b"), - ), - messages: []easyMsg{{content: `-axxb-ab-`}}, - output: []any{ - []any{"axxb", "xx"}, - []any{"ab", ""}, - }, - }, - "check regexp find all": { - input: methods( - literalFn("paranormal"), - method("re_find_all", "a."), - ), - output: []any{"ar", "an", "al"}, - }, - "check regexp find all bytes": { - input: methods( - function(`content`), - method("re_find_all", "a."), - ), - messages: []easyMsg{{content: `paranormal`}}, - output: []any{"ar", "an", "al"}, - }, - "check type": { - input: methods( - literalFn("foobar"), - method("type"), - ), - output: "string", - }, - "check has_prefix": { - input: methods( - literalFn("foobar"), - method("has_prefix", "foo"), - ), - output: true, - }, - "check has_prefix 2": { - input: methods( - function("content"), - method("has_prefix", "foo"), - ), - messages: []easyMsg{{content: `foobar`}}, - output: true, - }, - "check has_prefix neg": { - input: methods( - literalFn("foobar"), - method("has_prefix", "bar"), - ), - output: false, - }, - "check has_suffix": { - input: methods( - literalFn("foobar"), - method("has_suffix", "bar"), - ), - output: true, - }, - "check has_suffix 2": { - input: methods( - function("content"), - method("has_suffix", "bar"), - ), - messages: []easyMsg{{content: `foobar`}}, - output: true, - }, - "check has_suffix neg": { - input: methods( - literalFn("foobar"), - method("has_suffix", "foo"), - ), - output: false, - }, - "check bool": { - input: methods( - literalFn("true"), - method("bool"), - ), - output: true, - }, - "check bool 2": { - input: methods( - literalFn("false"), - method("bool"), - ), - output: false, - }, - "check bool 3": { - input: methods( - literalFn(true), - method("bool"), - ), - output: true, - }, - "check bool 4": { - input: methods( - literalFn(false), - method("bool"), - ), - output: false, - }, - "check bool 5": { - input: methods( - literalFn(int64(5)), - method("bool"), - ), - output: true, - }, - "check bool 6": { - input: methods( - literalFn(int64(0)), - method("bool"), - ), - output: false, - }, - "check bool 7": { - input: methods( - literalFn("nope"), - method("bool"), - ), - err: `expected bool value, got string from string literal ("nope")`, - }, - "check bool 8": { - input: methods( - literalFn("nope"), - method("bool", true), - ), - output: true, - }, - "check bool 9": { - input: methods( - literalFn("nope"), - method("bool", false), - ), - output: false, - }, - "check number": { - input: methods( - literalFn("21"), - method("number"), - ), - output: float64(21), - }, - "check number 2": { - input: methods( - literalFn("nope"), - method("number"), - ), - err: `string literal: strconv.ParseFloat: parsing "nope": invalid syntax`, - }, - "check number 3": { - input: methods( - literalFn("nope"), - method("number", 5.0), - ), - output: float64(5), - }, - "check number 4": { - input: methods( - literalFn("nope"), - method("number", 5.2), - ), - output: float64(5.2), - }, - "check not_null": { - input: methods( - literalFn(21.0), - method("not_null"), - ), - output: 21.0, - }, - "check not null 2": { - input: methods( - literalFn(nil), - method("not_null"), - ), - err: `null literal: value is null`, - }, - "check index": { - input: methods( - jsonFn(`["foo","bar","baz"]`), - method("index", int64(1)), - ), - output: "bar", - }, - "check index neg": { - input: methods( - jsonFn(`["foo","bar","baz"]`), - method("index", int64(-1)), - ), - output: "baz", - }, - "check index oob": { - input: methods( - jsonFn(`["foo","bar","baz"]`), - method("index", int64(4)), - method("catch", "buz"), - ), - output: "buz", - }, - "check index oob neg": { - input: methods( - jsonFn(`["foo","bar","baz"]`), - method("index", int64(-4)), - method("catch", "buz"), - ), - output: "buz", - }, - "check url escape query": { - input: methods( - literalFn("foo & bar"), - method("escape_url_query"), - ), - output: "foo+%26+bar", - }, - "check url escape query bytes": { - input: methods( - function("content"), - method("escape_url_query"), - ), - messages: []easyMsg{ - {content: `foo & bar`}, - }, - output: "foo+%26+bar", - }, - "check url unescape query": { - input: methods( - literalFn("foo+%26+bar"), - method("unescape_url_query"), - ), - output: "foo & bar", - }, - "check url unescape query bytes": { - input: methods( - function("content"), - method("unescape_url_query"), - ), - messages: []easyMsg{ - {content: `foo+%26+bar`}, - }, - output: "foo & bar", - }, - "check flatten": { - input: methods( - function("json"), - method("flatten"), - ), - messages: []easyMsg{ - {content: `["foo",["bar","baz"],"buz"]`}, - }, - output: []any{ - "foo", "bar", "baz", "buz", - }, - }, - "check flatten 2": { - input: methods( - function("json"), - method("flatten"), - ), - messages: []easyMsg{ - {content: `[]`}, - }, - output: []any{}, - }, - "check flatten 3": { - input: methods( - function("json"), - method("flatten"), - ), - messages: []easyMsg{ - {content: `["foo","bar","baz","buz"]`}, - }, - output: []any{ - "foo", "bar", "baz", "buz", - }, - }, - "check collapse": { - input: methods( - function("json"), - method("collapse"), - ), - messages: []easyMsg{ - {content: `{"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]}`}, - }, - output: map[string]any{ - "foo.0.bar": "1", - "foo.2.bar": "2", - }, - }, - "check collapse include empty": { - input: methods( - function("json"), - method("collapse", true), - ), - messages: []easyMsg{ - {content: `{"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]}`}, - }, - output: map[string]any{ - "foo.0.bar": "1", - "foo.1.bar": struct{}{}, - "foo.2.bar": "2", - "foo.3.bar": []struct{}{}, - }, - }, - "check sha1 hash": { - input: methods( - literalFn("hello world"), - method("hash", "sha1"), - method("encode", "hex"), - ), - output: `2aae6c35c94fcfb415dbe95f408b9ce91ee846ed`, - }, - "check hmac sha1 hash": { - input: methods( - literalFn("hello world"), - method("hash", "hmac_sha1", "static-key"), - method("encode", "hex"), - ), - output: `d87e5f068fa08fe90bb95bc7c8344cb809179d76`, - }, - "check hmac sha1 hash 2": { - input: methods( - literalFn("hello world"), - method("hash", "hmac_sha1", "foo"), - method("encode", "hex"), - ), - output: `20224529cc42a39bacc96459f6ead9d17da7f128`, - }, - "check sha256 hash": { - input: methods( - literalFn("hello world"), - method("hash", "sha256"), - method("encode", "hex"), - ), - output: `b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9`, - }, - "check hmac sha256 hash": { - input: methods( - literalFn("hello world"), - method("hash", "hmac_sha256", "static-key"), - method("encode", "hex"), - ), - output: `b1cdce8b2add1f96135b2506f8ab748ae8ef15c49c0320357a6d168c42e20746`, - }, - "check sha512 hash": { - input: methods( - literalFn("hello world"), - method("hash", "sha512"), - method("encode", "hex"), - ), - output: `309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f`, - }, - "check hmac sha512 hash": { - input: methods( - literalFn("hello world"), - method("hash", "hmac_sha512", "static-key"), - method("encode", "hex"), - ), - output: `fd5d5ed60b96e820ebaace4fed962a401adefd3e89c51a374f0bb7f49ed02892af8bc8591628dcbc8b5f065df6bb06588cba95d488c1c8b88faa7cbe08e4558d`, - }, - "check xxhash64 hash": { - input: methods( - literalFn("hello world"), - method("hash", "xxhash64"), - method("string"), - ), - output: `5020219685658847592`, - }, - "check md5 hash": { - input: methods( - literalFn("hello world"), - method("hash", "md5"), - method("encode", "hex"), - ), - output: `5eb63bbbe01eeed093cb22bb8f5acdc3`, - }, - "check crc32 hash IEEE (default)": { - input: methods( - literalFn("hello world"), - method("hash", "crc32"), - method("encode", "hex"), - ), - output: `0d4a1185`, - }, - "check crc32 hash IEEE (explicit)": { - input: methods( - literalFn("hello world"), - method("hash", "crc32", "", "IEEE"), - method("encode", "hex"), - ), - output: `0d4a1185`, - }, - "check crc32 hash Castagnoli": { - input: methods( - literalFn("hello world"), - method("hash", "crc32", "", "Castagnoli"), - method("encode", "hex"), - ), - output: `c99465aa`, - }, - "check crc32 hash Koopman": { - input: methods( - literalFn("hello world"), - method("hash", "crc32", "", "Koopman"), - method("encode", "hex"), - ), - output: `df373d3c`, - }, - "check crc32 hash not supported": { - input: methods( - literalFn("hello world"), - method("hash", "crc32", "", "not-supported"), - method("encode", "hex"), - ), - err: `string literal: unsupported crc32 hash key "not-supported"`, - }, - "check hex encode": { - input: methods( - literalFn("hello world"), - method("encode", "hex"), - ), - output: `68656c6c6f20776f726c64`, - }, - "check hex decode": { - input: methods( - literalFn("68656c6c6f20776f726c64"), - method("decode", "hex"), - method("string"), - ), - output: `hello world`, - }, - "check base64 encode": { - input: methods( - literalFn("hello world"), - method("encode", "base64"), - ), - output: `aGVsbG8gd29ybGQ=`, - }, - "check base64 decode": { - input: methods( - literalFn("aGVsbG8gd29ybGQ="), - method("decode", "base64"), - method("string"), - ), - output: `hello world`, - }, - "check base64url encode": { - input: methods( - literalFn("<>"), - method("encode", "base64url"), - ), - output: `PDw_Pz8-Pg==`, - }, - "check base64url decode": { - input: methods( - literalFn("PDw_Pz8-Pg=="), - method("decode", "base64url"), - method("string"), - ), - output: `<>`, - }, - "check z85 encode": { - input: methods( - literalFn("hello world!"), - method("encode", "z85"), - ), - output: `xK#0@zYthe plain old text

"), - method("strip_html"), - ), - output: `the plain old text`, - }, - "check strip html bytes": { - input: methods( - function("content"), - method("strip_html"), - ), - messages: []easyMsg{ - {content: `

the plain old text

`}, - }, - output: []byte(`the plain old text`), - }, - "check quote": { - input: methods( - NewFieldFunction(""), - method("quote"), - ), - value: func() *any { - var s any = linebreakStr - return &s - }(), - output: `"foo\nbar\nbaz"`, - }, - "check quote bytes": { - input: methods( - NewFieldFunction(""), - method("quote"), - ), - value: func() *any { - var s any = []byte(linebreakStr) - return &s - }(), - output: `"foo\nbar\nbaz"`, - }, - "check unquote": { - input: methods( - NewFieldFunction(""), - method("unquote"), - ), - value: func() *any { - var s any = "\"foo\\nbar\\nbaz\"" - return &s - }(), - output: linebreakStr, - }, - "check unquote bytes": { - input: methods( - NewFieldFunction(""), - method("unquote"), - ), - value: func() *any { - var s any = []byte("\"foo\\nbar\\nbaz\"") - return &s - }(), - output: linebreakStr, - }, - "check replace": { - input: methods( - literalFn("The foo ate my homework"), - method("replace_all", "foo", "dog"), - ), - output: "The dog ate my homework", - }, - "check replace bytes": { - input: methods( - function("content"), - method("replace_all", "foo", "dog"), - ), - messages: []easyMsg{ - {content: `The foo ate my homework`}, - }, - output: []byte("The dog ate my homework"), - }, - "check trim": { - input: methods( - literalFn(" the foo bar "), - method("trim"), - ), - output: "the foo bar", - }, - "check trim 2": { - input: methods( - literalFn("!!?!the foo bar!"), - method("trim", "!?"), - ), - output: "the foo bar", - }, - "check trim bytes": { - input: methods( - function(`content`), - method("trim"), - ), - messages: []easyMsg{ - {content: ` the foo bar `}, - }, - output: []byte("the foo bar"), - }, - "check trim bytes 2": { - input: methods( - function(`content`), - method("trim", "!?"), - ), - messages: []easyMsg{ - {content: `!!?!the foo bar!`}, - }, - output: []byte("the foo bar"), - }, - "check capitalize": { - input: methods( - literalFn("the foo bar"), - method("capitalize"), - ), - output: "The Foo Bar", - }, - "check capitalize bytes": { - input: methods( - function(`content`), - method("capitalize"), - ), - messages: []easyMsg{ - {content: `the foo bar`}, - }, - output: []byte("The Foo Bar"), - }, - "check split": { - input: methods( - literalFn("foo,bar,baz"), - method("split", ","), - ), - output: []any{"foo", "bar", "baz"}, - }, - "check split bytes": { - input: methods( - function("content"), - method("split", ","), - ), - messages: []easyMsg{ - {content: `foo,bar,baz,`}, - }, - output: []any{[]byte("foo"), []byte("bar"), []byte("baz"), []byte("")}, - }, - "check slice": { - input: methods( - literalFn("foo bar baz"), - method("slice", 0.0, 3.0), - ), - output: "foo", - }, - "check slice 2": { - input: methods( - literalFn("foo bar baz"), - method("slice", 8.0), - ), - output: "baz", - }, - "check slice neg start": { - input: methods( - literalFn("foo bar baz"), - method("slice", -1.0), - ), - output: "z", - }, - "check slice neg start 2": { - input: methods( - literalFn("foo bar baz"), - method("slice", -2.0), - ), - output: "az", - }, - "check slice neg start 3": { - input: methods( - literalFn("foo bar baz"), - method("slice", -100.0), - ), - output: "foo bar baz", - }, - "check slice neg end 1": { - input: methods( - literalFn("foo bar baz"), - method("slice", 0.0, -1.0), - ), - output: "foo bar ba", - }, - "check slice neg end 2": { - input: methods( - literalFn("foo bar baz"), - method("slice", 0.0, -2.0), - ), - output: "foo bar b", - }, - "check slice neg end 3": { - input: methods( - literalFn("foo bar baz"), - method("slice", 0.0, -100.0), - ), - output: "", - }, - "check slice oob string": { - input: methods( - literalFn("foo bar baz"), - method("slice", 0.0, 30.0), - ), - output: "foo bar baz", - }, - "check slice oob array": { - input: methods( - jsonFn(`["foo","bar","baz"]`), - method("slice", 0.0, 30.0), - ), - output: []any{"foo", "bar", "baz"}, - }, - "check slice invalid": { - input: methods( - literalFn(10.0), - method("slice", 8.0), - ), - err: `expected array or string value, got number from number literal (10)`, - }, - "check slice array": { - input: methods( - jsonFn(`["foo","bar","baz","buz"]`), - method("slice", 1.0, 3.0), - ), - output: []any{"bar", "baz"}, - }, - "check regexp match": { - input: methods( - literalFn(`"there are 10 puppies"`), - method("re_match", "[0-9]"), - ), - output: true, - }, - "check regexp match 2": { - input: methods( - literalFn(`"there are ten puppies"`), - method("re_match", "[0-9]"), - ), - output: false, - }, - "check regexp match dynamic": { - input: methods( - function("json", "input"), - method("re_match", function("json", "re")), - ), - messages: []easyMsg{ - {content: `{"input":"there are 10 puppies","re":"[0-9]"}`}, - }, - output: true, - }, - "check regexp replace": { - input: methods( - literalFn("foo ADD 70"), - method("re_replace_all", "ADD ([0-9]+)", "+($1)"), - ), - output: "foo +(70)", - }, - "check regexp replace dynamic": { - input: methods( - function("json", "input"), - method("re_replace_all", function("json", "re"), function("json", "replace")), - ), - messages: []easyMsg{ - {content: `{"input":"foo ADD 70","re":"ADD ([0-9]+)","replace":"+($1)"}`}, - }, - output: "foo +(70)", - }, - "check parse json": { - input: methods( - literalFn("{\"foo\":\"bar\"}"), - method("parse_json"), - ), - output: map[string]any{ - "foo": "bar", - }, - }, - "check parse json with use_number set to true": { - input: methods( - literalFn("{\"foo\":11380878173205700000000000000000000000000000000}"), - method("parse_json", true), - ), - output: map[string]any{ - "foo": json.Number("11380878173205700000000000000000000000000000000"), - }, - }, - "check parse json with use_number set to false": { - input: methods( - literalFn("{\"foo\":11380878173205700000000000000000000000000000000}"), - method("parse_json", false), - ), - output: map[string]any{ - "foo": 1.13808781732057e+46, - }, - }, - "check parse json with use_number not set": { - input: methods( - literalFn("{\"foo\":11380878173205700000000000000000000000000000000}"), - method("parse_json"), - ), - output: map[string]any{ - "foo": 1.13808781732057e+46, - }, - }, - "check parse json invalid": { - input: methods( - literalFn("not valid json"), - method("parse_json"), - ), - err: `string literal: failed to parse value as JSON: invalid character 'o' in literal null (expecting 'u')`, - }, - "check append": { - input: methods( - jsonFn(`["foo"]`), - method("append", "bar", "baz"), - ), - output: []any{ - "foo", "bar", "baz", - }, - }, - "check append 2": { - input: methods( - jsonFn(`["foo"]`), - method("map", methods( - NewFieldFunction(""), - method("append", NewFieldFunction("")), - )), - ), - output: []any{ - "foo", []any{"foo"}, - }, - }, - "check enumerated": { - input: methods( - jsonFn(`["foo","bar","baz"]`), - method("enumerated"), - ), - output: []any{ - map[string]any{ - "index": int64(0), - "value": "foo", - }, - map[string]any{ - "index": int64(1), - "value": "bar", - }, - map[string]any{ - "index": int64(2), - "value": "baz", - }, - }, - }, - "check merge": { - input: methods( - jsonFn(`{"foo":"val1"}`), - method("merge", jsonFn(`{"bar":"val2"}`)), - ), - output: map[string]any{ - "foo": "val1", - "bar": "val2", - }, - }, - "check merge 2": { - input: methods( - function("json"), - method("map", methods( - NewFieldFunction("foo"), - method("merge", NewFieldFunction("bar")), - )), - ), - messages: []easyMsg{ - {content: `{"bar":{"second":"val2","third":6},"foo":{"first":"val1","third":3}}`}, - }, - output: map[string]any{ - "first": "val1", - "second": "val2", - "third": []any{jn(3), jn(6)}, - }, - }, - "check merge 3": { - input: methods( - function("json"), - method("map", methods( - NewFieldFunction(""), - method("merge", NewFieldFunction("bar")), - )), - ), - messages: []easyMsg{ - {content: `{"bar":{"second":"val2","third":6},"foo":{"first":"val1","third":3}}`}, - }, - output: map[string]any{ - "foo": map[string]any{ - "first": "val1", - "third": jn(3), - }, - "bar": map[string]any{ - "second": "val2", - "third": jn(6), - }, - "second": "val2", - "third": jn(6), - }, - }, - "check merge 4": { - input: methods( - function("json"), - method("map", methods( - NewFieldFunction("foo"), - method("merge", NewFieldFunction("bar")), - )), - ), - messages: []easyMsg{ - {content: `{"bar":{"second":"val2","third":[6]},"foo":{"first":"val1","third":[3]}}`}, - }, - output: map[string]any{ - "first": "val1", - "second": "val2", - "third": []any{jn(3), jn(6)}, - }, - }, - "check merge 5": { - input: methods( - function("json"), - method("map", methods( - NewFieldFunction("foo"), - method("merge", NewFieldFunction("bar")), - method("merge", NewFieldFunction("foo")), - )), - ), - messages: []easyMsg{ - {content: `{"bar":{"second":"val2","third":[6]},"foo":{"first":"val1","third":[3]}}`}, - }, - output: map[string]any{ - "first": []any{"val1", "val1"}, - "second": "val2", - "third": []any{jn(3), jn(6), jn(3)}, - }, - }, - "check merge arrays": { - input: methods( - jsonFn("[]"), - method("merge", "foo"), - ), - messages: []easyMsg{ - {content: `{}`}, - }, - output: []any{"foo"}, - }, - "check merge arrays 2": { - input: methods( - jsonFn(`["foo"]`), - method("merge", []any{"bar", "baz"}), - ), - messages: []easyMsg{ - {content: `{}`}, - }, - output: []any{"foo", "bar", "baz"}, - }, - "check contains array": { - input: methods( - function("json"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `["nope","foo","bar"]`}}, - output: true, - }, - "check contains array 2": { - input: methods( - function("json"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `["nope","bar"]`}}, - output: false, - }, - "check contains array nums": { - input: methods( - function("json"), - method("contains", int64(10)), - ), - messages: []easyMsg{{content: `["nope",10.0,3]`}}, - output: true, - }, - "check contains array nums 2": { - input: methods( - function("json"), - method("contains", int64(10)), - ), - messages: []easyMsg{{content: `["nope",3]`}}, - output: false, - }, - "check contains map": { - input: methods( - function("json"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `{"1":"nope","2":"foo","3":"bar"}`}}, - output: true, - }, - "check contains map 2": { - input: methods( - function("json"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `{"1":"nope","3":"bar"}`}}, - output: false, - }, - "check contains invalid type": { - input: methods( - function("json", "nope"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `{"nope":false}`}}, - err: "expected string, array or object value, got bool from json path `nope` (false)", - }, - "check substr": { - input: methods( - function("json", "foo"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `{"foo":"hello foo world"}`}}, - output: true, - }, - "check substr 2": { - input: methods( - function("json", "foo"), - method("contains", "foo"), - ), - messages: []easyMsg{{content: `{"foo":"hello bar world"}`}}, - output: false, - }, - "check map each": { - input: methods( - jsonFn(`["foo","bar"]`), - method("map_each", methods( - NewFieldFunction(""), - method("uppercase"), - )), - ), - output: []any{"FOO", "BAR"}, - }, - "check map each 2": { - input: methods( - jsonFn(`["foo","bar"]`), - method("map_each", methods( - literalFn("(%v)"), - method("format", NewFieldFunction("")), - method("uppercase"), - )), - ), - output: []any{"(FOO)", "(BAR)"}, - }, - "check map each object": { - input: methods( - jsonFn(`{"foo":"hello world","bar":"this is ash"}`), - method("map_each", methods( - NewFieldFunction("value"), - method("uppercase"), - )), - ), - output: map[string]any{ - "foo": "HELLO WORLD", - "bar": "THIS IS ASH", - }, - }, - "check filter array": { - input: methods( - jsonFn(`[2,14,4,11,7]`), - method("filter", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", 10.0), - ArithmeticGt, - )), - ), - output: []any{14.0, 11.0}, - }, - "check filter object": { - input: methods( - jsonFn(`{"foo":"hello ! world","bar":"this is ash","baz":"im cool!"}`), - method("filter", methods( - NewFieldFunction("value"), - method("contains", "!"), - )), - ), - output: map[string]any{ - "foo": "hello ! world", - "baz": "im cool!", - }, - }, - "check fold": { - input: methods( - jsonFn(`[3,5,2]`), - method("fold", 0.0, arithmetic( - NewFieldFunction("tally"), - NewFieldFunction("value"), - ArithmeticAdd, - )), - ), - messages: []easyMsg{ - {content: `{}`}, - }, - output: float64(10), - }, - "check fold 2": { - input: methods( - jsonFn(`["foo","bar"]`), - method("fold", "", methods( - literalFn("%v%v"), - method("format", NewFieldFunction("tally"), NewFieldFunction("value")), - )), - ), - messages: []easyMsg{ - {content: `{}`}, - }, - output: "foobar", - }, - "check fold exec err 2": { - input: methods( - jsonFn(`["foo","bar"]`), - method("fold", jsonFn(`{"values":[]}`), methods( - NewFieldFunction("does.not.exist"), - method("number"), - )), - ), - messages: []easyMsg{ - {content: `{}`}, - }, - err: "expected number value, got null from field `this.does.not.exist`", - }, - "check keys literal": { - input: methods( - jsonFn(`{"foo":1,"bar":2}`), - method("keys"), - method("sort"), - ), - messages: []easyMsg{{content: `{}`}}, - output: []any{"bar", "foo"}, - }, - "check keys empty": { - input: methods( - jsonFn(`{}`), - method("keys"), - ), - messages: []easyMsg{{content: `{}`}}, - output: []any{}, - }, - "check keys function": { - input: methods( - function(`json`), - method("keys"), - method("sort"), - ), - messages: []easyMsg{{content: `{"bar":2,"foo":1}`}}, - output: []any{"bar", "foo"}, - }, - "check keys error": { - input: methods( - literalFn(`foo`), - method("keys"), - method("sort"), - ), - messages: []easyMsg{{content: `{"bar":2,"foo":1}`}}, - err: `expected object value, got string from string literal ("foo")`, - }, - "check values literal": { - input: methods( - jsonFn(`{"foo":1,"bar":2}`), - method("values"), - method("sort"), - ), - messages: []easyMsg{{content: `{}`}}, - output: []any{1.0, 2.0}, - }, - "check values empty": { - input: methods( - jsonFn(`{}`), - method("values"), - ), - messages: []easyMsg{{content: `{}`}}, - output: []any{}, - }, - "check values function": { - input: methods( - function(`json`), - method("values"), - method("sort"), - ), - messages: []easyMsg{{content: `{"bar":2,"foo":1}`}}, - output: []any{jn(1), jn(2)}, - }, - "check values error": { - input: methods( - literalFn(`foo`), - method("values"), - method("sort"), - ), - messages: []easyMsg{{content: `{"bar":2,"foo":1}`}}, - err: `expected object value, got string from string literal ("foo")`, - }, - "check aes-ctr encryption": { - input: methods( - literalFn("hello world!"), - method( - "encrypt_aes", "ctr", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"), - method("decode", "hex"), - ), - ), - method("encode", "hex"), - ), - output: `84e9b31ff7400bdf80be7254`, - }, - "check aes-ctr decryption": { - input: methods( - literalFn("84e9b31ff7400bdf80be7254"), - method("decode", "hex"), - method( - "decrypt_aes", "ctr", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"), - method("decode", "hex"), - ), - ), - method("string"), - ), - output: `hello world!`, - }, - "check aes-ofb encryption": { - input: methods( - literalFn("hello world!"), - method( - "encrypt_aes", "ofb", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("000102030405060708090a0b0c0d0e0f"), - method("decode", "hex"), - ), - ), - method("encode", "hex"), - ), - output: `389b0ba0f64d45d9a86553c8`, - }, - "check aes-ofb decryption": { - input: methods( - literalFn("389b0ba0f64d45d9a86553c8"), - method("decode", "hex"), - method( - "decrypt_aes", "ofb", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("000102030405060708090a0b0c0d0e0f"), - method("decode", "hex"), - ), - ), - method("string"), - ), - output: `hello world!`, - }, - "check aes-cbc encryption": { - input: methods( - literalFn("6bc1bee22e409f96e93d7e117393172a"), - method("decode", "hex"), - method( - "encrypt_aes", "cbc", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("000102030405060708090a0b0c0d0e0f"), - method("decode", "hex"), - ), - ), - method("encode", "hex"), - ), - output: `7649abac8119b246cee98e9b12e9197d`, - }, - "check aes-cbc encryption error": { - input: methods( - literalFn("hello world"), - method( - "encrypt_aes", "cbc", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("000102030405060708090a0b0c0d0e0f"), - method("decode", "hex"), - ), - ), - method("encode", "hex"), - ), - err: `string literal: plaintext is not a multiple of the block size`, - }, - "check aes-cbc decryption": { - input: methods( - literalFn("7649abac8119b246cee98e9b12e9197d"), - method("decode", "hex"), - method( - "decrypt_aes", "cbc", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("000102030405060708090a0b0c0d0e0f"), - method("decode", "hex"), - ), - ), - method("string"), - method("encode", "hex"), - ), - output: `6bc1bee22e409f96e93d7e117393172a`, - }, - "check aes-cbc decryption error": { - input: methods( - literalFn("7649abac81"), - method("decode", "hex"), - method( - "decrypt_aes", "cbc", - methods( - literalFn("2b7e151628aed2a6abf7158809cf4f3c"), - method("decode", "hex"), - ), - methods( - literalFn("000102030405060708090a0b0c0d0e0f"), - method("decode", "hex"), - ), - ), - method("string"), - method("encode", "hex"), - ), - err: `method decode: ciphertext is not a multiple of the block size`, - }, - "check any no array": { - input: methods( - literalFn("foo"), - method("any", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", "bar"), - ArithmeticEq, - )), - ), - err: "expected array value, got string from string literal (\"foo\")", - }, - "check any bad mapping": { - input: methods( - literalFn([]any{false, "bar", true}), - method("any", NewFieldFunction("")), - ), - err: "array literal: element 1: expected bool value, got string (\"bar\")", - }, - "check any true": { - input: methods( - literalFn([]any{"foo", "bar", "baz"}), - method("any", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", "bar"), - ArithmeticEq, - )), - ), - output: true, - }, - "check any false": { - input: methods( - literalFn([]any{"foo", "buz", "baz"}), - method("any", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", "bar"), - ArithmeticEq, - )), - ), - output: false, - }, - "check any empty": { - input: methods( - literalFn([]any{}), - method("any", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", 9.0), - ArithmeticLt, - )), - ), - output: false, - }, - "check all true": { - input: methods( - literalFn([]any{10.0, 11.0, 12.0}), - method("all", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", 9.0), - ArithmeticGt, - )), - ), - output: true, - }, - "check all false": { - input: methods( - literalFn([]any{10.0, 8.0, 12.0}), - method("all", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", 9.0), - ArithmeticGt, - )), - ), - output: false, - }, - "check all empty": { - input: methods( - literalFn([]any{}), - method("all", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", 9.0), - ArithmeticLt, - )), - ), - output: false, - }, - "check all bad mapping": { - input: methods( - literalFn([]any{true, "bar", false}), - method("all", NewFieldFunction("")), - ), - err: "array literal: element 1: expected bool value, got string (\"bar\")", - }, - "check all no array": { - input: methods( - literalFn("foo"), - method("any", arithmetic( - NewFieldFunction(""), - NewLiteralFunction("", "bar"), - ArithmeticEq, - )), - ), - err: "expected array value, got string from string literal (\"foo\")", - }, - "check floor": { - input: methods(literalFn(5.8), method("floor")), - output: int64(5), - }, - "check floor bad value": { - input: methods(literalFn("nope"), method("floor")), - err: "expected number value, got string from string literal (\"nope\")", - }, - "check floor int": { - input: methods(literalFn(int64(5)), method("floor")), - output: int64(5), - }, - "check floor uint": { - input: methods(literalFn(uint64(5)), method("floor")), - output: uint64(5), - }, - "check floor json.Number": { - input: methods(literalFn(json.Number("5.8")), method("floor")), - output: int64(5), - }, - "check round up": { - input: methods(literalFn(5.8), method("round")), - output: int64(6), - }, - "check round down": { - input: methods(literalFn(5.3), method("round")), - output: int64(5), - }, - "check replace_many string": { - input: methods(literalFn("hello world"), method("replace_all_many", []any{ - "", "BOLD", - "", "!BOLD", - "", "ITA", - "", "!ITA", - })), - output: "ITAhello!ITA BOLDworld!BOLD", - }, - "check replace_many bytes": { - input: methods(literalFn([]byte("hello world")), method("replace_all_many", []any{ - "", "BOLD", - "", "!BOLD", - "", "ITA", - "", "!ITA", - })), - output: []byte("ITAhello!ITA BOLDworld!BOLD"), - }, - "check index of": { - input: methods( - function(`content`), - method("index_of", "bar"), - ), - messages: []easyMsg{ - {content: `foobar`}, - }, - output: int64(3), - }, - "check index of no match": { - input: methods( - function(`content`), - method("index_of", "bar"), - ), - messages: []easyMsg{ - {content: `foofoo`}, - }, - output: int64(-1), - }, - "check reverse": { - input: methods( - function(`content`), - method("reverse"), - ), - messages: []easyMsg{ - {content: `foobar`}, - }, - output: []byte("raboof"), - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - msg := message.QuickBatch(nil) - for _, m := range test.messages { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - msg = append(msg, part) - } - - for i := 0; i < 10; i++ { - res, err := test.input.Exec(FunctionContext{ - Maps: map[string]Function{}, - Index: test.index, - MsgBatch: msg, - }.WithValueFunc(func() *any { return test.value })) - if test.err != "" { - require.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - } - require.Equal(t, test.output, res) - } - - // Ensure nothing changed - for i, m := range test.messages { - doc, err := msg.Get(i).AsStructuredMut() - if err == nil { - msg.Get(i).SetStructured(doc) - } - assert.Equal(t, m.content, string(msg.Get(i).AsBytes())) - } - }) - } -} - -func TestMethodTargets(t *testing.T) { - function := func(name string, args ...any) Function { - t.Helper() - fn, err := InitFunctionHelper(name, args...) - require.NoError(t, err) - return fn - } - method := func(fn Function, name string, args ...any) Function { - t.Helper() - fn, err := InitMethodHelper(name, fn, args...) - require.NoError(t, err) - return fn - } - - tests := map[string]struct { - input Function - maps map[string]Function - output []TargetPath - }{ - "get from json": { - input: method(function("json", "foo.bar"), "get", "baz.buz"), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "foo", "bar", "baz", "buz"), - }, - }, - "get from get from json": { - input: method(method(function("json", "foo.bar"), "get", "baz"), "get", "buz"), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "foo", "bar", "baz", "buz"), - }, - }, - "mapping get from json": { - input: method(NewFieldFunction("foo.bar"), "map", NewFieldFunction("baz")), - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "foo", "bar", "baz"), - }, - }, - "ref mapping get from json": { - input: method(NewFieldFunction("foo.bar"), "apply", "foomap"), - maps: map[string]Function{ - "foomap": NewFieldFunction("baz"), - }, - output: []TargetPath{ - NewTargetPath(TargetValue, "foo", "bar"), - NewTargetPath(TargetValue, "foo", "bar", "baz"), - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - _, res := test.input.QueryTargets(TargetsContext{ - Maps: test.maps, - }) - assert.Equal(t, test.output, res) - }) - } -} - -func TestMethodNoArgsTargets(t *testing.T) { - fn := NewFieldFunction("foo.bar.baz") - exp := NewTargetPath(TargetValue, "foo", "bar", "baz") - for k := range AllMethods.methods { - // Only tests methods that do not need arguments, we need manual checks - // for other methods. - m, err := InitMethodHelper(k, fn) - if err != nil { - continue - } - _, targets := m.QueryTargets(TargetsContext{ - Maps: map[string]Function{}, - }) - assert.Contains(t, targets, exp, "method: %v", k) - } -} diff --git a/internal/bloblang/query/package.go b/internal/bloblang/query/package.go deleted file mode 100644 index 44eb9a249b..0000000000 --- a/internal/bloblang/query/package.go +++ /dev/null @@ -1,162 +0,0 @@ -// Package query provides a parser for the right-hand side query part of the -// bloblang spec. This is useful as a separate package as it is used in -// isolation within interpolation functions. -package query - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -type badFunctionErr string - -func (e badFunctionErr) Error() string { - return fmt.Sprintf("unrecognised function '%v'", string(e)) -} - -type badMethodErr string - -func (e badMethodErr) Error() string { - return fmt.Sprintf("unrecognised method '%v'", string(e)) -} - -//------------------------------------------------------------------------------ - -// MessageBatch is an interface type to be given to a query function, it allows -// the function to resolve fields and metadata from a Benthos message batch. -type MessageBatch interface { - Get(p int) *message.Part - Len() int -} - -// MetaMsg provides access to the metadata of a message. -type MetaMsg interface { - MetaSetMut(key string, value any) - MetaGetStr(key string) string - MetaGetMut(key string) (any, bool) - MetaDelete(key string) - MetaIterMut(f func(k string, v any) error) error - MetaIterStr(f func(k, v string) error) error -} - -// FunctionContext provides access to a range of query targets for functions to -// reference. -type FunctionContext struct { - Maps map[string]Function - Vars map[string]any - Index int - MsgBatch MessageBatch - - // Reference new message being mapped - NewMeta MetaMsg - NewValue *any - - valueFn func() *any - value *any - nextValue *any - namedValue *namedContextValue - - // Used to track how many maps we've entered. - stackCount int -} - -type namedContextValue struct { - name string - value any - next *namedContextValue -} - -// IncrStackCount increases the count stored in the function context of how many -// maps we've entered and returns the current count. -func (ctx FunctionContext) IncrStackCount() (FunctionContext, int) { //nolint: gocritic // Ignore unnamedResult false positive - ctx.stackCount++ - return ctx, ctx.stackCount -} - -// NamedValue returns the value of a named context if it exists. -func (ctx FunctionContext) NamedValue(name string) (any, bool) { - current := ctx.namedValue - for current != nil { - if current.name == name { - return current.value, true - } - current = current.next - } - return nil, false -} - -// WithNamedValue returns a FunctionContext with a named value. -func (ctx FunctionContext) WithNamedValue(name string, value any) FunctionContext { - previous := ctx.namedValue - ctx.namedValue = &namedContextValue{ - name: name, - value: value, - next: previous, - } - return ctx -} - -// Value returns a lazily evaluated context value. A context value is not always -// available and can therefore be nil. -func (ctx FunctionContext) Value() *any { - if ctx.value != nil { - return ctx.value - } - if ctx.valueFn == nil { - return nil - } - return ctx.valueFn() -} - -// WithValueFunc returns a function context with a new value func. -func (ctx FunctionContext) WithValueFunc(fn func() *any) FunctionContext { - ctx.valueFn = fn - return ctx -} - -// WithValue returns a function context with a new value. -func (ctx FunctionContext) WithValue(value any) FunctionContext { - ctx.nextValue = ctx.value - ctx.value = &value - return ctx -} - -// PopValue returns the current default value, and a function context with the -// top value removed from the context stack. If the value returned is the -// absolute root value function then the context returned is unchanged. If there -// is no current default value then a nil value is returned and the context -// returned is unchanged. -func (ctx FunctionContext) PopValue() (*any, FunctionContext) { - retValue := ctx.Value() - - if ctx.nextValue != nil { - ctx.value = ctx.nextValue - ctx.nextValue = nil - } else { - ctx.value = nil - } - - return retValue, ctx -} - -//------------------------------------------------------------------------------ - -// ExecToString returns a string from a function execution. -func ExecToString(fn Function, ctx FunctionContext) (string, error) { - v, err := fn.Exec(ctx) - if err != nil { - return "", err - } - return value.IToString(v), nil -} - -// ExecToBytes returns a byte slice from a function execution. -func ExecToBytes(fn Function, ctx FunctionContext) ([]byte, error) { - v, err := fn.Exec(ctx) - if err != nil { - return nil, err - } - return value.IToBytes(v), nil -} diff --git a/internal/bloblang/query/params.go b/internal/bloblang/query/params.go deleted file mode 100644 index e4be6012e9..0000000000 --- a/internal/bloblang/query/params.go +++ /dev/null @@ -1,734 +0,0 @@ -package query - -import ( - "encoding/json" - "errors" - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// ParamDefinition describes a single parameter for a function or method. -type ParamDefinition struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - ValueType value.Type `json:"type"` - NoDynamic bool `json:"no_dynamic"` - ScalarsToLiteral bool `json:"scalars_to_literal"` - - // IsOptional is implicit when there's a DefaultValue. However, there are - // times when a parameter is used to change behaviour without having a - // default. - IsOptional bool `json:"is_optional,omitempty"` - DefaultValue *any `json:"default,omitempty"` -} - -func (d ParamDefinition) validate() error { - if !nameRegexp.MatchString(d.Name) { - return fmt.Errorf("parameter name '%v' does not match the required regular expression /%v/", d.Name, nameRegexpRaw) - } - return nil -} - -// ParamString creates a new string typed parameter. -func ParamString(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TString, - } -} - -// ParamTimestamp creates a new timestamp typed parameter. -func ParamTimestamp(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TTimestamp, - } -} - -// ParamInt64 creates a new integer typed parameter. -func ParamInt64(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TInt, - } -} - -// ParamFloat creates a new float typed parameter. -func ParamFloat(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TFloat, - } -} - -// ParamBool creates a new bool typed parameter. -func ParamBool(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TBool, - } -} - -// ParamArray creates a new array typed parameter. -func ParamArray(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TArray, - } -} - -// ParamObject creates a new object typed parameter. -func ParamObject(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TObject, - } -} - -// ParamQuery creates a new query typed parameter. The field wrapScalars -// determines whether non-query arguments are allowed, in which case they will -// be converted into literal functions. -func ParamQuery(name, description string, wrapScalars bool) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TQuery, - ScalarsToLiteral: wrapScalars, - } -} - -// ParamAny creates a new parameter that can be any type (excluding query). -func ParamAny(name, description string) ParamDefinition { - return ParamDefinition{ - Name: name, Description: description, - ValueType: value.TUnknown, - } -} - -// NoDynamic disables any form of dynamic assignment for this parameter. This is -// quite limiting (prevents variables from being used, etc) and so should only -// be used with caution. -func (d ParamDefinition) DisableDynamic() ParamDefinition { - d.NoDynamic = true - return d -} - -// Optional marks the parameter as optional. -func (d ParamDefinition) Optional() ParamDefinition { - d.IsOptional = true - return d -} - -// Default adds a default value to a parameter, also making it implicitly -// optional. -func (d ParamDefinition) Default(v any) ParamDefinition { - d.DefaultValue = &v - return d -} - -// PrettyDefault returns a marshalled version of the parameters default value, -// or an empty string if there isn't one. -func (d ParamDefinition) PrettyDefault() string { - if d.DefaultValue == nil { - return "" - } - b, err := json.Marshal(*d.DefaultValue) - if err != nil { - return "" - } - return string(b) -} - -func (d ParamDefinition) parseArgValue(v any) (any, error) { - switch d.ValueType { - case value.TInt: - return value.IGetInt(v) - case value.TFloat, value.TNumber: - return value.IGetNumber(v) - case value.TString: - switch t := v.(type) { - case string: - return t, nil - case []byte: - return string(t), nil - } - case value.TTimestamp: - return value.IGetTimestamp(v) - case value.TBool: - return value.IGetBool(v) - case value.TArray: - if _, isArray := v.([]any); isArray { - return v, nil - } - case value.TObject: - if _, isObj := v.(map[string]any); isObj { - return v, nil - } - case value.TQuery: - if _, isDyn := v.(Function); isDyn { - return v, nil - } - if d.ScalarsToLiteral { - return NewLiteralFunction("", v), nil - } - case value.TUnknown: - return v, nil - } - return nil, fmt.Errorf("wrong argument type, expected %v, got %v", d.ValueType, value.ITypeOf(v)) -} - -//------------------------------------------------------------------------------ - -// Params defines the expected arguments of a function or method. -type Params struct { - Variadic bool `json:"variadic,omitempty"` - Definitions []ParamDefinition `json:"named,omitempty"` - - // Used by parsed param frames, we instantiate this here so that it's - // allocated only once at parse time rather than execution time. - nameToIndex map[string]int -} - -// NewParams creates a new empty parameters definition. -func NewParams() Params { - return Params{ - nameToIndex: map[string]int{}, - } -} - -// VariadicParams creates a new empty parameters definition where any number of -// nameless arguments are considered valid. -func VariadicParams() Params { - return Params{ - Variadic: true, - nameToIndex: map[string]int{}, - } -} - -// Add a parameter to the spec. -func (p Params) Add(def ParamDefinition) Params { - p.Definitions = append(p.Definitions, def) - p.nameToIndex[def.Name] = len(p.Definitions) - 1 - return p -} - -// PopulateNameless returns a set of populated arguments from a list of nameless -// parameters. -func (p Params) PopulateNameless(args ...any) (*ParsedParams, error) { - procParams, err := p.processNameless(args) - if err != nil { - return nil, err - } - - dynArgs, err := p.gatherDynamicArgs(procParams) - if err != nil { - return nil, err - } - return &ParsedParams{ - source: p, - dynArgs: dynArgs, - values: procParams, - }, nil -} - -// PopulateNamed returns a set of populated arguments from a map of named -// parameters. -func (p Params) PopulateNamed(args map[string]any) (*ParsedParams, error) { - procParams, err := p.processNamed(args) - if err != nil { - return nil, err - } - - dynArgs, err := p.gatherDynamicArgs(procParams) - if err != nil { - return nil, err - } - return &ParsedParams{ - source: p, - dynArgs: dynArgs, - values: procParams, - }, nil -} - -func (p Params) validate() error { - if p.Variadic && len(p.Definitions) > 0 { - return errors.New("cannot add named parameters to a variadic parameter definition") - } - - seen := map[string]struct{}{} - for _, param := range p.Definitions { - if err := param.validate(); err != nil { - return err - } - if _, exists := seen[param.Name]; exists { - return fmt.Errorf("duplicate parameter name: %v", param.Name) - } - seen[param.Name] = struct{}{} - } - - return nil -} - -type dynamicArgIndex struct { - index int - fn Function -} - -func (p Params) gatherDynamicArgs(args []any) (dynArgs []dynamicArgIndex, err error) { - if p.Variadic { - for i, arg := range args { - if fn, isFn := arg.(Function); isFn { - dynArgs = append(dynArgs, dynamicArgIndex{index: i, fn: fn}) - } - } - return - } - for i, param := range p.Definitions { - if param.ValueType == value.TQuery { - continue - } - if fn, isFn := args[i].(Function); isFn { - if param.NoDynamic { - err = fmt.Errorf("param %v must not be dynamic", param.Name) - return - } - dynArgs = append(dynArgs, dynamicArgIndex{index: i, fn: fn}) - } - } - return -} - -func expandLiteralArgs(args []any) { - for i, dArg := range args { - if lit, isLit := dArg.(*Literal); isLit { - args[i] = lit.Value - } - } -} - -// processNameless attempts to validate a list of unnamed arguments, and -// populates elements with default values if they are omitted. -func (p Params) processNameless(args []any) ([]any, error) { - if p.Variadic { - expandLiteralArgs(args) - return args, nil - } - - if len(args) > len(p.Definitions) { - return nil, fmt.Errorf("wrong number of arguments, expected %v, got %v", len(p.Definitions), len(args)) - } - - newArgs := args - if len(newArgs) != len(p.Definitions) { - newArgs = make([]any, len(p.Definitions)) - copy(newArgs, args) - } - - var missingParams []string - for i, param := range p.Definitions { - var v any - if len(args) > i { - v = newArgs[i] - } else if param.DefaultValue != nil { - v = *param.DefaultValue - } else if param.IsOptional { - continue - } else { - missingParams = append(missingParams, param.Name) - continue - } - - if lit, isLit := v.(*Literal); isLit && param.ValueType != value.TQuery { - // Literal functions are expanded automatically when the parameter - // type is not a dynamic query. - v = lit.Value - } - if _, isDyn := v.(Function); isDyn { - // Dynamic arguments pass through as these need to be dealt with - // during execution. - newArgs[i] = v - continue - } - - var err error - if newArgs[i], err = param.parseArgValue(v); err != nil { - return nil, fmt.Errorf("field %v: %w", param.Name, err) - } - } - - if len(missingParams) == 1 { - return nil, fmt.Errorf("missing parameter: %v", missingParams[0]) - } else if len(missingParams) > 1 { - return nil, fmt.Errorf("missing parameters: %v", strings.Join(missingParams, ", ")) - } - return newArgs, nil -} - -// processNamed attempts to validate a map of named arguments, and populates -// elements with default values if they are omitted. -func (p Params) processNamed(args map[string]any) ([]any, error) { - if p.Variadic { - return nil, errors.New("named arguments are not supported") - } - - newArgs := make([]any, len(p.Definitions)) - - var missingParams []string - for i, param := range p.Definitions { - v, exists := args[param.Name] - if !exists { - if param.IsOptional { - continue - } - if param.DefaultValue == nil { - missingParams = append(missingParams, param.Name) - continue - } - v = *param.DefaultValue - } - - // Remove found parameter names, any left over are unexpected. - delete(args, param.Name) - - if lit, isLit := v.(*Literal); isLit && param.ValueType != value.TQuery { - // Literal functions are expanded automatically when the parameter - // type is not a dynamic query. - v = lit.Value - } - if _, isDyn := v.(Function); isDyn { - // Dynamic arguments pass through as these need to be dealt with - // during execution. - newArgs[i] = v - continue - } - - var err error - if newArgs[i], err = param.parseArgValue(v); err != nil { - return nil, fmt.Errorf("field %v: %w", param.Name, err) - } - } - - if len(args) > 0 { - var unexpected []string - for k := range args { - unexpected = append(unexpected, k) - } - sort.Strings(unexpected) - - optionsStr := "" - if len(missingParams) == 1 { - optionsStr = fmt.Sprintf(", did you mean %v?", missingParams[0]) - } else if len(missingParams) > 1 { - optionsStr = fmt.Sprintf(", expected %v", strings.Join(missingParams, ", ")) - } - - if len(unexpected) == 1 { - return nil, fmt.Errorf("unknown parameter %v%v", unexpected[0], optionsStr) - } - return nil, fmt.Errorf("unknown parameters %v%v", strings.Join(unexpected, ", "), optionsStr) - } - - if len(missingParams) == 1 { - return nil, fmt.Errorf("missing parameter: %v", missingParams[0]) - } else if len(missingParams) > 1 { - return nil, fmt.Errorf("missing parameters: %v", strings.Join(missingParams, ", ")) - } - return newArgs, nil -} - -//------------------------------------------------------------------------------ - -// ParsedParams is a reference to the arguments of a method or function -// instantiation. -type ParsedParams struct { - source Params - dynArgs []dynamicArgIndex - values []any -} - -// dynamic returns any argument functions that must be evaluated at query time. -// The purpose of this method is to use the list to extract function targets and -// other info, use ResolveDynamic for populating these values with a function -// context. -func (p *ParsedParams) dynamic() []Function { - if p == nil || len(p.dynArgs) == 0 { - return nil - } - fns := make([]Function, len(p.dynArgs)) - for i, v := range p.dynArgs { - fns[i] = v.fn - } - return fns -} - -// ResolveDynamic attempts to execute all dynamic arguments with a given context -// and populate a new parsed parameters set with the values, ready to be used in -// a function or method. -func (p *ParsedParams) ResolveDynamic(ctx FunctionContext) (*ParsedParams, error) { - if len(p.dynArgs) == 0 { - return p, nil - } - newValues := make([]any, len(p.values)) - copy(newValues, p.values) - for i, dyn := range p.dynArgs { - var sourceDef ParamDefinition - if len(p.source.Definitions) > dyn.index { - sourceDef = p.source.Definitions[dyn.index] - } else { - // Must be variadic arguments. - sourceDef = ParamAny(strconv.Itoa(i), "") - } - tmpValue, err := dyn.fn.Exec(ctx) - if err != nil { - return nil, fmt.Errorf("failed to extract input arg '%v': %w", sourceDef.Name, err) - } - if newValues[dyn.index], err = sourceDef.parseArgValue(tmpValue); err != nil { - return nil, fmt.Errorf("failed to extract input arg '%v': %w", sourceDef.Name, err) - } - } - return &ParsedParams{ - source: p.source, - values: newValues, - }, nil -} - -// Raw returns the arguments as a generic slice. -func (p *ParsedParams) Raw() []any { - if p == nil { - return nil - } - return p.values -} - -// Index returns an argument value at a given index. -func (p *ParsedParams) Index(i int) (any, error) { - if i < 0 || len(p.values) <= i { - return nil, fmt.Errorf("parameter index %v out of bounds", i) - } - return p.values[i], nil -} - -// Field returns an argument value with a given name. -func (p *ParsedParams) Field(n string) (any, error) { - index, ok := p.source.nameToIndex[n] - if !ok { - return nil, fmt.Errorf("parameter %v not found", n) - } - if index < 0 || len(p.values) <= index { - return nil, fmt.Errorf("parameter index %v out of bounds", index) - } - return p.values[index], nil -} - -// FieldArray returns an array value with a given name. -func (p *ParsedParams) FieldArray(n string) ([]any, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - a, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - return a, nil -} - -// FieldOptionalArray returns an optional array value with a given name. -func (p *ParsedParams) FieldOptionalArray(n string) (*[]any, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - a, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - return &a, nil -} - -// FieldString returns a string argument value with a given name. -func (p *ParsedParams) FieldString(n string) (string, error) { - v, err := p.Field(n) - if err != nil { - return "", err - } - str, ok := v.(string) - if !ok { - return "", value.NewTypeError(v, value.TString) - } - return str, nil -} - -// FieldOptionalString returns a string argument value with a given name if it -// was defined, otherwise nil. -func (p *ParsedParams) FieldOptionalString(n string) (*string, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - str, ok := v.(string) - if !ok { - return nil, value.NewTypeError(v, value.TString) - } - return &str, nil -} - -// FieldTimestamp returns a timestamp argument value with a given name. -func (p *ParsedParams) FieldTimestamp(n string) (time.Time, error) { - v, err := p.Field(n) - if err != nil { - return time.Time{}, err - } - t, ok := v.(time.Time) - if !ok { - return time.Time{}, value.NewTypeError(v, value.TTimestamp) - } - return t, nil -} - -// FieldOptionalTimestamp returns a timestamp argument value with a given name -// if it was defined, otherwise nil. -func (p *ParsedParams) FieldOptionalTimestamp(n string) (*time.Time, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - t, ok := v.(time.Time) - if !ok { - return nil, value.NewTypeError(v, value.TTimestamp) - } - return &t, nil -} - -// FieldInt64 returns an integer argument value with a given name. -func (p *ParsedParams) FieldInt64(n string) (int64, error) { - v, err := p.Field(n) - if err != nil { - return 0, err - } - i, ok := v.(int64) - if !ok { - return 0, value.NewTypeError(v, value.TInt) - } - return i, nil -} - -// FieldOptionalInt64 returns an int argument value with a given name if it was -// defined, otherwise nil. -func (p *ParsedParams) FieldOptionalInt64(n string) (*int64, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - i, ok := v.(int64) - if !ok { - return nil, value.NewTypeError(v, value.TInt) - } - return &i, nil -} - -// FieldFloat returns a float argument value with a given name. -func (p *ParsedParams) FieldFloat(n string) (float64, error) { - v, err := p.Field(n) - if err != nil { - return 0, err - } - f, ok := v.(float64) - if !ok { - return 0, value.NewTypeError(v, value.TFloat) - } - return f, nil -} - -// FieldOptionalFloat returns a float argument value with a given name if it was -// defined, otherwise nil. -func (p *ParsedParams) FieldOptionalFloat(n string) (*float64, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - f, ok := v.(float64) - if !ok { - return nil, value.NewTypeError(v, value.TFloat) - } - return &f, nil -} - -// FieldBool returns a bool argument value with a given name. -func (p *ParsedParams) FieldBool(n string) (bool, error) { - v, err := p.Field(n) - if err != nil { - return false, err - } - b, ok := v.(bool) - if !ok { - return false, value.NewTypeError(v, value.TBool) - } - return b, nil -} - -// FieldOptionalBool returns a bool argument value with a given name if it was -// defined, otherwise nil. -func (p *ParsedParams) FieldOptionalBool(n string) (*bool, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - b, ok := v.(bool) - if !ok { - return nil, value.NewTypeError(v, value.TBool) - } - return &b, nil -} - -// FieldQuery returns a query argument value with a given name. -func (p *ParsedParams) FieldQuery(n string) (Function, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - f, ok := v.(Function) - if !ok { - return nil, value.NewTypeError(v, value.TQuery) - } - return f, nil -} - -// FieldOptionalQuery returns a query argument value with a given name if it -// was defined, otherwise nil. -func (p *ParsedParams) FieldOptionalQuery(n string) (Function, error) { - v, err := p.Field(n) - if err != nil { - return nil, err - } - if v == nil { - return nil, nil - } - f, ok := v.(Function) - if !ok { - return nil, value.NewTypeError(v, value.TQuery) - } - return f, nil -} diff --git a/internal/bloblang/query/params_test.go b/internal/bloblang/query/params_test.go deleted file mode 100644 index 13177e69b3..0000000000 --- a/internal/bloblang/query/params_test.go +++ /dev/null @@ -1,758 +0,0 @@ -package query - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParamsValidation(t *testing.T) { - tests := []struct { - name string - params Params - errContains string - }{ - { - name: "basic fields all normal", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")). - Add(ParamFloat("fourth", "")). - Add(ParamQuery("fifth", "", false)). - Add(ParamArray("sixth", "")). - Add(ParamObject("seventh", "")), - }, - { - name: "variadic with fields", - params: VariadicParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - errContains: "cannot add named parameters to a variadic parameter definition", - }, - { - name: "empty field name", - params: NewParams(). - Add(ParamString("", "")), - errContains: "parameter name '' does not match", - }, - { - name: "bad field name", - params: NewParams(). - Add(ParamString("contains naughty chars!", "")), - errContains: "parameter name 'contains naughty chars!' does not match", - }, - { - name: "duplicate field names", - params: NewParams(). - Add(ParamString("foo", "")). - Add(ParamString("bar", "")). - Add(ParamString("foo", "")), - errContains: "duplicate parameter name: foo", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := test.params.validate() - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestParamsNameless(t *testing.T) { - tests := []struct { - name string - params Params - input []any - output []any - errContains string - }{ - { - name: "basic fields all populated", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")). - Add(ParamFloat("fourth", "")). - Add(ParamQuery("fifth", "", false)). - Add(ParamArray("sixth", "")). - Add(ParamObject("seventh", "")). - Add(ParamTimestamp("eighth", "")), - input: []any{ - "foo", 5, false, 6.4, NewFieldFunction("nah"), []any{"one", "two"}, map[string]any{"a": "aaa", "b": "bbb"}, 1697185186, - }, - output: []any{ - "foo", int64(5), false, 6.4, NewFieldFunction("nah"), []any{"one", "two"}, map[string]any{"a": "aaa", "b": "bbb"}, time.Unix(1697185186, 0), - }, - }, - { - name: "basic fields defaults", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)). - Add(ParamTimestamp("fourth", "").Default(1697185186)), - input: []any{"bar"}, - output: []any{ - "bar", int64(5), true, time.Unix(1697185186, 0), - }, - }, - { - name: "basic fields optional no defaults", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Optional()). - Add(ParamBool("third", "").Optional()). - Add(ParamTimestamp("fourth", "").Optional()), - input: []any{"bar"}, - output: []any{ - "bar", nil, nil, nil, - }, - }, - { - name: "missing field", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: []any{}, - errContains: "missing parameter: first", - }, - { - name: "multiple missing fields", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "")). - Add(ParamBool("third", "")), - input: []any{}, - errContains: "missing parameters: first, second, third", - }, - { - name: "too many args", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: []any{"foo", 10, false, "bar"}, - errContains: "wrong number of arguments, expected 3, got 4", - }, - { - name: "bad type args", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: []any{"foo", true, 10}, - errContains: "field second: expected number", - }, - { - name: "bad timestamp arg", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamTimestamp("second", "").Default(1697185186)). - Add(ParamBool("third", "").Default(true)), - input: []any{"foo", true, 10}, - errContains: "field second: expected number or string", - }, - { - name: "bad query type", - params: NewParams(). - Add(ParamQuery("first", "", false)), - input: []any{"foo"}, - errContains: "field first: wrong argument type, expected query expression", - }, - { - name: "recast scalar type", - params: NewParams(). - Add(ParamQuery("first", "", true)), - input: []any{"foo"}, - output: []any{NewLiteralFunction("", "foo")}, - }, - { - name: "dont recast query type", - params: NewParams(). - Add(ParamQuery("first", "", true)), - input: []any{NewFieldFunction("foo")}, - output: []any{NewFieldFunction("foo")}, - }, - { - name: "function args unchanged", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")), - input: []any{ - "foo", NewFieldFunction("doc.value"), false, - }, - output: []any{ - "foo", NewFieldFunction("doc.value"), false, - }, - }, - { - name: "literal args expanded", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")), - input: []any{ - "foo", NewLiteralFunction("testing", int64(7)), false, - }, - output: []any{ - "foo", int64(7), false, - }, - }, - { - name: "variadic args expanded", - params: VariadicParams(), - input: []any{ - "foo", NewLiteralFunction("testing", int64(7)), false, - }, - output: []any{ - "foo", int64(7), false, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - res, err := test.params.processNameless(test.input) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } - }) - } -} - -func TestParamsNamed(t *testing.T) { - tests := []struct { - name string - params Params - input map[string]any - output []any - errContains string - }{ - { - name: "basic fields all populated", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")). - Add(ParamFloat("fourth", "")). - Add(ParamQuery("fifth", "", false)). - Add(ParamArray("sixth", "")). - Add(ParamObject("seventh", "")), - input: map[string]any{ - "first": "foo", "second": 5, "third": false, "fourth": 6.4, - "fifth": NewFieldFunction("nah"), "sixth": []any{"one", "two"}, - "seventh": map[string]any{"a": "aaa", "b": "bbb"}, - }, - output: []any{ - "foo", int64(5), false, 6.4, NewFieldFunction("nah"), []any{"one", "two"}, map[string]any{"a": "aaa", "b": "bbb"}, - }, - }, - { - name: "basic fields defaults", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: map[string]any{"first": "bar"}, - output: []any{ - "bar", int64(5), true, - }, - }, - { - name: "basic fields optional no defaults", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Optional()). - Add(ParamBool("third", "").Optional()), - input: map[string]any{"first": "bar"}, - output: []any{ - "bar", nil, nil, - }, - }, - { - name: "missing field", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: map[string]any{}, - errContains: "missing parameter: first", - }, - { - name: "multiple missing fields", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "")). - Add(ParamBool("third", "")), - input: map[string]any{}, - errContains: "missing parameters: first, second, third", - }, - { - name: "too many args", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: map[string]any{ - "first": "foo", "second": 10, "third": false, "fourth": "bar", - }, - errContains: "unknown parameter fourth", - }, - { - name: "typo arg missing field", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "")). - Add(ParamBool("third", "").Default(true)), - input: map[string]any{ - "first": "foo", "seconde": 10, "third": false, - }, - errContains: "unknown parameter seconde, did you mean second?", - }, - { - name: "typo arg missing fields", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "")). - Add(ParamBool("third", "")), - input: map[string]any{ - "first": "foo", "seconde": 10, "thirde": false, - }, - errContains: "unknown parameters seconde, thirde, expected second, third", - }, - { - name: "bad type args", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "").Default(true)), - input: map[string]any{"first": "foo", "second": true, "third": 10}, - errContains: "field second: expected number", - }, - { - name: "bad query type", - params: NewParams(). - Add(ParamQuery("first", "", false)), - input: map[string]any{"first": "foo"}, - errContains: "field first: wrong argument type, expected query expression", - }, - { - name: "recast scalar type", - params: NewParams(). - Add(ParamQuery("first", "", true)), - input: map[string]any{"first": "foo"}, - output: []any{NewLiteralFunction("", "foo")}, - }, - { - name: "dont recast query type", - params: NewParams(). - Add(ParamQuery("first", "", true)), - input: map[string]any{"first": NewFieldFunction("foo")}, - output: []any{NewFieldFunction("foo")}, - }, - { - name: "function args unchanged", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")), - input: map[string]any{ - "first": "foo", "second": NewFieldFunction("doc.value"), "third": false, - }, - output: []any{ - "foo", NewFieldFunction("doc.value"), false, - }, - }, - { - name: "literal args expanded", - params: NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")), - input: map[string]any{ - "first": "foo", "second": NewLiteralFunction("testing", 7), "third": false, - }, - output: []any{ - "foo", int64(7), false, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - res, err := test.params.processNamed(test.input) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } - }) - } -} - -func TestDynamicArgs(t *testing.T) { - p := NewParams(). - Add(ParamString("foo", "")). - Add(ParamQuery("bar", "", false)). - Add(ParamString("baz", "")) - - exp := []dynamicArgIndex(nil) - res, err := p.gatherDynamicArgs([]any{"first", "second", "third"}) - require.NoError(t, err) - assert.Equal(t, exp, res) - - exp = []dynamicArgIndex{ - {index: 0, fn: NewFieldFunction("first")}, - {index: 2, fn: NewFieldFunction("third")}, - } - res, err = p.gatherDynamicArgs([]any{ - NewFieldFunction("first"), - NewFieldFunction("second"), - NewFieldFunction("third"), - }) - require.NoError(t, err) - assert.Equal(t, exp, res) -} - -func TestDynamicVariadicArgs(t *testing.T) { - p := VariadicParams() - - exp := []dynamicArgIndex(nil) - res, err := p.gatherDynamicArgs([]any{"first", "second", "third"}) - require.NoError(t, err) - assert.Equal(t, exp, res) - - dynArgs := []any{ - NewFieldFunction("first"), - NewFieldFunction("second"), - NewFieldFunction("third"), - } - - exp = []dynamicArgIndex{ - {index: 0, fn: NewFieldFunction("first")}, - {index: 1, fn: NewFieldFunction("second")}, - {index: 2, fn: NewFieldFunction("third")}, - } - res, err = p.gatherDynamicArgs(dynArgs) - require.NoError(t, err) - assert.Equal(t, exp, res) - - parsed, err := p.PopulateNameless(dynArgs...) - require.NoError(t, err) - - newParsed, err := parsed.ResolveDynamic(FunctionContext{}.WithValue(map[string]any{ - "first": "first value", - "second": "second value", - "third": "third value", - })) - require.NoError(t, err) - - assert.Equal(t, []any{"first value", "second value", "third value"}, newParsed.Raw()) -} - -func TestParsedParamsNameless(t *testing.T) { - params := NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")) - - parsed, err := params.PopulateNameless("foo", 9, true) - require.NoError(t, err) - - assert.Empty(t, parsed.dynamic()) - assert.Equal(t, []any{ - "foo", int64(9), true, - }, parsed.Raw()) - - v, err := parsed.Index(0) - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, err = parsed.Field("first") - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, err = parsed.Index(1) - require.NoError(t, err) - assert.Equal(t, int64(9), v) - - v, err = parsed.Field("second") - require.NoError(t, err) - assert.Equal(t, int64(9), v) - - v, err = parsed.Index(2) - require.NoError(t, err) - assert.Equal(t, true, v) - - v, err = parsed.Field("third") - require.NoError(t, err) - assert.Equal(t, true, v) - - _, err = parsed.Index(-1) - require.Error(t, err) - - _, err = parsed.Index(3) - require.Error(t, err) - - _, err = parsed.Field("fourth") - require.Error(t, err) -} - -func TestParsedNoDynamic(t *testing.T) { - params := NewParams(). - Add(ParamString("first", "").DisableDynamic()) - - _, err := params.PopulateNameless("bar") - require.NoError(t, err) - - _, err = params.PopulateNameless(NewVarFunction("foo")) - require.Error(t, err) -} - -func TestParsedParamsNamed(t *testing.T) { - params := NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")) - - parsed, err := params.PopulateNamed(map[string]any{ - "first": "foo", - "second": 9, - "third": true, - }) - require.NoError(t, err) - - assert.Empty(t, parsed.dynamic()) - assert.Equal(t, []any{ - "foo", int64(9), true, - }, parsed.Raw()) - - v, err := parsed.Index(0) - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, err = parsed.Field("first") - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, err = parsed.Index(1) - require.NoError(t, err) - assert.Equal(t, int64(9), v) - - v, err = parsed.Field("second") - require.NoError(t, err) - assert.Equal(t, int64(9), v) - - v, err = parsed.Index(2) - require.NoError(t, err) - assert.Equal(t, true, v) - - v, err = parsed.Field("third") - require.NoError(t, err) - assert.Equal(t, true, v) - - _, err = parsed.Index(-1) - require.Error(t, err) - - _, err = parsed.Index(3) - require.Error(t, err) - - _, err = parsed.Field("fourth") - require.Error(t, err) -} - -func TestParsedParamsDynamic(t *testing.T) { - params := NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")) - - parsed, err := params.PopulateNameless(NewFieldFunction("doc.foo"), 9, NewFieldFunction("doc.bar")) - require.NoError(t, err) - - assert.Equal(t, []Function{ - NewFieldFunction("doc.foo"), - NewFieldFunction("doc.bar"), - }, parsed.dynamic()) - - parsedTwo, err := parsed.ResolveDynamic(FunctionContext{}.WithValue(map[string]any{ - "doc": map[string]any{ - "foo": "foo first value", - "bar": true, - }, - })) - require.NoError(t, err) - - parsedThree, err := parsed.ResolveDynamic(FunctionContext{}.WithValue(map[string]any{ - "doc": map[string]any{ - "foo": "foo second value", - "bar": false, - }, - })) - require.NoError(t, err) - - assert.Empty(t, parsedTwo.dynamic()) - assert.Equal(t, []any{ - "foo first value", int64(9), true, - }, parsedTwo.Raw()) - - require.NoError(t, err) - assert.Empty(t, parsedThree.dynamic()) - assert.Equal(t, []any{ - "foo second value", int64(9), false, - }, parsedThree.Raw()) -} - -func TestParsedParamsDynamicErrors(t *testing.T) { - tests := []struct { - name string - input any - errContains string - }{ - { - name: "function fails", - errContains: "context was undefined", - }, - { - name: "function gives wrong type", - input: map[string]any{ - "doc": map[string]any{ - "foo": 60, - "bar": "not a bool", - }, - }, - errContains: "'first': wrong argument type", - }, - } - - params := NewParams(). - Add(ParamString("first", "")). - Add(ParamInt64("second", "").Default(5)). - Add(ParamBool("third", "")) - - parsed, err := params.PopulateNameless(NewFieldFunction("doc.foo"), 9, NewFieldFunction("doc.bar")) - require.NoError(t, err) - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ctx := FunctionContext{} - if test.input != nil { - ctx = ctx.WithValue(test.input) - } - _, err := parsed.ResolveDynamic(ctx) - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - }) - } -} - -func TestParsedParams(t *testing.T) { - params := NewParams(). - Add(ParamString("first", "").Optional()). - Add(ParamInt64("second", "").Optional()). - Add(ParamFloat("third", "").Optional()). - Add(ParamBool("fourth", "").Optional()). - Add(ParamQuery("fifth", "", false).Optional()) - - parsed, err := params.PopulateNameless("one", 2, 3.0, true, NewFieldFunction("doc.foo")) - require.NoError(t, err) - - s, err := parsed.FieldString("first") - require.NoError(t, err) - assert.Equal(t, "one", s) - - i, err := parsed.FieldInt64("second") - require.NoError(t, err) - assert.Equal(t, int64(2), i) - - f, err := parsed.FieldFloat("third") - require.NoError(t, err) - assert.Equal(t, 3.0, f) - - b, err := parsed.FieldBool("fourth") - require.NoError(t, err) - assert.True(t, b) - - q, err := parsed.FieldQuery("fifth") - require.NoError(t, err) - assert.Equal(t, NewFieldFunction("doc.foo"), q) -} - -func TestParsedParamsOptional(t *testing.T) { - params := NewParams(). - Add(ParamString("first", "").Optional()). - Add(ParamInt64("second", "").Optional()). - Add(ParamFloat("third", "").Optional()). - Add(ParamBool("fourth", "").Optional()). - Add(ParamQuery("fifth", "", false).Optional()) - - parsed, err := params.PopulateNameless("one", 2, 3.0, true, NewFieldFunction("doc.foo")) - require.NoError(t, err) - - s, err := parsed.FieldOptionalString("first") - require.NoError(t, err) - require.NotNil(t, s) - assert.Equal(t, "one", *s) - - i, err := parsed.FieldOptionalInt64("second") - require.NoError(t, err) - require.NotNil(t, i) - assert.Equal(t, int64(2), *i) - - f, err := parsed.FieldOptionalFloat("third") - require.NoError(t, err) - require.NotNil(t, f) - assert.Equal(t, 3.0, *f) - - b, err := parsed.FieldOptionalBool("fourth") - require.NoError(t, err) - require.NotNil(t, b) - assert.True(t, *b) - - q, err := parsed.FieldOptionalQuery("fifth") - require.NoError(t, err) - require.NotNil(t, q) - assert.Equal(t, NewFieldFunction("doc.foo"), q) - - // Without any args - parsed, err = params.PopulateNameless() - require.NoError(t, err) - - s, err = parsed.FieldOptionalString("first") - require.NoError(t, err) - assert.Nil(t, s) - - i, err = parsed.FieldOptionalInt64("second") - require.NoError(t, err) - assert.Nil(t, i) - - f, err = parsed.FieldOptionalFloat("third") - require.NoError(t, err) - assert.Nil(t, f) - - b, err = parsed.FieldOptionalBool("fourth") - require.NoError(t, err) - assert.Nil(t, b) - - q, err = parsed.FieldOptionalQuery("fifth") - require.NoError(t, err) - assert.Nil(t, q) -} diff --git a/internal/bloblang/query/parsed_test.go b/internal/bloblang/query/parsed_test.go deleted file mode 100644 index 5f1d01e9e0..0000000000 --- a/internal/bloblang/query/parsed_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package query_test - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestMappings(t *testing.T) { - tests := []struct { - name string - mapping string - inputOutputs [][2]string - }{ - { - name: "format_timestamp one nameless arg", - mapping: `root.something_at = this.created_at.format_timestamp("2006-Jan-02 15:04:05")`, - inputOutputs: [][2]string{ - { - `{"created_at":"2020-08-14T11:50:26.371Z"}`, - `{"something_at":"2020-Aug-14 11:50:26"}`, - }, - }, - }, - { - name: "format_timestamp both nameless args", - mapping: `root.something_at = this.created_at.format_timestamp("2006-Jan-02 15:04:05", "America/New_York")`, - inputOutputs: [][2]string{ - { - `{"created_at":1597405526}`, - `{"something_at":"2020-Aug-14 07:45:26"}`, - }, - { - `{"created_at":"2020-08-14T11:50:26.371Z"}`, - `{"something_at":"2020-Aug-14 07:50:26"}`, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - m, err := bloblang.GlobalEnvironment().NewMapping(test.mapping) - require.NoError(t, err) - - for i, io := range test.inputOutputs { - msg := message.QuickBatch([][]byte{[]byte(io[0])}) - p, err := m.MapPart(0, msg) - exp := io[1] - if strings.HasPrefix(exp, "Error(") { - exp = exp[7 : len(exp)-2] - require.EqualError(t, err, exp, fmt.Sprintf("%v", i)) - } else if exp == "" { - require.NoError(t, err) - require.Nil(t, p) - } else { - require.NoError(t, err) - assert.Equal(t, exp, string(p.AsBytes()), fmt.Sprintf("%v", i)) - } - } - }) - } -} diff --git a/internal/bloblang/query/target.go b/internal/bloblang/query/target.go deleted file mode 100644 index fec71de5b1..0000000000 --- a/internal/bloblang/query/target.go +++ /dev/null @@ -1,159 +0,0 @@ -package query - -import "strings" - -// SliceToDotPath returns a valid dot path from a slice of path segments. -func SliceToDotPath(path ...string) string { - escapes := make([]string, len(path)) - for i, s := range path { - s = strings.ReplaceAll(s, "~", "~0") - s = strings.ReplaceAll(s, ".", "~1") - escapes[i] = s - } - return strings.Join(escapes, ".") -} - -// TargetType represents a query target type, which is a source of information -// that a query might require, such as metadata, structured message contents, -// the context, etc. -type TargetType int - -// TargetTypes. -const ( - TargetMetadata TargetType = iota - TargetValue - TargetRoot - TargetVariable -) - -// TargetPath represents a target type and segmented path that a query function -// references. An empty path indicates the root of the type is targeted. -type TargetPath struct { - Type TargetType - Path []string -} - -// NewTargetPath constructs a new target path from a type and zero or more path -// segments. -func NewTargetPath(t TargetType, path ...string) TargetPath { - return TargetPath{ - Type: t, - Path: path, - } -} - -func aggregateTargetPaths(fns ...Function) func(ctx TargetsContext) (TargetsContext, []TargetPath) { - return func(ctx TargetsContext) (TargetsContext, []TargetPath) { - var paths []TargetPath - for _, fn := range fns { - if fn != nil { - _, tmpPaths := fn.QueryTargets(ctx) - paths = append(paths, tmpPaths...) - } - } - return ctx, paths - } -} - -// TargetsContext describes the current Bloblang execution environment from the -// perspective of a particular query function in a way that allows it to -// determine which values it is targeting and the origins of those values. -// -// The environment consists of named maps that are globally accessible, the -// current value that is being executed upon by methods (when applicable), the -// general (main) context (referenced by the keyword `this`) and any other named -// contexts accessible at this point. -// -// Since it's possible for any query function to reference and return multiple -// target candidates (match expressions, etc) then each context and the current -// value are lists of paths, each being a candidate at runtime. -type TargetsContext struct { - Maps map[string]Function - - currentValues []TargetPath - mainContext []TargetPath - prevContext *prevContextPath - namedContext *namedContextPath -} - -type prevContextPath struct { - paths []TargetPath - next *prevContextPath -} - -type namedContextPath struct { - name string - paths []TargetPath - next *namedContextPath -} - -// Value returns the current value of the targets context, which is the path(s) -// being executed upon by methods. -func (ctx TargetsContext) Value() []TargetPath { - return ctx.currentValues -} - -// MainContext returns the path of the main context. -func (ctx TargetsContext) MainContext() []TargetPath { - return ctx.mainContext -} - -// NamedContext returns the path of a named context if it exists. -func (ctx TargetsContext) NamedContext(name string) []TargetPath { - current := ctx.namedContext - for current != nil { - if current.name == name { - return current.paths - } - current = current.next - } - return nil -} - -// WithValues returns a targets context where the current value being executed -// upon by methods is set to something new. -func (ctx TargetsContext) WithValues(paths []TargetPath) TargetsContext { - ctx.currentValues = paths - return ctx -} - -// WithValuesAsContext returns a targets context where the current value being -// executed upon by methods is now the main context. This happens when a query -// function is executed as a method, or within branches of match expressions. -func (ctx TargetsContext) WithValuesAsContext() TargetsContext { - ctx.prevContext = &prevContextPath{ - paths: ctx.mainContext, - next: ctx.prevContext, - } - ctx.mainContext = ctx.currentValues - ctx.currentValues = nil - return ctx -} - -// WithContextAsNamed moves the latest context into a named context and returns -// the context prior to that one to the main context. This is a way for named -// context mappings to correct the contexts so that the child query function -// returns the right paths. -func (ctx TargetsContext) WithContextAsNamed(name string) TargetsContext { - previous := ctx.namedContext - ctx.namedContext = &namedContextPath{ - name: name, - paths: ctx.mainContext, - next: previous, - } - if ctx.prevContext != nil { - ctx.mainContext = ctx.prevContext.paths - ctx.prevContext = ctx.prevContext.next - } - return ctx -} - -// PopContext returns a targets context with the latest context dropped and the -// previous (when applicable) returned. -func (ctx TargetsContext) PopContext() TargetsContext { - if ctx.prevContext != nil { - ctx.mainContext = ctx.prevContext.paths - ctx.prevContext = ctx.prevContext.next - } - return ctx -} diff --git a/internal/bundle/buffers.go b/internal/bundle/buffers.go deleted file mode 100644 index e8912a4c25..0000000000 --- a/internal/bundle/buffers.go +++ /dev/null @@ -1,98 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllBuffers is a set containing every single buffer that has been imported. -var AllBuffers = &BufferSet{ - specs: map[string]bufferSpec{}, -} - -//------------------------------------------------------------------------------ - -// BufferAdd adds a new buffer to this environment by providing a constructor -// and documentation. -func (e *Environment) BufferAdd(constructor BufferConstructor, spec docs.ComponentSpec) error { - return e.buffers.Add(constructor, spec) -} - -// BufferInit attempts to initialise a buffer from a config. -func (e *Environment) BufferInit(conf buffer.Config, mgr NewManagement) (buffer.Streamed, error) { - return e.buffers.Init(conf, mgr) -} - -// BufferDocs returns a slice of buffer specs, which document each method. -func (e *Environment) BufferDocs() []docs.ComponentSpec { - return e.buffers.Docs() -} - -//------------------------------------------------------------------------------ - -// BufferConstructor constructs an buffer component. -type BufferConstructor func(buffer.Config, NewManagement) (buffer.Streamed, error) - -type bufferSpec struct { - constructor BufferConstructor - spec docs.ComponentSpec -} - -// BufferSet contains an explicit set of buffers available to a Benthos service. -type BufferSet struct { - specs map[string]bufferSpec -} - -// Add a new buffer to this set by providing a spec (name, documentation, and -// constructor). -func (s *BufferSet) Add(constructor BufferConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]bufferSpec{} - } - spec.Type = docs.TypeBuffer - s.specs[spec.Name] = bufferSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an buffer from a config. -func (s *BufferSet) Init(conf buffer.Config, mgr NewManagement) (buffer.Streamed, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("buffer", conf.Type) - } - c, err := spec.constructor(conf, mgr) - err = wrapComponentErr(mgr, "buffer", err) - return c, err -} - -// Docs returns a slice of buffer specs, which document each method. -func (s *BufferSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *BufferSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/caches.go b/internal/bundle/caches.go deleted file mode 100644 index 4c44a9a062..0000000000 --- a/internal/bundle/caches.go +++ /dev/null @@ -1,98 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllCaches is a set containing every single cache that has been imported. -var AllCaches = &CacheSet{ - specs: map[string]cacheSpec{}, -} - -//------------------------------------------------------------------------------ - -// CacheAdd adds a new cache to this environment by providing a constructor -// and documentation. -func (e *Environment) CacheAdd(constructor CacheConstructor, spec docs.ComponentSpec) error { - return e.caches.Add(constructor, spec) -} - -// CacheInit attempts to initialise a cache from a config. -func (e *Environment) CacheInit(conf cache.Config, mgr NewManagement) (cache.V1, error) { - return e.caches.Init(conf, mgr) -} - -// CacheDocs returns a slice of cache specs, which document each method. -func (e *Environment) CacheDocs() []docs.ComponentSpec { - return e.caches.Docs() -} - -//------------------------------------------------------------------------------ - -// CacheConstructor constructs an cache component. -type CacheConstructor func(cache.Config, NewManagement) (cache.V1, error) - -type cacheSpec struct { - constructor CacheConstructor - spec docs.ComponentSpec -} - -// CacheSet contains an explicit set of caches available to a Benthos service. -type CacheSet struct { - specs map[string]cacheSpec -} - -// Add a new cache to this set by providing a spec (name, documentation, and -// constructor). -func (s *CacheSet) Add(constructor CacheConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]cacheSpec{} - } - spec.Type = docs.TypeCache - s.specs[spec.Name] = cacheSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an cache from a config. -func (s *CacheSet) Init(conf cache.Config, mgr NewManagement) (cache.V1, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("cache", conf.Type) - } - c, err := spec.constructor(conf, mgr) - err = wrapComponentErr(mgr, "cache", err) - return c, err -} - -// Docs returns a slice of cache specs, which document each method. -func (s *CacheSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *CacheSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/environment.go b/internal/bundle/environment.go deleted file mode 100644 index ad6d442644..0000000000 --- a/internal/bundle/environment.go +++ /dev/null @@ -1,112 +0,0 @@ -package bundle - -import ( - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Environment is a collection of Benthos component plugins that can be used in -// order to build and run streaming pipelines with access to different sets of -// plugins. This is useful for sandboxing, testing, etc. -type Environment struct { - buffers *BufferSet - caches *CacheSet - inputs *InputSet - outputs *OutputSet - processors *ProcessorSet - rateLimits *RateLimitSet - metrics *MetricsSet - tracers *TracerSet - - scanners *ScannerSet -} - -// NewEnvironment creates an empty environment. -func NewEnvironment() *Environment { - return &Environment{ - buffers: &BufferSet{}, - caches: &CacheSet{}, - inputs: &InputSet{}, - outputs: &OutputSet{}, - processors: &ProcessorSet{}, - rateLimits: &RateLimitSet{}, - metrics: &MetricsSet{}, - tracers: &TracerSet{}, - scanners: &ScannerSet{}, - } -} - -// Clone an existing environment to a new one that can be modified -// independently. -func (e *Environment) Clone() *Environment { - newEnv := NewEnvironment() - for _, v := range e.buffers.specs { - _ = newEnv.buffers.Add(v.constructor, v.spec) - } - for _, v := range e.caches.specs { - _ = newEnv.caches.Add(v.constructor, v.spec) - } - for _, v := range e.inputs.specs { - _ = newEnv.inputs.Add(v.constructor, v.spec) - } - for _, v := range e.outputs.specs { - _ = newEnv.outputs.Add(v.constructor, v.spec) - } - for _, v := range e.processors.specs { - _ = newEnv.processors.Add(v.constructor, v.spec) - } - for _, v := range e.rateLimits.specs { - _ = newEnv.rateLimits.Add(v.constructor, v.spec) - } - for _, v := range e.metrics.specs { - _ = newEnv.metrics.Add(v.constructor, v.spec) - } - for _, v := range e.tracers.specs { - _ = newEnv.tracers.Add(v.constructor, v.spec) - } - for _, v := range e.scanners.specs { - _ = newEnv.scanners.Add(v.constructor, v.spec) - } - return newEnv -} - -// GetDocs returns a documentation spec for an implementation of a component. -func (e *Environment) GetDocs(name string, ctype docs.Type) (docs.ComponentSpec, bool) { - var spec docs.ComponentSpec - var ok bool - - switch ctype { - case docs.TypeBuffer: - spec, ok = e.buffers.DocsFor(name) - case docs.TypeCache: - spec, ok = e.caches.DocsFor(name) - case docs.TypeInput: - spec, ok = e.inputs.DocsFor(name) - case docs.TypeOutput: - spec, ok = e.outputs.DocsFor(name) - case docs.TypeProcessor: - spec, ok = e.processors.DocsFor(name) - case docs.TypeRateLimit: - spec, ok = e.rateLimits.DocsFor(name) - case docs.TypeMetrics: - spec, ok = e.metrics.DocsFor(name) - case docs.TypeTracer: - spec, ok = e.tracers.DocsFor(name) - case docs.TypeScanner: - spec, ok = e.scanners.DocsFor(name) - } - - return spec, ok -} - -// GlobalEnvironment contains service-wide singleton bundles. -var GlobalEnvironment = &Environment{ - buffers: AllBuffers, - caches: AllCaches, - inputs: AllInputs, - outputs: AllOutputs, - processors: AllProcessors, - rateLimits: AllRateLimits, - metrics: AllMetrics, - tracers: AllTracers, - scanners: AllScanners, -} diff --git a/internal/bundle/inputs.go b/internal/bundle/inputs.go deleted file mode 100644 index 1dbca4b068..0000000000 --- a/internal/bundle/inputs.go +++ /dev/null @@ -1,97 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllInputs is a set containing every single input that has been imported. -var AllInputs = &InputSet{ - specs: map[string]inputSpec{}, -} - -//------------------------------------------------------------------------------ - -// InputAdd adds a new input to this environment by providing a constructor and -// documentation. -func (e *Environment) InputAdd(constructor InputConstructor, spec docs.ComponentSpec) error { - return e.inputs.Add(constructor, spec) -} - -// InputInit attempts to initialise an input from a config. -func (e *Environment) InputInit(conf input.Config, mgr NewManagement) (input.Streamed, error) { - return e.inputs.Init(conf, mgr) -} - -// InputDocs returns a slice of input specs, which document each method. -func (e *Environment) InputDocs() []docs.ComponentSpec { - return e.inputs.Docs() -} - -//------------------------------------------------------------------------------ - -// InputConstructor constructs an input component. -type InputConstructor func(input.Config, NewManagement) (input.Streamed, error) - -type inputSpec struct { - constructor InputConstructor - spec docs.ComponentSpec -} - -// InputSet contains an explicit set of inputs available to a Benthos service. -type InputSet struct { - specs map[string]inputSpec -} - -// Add a new input to this set by providing a constructor and documentation. -func (s *InputSet) Add(constructor InputConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]inputSpec{} - } - spec.Type = docs.TypeInput - s.specs[spec.Name] = inputSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an input from a config. -func (s *InputSet) Init(conf input.Config, mgr NewManagement) (input.Streamed, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("input", conf.Type) - } - c, err := spec.constructor(conf, mgr) - err = wrapComponentErr(mgr, "input", err) - return c, err -} - -// Docs returns a slice of input specs, which document each method. -func (s *InputSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *InputSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/metrics.go b/internal/bundle/metrics.go deleted file mode 100644 index 627953e4c7..0000000000 --- a/internal/bundle/metrics.go +++ /dev/null @@ -1,111 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllMetrics is a set containing every single metrics that has been imported. -var AllMetrics = &MetricsSet{ - specs: map[string]metricsSpec{}, -} - -//------------------------------------------------------------------------------ - -// MetricsAdd adds a new metrics exporter to this environment by providing a -// constructor and documentation. -func (e *Environment) MetricsAdd(constructor MetricConstructor, spec docs.ComponentSpec) error { - return e.metrics.Add(constructor, spec) -} - -// MetricsInit attempts to initialise a metrics exporter from a config. -func (e *Environment) MetricsInit(conf metrics.Config, nm NewManagement) (*metrics.Namespaced, error) { - return e.metrics.Init(conf, nm) -} - -// MetricsDocs returns a slice of metrics exporter specs. -func (e *Environment) MetricsDocs() []docs.ComponentSpec { - return e.metrics.Docs() -} - -//------------------------------------------------------------------------------ - -// MetricConstructor constructs an metrics component. -type MetricConstructor func(conf metrics.Config, nm NewManagement) (metrics.Type, error) - -type metricsSpec struct { - constructor MetricConstructor - spec docs.ComponentSpec -} - -// MetricsSet contains an explicit set of metrics available to a Benthos -// service. -type MetricsSet struct { - specs map[string]metricsSpec -} - -// Add a new metrics to this set by providing a spec (name, documentation, and -// constructor). -func (s *MetricsSet) Add(constructor MetricConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]metricsSpec{} - } - spec.Type = docs.TypeMetrics - s.specs[spec.Name] = metricsSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an metrics from a config. -func (s *MetricsSet) Init(conf metrics.Config, nm NewManagement) (*metrics.Namespaced, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("metric", conf.Type) - } - - m, err := spec.constructor(conf, nm) - if err != nil { - return nil, err - } - - ns := metrics.NewNamespaced(m) - if conf.Mapping != "" { - mmap, err := metrics.NewMapping(conf.Mapping, nm.Logger()) - if err != nil { - return nil, err - } - ns = ns.WithMapping(mmap) - } - return ns, nil -} - -// Docs returns a slice of metrics specs, which document each method. -func (s *MetricsSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *MetricsSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/outputs.go b/internal/bundle/outputs.go deleted file mode 100644 index 6a3416abbe..0000000000 --- a/internal/bundle/outputs.go +++ /dev/null @@ -1,107 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllOutputs is a set containing every single output that has been imported. -var AllOutputs = &OutputSet{ - specs: map[string]outputSpec{}, -} - -//------------------------------------------------------------------------------ - -// OutputAdd adds a new output to this environment by providing a constructor -// and documentation. -func (e *Environment) OutputAdd(constructor OutputConstructor, spec docs.ComponentSpec) error { - return e.outputs.Add(constructor, spec) -} - -// OutputInit attempts to initialise a output from a config. -func (e *Environment) OutputInit( - conf output.Config, - mgr NewManagement, - pipelines ...processor.PipelineConstructorFunc, -) (output.Streamed, error) { - return e.outputs.Init(conf, mgr, pipelines...) -} - -// OutputDocs returns a slice of output specs, which document each method. -func (e *Environment) OutputDocs() []docs.ComponentSpec { - return e.outputs.Docs() -} - -//------------------------------------------------------------------------------ - -// OutputConstructor constructs an output component. -type OutputConstructor func(output.Config, NewManagement, ...processor.PipelineConstructorFunc) (output.Streamed, error) - -type outputSpec struct { - constructor OutputConstructor - spec docs.ComponentSpec -} - -// OutputSet contains an explicit set of outputs available to a Benthos service. -type OutputSet struct { - specs map[string]outputSpec -} - -// Add a new output to this set by providing a spec (name, documentation, and -// constructor). -func (s *OutputSet) Add(constructor OutputConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]outputSpec{} - } - spec.Type = docs.TypeOutput - s.specs[spec.Name] = outputSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an output from a config. -func (s *OutputSet) Init( - conf output.Config, - mgr NewManagement, - pipelines ...processor.PipelineConstructorFunc, -) (output.Streamed, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("output", conf.Type) - } - c, err := spec.constructor(conf, mgr, pipelines...) - err = wrapComponentErr(mgr, "output", err) - return c, err -} - -// Docs returns a slice of output specs, which document each method. -func (s *OutputSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *OutputSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/package.go b/internal/bundle/package.go deleted file mode 100644 index 3e0155d2f0..0000000000 --- a/internal/bundle/package.go +++ /dev/null @@ -1,135 +0,0 @@ -// Package bundle contains singletons referenced throughout the Benthos codebase -// that allow imported components to add their constructors and documentation to -// a service. -// -// Each component type has it's own singleton bundle containing all imported -// implementations of the component, and from this bundle more can be derived -// that modify the components that are available. -package bundle - -import ( - "context" - "errors" - "fmt" - "net/http" - "regexp" - - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var ( - nameRegexpRaw = `^[a-z0-9]+(_[a-z0-9]+)*$` - nameRegexp = regexp.MustCompile(nameRegexpRaw) -) - -// NewManagement defines the latest API for a Benthos manager, which will become -// the only API (internally) in Benthos V4. -type NewManagement interface { - ForStream(id string) NewManagement - IntoPath(segments ...string) NewManagement - WithAddedMetrics(m metrics.Type) NewManagement - - EngineVersion() string - - Path() []string - Label() string - - Metrics() metrics.Type - Logger() log.Modular - Tracer() trace.TracerProvider - FS() ifs.FS - Environment() *Environment - BloblEnvironment() *bloblang.Environment - - RegisterEndpoint(path, desc string, h http.HandlerFunc) - - NewBuffer(conf buffer.Config) (buffer.Streamed, error) - NewCache(conf cache.Config) (cache.V1, error) - NewInput(conf input.Config) (input.Streamed, error) - NewProcessor(conf processor.Config) (processor.V1, error) - NewOutput(conf output.Config, pipelines ...processor.PipelineConstructorFunc) (output.Streamed, error) - NewRateLimit(conf ratelimit.Config) (ratelimit.V1, error) - NewScanner(conf scanner.Config) (scanner.Creator, error) - - ProbeCache(name string) bool - AccessCache(ctx context.Context, name string, fn func(cache.V1)) error - StoreCache(ctx context.Context, name string, conf cache.Config) error - RemoveCache(ctx context.Context, name string) error - - ProbeInput(name string) bool - AccessInput(ctx context.Context, name string, fn func(input.Streamed)) error - StoreInput(ctx context.Context, name string, conf input.Config) error - RemoveInput(ctx context.Context, name string) error - - ProbeProcessor(name string) bool - AccessProcessor(ctx context.Context, name string, fn func(processor.V1)) error - StoreProcessor(ctx context.Context, name string, conf processor.Config) error - RemoveProcessor(ctx context.Context, name string) error - - ProbeOutput(name string) bool - AccessOutput(ctx context.Context, name string, fn func(output.Sync)) error - StoreOutput(ctx context.Context, name string, conf output.Config) error - RemoveOutput(ctx context.Context, name string) error - - ProbeRateLimit(name string) bool - AccessRateLimit(ctx context.Context, name string, fn func(ratelimit.V1)) error - StoreRateLimit(ctx context.Context, name string, conf ratelimit.Config) error - RemoveRateLimit(ctx context.Context, name string) error - - GetPipe(name string) (<-chan message.Transaction, error) - SetPipe(name string, t <-chan message.Transaction) - UnsetPipe(name string, t <-chan message.Transaction) -} - -type componentErr struct { - typeStr string - annotation string - err error -} - -func (c *componentErr) Error() string { - return fmt.Sprintf("failed to init %v %v: %v", c.typeStr, c.annotation, c.err) -} - -func (c *componentErr) Unwrap() error { - return c.err -} - -func wrapComponentErr(mgr NewManagement, typeStr string, err error) error { - if err == nil { - return nil - } - - var existing *componentErr - if errors.As(err, &existing) { - return err - } - - annotation := "" - if mgr.Label() != "" { - annotation = "'" + mgr.Label() + "'" - } - if p := mgr.Path(); len(p) > 0 { - annotation += " path root." - annotation += query.SliceToDotPath(mgr.Path()...) - } - return &componentErr{ - typeStr: typeStr, - annotation: annotation, - err: err, - } -} diff --git a/internal/bundle/processors.go b/internal/bundle/processors.go deleted file mode 100644 index 21ab06cd8d..0000000000 --- a/internal/bundle/processors.go +++ /dev/null @@ -1,100 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllProcessors is a set containing every single processor that has been -// imported. -var AllProcessors = &ProcessorSet{ - specs: map[string]processorSpec{}, -} - -//------------------------------------------------------------------------------ - -// ProcessorAdd adds a new processor to this environment by providing a -// constructor and documentation. -func (e *Environment) ProcessorAdd(constructor ProcessorConstructor, spec docs.ComponentSpec) error { - return e.processors.Add(constructor, spec) -} - -// ProcessorInit attempts to initialise a processor from a config. -func (e *Environment) ProcessorInit(conf processor.Config, mgr NewManagement) (processor.V1, error) { - return e.processors.Init(conf, mgr) -} - -// ProcessorDocs returns a slice of processor specs, which document each method. -func (e *Environment) ProcessorDocs() []docs.ComponentSpec { - return e.processors.Docs() -} - -//------------------------------------------------------------------------------ - -// ProcessorConstructor constructs an processor component. -type ProcessorConstructor func(conf processor.Config, mgr NewManagement) (processor.V1, error) - -type processorSpec struct { - constructor ProcessorConstructor - spec docs.ComponentSpec -} - -// ProcessorSet contains an explicit set of processors available to a Benthos -// service. -type ProcessorSet struct { - specs map[string]processorSpec -} - -// Add a new processor to this set by providing a spec (name, documentation, and -// constructor). -func (s *ProcessorSet) Add(constructor ProcessorConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]processorSpec{} - } - spec.Type = docs.TypeProcessor - s.specs[spec.Name] = processorSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an processor from a config. -func (s *ProcessorSet) Init(conf processor.Config, mgr NewManagement) (processor.V1, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("processor", conf.Type) - } - c, err := spec.constructor(conf, mgr) - err = wrapComponentErr(mgr, "processor", err) - return c, err -} - -// Docs returns a slice of processor specs, which document each method. -func (s *ProcessorSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *ProcessorSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/ratelimits.go b/internal/bundle/ratelimits.go deleted file mode 100644 index 701057d41a..0000000000 --- a/internal/bundle/ratelimits.go +++ /dev/null @@ -1,98 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllRateLimits is a set containing every single ratelimit that has been imported. -var AllRateLimits = &RateLimitSet{ - specs: map[string]rateLimitSpec{}, -} - -//------------------------------------------------------------------------------ - -// RateLimitAdd adds a new ratelimit to this environment by providing a -// constructor and documentation. -func (e *Environment) RateLimitAdd(constructor RateLimitConstructor, spec docs.ComponentSpec) error { - return e.rateLimits.Add(constructor, spec) -} - -// RateLimitInit attempts to initialise a ratelimit from a config. -func (e *Environment) RateLimitInit(conf ratelimit.Config, mgr NewManagement) (ratelimit.V1, error) { - return e.rateLimits.Init(conf, mgr) -} - -// RateLimitDocs returns a slice of ratelimit specs, which document each method. -func (e *Environment) RateLimitDocs() []docs.ComponentSpec { - return e.rateLimits.Docs() -} - -//------------------------------------------------------------------------------ - -// RateLimitConstructor constructs an ratelimit component. -type RateLimitConstructor func(ratelimit.Config, NewManagement) (ratelimit.V1, error) - -type rateLimitSpec struct { - constructor RateLimitConstructor - spec docs.ComponentSpec -} - -// RateLimitSet contains an explicit set of ratelimits available to a Benthos service. -type RateLimitSet struct { - specs map[string]rateLimitSpec -} - -// Add a new ratelimit to this set by providing a spec (name, documentation, and -// constructor). -func (s *RateLimitSet) Add(constructor RateLimitConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]rateLimitSpec{} - } - spec.Type = docs.TypeRateLimit - s.specs[spec.Name] = rateLimitSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an ratelimit from a config. -func (s *RateLimitSet) Init(conf ratelimit.Config, mgr NewManagement) (ratelimit.V1, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("rate_limit", conf.Type) - } - c, err := spec.constructor(conf, mgr) - err = wrapComponentErr(mgr, "rate_limit", err) - return c, err -} - -// Docs returns a slice of ratelimit specs, which document each method. -func (s *RateLimitSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *RateLimitSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/scanners.go b/internal/bundle/scanners.go deleted file mode 100644 index 4bd84483b7..0000000000 --- a/internal/bundle/scanners.go +++ /dev/null @@ -1,97 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllScanners is a set containing every single scanner that has been imported. -var AllScanners = &ScannerSet{ - specs: map[string]scannerSpec{}, -} - -//------------------------------------------------------------------------------ - -// ScannerAdd adds a new scanner to this environment by providing a constructor -// and documentation. -func (e *Environment) ScannerAdd(constructor ScannerConstructor, spec docs.ComponentSpec) error { - return e.scanners.Add(constructor, spec) -} - -// ScannerInit attempts to initialise a scanner creator from a config. -func (e *Environment) ScannerInit(conf scanner.Config, nm NewManagement) (scanner.Creator, error) { - return e.scanners.Init(conf, nm) -} - -// ScannerDocs returns a slice of scanner specs. -func (e *Environment) ScannerDocs() []docs.ComponentSpec { - return e.scanners.Docs() -} - -//------------------------------------------------------------------------------ - -// ScannerConstructor constructs a scanner component. -type ScannerConstructor func(scanner.Config, NewManagement) (scanner.Creator, error) - -type scannerSpec struct { - constructor ScannerConstructor - spec docs.ComponentSpec -} - -// ScannerSet contains an explicit set of scanners available to a Benthos -// service. -type ScannerSet struct { - specs map[string]scannerSpec -} - -// Add a new scanner to this set by providing a spec (name, documentation, and -// constructor). -func (s *ScannerSet) Add(constructor ScannerConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]scannerSpec{} - } - spec.Type = docs.TypeScanner - s.specs[spec.Name] = scannerSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise a scanner from a config. -func (s *ScannerSet) Init(conf scanner.Config, nm NewManagement) (scanner.Creator, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("scanner", conf.Type) - } - return spec.constructor(conf, nm) -} - -// Docs returns a slice of scanner specs, which document each method. -func (s *ScannerSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *ScannerSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/tracers.go b/internal/bundle/tracers.go deleted file mode 100644 index b5055b68ff..0000000000 --- a/internal/bundle/tracers.go +++ /dev/null @@ -1,98 +0,0 @@ -package bundle - -import ( - "fmt" - "sort" - - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/tracer" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// AllTracers is a set containing every single tracer that has been imported. -var AllTracers = &TracerSet{ - specs: map[string]tracerSpec{}, -} - -//------------------------------------------------------------------------------ - -// TracersAdd adds a new tracers exporter to this environment by providing a -// constructor and documentation. -func (e *Environment) TracersAdd(constructor TracerConstructor, spec docs.ComponentSpec) error { - return e.tracers.Add(constructor, spec) -} - -// TracersInit attempts to initialise a tracers exporter from a config. -func (e *Environment) TracersInit(conf tracer.Config, nm NewManagement) (trace.TracerProvider, error) { - return e.tracers.Init(conf, nm) -} - -// TracersDocs returns a slice of tracers exporter specs. -func (e *Environment) TracersDocs() []docs.ComponentSpec { - return e.tracers.Docs() -} - -//------------------------------------------------------------------------------ - -// TracerConstructor constructs an tracer component. -type TracerConstructor func(tracer.Config, NewManagement) (trace.TracerProvider, error) - -type tracerSpec struct { - constructor TracerConstructor - spec docs.ComponentSpec -} - -// TracerSet contains an explicit set of tracers available to a Benthos service. -type TracerSet struct { - specs map[string]tracerSpec -} - -// Add a new tracer to this set by providing a spec (name, documentation, and -// constructor). -func (s *TracerSet) Add(constructor TracerConstructor, spec docs.ComponentSpec) error { - if !nameRegexp.MatchString(spec.Name) { - return fmt.Errorf("component name '%v' does not match the required regular expression /%v/", spec.Name, nameRegexpRaw) - } - if s.specs == nil { - s.specs = map[string]tracerSpec{} - } - spec.Type = docs.TypeTracer - s.specs[spec.Name] = tracerSpec{ - constructor: constructor, - spec: spec, - } - return nil -} - -// Init attempts to initialise an tracer from a config. -func (s *TracerSet) Init(conf tracer.Config, nm NewManagement) (trace.TracerProvider, error) { - spec, exists := s.specs[conf.Type] - if !exists { - return nil, component.ErrInvalidType("tracer", conf.Type) - } - return spec.constructor(conf, nm) -} - -// Docs returns a slice of tracer specs, which document each method. -func (s *TracerSet) Docs() []docs.ComponentSpec { - var docs []docs.ComponentSpec - for _, v := range s.specs { - docs = append(docs, v.spec) - } - sort.Slice(docs, func(i, j int) bool { - return docs[i].Name < docs[j].Name - }) - return docs -} - -// DocsFor returns the documentation for a given component name, returns a -// boolean indicating whether the component name exists. -func (s *TracerSet) DocsFor(name string) (docs.ComponentSpec, bool) { - c, ok := s.specs[name] - if !ok { - return docs.ComponentSpec{}, false - } - return c.spec, true -} diff --git a/internal/bundle/tracing/bundle.go b/internal/bundle/tracing/bundle.go deleted file mode 100644 index fdb3fe80bd..0000000000 --- a/internal/bundle/tracing/bundle.go +++ /dev/null @@ -1,73 +0,0 @@ -package tracing - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/output/processors" - "github.com/benthosdev/benthos/v4/internal/component/processor" -) - -// TracedBundle modifies a provided bundle environment so that traceable -// components are wrapped by components that add trace events to the returned -// summary. -func TracedBundle(b *bundle.Environment) (*bundle.Environment, *Summary) { - summary := NewSummary() - tracedEnv := b.Clone() - - for _, spec := range b.InputDocs() { - _ = tracedEnv.InputAdd(func(conf input.Config, nm bundle.NewManagement) (input.Streamed, error) { - i, err := b.InputInit(conf, nm) - if err != nil { - return nil, err - } - key := nm.Label() - if key == "" { - key = "root." + query.SliceToDotPath(nm.Path()...) - } - iEvents, ctr := summary.wInputEvents(key) - i = traceInput(iEvents, ctr, i) - return i, err - }, spec) - } - - for _, spec := range b.ProcessorDocs() { - _ = tracedEnv.ProcessorAdd(func(conf processor.Config, nm bundle.NewManagement) (processor.V1, error) { - i, err := b.ProcessorInit(conf, nm) - if err != nil { - return nil, err - } - key := nm.Label() - if key == "" { - key = "root." + query.SliceToDotPath(nm.Path()...) - } - pEvents, errCtr := summary.wProcessorEvents(key) - i = traceProcessor(pEvents, errCtr, i) - return i, err - }, spec) - } - - for _, spec := range b.OutputDocs() { - _ = tracedEnv.OutputAdd(func(conf output.Config, nm bundle.NewManagement, pcf ...processor.PipelineConstructorFunc) (output.Streamed, error) { - pcf = processors.AppendFromConfig(conf, nm, pcf...) - conf.Processors = nil - - o, err := b.OutputInit(conf, nm) - if err != nil { - return nil, err - } - - key := nm.Label() - if key == "" { - key = "root." + query.SliceToDotPath(nm.Path()...) - } - oEvents, ctr := summary.wOutputEvents(key) - o = traceOutput(oEvents, ctr, o) - - return output.WrapWithPipelines(o, pcf...) - }, spec) - } - - return tracedEnv, summary -} diff --git a/internal/bundle/tracing/bundle_test.go b/internal/bundle/tracing/bundle_test.go deleted file mode 100644 index e2509f75f2..0000000000 --- a/internal/bundle/tracing/bundle_test.go +++ /dev/null @@ -1,635 +0,0 @@ -package tracing_test - -import ( - "context" - "fmt" - "strconv" - "testing" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/bundle/tracing" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestBundleInputTracing(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - - inConfig, err := testutil.InputFromYAML(` -label: foo -generate: - count: 10 - interval: 1us - mapping: 'root.count = counter()' -`) - require.NoError(t, err) - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - in, err := mgr.NewInput(inConfig) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - for i := 0; i < 10; i++ { - select { - case tran := <-in.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - in.TriggerStopConsuming() - require.NoError(t, in.WaitForClose(ctx)) - - assert.Equal(t, uint64(10), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(0), summary.Output) - - inEvents := summary.InputEvents(false) - require.Contains(t, inEvents, "foo") - - events := inEvents["foo"] - require.Len(t, events, 10) - - for i, e := range events { - assert.Equal(t, tracing.EventProduce, e.Type) - assert.Equal(t, fmt.Sprintf(`{"count":%v}`, i+1), e.Content) - } -} - -func TestBundleInputTracingFlush(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - - inConfig, err := testutil.InputFromYAML(` -label: foo -generate: - count: 10 - interval: 1us - mapping: 'root.count = counter()' -`) - require.NoError(t, err) - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - in, err := mgr.NewInput(inConfig) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - for i := 0; i < 10; i++ { - select { - case tran := <-in.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - in.TriggerStopConsuming() - require.NoError(t, in.WaitForClose(ctx)) - - assert.Equal(t, uint64(10), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(0), summary.Output) - - inEvents := summary.InputEvents(false) - require.Contains(t, inEvents, "foo") - - events := inEvents["foo"] - require.Len(t, events, 10) - - for i, e := range events { - assert.Equal(t, tracing.EventProduce, e.Type) - assert.Equal(t, fmt.Sprintf(`{"count":%v}`, i+1), e.Content) - } - - // Not flushed - inEvents = summary.InputEvents(true) - require.Contains(t, inEvents, "foo") - require.Len(t, inEvents["foo"], 10) - - // Now it's flushed - inEvents = summary.InputEvents(true) - require.Contains(t, inEvents, "foo") - require.Empty(t, inEvents["foo"]) - - // Run more stuff - inConfig, err = testutil.InputFromYAML(` -label: foo -generate: - count: 5 - interval: 1us - mapping: 'root.count = counter()' -`) - require.NoError(t, err) - - in, err = mgr.NewInput(inConfig) - require.NoError(t, err) - - for i := 0; i < 5; i++ { - select { - case tran := <-in.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - in.TriggerStopConsuming() - require.NoError(t, in.WaitForClose(ctx)) - - inEvents = summary.InputEvents(false) - require.Contains(t, inEvents, "foo") - - events = inEvents["foo"] - require.Len(t, events, 5) - - for i, e := range events { - assert.Equal(t, tracing.EventProduce, e.Type) - assert.Equal(t, fmt.Sprintf(`{"count":%v}`, i+1), e.Content) - } -} - -func TestBundleInputTracingDisabled(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - summary.SetEnabled(false) - - inConfig, err := testutil.InputFromYAML(` -label: foo -generate: - count: 10 - interval: 1us - mapping: 'root.count = counter()' -`) - require.NoError(t, err) - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - in, err := mgr.NewInput(inConfig) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - for i := 0; i < 10; i++ { - select { - case tran := <-in.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - in.TriggerStopConsuming() - require.NoError(t, in.WaitForClose(ctx)) - - assert.Equal(t, uint64(0), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(0), summary.Output) - - inEvents := summary.InputEvents(false) - require.Contains(t, inEvents, "foo") - - events := inEvents["foo"] - require.Empty(t, events) -} - -func TestBundleOutputTracing(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - - outConfig := output.NewConfig() - outConfig.Label = "foo" - outConfig.Type = "drop" - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - out, err := mgr.NewOutput(outConfig) - require.NoError(t, err) - - tranChan := make(chan message.Transaction) - require.NoError(t, out.Consume(tranChan)) - - for i := 0; i < 10; i++ { - resChan := make(chan error) - tran := message.NewTransaction(message.QuickBatch([][]byte{[]byte(strconv.Itoa(i))}), resChan) - select { - case tranChan <- tran: - select { - case <-resChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - out.TriggerCloseNow() - require.NoError(t, out.WaitForClose(ctx)) - - assert.Equal(t, uint64(0), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(10), summary.Output) - - outEvents := summary.OutputEvents(false) - require.Contains(t, outEvents, "foo") - - events := outEvents["foo"] - require.Len(t, events, 10) - - for i, e := range events { - assert.Equal(t, tracing.EventConsume, e.Type) - assert.Equal(t, strconv.Itoa(i), e.Content) - } -} - -func TestBundleOutputTracingDisabled(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - summary.SetEnabled(false) - - outConfig := output.NewConfig() - outConfig.Label = "foo" - outConfig.Type = "drop" - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - out, err := mgr.NewOutput(outConfig) - require.NoError(t, err) - - tranChan := make(chan message.Transaction) - require.NoError(t, out.Consume(tranChan)) - - for i := 0; i < 10; i++ { - resChan := make(chan error) - tran := message.NewTransaction(message.QuickBatch([][]byte{[]byte(strconv.Itoa(i))}), resChan) - select { - case tranChan <- tran: - select { - case <-resChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - out.TriggerCloseNow() - require.NoError(t, out.WaitForClose(ctx)) - - assert.Equal(t, uint64(0), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(0), summary.Output) - - outEvents := summary.OutputEvents(false) - require.Contains(t, outEvents, "foo") - - events := outEvents["foo"] - require.Empty(t, events) -} - -func TestBundleOutputWithProcessorsTracing(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - - outConfig := output.NewConfig() - outConfig.Label = "foo" - outConfig.Type = "drop" - - blobConf := processor.NewConfig() - blobConf.Type = "bloblang" - blobConf.Plugin = "root = content().uppercase()" - outConfig.Processors = append(outConfig.Processors, blobConf) - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - out, err := mgr.NewOutput(outConfig) - require.NoError(t, err) - - tranChan := make(chan message.Transaction) - require.NoError(t, out.Consume(tranChan)) - - for i := 0; i < 10; i++ { - resChan := make(chan error) - tran := message.NewTransaction(message.QuickBatch([][]byte{[]byte("hello world " + strconv.Itoa(i))}), resChan) - select { - case tranChan <- tran: - select { - case <-resChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - close(tranChan) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, out.WaitForClose(ctx)) - - assert.Equal(t, uint64(0), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(10), summary.Output) - - outEvents := summary.OutputEvents(false) - require.Contains(t, outEvents, "foo") - - outputEvents := outEvents["foo"] - require.Len(t, outputEvents, 10) - - for i, e := range outputEvents { - assert.Equal(t, tracing.EventConsume, e.Type) - assert.Equal(t, "HELLO WORLD "+strconv.Itoa(i), e.Content) - } - - procEvents := summary.ProcessorEvents(false) - require.Contains(t, procEvents, "root.processors.0") - - processorEvents := procEvents["root.processors.0"] - require.Len(t, processorEvents, 20) - - for i := 0; i < len(processorEvents); i += 2 { - consumeEvent := processorEvents[i] - produceEvent := processorEvents[i+1] - - assert.Equal(t, tracing.EventConsume, consumeEvent.Type) - assert.Equal(t, tracing.EventProduce, produceEvent.Type) - - assert.Equal(t, "hello world "+strconv.Itoa(i/2), consumeEvent.Content) - assert.Equal(t, "HELLO WORLD "+strconv.Itoa(i/2), produceEvent.Content) - } -} - -func TestBundleOutputWithBatchProcessorsTracing(t *testing.T) { - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - - outConfig, err := testutil.OutputFromYAML(` -broker: - outputs: - - label: foo - drop: {} - batching: - count: 2 - processors: - - mapping: 'root = content().uppercase()' -`) - require.NoError(t, err) - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - out, err := mgr.NewOutput(outConfig) - require.NoError(t, err) - - tranChan := make(chan message.Transaction) - require.NoError(t, out.Consume(tranChan)) - - for i := 0; i < 5; i++ { - resChan := make(chan error) - tran := message.NewTransaction(message.QuickBatch([][]byte{ - []byte("hello world " + strconv.Itoa(i*2)), - []byte("hello world " + strconv.Itoa(i*2+1)), - }), resChan) - select { - case tranChan <- tran: - select { - case <-resChan: - case <-time.After(time.Second): - t.Fatal("timed out", i) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - out.TriggerCloseNow() - require.NoError(t, out.WaitForClose(ctx)) - - assert.Equal(t, 0, int(summary.Input)) - assert.Equal(t, 0, int(summary.ProcessorErrors)) - assert.Equal(t, 20, int(summary.Output)) - - outEvents := summary.OutputEvents(false) - require.Contains(t, outEvents, "foo") - - outputEvents := outEvents["foo"] - require.Len(t, outputEvents, 10) - - for i, e := range outputEvents { - assert.Equal(t, tracing.EventConsume, e.Type) - assert.Equal(t, "HELLO WORLD "+strconv.Itoa(i), e.Content) - } - - procEvents := summary.ProcessorEvents(false) - require.Contains(t, procEvents, "root.batching.processors.0") - - processorEvents := procEvents["root.batching.processors.0"] - require.Len(t, processorEvents, 20) - - for i := 0; i < len(processorEvents); i += 4 { - consumeEventA := processorEvents[i] - consumeEventB := processorEvents[i+1] - produceEventA := processorEvents[i+2] - produceEventB := processorEvents[i+3] - - assert.Equal(t, tracing.EventConsume, consumeEventA.Type) - assert.Equal(t, tracing.EventConsume, consumeEventB.Type) - assert.Equal(t, tracing.EventProduce, produceEventA.Type) - assert.Equal(t, tracing.EventProduce, produceEventB.Type) - - assert.Equal(t, "hello world "+strconv.Itoa(i/2), consumeEventA.Content, i) - assert.Equal(t, "hello world "+strconv.Itoa(i/2+1), consumeEventB.Content, i) - assert.Equal(t, "HELLO WORLD "+strconv.Itoa(i/2), produceEventA.Content, i) - assert.Equal(t, "HELLO WORLD "+strconv.Itoa(i/2+1), produceEventB.Content, i) - } -} - -func TestBundleProcessorTracing(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - - procConfig := processor.NewConfig() - procConfig.Label = "foo" - procConfig.Type = "bloblang" - procConfig.Plugin = ` -let ctr = content().number() -root.count = if $ctr % 2 == 0 { throw("nah %v".format($ctr)) } else { $ctr } -meta bar = "new bar value" -` - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(procConfig) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - part := message.NewPart([]byte(strconv.Itoa(i))) - part.MetaSetMut("foo", fmt.Sprintf("foo value %v", i)) - batch, res := proc.ProcessBatch(tCtx, message.Batch{part}) - require.NoError(t, res) - require.Len(t, batch, 1) - assert.Equal(t, 1, batch[0].Len()) - } - - require.NoError(t, proc.Close(tCtx)) - - assert.Equal(t, uint64(0), summary.Input) - assert.Equal(t, uint64(5), summary.ProcessorErrors) - assert.Equal(t, uint64(0), summary.Output) - - procEvents := summary.ProcessorEvents(false) - require.Contains(t, procEvents, "foo") - - events := procEvents["foo"] - require.Len(t, events, 25) - - type tMap = map[string]any - - assert.Equal(t, []tracing.NodeEvent{ - {Type: "CONSUME", Content: "0", Meta: tMap{"foo": "foo value 0"}}, - {Type: "PRODUCE", Content: "0", Meta: tMap{"foo": "foo value 0"}}, - {Type: "ERROR", Content: "failed assignment (line 3): nah 0"}, - {Type: "CONSUME", Content: "1", Meta: tMap{"foo": "foo value 1"}}, - {Type: "PRODUCE", Content: "{\"count\":1}", Meta: tMap{"bar": "new bar value", "foo": "foo value 1"}}, - {Type: "CONSUME", Content: "2", Meta: tMap{"foo": "foo value 2"}}, - {Type: "PRODUCE", Content: "2", Meta: tMap{"foo": "foo value 2"}}, - {Type: "ERROR", Content: "failed assignment (line 3): nah 2"}, - {Type: "CONSUME", Content: "3", Meta: tMap{"foo": "foo value 3"}}, - {Type: "PRODUCE", Content: "{\"count\":3}", Meta: tMap{"bar": "new bar value", "foo": "foo value 3"}}, - {Type: "CONSUME", Content: "4", Meta: tMap{"foo": "foo value 4"}}, - {Type: "PRODUCE", Content: "4", Meta: tMap{"foo": "foo value 4"}}, - {Type: "ERROR", Content: "failed assignment (line 3): nah 4"}, - {Type: "CONSUME", Content: "5", Meta: tMap{"foo": "foo value 5"}}, - {Type: "PRODUCE", Content: "{\"count\":5}", Meta: tMap{"bar": "new bar value", "foo": "foo value 5"}}, - {Type: "CONSUME", Content: "6", Meta: tMap{"foo": "foo value 6"}}, - {Type: "PRODUCE", Content: "6", Meta: tMap{"foo": "foo value 6"}}, - {Type: "ERROR", Content: "failed assignment (line 3): nah 6"}, - {Type: "CONSUME", Content: "7", Meta: tMap{"foo": "foo value 7"}}, - {Type: "PRODUCE", Content: "{\"count\":7}", Meta: tMap{"bar": "new bar value", "foo": "foo value 7"}}, - {Type: "CONSUME", Content: "8", Meta: tMap{"foo": "foo value 8"}}, - {Type: "PRODUCE", Content: "8", Meta: tMap{"foo": "foo value 8"}}, - {Type: "ERROR", Content: "failed assignment (line 3): nah 8"}, - {Type: "CONSUME", Content: "9", Meta: tMap{"foo": "foo value 9"}}, - {Type: "PRODUCE", Content: "{\"count\":9}", Meta: tMap{"bar": "new bar value", "foo": "foo value 9"}}, - }, events) -} - -func TestBundleProcessorTracingError(t *testing.T) { - tenv, _ := tracing.TracedBundle(bundle.GlobalEnvironment) - - procConfig := processor.NewConfig() - procConfig.Label = "foo" - procConfig.Type = "bloblang" - procConfig.Plugin = `let nope` - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - _, err = mgr.NewProcessor(procConfig) - require.EqualError(t, err, "failed to init processor 'foo': line 1 char 9: expected whitespace") -} - -func TestBundleProcessorTracingDisabled(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tenv, summary := tracing.TracedBundle(bundle.GlobalEnvironment) - summary.SetEnabled(false) - - procConfig := processor.NewConfig() - procConfig.Label = "foo" - procConfig.Type = "bloblang" - procConfig.Plugin = ` -let ctr = content().number() -root.count = if $ctr % 2 == 0 { throw("nah %v".format($ctr)) } else { $ctr } -meta bar = "new bar value" -` - - mgr, err := manager.New( - manager.ResourceConfig{}, - manager.OptSetEnvironment(tenv), - ) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(procConfig) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - part := message.NewPart([]byte(strconv.Itoa(i))) - part.MetaSetMut("foo", fmt.Sprintf("foo value %v", i)) - batch, res := proc.ProcessBatch(tCtx, message.Batch{part}) - require.NoError(t, res) - require.Len(t, batch, 1) - assert.Equal(t, 1, batch[0].Len()) - } - - require.NoError(t, proc.Close(tCtx)) - - assert.Equal(t, uint64(0), summary.Input) - assert.Equal(t, uint64(0), summary.ProcessorErrors) - assert.Equal(t, uint64(0), summary.Output) - - procEvents := summary.ProcessorEvents(false) - require.Contains(t, procEvents, "foo") - - events := procEvents["foo"] - require.Empty(t, events) -} diff --git a/internal/bundle/tracing/events.go b/internal/bundle/tracing/events.go deleted file mode 100644 index 30548d11b5..0000000000 --- a/internal/bundle/tracing/events.go +++ /dev/null @@ -1,233 +0,0 @@ -package tracing - -import ( - "sync" - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// EventType describes the type of event a component might experience during -// a config run. -type EventType string - -// Various event types. -var ( - EventProduce EventType = "PRODUCE" - EventConsume EventType = "CONSUME" - EventDelete EventType = "DELETE" - EventError EventType = "ERROR" -) - -// NodeEvent represents a single event that occurred within the stream. -type NodeEvent struct { - Type EventType - Content string - Meta map[string]any -} - -// EventProduceOf creates a produce event from a message part. -func EventProduceOf(part *message.Part) NodeEvent { - meta := map[string]any{} - _ = part.MetaIterMut(func(s string, a any) error { - meta[s] = message.CopyJSON(a) - return nil - }) - - return NodeEvent{ - Type: EventProduce, - Content: string(part.AsBytes()), - Meta: meta, - } -} - -// EventConsumeOf creates a consumed event from a message part. -func EventConsumeOf(part *message.Part) NodeEvent { - meta := map[string]any{} - _ = part.MetaIterMut(func(s string, a any) error { - meta[s] = message.CopyJSON(a) - return nil - }) - - return NodeEvent{ - Type: EventConsume, - Content: string(part.AsBytes()), - Meta: meta, - } -} - -// EventDeleteOf creates a deleted event from a message part. -func EventDeleteOf() NodeEvent { - return NodeEvent{ - Type: EventDelete, - } -} - -// EventErrorOf creates an error event from a message part. -func EventErrorOf(err error) NodeEvent { - return NodeEvent{ - Type: EventError, - Content: err.Error(), - } -} - -type control struct { - isEnabled int32 - eventLimit int64 -} - -func (c *control) SetEnabled(e bool) { - if e { - atomic.StoreInt32(&c.isEnabled, 1) - } else { - atomic.StoreInt32(&c.isEnabled, 0) - } -} - -func (c *control) SetEventLimit(n int64) { - atomic.StoreInt64(&c.eventLimit, n) -} - -func (c *control) IsEnabled() bool { - return atomic.LoadInt32(&c.isEnabled) > 0 -} - -func (c *control) EventLimit() int64 { - return atomic.LoadInt64(&c.eventLimit) -} - -// Summary is a high level description of all traced events. -type Summary struct { - Input uint64 - Output uint64 - ProcessorErrors uint64 - - ctrl *control - - inputEvents sync.Map - processorEvents sync.Map - outputEvents sync.Map -} - -// NewSummary creates a new tracing summary that can be passed to component -// constructors for adding traces. -func NewSummary() *Summary { - return &Summary{ - ctrl: &control{isEnabled: 1}, - } -} - -// SetEnabled sets whether tracing events are enabled across the stream. -func (s *Summary) SetEnabled(e bool) { - s.ctrl.SetEnabled(e) -} - -// SetEventLimit sets a limit as to how many event traces are stored, this limit -// is per component that's traced. -func (s *Summary) SetEventLimit(n int64) { - s.ctrl.SetEventLimit(n) -} - -func getEvents(flush bool, from *sync.Map) map[string][]NodeEvent { - m := map[string][]NodeEvent{} - from.Range(func(key, value any) bool { - e := value.(*events) - var extracted []NodeEvent - if flush { - extracted = e.Flush() - } else { - extracted = e.Extract() - } - m[key.(string)] = extracted - return true - }) - return m -} - -// InputEvents returns a map of input labels to events traced during the -// execution of a stream pipeline. Set flush to true in order to clear the -// events after obtaining them. -func (s *Summary) InputEvents(flush bool) map[string][]NodeEvent { - return getEvents(flush, &s.inputEvents) -} - -// ProcessorEvents returns a map of processor labels to events traced during the -// execution of a stream pipeline. -func (s *Summary) ProcessorEvents(flush bool) map[string][]NodeEvent { - return getEvents(flush, &s.processorEvents) -} - -// OutputEvents returns a map of output labels to events traced during the -// execution of a stream pipeline. -func (s *Summary) OutputEvents(flush bool) map[string][]NodeEvent { - return getEvents(flush, &s.outputEvents) -} - -//------------------------------------------------------------------------------ - -func (s *Summary) wInputEvents(label string) (e *events, counter *uint64) { - i, _ := s.inputEvents.LoadOrStore(label, &events{ - ctrl: s.ctrl, - }) - return i.(*events), &s.Input -} - -func (s *Summary) wOutputEvents(label string) (e *events, counter *uint64) { - i, _ := s.outputEvents.LoadOrStore(label, &events{ - ctrl: s.ctrl, - }) - return i.(*events), &s.Output -} - -func (s *Summary) wProcessorEvents(label string) (e *events, errCounter *uint64) { - i, _ := s.processorEvents.LoadOrStore(label, &events{ - ctrl: s.ctrl, - }) - return i.(*events), &s.ProcessorErrors -} - -type events struct { - mut sync.Mutex - m []NodeEvent - mLen int64 - - ctrl *control -} - -func (e *events) IsEnabled() bool { - if !e.ctrl.IsEnabled() { - return false - } - if limit := e.ctrl.EventLimit(); limit > 0 { - return atomic.LoadInt64(&e.mLen) < limit - } - return true -} - -func (e *events) Add(event NodeEvent) { - e.mut.Lock() - defer e.mut.Unlock() - - atomic.AddInt64(&e.mLen, 1) - e.m = append(e.m, event) -} - -func (e *events) Extract() []NodeEvent { - e.mut.Lock() - defer e.mut.Unlock() - - eventsCopy := make([]NodeEvent, len(e.m)) - copy(eventsCopy, e.m) - - return eventsCopy -} - -func (e *events) Flush() []NodeEvent { - e.mut.Lock() - defer e.mut.Unlock() - - tmpEvents := e.m - e.m = nil - atomic.StoreInt64(&e.mLen, 0) - return tmpEvents -} diff --git a/internal/bundle/tracing/input.go b/internal/bundle/tracing/input.go deleted file mode 100644 index 9823e4a84e..0000000000 --- a/internal/bundle/tracing/input.go +++ /dev/null @@ -1,88 +0,0 @@ -package tracing - -import ( - "context" - "sync/atomic" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type tracedInput struct { - e *events - ctr *uint64 - wrapped input.Streamed - tChan chan message.Transaction - shutSig *shutdown.Signaller -} - -func traceInput(e *events, counter *uint64, i input.Streamed) input.Streamed { - t := &tracedInput{ - e: e, - ctr: counter, - wrapped: i, - tChan: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - go t.loop() - return t -} - -func (t *tracedInput) UnwrapInput() input.Streamed { - return t.wrapped -} - -func (t *tracedInput) loop() { - defer close(t.tChan) - readChan := t.wrapped.TransactionChan() - for { - var tran message.Transaction - var open bool - select { - case tran, open = <-readChan: - if !open { - return - } - case <-t.shutSig.HardStopChan(): - return - } - if t.e.IsEnabled() { - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - _ = atomic.AddUint64(t.ctr, 1) - t.e.Add(EventProduceOf(part)) - return nil - }) - } - select { - case t.tChan <- tran: - case <-t.shutSig.HardStopChan(): - // Stop flushing if we fully timed out - return - } - } -} - -func (t *tracedInput) TransactionChan() <-chan message.Transaction { - return t.tChan -} - -func (t *tracedInput) Connected() bool { - return t.wrapped.Connected() -} - -func (t *tracedInput) TriggerStopConsuming() { - t.wrapped.TriggerStopConsuming() -} - -func (t *tracedInput) TriggerCloseNow() { - t.wrapped.TriggerCloseNow() - t.shutSig.TriggerHardStop() -} - -func (t *tracedInput) WaitForClose(ctx context.Context) error { - err := t.wrapped.WaitForClose(ctx) - t.shutSig.TriggerHardStop() - return err -} diff --git a/internal/bundle/tracing/output.go b/internal/bundle/tracing/output.go deleted file mode 100644 index 0b62455482..0000000000 --- a/internal/bundle/tracing/output.go +++ /dev/null @@ -1,83 +0,0 @@ -package tracing - -import ( - "context" - "sync/atomic" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type tracedOutput struct { - e *events - ctr *uint64 - wrapped output.Streamed - tChan chan message.Transaction - shutSig *shutdown.Signaller -} - -func traceOutput(e *events, ctr *uint64, i output.Streamed) output.Streamed { - t := &tracedOutput{ - e: e, - ctr: ctr, - wrapped: i, - tChan: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - return t -} - -func (t *tracedOutput) UnwrapOutput() output.Streamed { - return t.wrapped -} - -func (t *tracedOutput) loop(inChan <-chan message.Transaction) { - defer close(t.tChan) - for { - var tran message.Transaction - var open bool - select { - case tran, open = <-inChan: - if !open { - return - } - case <-t.shutSig.HardStopChan(): - return - } - if t.e.IsEnabled() { - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - _ = atomic.AddUint64(t.ctr, 1) - t.e.Add(EventConsumeOf(part)) - return nil - }) - } - select { - case t.tChan <- tran: - case <-t.shutSig.HardStopChan(): - // Stop flushing if we fully timed out - return - } - } -} - -func (t *tracedOutput) Consume(inChan <-chan message.Transaction) error { - go t.loop(inChan) - return t.wrapped.Consume(t.tChan) -} - -func (t *tracedOutput) Connected() bool { - return t.wrapped.Connected() -} - -func (t *tracedOutput) TriggerCloseNow() { - t.wrapped.TriggerCloseNow() - t.shutSig.TriggerHardStop() -} - -func (t *tracedOutput) WaitForClose(ctx context.Context) error { - err := t.wrapped.WaitForClose(ctx) - t.shutSig.TriggerHardStop() - return err -} diff --git a/internal/bundle/tracing/processor.go b/internal/bundle/tracing/processor.go deleted file mode 100644 index 56b5acee59..0000000000 --- a/internal/bundle/tracing/processor.go +++ /dev/null @@ -1,69 +0,0 @@ -package tracing - -import ( - "context" - "sync/atomic" - - iprocessor "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type tracedProcessor struct { - e *events - errCtr *uint64 - wrapped iprocessor.V1 -} - -func traceProcessor(e *events, errCtr *uint64, p iprocessor.V1) iprocessor.V1 { - t := &tracedProcessor{ - e: e, - errCtr: errCtr, - wrapped: p, - } - return t -} - -func (t *tracedProcessor) UnwrapProc() iprocessor.V1 { - return t.wrapped -} - -func (t *tracedProcessor) ProcessBatch(ctx context.Context, m message.Batch) ([]message.Batch, error) { - if !t.e.IsEnabled() { - return t.wrapped.ProcessBatch(ctx, m) - } - - prevErrs := make([]error, m.Len()) - _ = m.Iter(func(i int, part *message.Part) error { - t.e.Add(EventConsumeOf(part)) - prevErrs[i] = part.ErrorGet() - return nil - }) - - outMsgs, res := t.wrapped.ProcessBatch(ctx, m) - for _, outMsg := range outMsgs { - _ = outMsg.Iter(func(i int, part *message.Part) error { - t.e.Add(EventProduceOf(part)) - fail := part.ErrorGet() - if fail == nil { - return nil - } - // TODO: Improve mechanism for tracking the introduction of errors? - if len(prevErrs) <= i || prevErrs[i] == fail { - return nil - } - _ = atomic.AddUint64(t.errCtr, 1) - t.e.Add(EventErrorOf(fail)) - return nil - }) - } - if len(outMsgs) == 0 { - // TODO: Find a better way of locating deletes (using batch index tracking). - t.e.Add(EventDeleteOf()) - } - - return outMsgs, res -} - -func (t *tracedProcessor) Close(ctx context.Context) error { - return t.wrapped.Close(ctx) -} diff --git a/internal/cli/blobl/cli.go b/internal/cli/blobl/cli.go deleted file mode 100644 index 63b3463ec8..0000000000 --- a/internal/cli/blobl/cli.go +++ /dev/null @@ -1,300 +0,0 @@ -package blobl - -import ( - "bufio" - "errors" - "fmt" - "os" - "sync" - - "github.com/Jeffail/gabs/v2" - "github.com/fatih/color" - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -var red = color.New(color.FgRed).SprintFunc() - -// CliCommand is a cli.Command definition for running a blobl mapping. -func CliCommand(opts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "blobl", - Usage: opts.ExecTemplate("Execute a {{.ProductName}} mapping on documents consumed via stdin"), - Description: opts.ExecTemplate(` -Provides a convenient tool for mapping JSON documents over the command line: - - cat documents.jsonl | {{.BinaryName}} blobl 'foo.bar.map_each(this.uppercase())' - - echo '{"foo":"bar"}' | {{.BinaryName}} blobl -f ./mapping.blobl - -Find out more about Bloblang at: {{.DocumentationURL}}/guides/bloblang/about`)[1:], - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "threads", - Aliases: []string{"t"}, - Value: 1, - Usage: "the number of processing threads to use, when >1 ordering is no longer guaranteed.", - }, - &cli.BoolFlag{ - Name: "raw", - Aliases: []string{"r"}, - Usage: "consume raw strings.", - }, - &cli.BoolFlag{ - Name: "pretty", - Aliases: []string{"p"}, - Usage: "pretty-print output.", - }, - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: "execute a mapping from a file.", - }, - &cli.IntFlag{ - Name: "max-token-length", - Usage: "Set the buffer size for document lines.", - Value: bufio.MaxScanTokenSize, - }, - }, - Action: run, - Subcommands: []*cli.Command{ - { - Name: "server", - Usage: "EXPERIMENTAL: Run a web server that hosts a Bloblang app", - Description: "Run a web server that provides an interactive application for writing and testing Bloblang mappings.", - Action: runServer, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "host", - Value: "localhost", - Usage: "the host to bind to.", - }, - &cli.StringFlag{ - Name: "port", - Value: "4195", - Aliases: []string{"p"}, - Usage: "the port to bind to.", - }, - &cli.BoolFlag{ - Name: "no-open", - Value: false, - Aliases: []string{"n"}, - Usage: "do not open the app in the browser automatically.", - }, - &cli.StringFlag{ - Name: "mapping-file", - Value: "", - Aliases: []string{"m"}, - Usage: "an optional path to a mapping file to load as the initial mapping within the app.", - }, - &cli.StringFlag{ - Name: "input-file", - Value: "", - Aliases: []string{"i"}, - Usage: "an optional path to an input file to load as the initial input to the mapping within the app.", - }, - &cli.BoolFlag{ - Name: "write", - Value: false, - Aliases: []string{"w"}, - Usage: "when editing a mapping and/or input file write changes made back to the respective source file, if the file does not exist it will be created.", - }, - }, - }, - }, - } -} - -type execCache struct { - msg message.Batch - vars map[string]any -} - -func newExecCache() *execCache { - return &execCache{ - msg: message.QuickBatch([][]byte{[]byte(nil)}), - vars: map[string]any{}, - } -} - -func (e *execCache) executeMapping(exec *mapping.Executor, rawInput, prettyOutput bool, input []byte) (string, error) { - e.msg.Get(0).SetBytes(input) - - var valuePtr *any - var parseErr error - - lazyValue := func() *any { - if valuePtr == nil && parseErr == nil { - if rawInput { - var value any = input - valuePtr = &value - } else { - if jObj, err := e.msg.Get(0).AsStructured(); err == nil { - valuePtr = &jObj - } else { - if errors.Is(err, message.ErrMessagePartNotExist) { - parseErr = errors.New("message is empty") - } else { - parseErr = fmt.Errorf("parse as json: %w", err) - } - } - } - } - return valuePtr - } - - for k := range e.vars { - delete(e.vars, k) - } - - var result any = value.Nothing(nil) - err := exec.ExecOnto(query.FunctionContext{ - Maps: exec.Maps(), - Vars: e.vars, - MsgBatch: e.msg, - NewMeta: e.msg.Get(0), - NewValue: &result, - }.WithValueFunc(lazyValue), mapping.AssignmentContext{ - Vars: e.vars, - Meta: e.msg.Get(0), - Value: &result, - }) - if err != nil { - var ctxErr query.ErrNoContext - if parseErr != nil && errors.As(err, &ctxErr) { - if ctxErr.FieldName != "" { - err = fmt.Errorf("unable to reference message as structured (with 'this.%v'): %w", ctxErr.FieldName, parseErr) - } else { - err = fmt.Errorf("unable to reference message as structured (with 'this'): %w", parseErr) - } - } - return "", err - } - - var resultStr string - switch t := result.(type) { - case string: - resultStr = t - case []byte: - resultStr = string(t) - case value.Delete: - return "", nil - case value.Nothing: - // Do not change the original contents - if v := lazyValue(); v != nil { - gObj := gabs.Wrap(v) - if prettyOutput { - resultStr = gObj.StringIndent("", " ") - } else { - resultStr = gObj.String() - } - } else { - resultStr = string(input) - } - default: - gObj := gabs.Wrap(result) - if prettyOutput { - resultStr = gObj.StringIndent("", " ") - } else { - resultStr = gObj.String() - } - } - - // TODO: Return metadata as well? - return resultStr, nil -} - -func run(c *cli.Context) error { - t := c.Int("threads") - if t < 1 { - t = 1 - } - raw := c.Bool("raw") - pretty := c.Bool("pretty") - file := c.String("file") - m := c.Args().First() - - if file != "" { - if m != "" { - fmt.Fprintln(os.Stderr, red("invalid flags, unable to execute both a file mapping and an inline mapping")) - os.Exit(1) - } - mappingBytes, err := ifs.ReadFile(ifs.OS(), file) - if err != nil { - fmt.Fprintf(os.Stderr, red("failed to read mapping file: %v\n"), err) - os.Exit(1) - } - m = string(mappingBytes) - } - - bEnv := bloblang.NewEnvironment().WithImporterRelativeToFile(file) - exec, err := bEnv.NewMapping(m) - if err != nil { - if perr, ok := err.(*parser.Error); ok { - fmt.Fprintf(os.Stderr, "%v %v\n", red("failed to parse mapping:"), perr.ErrorAtPositionStructured("", []rune(m))) - } else { - fmt.Fprintln(os.Stderr, red(err.Error())) - } - os.Exit(1) - } - - inputsChan := make(chan []byte) - go func() { - defer close(inputsChan) - - scanner := bufio.NewScanner(os.Stdin) - scanner.Buffer(nil, c.Int("max-token-length")) - for scanner.Scan() { - input := make([]byte, len(scanner.Bytes())) - copy(input, scanner.Bytes()) - inputsChan <- input - } - if scanner.Err() != nil { - fmt.Fprintln(os.Stderr, red(scanner.Err())) - os.Exit(1) - } - }() - - wg := sync.WaitGroup{} - wg.Add(t) - resultsChan := make(chan string) - go func() { - wg.Wait() - close(resultsChan) - }() - - for i := 0; i < t; i++ { - go func() { - defer wg.Done() - - execCache := newExecCache() - for { - input, open := <-inputsChan - if !open { - return - } - - resultStr, err := execCache.executeMapping(exec, raw, pretty, input) - if err != nil { - fmt.Fprintln(os.Stderr, red(fmt.Sprintf("failed to execute map: %v", err))) - continue - } - resultsChan <- resultStr - } - }() - } - - for res := range resultsChan { - fmt.Println(res) - } - os.Exit(0) - return nil -} diff --git a/internal/cli/blobl/resources/bloblang_editor_page.html b/internal/cli/blobl/resources/bloblang_editor_page.html deleted file mode 100644 index 98fde5218e..0000000000 --- a/internal/cli/blobl/resources/bloblang_editor_page.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - Bloblang Editor - - - -
-

Input

- -
- -
-

Output

-

-
-
-

Mapping

- -
- - - - - - - - - - \ No newline at end of file diff --git a/internal/cli/blobl/server.go b/internal/cli/blobl/server.go deleted file mode 100644 index 1d248d4aab..0000000000 --- a/internal/cli/blobl/server.go +++ /dev/null @@ -1,242 +0,0 @@ -package blobl - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "html/template" - "io/fs" - "log" - "net/http" - "net/url" - "os" - "os/exec" - "os/signal" - "runtime" - "sync" - "syscall" - "time" - - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - - _ "embed" -) - -//go:embed resources/bloblang_editor_page.html -var bloblangEditorPage string - -func openBrowserAt(url string) { - switch runtime.GOOS { - case "linux": - _ = exec.Command("xdg-open", url).Start() - case "windows": - _ = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - _ = exec.Command("open", url).Start() - } -} - -type fileSync struct { - mut sync.Mutex - - dirty bool - mappingString string - inputString string - - writeBack bool - mappingFile string - inputFile string -} - -func newFileSync(inputFile, mappingFile string, writeBack bool) *fileSync { - f := &fileSync{ - inputString: `{"message":"hello world"}`, - mappingString: "root = this", - writeBack: writeBack, - inputFile: inputFile, - mappingFile: mappingFile, - } - - if inputFile != "" { - inputBytes, err := ifs.ReadFile(ifs.OS(), inputFile) - if err != nil { - if !writeBack || !errors.Is(err, fs.ErrNotExist) { - log.Fatal(err) - } - } else { - f.inputString = string(inputBytes) - } - } - - if mappingFile != "" { - mappingBytes, err := ifs.ReadFile(ifs.OS(), mappingFile) - if err != nil { - if !writeBack || !errors.Is(err, fs.ErrNotExist) { - log.Fatal(err) - } - } else { - f.mappingString = string(mappingBytes) - } - } - - if writeBack { - go func() { - t := time.NewTicker(time.Second * 5) - for { - <-t.C - f.write() - } - }() - } - - return f -} - -func (f *fileSync) update(input, mapping string) { - f.mut.Lock() - if mapping != f.mappingString || input != f.inputString { - f.dirty = true - } - f.mappingString = mapping - f.inputString = input - f.mut.Unlock() -} - -func (f *fileSync) write() { - f.mut.Lock() - defer f.mut.Unlock() - - if !f.writeBack || !f.dirty { - return - } - - if f.inputFile != "" { - if err := ifs.WriteFile(ifs.OS(), f.inputFile, []byte(f.inputString), 0o644); err != nil { - log.Printf("Failed to write input file: %v\n", err) - } - } - if f.mappingFile != "" { - if err := ifs.WriteFile(ifs.OS(), f.mappingFile, []byte(f.mappingString), 0o644); err != nil { - log.Printf("Failed to write mapping file: %v\n", err) - } - } - f.dirty = false -} - -func (f *fileSync) input() string { - f.mut.Lock() - defer f.mut.Unlock() - return f.inputString -} - -func (f *fileSync) mapping() string { - f.mut.Lock() - defer f.mut.Unlock() - return f.mappingString -} - -func runServer(c *cli.Context) error { - fSync := newFileSync(c.String("input-file"), c.String("mapping-file"), c.Bool("write")) - defer fSync.write() - - mux := http.NewServeMux() - - mux.HandleFunc("/execute", func(w http.ResponseWriter, r *http.Request) { - req := struct { - Mapping string `json:"mapping"` - Input string `json:"input"` - }{} - dec := json.NewDecoder(r.Body) - if err := dec.Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - fSync.update(req.Input, req.Mapping) - - res := struct { - ParseError string `json:"parse_error"` - MappingError string `json:"mapping_error"` - Result string `json:"result"` - }{} - defer func() { - resBytes, err := json.Marshal(res) - if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - _, _ = w.Write(resBytes) - }() - - exec, err := bloblang.GlobalEnvironment().NewMapping(req.Mapping) - if err != nil { - if perr, ok := err.(*parser.Error); ok { - res.ParseError = fmt.Sprintf("failed to parse mapping: %v\n", perr.ErrorAtPositionStructured("", []rune(req.Mapping))) - } else { - res.ParseError = err.Error() - } - return - } - - execCache := newExecCache() - output, err := execCache.executeMapping(exec, false, true, []byte(req.Input)) - if err != nil { - res.MappingError = err.Error() - } else { - res.Result = output - } - }) - - indexTemplate := template.Must(template.New("index").Parse(bloblangEditorPage)) - - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - err := indexTemplate.Execute(w, struct { - InitialInput string - InitialMapping string - }{ - fSync.input(), - fSync.mapping(), - }) - if err != nil { - http.Error(w, "Template error", http.StatusBadGateway) - return - } - }) - - host, port := c.String("host"), c.String("port") - bindAddress := host + ":" + port - - if !c.Bool("no-open") { - u, err := url.Parse("http://localhost:" + port) - if err != nil { - return fmt.Errorf("failed to parse URL: %w", err) - } - openBrowserAt(u.String()) - } - - log.Printf("Serving at: http://%v\n", bindAddress) - - server := http.Server{ - Addr: bindAddress, - Handler: mux, - } - - go func() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - // Wait for termination signal - <-sigChan - _ = server.Shutdown(context.Background()) - }() - - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - return fmt.Errorf("failed to listen and serve: %w", err) - } - return nil -} diff --git a/internal/cli/common/logger.go b/internal/cli/common/logger.go deleted file mode 100644 index 78e30851d0..0000000000 --- a/internal/cli/common/logger.go +++ /dev/null @@ -1,31 +0,0 @@ -package common - -import ( - "os" - "strings" - - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// CreateLogger from a CLI context and a stream config. -func CreateLogger(c *cli.Context, opts *CLIOpts, conf config.Type, streamsMode bool) (logger log.Modular, err error) { - if overrideLogLevel := c.String("log.level"); overrideLogLevel != "" { - conf.Logger.LogLevel = strings.ToUpper(overrideLogLevel) - } - - defaultStream := os.Stdout - if !streamsMode && conf.Output.Type == "stdout" { - defaultStream = os.Stderr - } - if logger, err = log.New(defaultStream, ifs.OS(), conf.Logger); err != nil { - return - } - if logger, err = opts.OnLoggerInit(logger); err != nil { - return - } - return -} diff --git a/internal/cli/common/manager.go b/internal/cli/common/manager.go deleted file mode 100644 index 82d600ce31..0000000000 --- a/internal/cli/common/manager.go +++ /dev/null @@ -1,290 +0,0 @@ -package common - -import ( - "context" - "fmt" - "net/http" - "os" - "os/signal" - "runtime/pprof" - "syscall" - "time" - - "github.com/urfave/cli/v2" - "go.opentelemetry.io/otel/trace" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -// CreateManager from a CLI context and a stream config. -func CreateManager( - c *cli.Context, - cliOpts *CLIOpts, - logger log.Modular, - streamsMode bool, - conf config.Type, - mgrOpts ...manager.OptFunc, -) (stoppableMgr *StoppableManager, err error) { - var stats *metrics.Namespaced - var trac trace.TracerProvider - defer func() { - if err == nil { - return - } - if trac != nil { - if shutter, ok := trac.(interface { - Shutdown(context.Context) error - }); ok { - _ = shutter.Shutdown(context.Background()) - } - } - if stats != nil { - _ = stats.Close() - } - }() - - // We use a temporary manager with just the logger initialised for metrics - // instantiation. Doing this means that metrics plugins will use a global - // environment for child plugins and bloblang mappings, which we might want - // to revise in future. - tmpMgr := mock.NewManager() - tmpMgr.L = logger - tmpMgr.Version = cliOpts.Version - - // Create our metrics type. - if stats, err = bundle.AllMetrics.Init(conf.Metrics, tmpMgr); err != nil { - err = fmt.Errorf("failed to connect to metrics aggregator: %w", err) - return - } - - // Create our tracer type. - if trac, err = bundle.AllTracers.Init(conf.Tracer, tmpMgr); err != nil { - err = fmt.Errorf("failed to initialise tracer: %w", err) - return - } - - // Create HTTP API with a sanitised service config. - var sanitNode yaml.Node - if err = sanitNode.Encode(conf); err == nil { - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - sanitSpec := cliOpts.MainConfigSpecCtor() - if streamsMode { - sanitSpec = config.SpecWithoutStream(cliOpts.MainConfigSpecCtor()) - } - err = sanitSpec.SanitiseYAML(&sanitNode, sanitConf) - } - if err != nil { - err = fmt.Errorf("failed to generate sanitised config: %w", err) - return - } - - var httpServer *api.Type - if httpServer, err = api.New(cliOpts.Version, cliOpts.DateBuilt, conf.HTTP, sanitNode, logger, stats); err != nil { - err = fmt.Errorf("failed to initialise API: %w", err) - return - } - - mgrOpts = append([]manager.OptFunc{ - manager.OptSetAPIReg(httpServer), - manager.OptSetEngineVersion(cliOpts.Version), - manager.OptSetStreamHTTPNamespacing(c.Bool("prefix-stream-endpoints")), - manager.OptSetLogger(logger), - manager.OptSetMetrics(stats), - manager.OptSetTracer(trac), - manager.OptSetStreamsMode(streamsMode), - }, mgrOpts...) - - // Create resource manager. - var mgr *manager.Type - if mgr, err = manager.New(conf.ResourceConfig, mgrOpts...); err != nil { - err = fmt.Errorf("failed to initialise resources: %w", err) - return - } - - stoppableMgr = newStoppableManager(httpServer, mgr) - return -} - -// RunManagerUntilStopped will run the provided HTTP server and block until -// either a provided stream stoppable is gracefully terminated (via the -// dataStreamClosedChan) or a signal is given to the process to terminate, at -// which point the provided HTTP server, the manager, and the stoppable is -// stopped according to the configured shutdown timeout. -func RunManagerUntilStopped( - c *cli.Context, - conf config.Type, - stopMgr *StoppableManager, - stopStrm Stoppable, - dataStreamClosedChan chan struct{}, -) int { - var exitDelay time.Duration - if td := conf.SystemCloseDelay; td != "" { - var err error - if exitDelay, err = time.ParseDuration(td); err != nil { - stopMgr.Manager().Logger().Error("Failed to parse shutdown delay period string: %v\n", err) - return 1 - } - } - - var exitTimeout time.Duration - if tout := conf.SystemCloseTimeout; tout != "" { - var err error - if exitTimeout, err = time.ParseDuration(tout); err != nil { - stopMgr.Manager().Logger().Error("Failed to parse shutdown timeout period string: %v\n", err) - return 1 - } - } - - // Defer clean up. - defer func() { - if exitDelay > 0 { - stopMgr.Manager().Logger().Info("Shutdown delay is in effect for %s\n", exitDelay) - if err := DelayShutdown(c.Context, exitDelay); err != nil { - stopMgr.Manager().Logger().Error("Shutdown delay failed: %s", err) - } - } - - go func() { - <-time.After(exitTimeout + time.Second) - stopMgr.Manager().Logger().Warn( - "Service failed to close cleanly within allocated time." + - " Exiting forcefully and dumping stack trace to stderr", - ) - _ = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) - os.Exit(1) - }() - - ctx, done := context.WithTimeout(c.Context, exitTimeout) - if err := stopStrm.Stop(ctx); err != nil { - os.Exit(1) - } - - if err := stopMgr.Stop(ctx); err != nil { - stopMgr.Manager().Logger().Warn( - "Service failed to close resources cleanly within allocated time: %v."+ - " Exiting forcefully and dumping stack trace to stderr\n", err, - ) - _ = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) - os.Exit(1) - } - done() - }() - - var deadLineTrigger <-chan time.Time - if dl, exists := c.Context.Deadline(); exists { - // If a deadline has been set by the cli context then we need to trigger - // graceful termination before it's reached, otherwise it'll never - // happen as the context will cancel the cleanup. - // - // We make a best attempt at doing this by starting termination earlier - // than the deadline (by 10%, capped at one second). - dlTriggersBy := time.Until(dl) - - earlierBy := dlTriggersBy / 10 - if earlierBy > time.Second { - earlierBy = time.Second - } - deadLineTrigger = time.After(dlTriggersBy - earlierBy) - } - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - // Wait for termination signal - select { - case sig := <-sigChan: - var sigName string - switch sig { - case os.Interrupt: - sigName = "SIGINT" - case syscall.SIGTERM: - sigName = "SIGTERM" - default: - sigName = sig.String() - } - stopMgr.Manager().Logger().Info("Received %s, the service is closing", sigName) - case <-dataStreamClosedChan: - stopMgr.Manager().Logger().Info("Pipeline has terminated. Shutting down the service") - case <-deadLineTrigger: - stopMgr.Manager().Logger().Info("Run context deadline about to be reached. Shutting down the service") - case <-c.Context.Done(): - stopMgr.Manager().Logger().Info("Run context was cancelled. Shutting down the service") - } - return 0 -} - -func newStoppableManager(api *api.Type, mgr *manager.Type) *StoppableManager { - s := &StoppableManager{ - api: api, - apiClosedChan: make(chan struct{}), - mgr: mgr, - } - // Start HTTP server. - go func() { - httpErr := api.ListenAndServe() - if httpErr != nil && httpErr != http.ErrServerClosed { - mgr.Logger().Error("HTTP Server error: %v\n", httpErr) - } - close(s.apiClosedChan) - }() - return s -} - -// StoppableManager wraps a manager and API type that potentially outlives one -// or more dependent streams and encapsulates the logic for shutting them down -// within the deadline of a given context. -type StoppableManager struct { - api *api.Type - apiClosedChan chan struct{} - mgr *manager.Type -} - -// Manager returns the underlying manager type. -func (s *StoppableManager) Manager() *manager.Type { - return s.mgr -} - -// API returns the underlying api type. -func (s *StoppableManager) API() *api.Type { - return s.api -} - -// Stop the manager and the API server, gracefully if possible. If the context -// has a deadline then this will be used as a mechanism for pre-emptively -// attempting ungraceful stopping when nearing the deadline. -func (s *StoppableManager) Stop(ctx context.Context) error { - var gracefulCutOff <-chan time.Time - if dl, exists := ctx.Deadline(); exists { - gracefulCutOff = time.After(time.Until(dl) / 2) - } - - go func() { - _ = s.api.Shutdown(ctx) - select { - case <-s.apiClosedChan: - return - case <-ctx.Done(): - case <-gracefulCutOff: - } - s.mgr.Logger().Warn("Service failed to close HTTP server gracefully in time") - }() - - s.mgr.TriggerStopConsuming() - if err := s.mgr.WaitForClose(ctx); err != nil { - return err - } - if err := s.mgr.CloseObservability(ctx); err != nil { - s.mgr.Logger().Error("Failed to cleanly close observability components: %w", err) - } - return nil -} diff --git a/internal/cli/common/opts.go b/internal/cli/common/opts.go deleted file mode 100644 index 9f95bfe0d3..0000000000 --- a/internal/cli/common/opts.go +++ /dev/null @@ -1,70 +0,0 @@ -package common - -import ( - "bytes" - "os" - "text/template" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -type CLIStreamBootstrapFunc func() - -type CLIOpts struct { - Version string - DateBuilt string - - BinaryName string - ProductName string - DocumentationURL string - - MainConfigSpecCtor func() docs.FieldSpecs // TODO: This becomes a service.Environment - OnManagerInitialised func(mgr bundle.NewManagement, pConf *docs.ParsedConfig) error - OnLoggerInit func(l log.Modular) (log.Modular, error) -} - -func NewCLIOpts(version, dateBuilt string) *CLIOpts { - binaryName := "" - if len(os.Args) > 0 { - binaryName = os.Args[0] - } - return &CLIOpts{ - Version: version, - DateBuilt: dateBuilt, - BinaryName: binaryName, - ProductName: "Benthos", - DocumentationURL: "https://benthos.dev/docs", - MainConfigSpecCtor: config.Spec, - OnManagerInitialised: func(mgr bundle.NewManagement, pConf *docs.ParsedConfig) error { - return nil - }, - OnLoggerInit: func(l log.Modular) (log.Modular, error) { - return l, nil - }, - } -} - -func (c *CLIOpts) ExecTemplate(str string) string { - t, err := template.New("cli").Parse(str) - if err != nil { - return str - } - - var buf bytes.Buffer - if err := t.Execute(&buf, struct { - BinaryName string - ProductName string - DocumentationURL string - }{ - BinaryName: c.BinaryName, - ProductName: c.ProductName, - DocumentationURL: c.DocumentationURL, - }); err != nil { - return str - } - - return buf.String() -} diff --git a/internal/cli/common/reader.go b/internal/cli/common/reader.go deleted file mode 100644 index 20687f3faf..0000000000 --- a/internal/cli/common/reader.go +++ /dev/null @@ -1,38 +0,0 @@ -package common - -import ( - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - - "github.com/urfave/cli/v2" -) - -// ReadConfig attempts to read a general service wide config via a returned -// config.Reader based on input CLI flags. This includes applying any config -// overrides expressed by the --set flag. -func ReadConfig(c *cli.Context, cliOpts *CLIOpts, streamsMode bool) (mainPath string, inferred bool, conf *config.Reader) { - path := c.String("config") - if path == "" { - // Iterate default config paths - for _, dpath := range []string{ - "/benthos.yaml", - "/etc/benthos/config.yaml", - "/etc/benthos.yaml", - } { - if _, err := ifs.OS().Stat(dpath); err == nil { - inferred = true - path = dpath - break - } - } - } - opts := []config.OptFunc{ - config.OptSetFullSpec(cliOpts.MainConfigSpecCtor), - config.OptAddOverrides(c.StringSlice("set")...), - config.OptTestSuffix("_benthos_test"), - } - if streamsMode { - opts = append(opts, config.OptSetStreamPaths(c.Args().Slice()...)) - } - return path, inferred, config.NewReader(path, c.StringSlice("resources"), opts...) -} diff --git a/internal/cli/common/service.go b/internal/cli/common/service.go deleted file mode 100644 index dae384cd2b..0000000000 --- a/internal/cli/common/service.go +++ /dev/null @@ -1,231 +0,0 @@ -package common - -import ( - "context" - "errors" - "fmt" - "os" - "os/signal" - "sync" - "syscall" - "time" - - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" - strmmgr "github.com/benthosdev/benthos/v4/internal/stream/manager" - - "github.com/urfave/cli/v2" -) - -// RunService runs a service command (either the default or the streams -// subcommand). -func RunService(c *cli.Context, cliOpts *CLIOpts, streamsMode bool) int { - mainPath, inferredMainPath, confReader := ReadConfig(c, cliOpts, streamsMode) - - conf, pConf, lints, err := confReader.Read() - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file read error: %v\n", err) - return 1 - } - defer func() { - _ = confReader.Close(c.Context) - }() - - logger, err := CreateLogger(c, cliOpts, conf, streamsMode) - if err != nil { - fmt.Printf("Failed to create logger: %v\n", err) - return 1 - } - - verLogger := logger.With("benthos_version", cliOpts.Version) - if mainPath == "" { - verLogger.Info("Running without a main config file") - } else if inferredMainPath { - verLogger.With("path", mainPath).Info("Running main config from file found in a default path") - } else { - verLogger.With("path", mainPath).Info("Running main config from specified file") - } - - strict := !c.Bool("chilled") - for _, lint := range lints { - if strict { - logger.With("lint", lint).Error("Config lint error") - } else { - logger.With("lint", lint).Warn("Config lint error") - } - } - if strict && len(lints) > 0 { - logger.Error(cliOpts.ExecTemplate("Shutting down due to linter errors, to prevent shutdown run {{.ProductName}} with --chilled")) - return 1 - } - - stoppableManager, err := CreateManager(c, cliOpts, logger, streamsMode, conf) - if err != nil { - logger.Error(err.Error()) - return 1 - } - - if err := cliOpts.OnManagerInitialised(stoppableManager.mgr, pConf); err != nil { - logger.Error(err.Error()) - return 1 - } - - var stoppableStream Stoppable - var dataStreamClosedChan chan struct{} - - // Create data streams. - watching := c.Bool("watcher") - if streamsMode { - enableStreamsAPI := !c.Bool("no-api") - stoppableStream = initStreamsMode(cliOpts, strict, watching, enableStreamsAPI, confReader, stoppableManager.Manager()) - } else { - stoppableStream, dataStreamClosedChan = initNormalMode(cliOpts, conf, strict, watching, confReader, stoppableManager.Manager()) - } - - return RunManagerUntilStopped(c, conf, stoppableManager, stoppableStream, dataStreamClosedChan) -} - -// DelayShutdown attempts to block until either: -// - The delay period ends -// - The provided context is cancelled -// - The process receives an interrupt or sigterm -func DelayShutdown(ctx context.Context, duration time.Duration) error { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - delayCtx, cancel := context.WithTimeout(ctx, duration) - defer cancel() - - select { - case <-delayCtx.Done(): - err := delayCtx.Err() - if err != nil && err != context.DeadlineExceeded { - return err - } - case sig := <-sigChan: - return fmt.Errorf("shutdown delay interrupted by signal: %s", sig) - } - - return nil -} - -func initStreamsMode( - opts *CLIOpts, - strict, watching, enableAPI bool, - confReader *config.Reader, - mgr *manager.Type, -) Stoppable { - logger := mgr.Logger() - streamMgr := strmmgr.New(mgr, strmmgr.OptAPIEnabled(enableAPI)) - - streamConfs := map[string]stream.Config{} - lints, err := confReader.ReadStreams(streamConfs) - if err != nil { - fmt.Fprintf(os.Stderr, "Stream configuration file read error: %v\n", err) - os.Exit(1) - } - - for _, lint := range lints { - if strict { - logger.With("lint", lint).Error("Config lint error") - } else { - logger.With("lint", lint).Warn("Config lint error") - } - } - if strict && len(lints) > 0 { - logger.Error(opts.ExecTemplate("Shutting down due to stream linter errors, to prevent shutdown run {{.ProductName}} with --chilled")) - os.Exit(1) - } - - for id, conf := range streamConfs { - if err := streamMgr.Create(id, conf); err != nil { - logger.Error("Failed to create stream (%v): %v\n", id, err) - os.Exit(1) - } - } - logger.Info(opts.ExecTemplate("Launching {{.ProductName}} in streams mode, use CTRL+C to close")) - - if err := confReader.SubscribeStreamChanges(func(id string, newStreamConf *stream.Config) error { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - var updateErr error - if newStreamConf != nil { - if updateErr = streamMgr.Update(ctx, id, *newStreamConf); updateErr != nil && errors.Is(updateErr, strmmgr.ErrStreamDoesNotExist) { - updateErr = streamMgr.Create(id, *newStreamConf) - } - } else { - if updateErr = streamMgr.Delete(ctx, id); updateErr != nil && errors.Is(updateErr, strmmgr.ErrStreamDoesNotExist) { - updateErr = nil - } - } - return updateErr - }); err != nil { - logger.Error("Failed to create stream config watcher: %v", err) - os.Exit(1) - } - - if watching { - if err := confReader.BeginFileWatching(mgr, strict); err != nil { - logger.Error("Failed to create stream config watcher: %v", err) - os.Exit(1) - } - } - return streamMgr -} - -func initNormalMode( - opts *CLIOpts, - conf config.Type, - strict, watching bool, - confReader *config.Reader, - mgr *manager.Type, -) (newStream Stoppable, stoppedChan chan struct{}) { - logger := mgr.Logger() - - stoppedChan = make(chan struct{}) - var closeOnce sync.Once - streamInit := func() (Stoppable, error) { - return stream.New(conf.Config, mgr, stream.OptOnClose(func() { - if !watching { - closeOnce.Do(func() { - close(stoppedChan) - }) - } - })) - } - - initStream, err := streamInit() - if err != nil { - logger.Error("Service closing due to: %v\n", err) - os.Exit(1) - } - - stoppableStream := NewSwappableStopper(initStream) - - logger.Info(opts.ExecTemplate("Launching a {{.ProductName}} instance, use CTRL+C to close")) - - if err := confReader.SubscribeConfigChanges(func(newStreamConf *config.Type) error { - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - // NOTE: We're ignoring observability field changes for now. - return stoppableStream.Replace(ctx, func() (Stoppable, error) { - conf.Config = newStreamConf.Config - return streamInit() - }) - }); err != nil { - logger.Error("Failed to create config file watcher: %v", err) - os.Exit(1) - } - - if watching { - if err := confReader.BeginFileWatching(mgr, strict); err != nil { - logger.Error("Failed to create config file watcher: %v", err) - os.Exit(1) - } - } - - newStream = stoppableStream - return -} diff --git a/internal/cli/common/swappable.go b/internal/cli/common/swappable.go deleted file mode 100644 index fb4740490e..0000000000 --- a/internal/cli/common/swappable.go +++ /dev/null @@ -1,94 +0,0 @@ -package common - -import ( - "context" - "fmt" - "sync" -) - -// Stoppable represents a resource (a Benthos stream) that can be stopped. -type Stoppable interface { - Stop(ctx context.Context) error -} - -// CombineStoppables returns a single Stoppable that will call each provided -// Stoppable in the order they are specified on a Stop. If any stoppable returns -// an error all subsequent stoppables will still be called before an error is -// returned. -func CombineStoppables(stoppables ...Stoppable) Stoppable { - return &combinedStoppables{ - stoppables: stoppables, - } -} - -type combinedStoppables struct { - stoppables []Stoppable -} - -func (c *combinedStoppables) Stop(ctx context.Context) (stopErr error) { - for _, s := range c.stoppables { - if err := s.Stop(ctx); err != nil && stopErr == nil { - stopErr = err - } - } - return -} - -// SwappableStopper wraps an active Stoppable resource in a mechanism that -// allows changing the resource for something else after stopping it. -type SwappableStopper struct { - stopped bool - current Stoppable - mut sync.Mutex -} - -// NewSwappableStopper creates a new swappable stopper resource around an -// initial stoppable. -func NewSwappableStopper(s Stoppable) *SwappableStopper { - return &SwappableStopper{ - current: s, - } -} - -// Stop the wrapped resource. -func (s *SwappableStopper) Stop(ctx context.Context) error { - s.mut.Lock() - defer s.mut.Unlock() - - if s.stopped { - return nil - } - - s.stopped = true - return s.current.Stop(ctx) -} - -// Replace the resource with something new only once the existing one is -// stopped. In order to avoid unnecessary start up of the swapping resource we -// accept a closure that constructs it and is only called when we're ready. -func (s *SwappableStopper) Replace(ctx context.Context, fn func() (Stoppable, error)) error { - s.mut.Lock() - defer s.mut.Unlock() - - if s.stopped { - // If the outer stream has been stopped then do not create a new one. - return nil - } - - // The underlying implementation is expected to continue shutting resources - // down in the background. An error here indicates that it hasn't managed to - // fully clean up before reaching a context deadline. - // - // However, aborting the creation of the replacement would not be - // appropriate as it would leave the service stateless, we therefore stop - // blocking and proceed. - _ = s.current.Stop(ctx) - - newStoppable, err := fn() - if err != nil { - return fmt.Errorf("failed to init updated stream: %w", err) - } - - s.current = newStoppable - return nil -} diff --git a/internal/cli/create.go b/internal/cli/create.go deleted file mode 100644 index cb137f379a..0000000000 --- a/internal/cli/create.go +++ /dev/null @@ -1,192 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/urfave/cli/v2" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -func addExpression(conf map[string]any, expression string) error { - var inputTypes, processorTypes, outputTypes []string - componentTypes := strings.Split(expression, "/") - for i, str := range componentTypes { - for _, t := range strings.Split(str, ",") { - if t = strings.TrimSpace(t); t != "" { - switch i { - case 0: - inputTypes = append(inputTypes, t) - case 1: - processorTypes = append(processorTypes, t) - case 2: - outputTypes = append(outputTypes, t) - default: - return errors.New("more component separators than expected") - } - } - } - } - - if lInputs := len(inputTypes); lInputs == 1 { - t := inputTypes[0] - if _, exists := bundle.AllInputs.DocsFor(t); exists { - conf["input"] = map[string]any{ - "type": t, - } - } else { - return fmt.Errorf("unrecognised input type '%v'", t) - } - } else if lInputs > 1 { - var inputsList []any - for _, t := range inputTypes { - if _, exists := bundle.AllInputs.DocsFor(t); exists { - inputsList = append(inputsList, map[string]any{ - "type": t, - }) - } else { - return fmt.Errorf("unrecognised input type '%v'", t) - } - } - conf["input"] = map[string]any{ - "broker": map[string]any{ - "inputs": inputsList, - }, - } - } - - if len(processorTypes) > 0 { - var procsList []any - for _, t := range processorTypes { - if _, exists := bundle.AllProcessors.DocsFor(t); exists { - procsList = append(procsList, map[string]any{ - "type": t, - }) - } else { - return fmt.Errorf("unrecognised processor type '%v'", t) - } - } - conf["pipeline"] = map[string]any{ - "processors": procsList, - } - } - - if lOutputs := len(outputTypes); lOutputs == 1 { - t := outputTypes[0] - if _, exists := bundle.AllOutputs.DocsFor(t); exists { - conf["output"] = map[string]any{ - "type": t, - } - } else { - return fmt.Errorf("unrecognised output type '%v'", t) - } - } else if lOutputs > 1 { - var outputsList []any - for _, t := range outputTypes { - if _, exists := bundle.AllOutputs.DocsFor(t); exists { - outputsList = append(outputsList, map[string]any{ - "type": t, - }) - } else { - return fmt.Errorf("unrecognised output type '%v'", t) - } - } - - conf["output"] = map[string]any{ - "broker": map[string]any{ - "outputs": outputsList, - }, - } - } - return nil -} - -func createCliCommand(cliOpts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "create", - Usage: cliOpts.ExecTemplate("Create a new {{.ProductName}} config"), - Description: cliOpts.ExecTemplate(` -Prints a new {{.ProductName}} config to stdout containing specified components -according to an expression. The expression must take the form of three -comma-separated lists of inputs, processors and outputs, divided by -forward slashes: - - {{.BinaryName}} create stdin/bloblang,awk/nats - {{.BinaryName}} create file,http_server/protobuf/http_client - -If the expression is omitted a default config is created.`)[1:], - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "small", - Aliases: []string{"s"}, - Value: false, - Usage: cliOpts.ExecTemplate("Print only the main components of a {{.ProductName}} config (input, pipeline, output) and omit all fields marked as advanced."), - }, - }, - Action: func(c *cli.Context) error { - conf := map[string]any{ - "input": map[string]any{ - "stdin": map[string]any{}, - }, - "pipeline": map[string]any{ - "processors": []any{}, - }, - "output": map[string]any{ - "stdout": map[string]any{}, - }, - } - if expression := c.Args().First(); expression != "" { - if err := addExpression(conf, expression); err != nil { - fmt.Fprintf(os.Stderr, "Generate error: %v\n", err) - os.Exit(1) - } - } - - spec := cliOpts.MainConfigSpecCtor() - var filter docs.FieldFilter - if c.Bool("small") { - spec = stream.Spec() - filter = func(spec docs.FieldSpec, _ any) bool { - return !spec.IsAdvanced - } - } - - conf, err := spec.AnyToMap(conf, docs.ToValueConfig{ - FallbackToAny: true, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "Generate error: %v\n", err) - os.Exit(1) - } - - var node yaml.Node - if err = node.Encode(conf); err == nil { - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.RemoveDeprecated = true - sanitConf.ForExample = true - sanitConf.Filter = filter - - err = spec.SanitiseYAML(&node, sanitConf) - } - if err == nil { - var configYAML []byte - if configYAML, err = docs.MarshalYAML(node); err == nil { - fmt.Println(string(configYAML)) - } - } - if err != nil { - fmt.Fprintf(os.Stderr, "Generate error: %v\n", err) - os.Exit(1) - } - return nil - }, - } -} diff --git a/internal/cli/lint.go b/internal/cli/lint.go deleted file mode 100644 index c5bdf8550e..0000000000 --- a/internal/cli/lint.go +++ /dev/null @@ -1,242 +0,0 @@ -package cli - -import ( - "bytes" - "errors" - "fmt" - "io" - "os" - "path" - "runtime" - "sync" - - "github.com/fatih/color" - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - ifilepath "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -var ( - red = color.New(color.FgRed).SprintFunc() - yellow = color.New(color.FgYellow).SprintFunc() -) - -type pathLint struct { - source string - lint docs.Lint -} - -func lintFile(path string, skipEnvVarCheck bool, spec docs.FieldSpecs, lConf docs.LintConfig) (pathLints []pathLint) { - _, lints, err := config.ReadYAMLFileLinted(ifs.OS(), spec, path, skipEnvVarCheck, lConf) - if err != nil { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(1, docs.LintFailedRead, err), - }) - return - } - for _, l := range lints { - pathLints = append(pathLints, pathLint{ - source: path, - lint: l, - }) - } - return -} - -func lintMDSnippets(path string, spec docs.FieldSpecs, lConf docs.LintConfig) (pathLints []pathLint) { - rawBytes, err := ifs.ReadFile(ifs.OS(), path) - if err != nil { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(1, docs.LintFailedRead, err), - }) - return - } - - startTag, endTag := []byte("```yaml"), []byte("```") - - nextSnippet := bytes.Index(rawBytes, startTag) - for nextSnippet != -1 { - nextSnippet += len(startTag) - - snippetLine := bytes.Count(rawBytes[:nextSnippet], []byte("\n")) + 1 - - endOfSnippet := bytes.Index(rawBytes[nextSnippet:], endTag) - if endOfSnippet == -1 { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(snippetLine, docs.LintFailedRead, errors.New("markdown snippet not terminated")), - }) - return - } - endOfSnippet = nextSnippet + endOfSnippet + len(endTag) - - configBytes := rawBytes[nextSnippet : endOfSnippet-len(endTag)] - if nextSnippet = bytes.Index(rawBytes[endOfSnippet:], []byte("```yaml")); nextSnippet != -1 { - nextSnippet += endOfSnippet - } - - cNode, err := docs.UnmarshalYAML(configBytes) - if err != nil { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(snippetLine, docs.LintFailedRead, err), - }) - continue - } - - pConf, err := spec.ParsedConfigFromAny(cNode) - if err != nil { - var l docs.Lint - if errors.As(err, &l) { - l.Line += snippetLine - 1 - pathLints = append(pathLints, pathLint{ - source: path, - lint: l, - }) - } else { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(snippetLine, docs.LintFailedRead, err), - }) - } - } - - if _, err := config.FromParsed(lConf.DocsProvider, pConf, nil); err != nil { - var l docs.Lint - if errors.As(err, &l) { - l.Line += snippetLine - 1 - pathLints = append(pathLints, pathLint{ - source: path, - lint: l, - }) - } else { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(snippetLine, docs.LintFailedRead, err), - }) - } - } else { - for _, l := range spec.LintYAML(docs.NewLintContext(lConf), cNode) { - l.Line += snippetLine - 1 - pathLints = append(pathLints, pathLint{ - source: path, - lint: l, - }) - } - } - } - return -} - -func lintCliCommand(cliOpts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "lint", - Usage: cliOpts.ExecTemplate("Parse {{.ProductName}} configs and report any linting errors"), - Description: cliOpts.ExecTemplate(` -Exits with a status code 1 if any linting errors are detected: - - {{.BinaryName}} -c target.yaml lint - {{.BinaryName}} lint ./configs/*.yaml - {{.BinaryName}} lint ./foo.yaml ./bar.yaml - {{.BinaryName}} lint ./configs/... - -If a path ends with '...' then {{.ProductName}} will walk the target and lint any -files with the .yaml or .yml extension.`)[1:], - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "deprecated", - Value: false, - Usage: "Print linting errors for the presence of deprecated fields.", - }, - &cli.BoolFlag{ - Name: "labels", - Value: false, - Usage: "Print linting errors when components do not have labels.", - }, - &cli.BoolFlag{ - Name: "skip-env-var-check", - Value: false, - Usage: "Do not produce lint errors when environment interpolations exist without defaults within configs but aren't defined.", - }, - }, - Action: func(c *cli.Context) error { - if code := LintAction(c, cliOpts, os.Stderr); code != 0 { - os.Exit(code) - } - return nil - }, - } -} - -// LintAction performs the benthos lint subcommand and returns the appropriate -// exit code. This function is exported for testing purposes only. -func LintAction(c *cli.Context, opts *common.CLIOpts, stderr io.Writer) int { - targets, err := ifilepath.GlobsAndSuperPaths(ifs.OS(), c.Args().Slice(), "yaml", "yml") - if err != nil { - fmt.Fprintf(stderr, "Lint paths error: %v\n", err) - return 1 - } - if conf := c.String("config"); conf != "" { - targets = append(targets, conf) - } - targets = append(targets, c.StringSlice("resources")...) - - lConf := docs.NewLintConfig(bundle.GlobalEnvironment) - lConf.RejectDeprecated = c.Bool("deprecated") - lConf.RequireLabels = c.Bool("labels") - skipEnvVarCheck := c.Bool("skip-env-var-check") - - spec := opts.MainConfigSpecCtor() - - var pathLintMut sync.Mutex - var pathLints []pathLint - threads := runtime.NumCPU() - var wg sync.WaitGroup - wg.Add(threads) - for i := 0; i < threads; i++ { - go func(threadID int) { - defer wg.Done() - for j, target := range targets { - if j%threads != threadID { - continue - } - if target == "" { - continue - } - var lints []pathLint - if path.Ext(target) == ".md" { - lints = lintMDSnippets(target, spec, lConf) - } else { - lints = lintFile(target, skipEnvVarCheck, spec, lConf) - } - if len(lints) > 0 { - pathLintMut.Lock() - pathLints = append(pathLints, lints...) - pathLintMut.Unlock() - } - } - }(i) - } - wg.Wait() - - if len(pathLints) == 0 { - return 0 - } - - for _, lint := range pathLints { - lintText := fmt.Sprintf("%v%v\n", lint.source, lint.lint.Error()) - if lint.lint.Type == docs.LintFailedRead || lint.lint.Type == docs.LintComponentMissing { - fmt.Fprint(stderr, red(lintText)) - } else { - fmt.Fprint(stderr, yellow(lintText)) - } - } - return 1 -} diff --git a/internal/cli/lint_test.go b/internal/cli/lint_test.go deleted file mode 100644 index bb4c1455a0..0000000000 --- a/internal/cli/lint_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package cli_test - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - - icli "github.com/benthosdev/benthos/v4/internal/cli" - "github.com/benthosdev/benthos/v4/internal/cli/common" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func executeLintSubcmd(t *testing.T, args []string) (exitCode int, printedErr string) { - opts := common.NewCLIOpts("1.2.3", "now") - cliApp := icli.App(opts) - for _, c := range cliApp.Commands { - if c.Name == "lint" { - c.Action = func(ctx *cli.Context) error { - var buf bytes.Buffer - exitCode = icli.LintAction(ctx, opts, &buf) - printedErr = buf.String() - return nil - } - } - } - require.NoError(t, cliApp.Run(args)) - return -} - -func TestLints(t *testing.T) { - tmpDir := t.TempDir() - tFile := func(name string) string { - return filepath.Join(tmpDir, name) - } - - tests := []struct { - name string - files map[string]string - args []string - expectedCode int - expectedLints []string - }{ - { - name: "one file no errors", - args: []string{"benthos", "lint", tFile("foo.yaml")}, - files: map[string]string{ - "foo.yaml": ` -input: - generate: - mapping: 'root.id = uuid_v4()' -output: - drop: {} -`, - }, - }, - { - name: "one file unexpected fields", - args: []string{"benthos", "lint", tFile("foo.yaml")}, - files: map[string]string{ - "foo.yaml": ` -input: - generate: - huh: what - mapping: 'root.id = uuid_v4()' -output: - nah: nope - drop: {} -`, - }, - expectedCode: 1, - expectedLints: []string{ - "field huh not recognised", - "field nah is invalid", - }, - }, - { - name: "one file with c flag", - args: []string{"benthos", "-c", tFile("foo.yaml"), "lint"}, - files: map[string]string{ - "foo.yaml": ` -input: - generate: - huh: what - mapping: 'root.id = uuid_v4()' -output: - nah: nope - drop: {} -`, - }, - expectedCode: 1, - expectedLints: []string{ - "field huh not recognised", - "field nah is invalid", - }, - }, - { - name: "one file with r flag", - args: []string{"benthos", "-r", tFile("foo.yaml"), "lint"}, - files: map[string]string{ - "foo.yaml": ` -input: - generate: - huh: what - mapping: 'root.id = uuid_v4()' -output: - nah: nope - drop: {} -`, - }, - expectedCode: 1, - expectedLints: []string{ - "field huh not recognised", - "field nah is invalid", - }, - }, - { - name: "env var missing", - args: []string{"benthos", "lint", tFile("foo.yaml")}, - files: map[string]string{ - "foo.yaml": ` -input: - generate: - mapping: 'root.id = "${BENTHOS_ENV_VAR_HOPEFULLY_MISSING}"' -output: - drop: {} -`, - }, - expectedCode: 1, - expectedLints: []string{ - "required environment variables were not set: [BENTHOS_ENV_VAR_HOPEFULLY_MISSING]", - }, - }, - { - name: "env var missing but we dont care", - args: []string{"benthos", "lint", "--skip-env-var-check", tFile("foo.yaml")}, - files: map[string]string{ - "foo.yaml": ` -input: - generate: - mapping: 'root.id = "${BENTHOS_ENV_VAR_HOPEFULLY_MISSING}"' -output: - drop: {} -`, - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - for name, c := range test.files { - require.NoError(t, os.WriteFile(tFile(name), []byte(c), 0o644)) - } - - code, outStr := executeLintSubcmd(t, test.args) - assert.Equal(t, test.expectedCode, code) - - if len(test.expectedLints) == 0 { - assert.Empty(t, outStr) - } else { - for _, l := range test.expectedLints { - assert.Contains(t, outStr, l) - } - } - }) - } -} diff --git a/internal/cli/list.go b/internal/cli/list.go deleted file mode 100644 index a1c81c5ae5..0000000000 --- a/internal/cli/list.go +++ /dev/null @@ -1,125 +0,0 @@ -package cli - -import ( - "encoding/json" - "fmt" - "os" - "strings" - - "github.com/urfave/cli/v2" - "golang.org/x/text/cases" - "golang.org/x/text/language" - - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/config/schema" - "github.com/benthosdev/benthos/v4/internal/cuegen" -) - -func listCliCommand(opts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "list", - Usage: opts.ExecTemplate("List all {{.ProductName}} component types"), - Description: opts.ExecTemplate(` -If any component types are explicitly listed then only types of those -components will be shown. - - {{.BinaryName}} list - {{.BinaryName}} list --format json inputs output - {{.BinaryName}} list rate-limits buffers`)[1:], - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "format", - Value: "text", - Usage: "Print the component list in a specific format. Options are text, json or cue.", - }, - &cli.StringFlag{ - Name: "status", - Value: "", - Usage: "Filter the component list to only those matching the given status. Options are stable, beta or experimental.", - }, - }, - Action: func(c *cli.Context) error { - listComponents(c, opts) - os.Exit(0) - return nil - }, - } -} - -func listComponents(c *cli.Context, opts *common.CLIOpts) { - ofTypes := map[string]struct{}{} - for _, k := range c.Args().Slice() { - ofTypes[k] = struct{}{} - } - - schema := schema.New(opts.Version, opts.DateBuilt) - if status := c.String("status"); status != "" { - schema.ReduceToStatus(status) - } - schema.Config = opts.MainConfigSpecCtor() - - switch c.String("format") { - case "text": - flat := schema.Flattened() - i := 0 - for _, k := range []string{ - "inputs", - "processors", - "outputs", - "caches", - "rate-limits", - "buffers", - "metrics", - "tracers", - "scanners", - "bloblang-functions", - "bloblang-methods", - } { - if _, exists := ofTypes[k]; len(ofTypes) > 0 && !exists { - continue - } - if i > 0 { - fmt.Println("") - } - i++ - title := cases.Title(language.English).String(strings.ReplaceAll(k, "-", " ")) - fmt.Printf("%v:\n", title) - for _, t := range flat[k] { - fmt.Printf(" - %v\n", t) - } - } - case "json": - flat := schema.Flattened() - if len(ofTypes) > 0 { - for k := range flat { - if _, exists := ofTypes[k]; !exists { - delete(flat, k) - } - } - } - jsonBytes, err := json.Marshal(flat) - if err != nil { - panic(err) - } - fmt.Println(string(jsonBytes)) - case "json-full": - jsonBytes, err := json.Marshal(schema) - if err != nil { - panic(err) - } - fmt.Println(string(jsonBytes)) - case "json-full-scrubbed": - schema.Scrub() - jsonBytes, err := json.Marshal(schema) - if err != nil { - panic(err) - } - fmt.Println(string(jsonBytes)) - case "cue": - source, err := cuegen.GenerateSchema(schema) - if err != nil { - panic(err) - } - fmt.Println(string(source)) - } -} diff --git a/internal/cli/run.go b/internal/cli/run.go deleted file mode 100644 index 2143e8121a..0000000000 --- a/internal/cli/run.go +++ /dev/null @@ -1,314 +0,0 @@ -package cli - -import ( - "encoding/json" - "fmt" - "os" - "runtime/debug" - "strings" - - "github.com/urfave/cli/v2" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/cli/blobl" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/cli/studio" - clitemplate "github.com/benthosdev/benthos/v4/internal/cli/template" - "github.com/benthosdev/benthos/v4/internal/cli/test" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/template" -) - -// Build stamps. -var ( - Version = "unknown" - DateBuilt = "unknown" -) - -func init() { - if Version != "unknown" { - return - } - if info, ok := debug.ReadBuildInfo(); ok { - for _, mod := range info.Deps { - if mod.Path == "github.com/benthosdev/benthos/v4" { - if mod.Version != "(devel)" { - Version = mod.Version - } - if mod.Replace != nil { - v := mod.Replace.Version - if v != "" && v != "(devel)" { - Version = v - } - } - } - } - for _, s := range info.Settings { - if s.Key == "vcs.revision" && Version == "unknown" { - Version = s.Value - } - if s.Key == "vcs.time" && DateBuilt == "unknown" { - DateBuilt = s.Value - } - } - } -} - -//------------------------------------------------------------------------------ - -type pluginHelp struct { - Path string `json:"path,omitempty"` - Short string `json:"short,omitempty"` - Long string `json:"long,omitempty"` - Args []string `json:"args,omitempty"` -} - -// In support of --help-autocomplete. -func traverseHelp(cmd *cli.Command, pieces []string) []pluginHelp { - pieces = append(pieces, cmd.Name) - var args []string - for _, a := range cmd.Flags { - args = append(args, "--"+a.Names()[0]) - } - help := []pluginHelp{{ - Path: strings.Join(pieces, "_"), - Short: cmd.Usage, - Long: cmd.Description, - Args: args, - }} - for _, cmd := range cmd.Subcommands { - help = append(help, traverseHelp(cmd, pieces)...) - } - return help -} - -// App returns the full CLI app definition, this is useful for writing unit -// tests around the CLI. -func App(opts *common.CLIOpts) *cli.App { - flags := []cli.Flag{ - &cli.BoolFlag{ - Name: "version", - Aliases: []string{"v"}, - Value: false, - Usage: "display version info, then exit", - }, - &cli.StringSliceFlag{ - Name: "env-file", - Aliases: []string{"e"}, - Value: cli.NewStringSlice(), - Usage: "import environment variables from a dotenv file", - }, - &cli.StringFlag{ - Name: "log.level", - Value: "", - Usage: "override the configured log level, options are: off, error, warn, info, debug, trace", - }, - &cli.StringSliceFlag{ - Name: "set", - Aliases: []string{"s"}, - Usage: "set a field (identified by a dot path) in the main configuration file, e.g. `\"metrics.type=prometheus\"`", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Value: "", - Usage: "a path to a configuration file", - }, - &cli.StringSliceFlag{ - Name: "resources", - Aliases: []string{"r"}, - Usage: "pull in extra resources from a file, which can be referenced the same as resources defined in the main config, supports glob patterns (requires quotes)", - }, - &cli.StringSliceFlag{ - Name: "templates", - Aliases: []string{"t"}, - Usage: opts.ExecTemplate("EXPERIMENTAL: import {{.ProductName}} templates, supports glob patterns (requires quotes)"), - }, - &cli.BoolFlag{ - Name: "chilled", - Value: false, - Usage: "continue to execute a config containing linter errors", - }, - &cli.BoolFlag{ - Name: "watcher", - Aliases: []string{"w"}, - Value: false, - Usage: "EXPERIMENTAL: watch config files for changes and automatically apply them", - }, - &cli.BoolFlag{ - Name: "help-autocomplete", - Value: false, - Usage: "print json serialised cli argument definitions to assist with autocomplete", - Hidden: true, - }, - } - - app := &cli.App{ - Name: opts.BinaryName, - Usage: opts.ExecTemplate("A stream processor for mundane tasks - {{.DocumentationURL}}"), - Description: opts.ExecTemplate(` -Either run {{.ProductName}} as a stream processor or choose a command: - - {{.BinaryName}} list inputs - {{.BinaryName}} create kafka//file > ./config.yaml - {{.BinaryName}} -c ./config.yaml - {{.BinaryName}} -r "./production/*.yaml" -c ./config.yaml`)[1:], - Flags: flags, - Before: func(c *cli.Context) error { - dotEnvPaths, err := filepath.Globs(ifs.OS(), c.StringSlice("env-file")) - if err != nil { - fmt.Printf("Failed to resolve env file glob pattern: %v\n", err) - os.Exit(1) - } - for _, dotEnvFile := range dotEnvPaths { - dotEnvBytes, err := ifs.ReadFile(ifs.OS(), dotEnvFile) - if err != nil { - fmt.Printf("Failed to read dotenv file: %v\n", err) - os.Exit(1) - } - vars, err := parser.ParseDotEnvFile(dotEnvBytes) - if err != nil { - fmt.Printf("Failed to parse dotenv file: %v\n", err) - os.Exit(1) - } - for k, v := range vars { - if err = os.Setenv(k, v); err != nil { - fmt.Printf("Failed to set env var '%v': %v\n", k, err) - os.Exit(1) - } - } - } - - templatesPaths, err := filepath.Globs(ifs.OS(), c.StringSlice("templates")) - if err != nil { - fmt.Printf("Failed to resolve template glob pattern: %v\n", err) - os.Exit(1) - } - lints, err := template.InitTemplates(templatesPaths...) - if err != nil { - fmt.Fprintf(os.Stderr, "Template file read error: %v\n", err) - os.Exit(1) - } - if !c.Bool("chilled") && len(lints) > 0 { - for _, lint := range lints { - fmt.Fprintln(os.Stderr, lint) - } - fmt.Println(opts.ExecTemplate("Shutting down due to linter errors, to prevent shutdown run {{.ProductName}} with --chilled")) - os.Exit(1) - } - return nil - }, - Action: func(c *cli.Context) error { - if c.Bool("version") { - fmt.Printf("Version: %v\nDate: %v\n", opts.Version, opts.DateBuilt) - os.Exit(0) - } - if c.Bool("help-autocomplete") { - _ = json.NewEncoder(os.Stdout).Encode(traverseHelp(c.Command, nil)) - os.Exit(0) - } - if c.Args().Len() > 0 { - fmt.Fprintf(os.Stderr, "Unrecognised command: %v\n", c.Args().First()) - _ = cli.ShowAppHelp(c) - os.Exit(1) - } - - if code := common.RunService(c, opts, false); code != 0 { - os.Exit(code) - } - return nil - }, - Commands: []*cli.Command{ - { - Name: "echo", - Usage: "Parse a config file and echo back a normalised version", - Description: opts.ExecTemplate(` -This simple command is useful for sanity checking a config if it isn't -behaving as expected, as it shows you a normalised version after environment -variables have been resolved: - - {{.BinaryName}} -c ./config.yaml echo | less`)[1:], - Action: func(c *cli.Context) error { - _, _, confReader := common.ReadConfig(c, opts, false) - _, pConf, _, err := confReader.Read() - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file read error: %v\n", err) - os.Exit(1) - } - var node yaml.Node - if err = node.Encode(pConf.Raw()); err == nil { - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - err = opts.MainConfigSpecCtor().SanitiseYAML(&node, sanitConf) - } - if err == nil { - var configYAML []byte - if configYAML, err = docs.MarshalYAML(node); err == nil { - fmt.Println(string(configYAML)) - } - } - if err != nil { - fmt.Fprintf(os.Stderr, "Echo error: %v\n", err) - os.Exit(1) - } - return nil - }, - }, - lintCliCommand(opts), - { - Name: "streams", - Usage: opts.ExecTemplate("Run {{.ProductName}} in streams mode"), - Description: opts.ExecTemplate(` -Run {{.ProductName}} in streams mode, where multiple pipelines can be executed in a -single process and can be created, updated and removed via REST HTTP -endpoints. - - {{.BinaryName}} streams - {{.BinaryName}} -c ./root_config.yaml streams - {{.BinaryName}} streams ./path/to/stream/configs ./and/some/more - {{.BinaryName}} -c ./root_config.yaml streams ./streams/*.yaml - -In streams mode the stream fields of a root target config (input, buffer, -pipeline, output) will be ignored. Other fields will be shared across all -loaded streams (resources, metrics, etc). - -For more information check out the docs at: -{{.DocumentationURL}}/guides/streams_mode/about`)[1:], - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "no-api", - Value: false, - Usage: "Disable the HTTP API for streams mode", - }, - &cli.BoolFlag{ - Name: "prefix-stream-endpoints", - Value: true, - Usage: "Whether HTTP endpoints registered by stream configs should be prefixed with the stream ID", - }, - }, - Action: func(c *cli.Context) error { - os.Exit(common.RunService(c, opts, true)) - return nil - }, - }, - listCliCommand(opts), - createCliCommand(opts), - test.CliCommand(opts), - clitemplate.CliCommand(opts), - blobl.CliCommand(opts), - studio.CliCommand(opts), - }, - } - - app.OnUsageError = func(context *cli.Context, err error, isSubcommand bool) error { - fmt.Printf("Usage error: %v\n", err) - _ = cli.ShowAppHelp(context) - return err - } - return app -} diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go deleted file mode 100644 index 91ebe63e91..0000000000 --- a/internal/cli/run_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package cli_test - -import ( - "context" - "fmt" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - icli "github.com/benthosdev/benthos/v4/internal/cli" - "github.com/benthosdev/benthos/v4/internal/cli/common" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestRunCLIShutdown(t *testing.T) { - tmpDir := t.TempDir() - confPath := filepath.Join(tmpDir, "foo.yaml") - outPath := filepath.Join(tmpDir, "out.txt") - - require.NoError(t, os.WriteFile(confPath, fmt.Appendf(nil, ` -input: - generate: - mapping: 'root.id = "foobar"' - interval: "100ms" -output: - file: - codec: lines - path: %v -`, outPath), 0o644)) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) - defer cancel() - - require.NoError(t, icli.App(common.NewCLIOpts("1.2.3", "aaa")).RunContext(ctx, []string{"benthos", "-c", confPath})) - - data, _ := os.ReadFile(outPath) - assert.Contains(t, string(data), "foobar") -} diff --git a/internal/cli/studio/cli.go b/internal/cli/studio/cli.go deleted file mode 100644 index 58f9531791..0000000000 --- a/internal/cli/studio/cli.go +++ /dev/null @@ -1,30 +0,0 @@ -package studio - -import ( - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/cli/common" -) - -// CliCommand is a cli.Command definition for interacting with Benthos studio. -func CliCommand(cliOpts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "studio", - Usage: "Interact with Benthos studio (https://studio.benthos.dev)", - Description: ` -EXPERIMENTAL: This subcommand is experimental and therefore are subject to -change outside of major version releases.`[1:], - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "endpoint", - Aliases: []string{"e"}, - Value: "https://studio.benthos.dev", - Usage: "Specify the URL of the Benthos studio server to connect to.", - }, - }, - Subcommands: []*cli.Command{ - syncSchemaCommand(cliOpts), - pullCommand(cliOpts), - }, - } -} diff --git a/internal/cli/studio/logger.go b/internal/cli/studio/logger.go deleted file mode 100644 index d917d15dcc..0000000000 --- a/internal/cli/studio/logger.go +++ /dev/null @@ -1,49 +0,0 @@ -package studio - -import ( - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/log" -) - -var _ log.Modular = &hotSwapLogger{} - -type hotSwapLogger struct { - lPtr atomic.Pointer[log.Modular] -} - -func (h *hotSwapLogger) swap(l log.Modular) { - h.lPtr.Store(&l) -} - -func (h *hotSwapLogger) WithFields(fields map[string]string) log.Modular { - return (*h.lPtr.Load()).WithFields(fields) -} - -func (h *hotSwapLogger) With(keyValues ...any) log.Modular { - return (*h.lPtr.Load()).With(keyValues...) -} - -func (h *hotSwapLogger) Fatal(format string, v ...any) { - (*h.lPtr.Load()).Fatal(format, v...) -} - -func (h *hotSwapLogger) Error(format string, v ...any) { - (*h.lPtr.Load()).Error(format, v...) -} - -func (h *hotSwapLogger) Warn(format string, v ...any) { - (*h.lPtr.Load()).Warn(format, v...) -} - -func (h *hotSwapLogger) Info(format string, v ...any) { - (*h.lPtr.Load()).Info(format, v...) -} - -func (h *hotSwapLogger) Debug(format string, v ...any) { - (*h.lPtr.Load()).Debug(format, v...) -} - -func (h *hotSwapLogger) Trace(format string, v ...any) { - (*h.lPtr.Load()).Trace(format, v...) -} diff --git a/internal/cli/studio/metrics/observed.go b/internal/cli/studio/metrics/observed.go deleted file mode 100644 index 6ccd952075..0000000000 --- a/internal/cli/studio/metrics/observed.go +++ /dev/null @@ -1,35 +0,0 @@ -package metrics - -// ObservedInput is a subset of input metrics that we're interested in. -type ObservedInput struct { - Received int64 `json:"received"` -} - -// ObservedOutput is a subset of output metrics that we're interested in. -type ObservedOutput struct { - Sent int64 `json:"sent"` - Error int64 `json:"error"` -} - -// ObservedProcessor is a subset of processor metrics that we're interested in. -type ObservedProcessor struct { - Received int64 `json:"received"` - Sent int64 `json:"sent"` - Error int64 `json:"error"` -} - -// Observed is a subset of typical Benthos metrics collected by streams that -// we're interested in for studios purposes. -type Observed struct { - Input map[string]ObservedInput `json:"input"` - Processor map[string]ObservedProcessor `json:"processor"` - Output map[string]ObservedOutput `json:"output"` -} - -func newObserved() *Observed { - return &Observed{ - Input: map[string]ObservedInput{}, - Processor: map[string]ObservedProcessor{}, - Output: map[string]ObservedOutput{}, - } -} diff --git a/internal/cli/studio/metrics/tracker.go b/internal/cli/studio/metrics/tracker.go deleted file mode 100644 index a0aa9f1ab7..0000000000 --- a/internal/cli/studio/metrics/tracker.go +++ /dev/null @@ -1,204 +0,0 @@ -package metrics - -import ( - "net/http" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -// Tracker keeps a reference to observed metrics and is capable of flushing the -// currently observed counters. This also implements the internal metrics type -// as it's used as a drop-in replacement in order to gather those observed -// counters. -type Tracker struct { - currentEpoch *Observed - lastFlushed time.Time - tNowFn func() time.Time - m sync.Mutex -} - -// OptSetNowFn sets the function used to obtain a new time value representing -// now. By default time.Now is used. -func OptSetNowFn(fn func() time.Time) func(*Tracker) { - return func(t *Tracker) { - t.tNowFn = fn - } -} - -// NewTracker returns a metrics implementation that records studio specific -// metrics information. -func NewTracker(opts ...func(t *Tracker)) *Tracker { - t := &Tracker{ - currentEpoch: newObserved(), - tNowFn: time.Now, - } - for _, opt := range opts { - opt(t) - } - t.lastFlushed = t.tNowFn() - return t -} - -// Flush the latest observed metrics and reset all counters for the next epoch. -func (t *Tracker) Flush() *Observed { - t.m.Lock() - current := t.currentEpoch - t.currentEpoch = newObserved() - t.lastFlushed = t.tNowFn() - t.m.Unlock() - return current -} - -// LastFlushed returns the time at which the metrics were last flushed. -func (t *Tracker) LastFlushed() time.Time { - t.m.Lock() - v := t.lastFlushed - t.m.Unlock() - return v -} - -func (t *Tracker) withCurrentEpoch(fn func(*Observed)) { - t.m.Lock() - fn(t.currentEpoch) - t.m.Unlock() -} - -type closureStat func(v int64) - -func (c closureStat) IncrFloat64(v float64) { - c(int64(v)) -} - -func (c closureStat) Incr(v int64) { - c(v) -} - -func (c closureStat) Decr(v int64) { - c(v) -} - -func (c closureStat) Set(v int64) { - c(v) -} - -func (c closureStat) Timing(v int64) { - c(v) -} - -func (t *Tracker) counterForNameAndLabel(path, label string) metrics.StatCounter { - switch path { - case "input_received": - return closureStat(func(v int64) { - t.withCurrentEpoch(func(m *Observed) { - i := m.Input[label] - i.Received += v - m.Input[label] = i - }) - }) - case "processor_received": - return closureStat(func(v int64) { - t.withCurrentEpoch(func(m *Observed) { - p := m.Processor[label] - p.Received += v - m.Processor[label] = p - }) - }) - case "processor_sent": - return closureStat(func(v int64) { - t.withCurrentEpoch(func(m *Observed) { - p := m.Processor[label] - p.Sent += v - m.Processor[label] = p - }) - }) - case "processor_error": - return closureStat(func(v int64) { - t.withCurrentEpoch(func(m *Observed) { - p := m.Processor[label] - p.Error += v - m.Processor[label] = p - }) - }) - case "output_sent": - return closureStat(func(v int64) { - t.withCurrentEpoch(func(m *Observed) { - o := m.Output[label] - o.Sent += v - m.Output[label] = o - }) - }) - case "output_error": - return closureStat(func(v int64) { - t.withCurrentEpoch(func(m *Observed) { - o := m.Output[label] - o.Error += v - m.Output[label] = o - }) - }) - } - return metrics.DudStat{} -} - -// GetCounter returns an editable counter stat for a given path. -func (t *Tracker) GetCounter(name string) metrics.StatCounter { - return t.counterForNameAndLabel(name, "") -} - -// GetCounterVec returns an editable counter stat for a given path with labels, -// these labels must be consistent with any other metrics registered on the -// same path. -func (t *Tracker) GetCounterVec(name string, labelNames ...string) metrics.StatCounterVec { - labelNamesToIndex := map[string]int{} - for i, n := range labelNames { - labelNamesToIndex[n] = i - } - return metrics.FakeCounterVec(func(s ...string) metrics.StatCounter { - label := "" - if index, exists := labelNamesToIndex["label"]; exists && len(s) > index { - label = s[index] - } - return t.counterForNameAndLabel(name, label) - }) -} - -// GetTimer returns an editable timer stat for a given path. -func (t *Tracker) GetTimer(name string) metrics.StatTimer { - return metrics.DudStat{} // Not using any of these metrics (yet) -} - -// GetTimerVec returns an editable timer stat for a given path with labels, -// these labels must be consistent with any other metrics registered on the -// same path. -func (t *Tracker) GetTimerVec(name string, labelNames ...string) metrics.StatTimerVec { - return metrics.FakeTimerVec(func(s ...string) metrics.StatTimer { - return metrics.DudStat{} // Not using any of these metrics (yet) - }) -} - -// GetGauge returns an editable gauge stat for a given path. -func (t *Tracker) GetGauge(name string) metrics.StatGauge { - return metrics.DudStat{} // Not using any of these metrics (yet) -} - -// GetGaugeVec returns an editable gauge stat for a given path with labels, -// these labels must be consistent with any other metrics registered on the -// same path. -func (t *Tracker) GetGaugeVec(name string, labelNames ...string) metrics.StatGaugeVec { - return metrics.FakeGaugeVec(func(s ...string) metrics.StatGauge { - return metrics.DudStat{} // Not using any of these metrics (yet) - }) -} - -// HandlerFunc returns an optional HTTP request handler that exposes metrics -// from the implementation. If nil is returned then no endpoint will be -// registered. -func (t *Tracker) HandlerFunc() http.HandlerFunc { - return nil -} - -// Close stops aggregating stats and cleans up resources. -func (t *Tracker) Close() error { - return nil -} diff --git a/internal/cli/studio/pull.go b/internal/cli/studio/pull.go deleted file mode 100644 index 63a72018e1..0000000000 --- a/internal/cli/studio/pull.go +++ /dev/null @@ -1,135 +0,0 @@ -package studio - -import ( - "context" - "fmt" - "os" - "os/signal" - "sync" - "syscall" - "time" - - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/cli/common" -) - -func pullCommand(cliOpts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "pull", - Usage: "Run deployments configured within a Benthos Studio session", - Description: ` -When a Studio session has one or more deployments added this command will -synchronise with the session and obtain a deployment assignment. The assigned -deployment will then determine which configs from the session to download and -execute. - -When either changes are made to files of an assigned deployment, or when a new -deployment is assigned, this service will automatically download the new config -files and execute them, replacing the previous stream running.`[1:], - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "session", - Aliases: []string{"s"}, - Required: true, - Value: "", - Usage: "The session ID to synchronise with.", - }, - &cli.StringFlag{ - Name: "name", - Value: "", - Usage: "An explicit name to adopt in this instance, used to identify its connection to the session. Each running node must have a unique name, if left unset a name is generated each time the command is run.", - }, - &cli.StringFlag{ - Name: "token", - Value: "", - Usage: "A token for the session, used to authenticate requests. If left blank the environment variable BSTDIO_NODE_TOKEN will be used instead.", - }, - &cli.StringFlag{ - Name: "token-secret", - Value: "", - Usage: "A token secret the session, used to authenticate requests. If left blank the environment variable BSTDIO_NODE_SECRET will be used instead.", - }, - &cli.BoolFlag{ - Name: "send-traces", - Value: false, - Usage: "Whether to send trace data back to Studio during execution. This is opt-in and is used as a way to add trace events to the graph editor for testing and debugging configs. This is a very useful feature but should be used with caution as it exports information about messages passing through the stream.", - }, - }, - Action: func(c *cli.Context) error { - // Start off by warning about all unsupported flags - if c.Bool("watcher") { - fmt.Fprintln(os.Stderr, "The --watcher/-w flag is not supported in this mode of operation") - os.Exit(1) - } - - token, secret := c.String("token"), c.String("token-secret") - if token == "" { - if token = os.Getenv("BSTDIO_NODE_TOKEN"); token == "" { - fmt.Fprintln(os.Stderr, "Must specify either --token or BSTDIO_NODE_TOKEN") - } - } - if secret == "" { - if secret = os.Getenv("BSTDIO_NODE_SECRET"); secret == "" { - fmt.Fprintln(os.Stderr, "Must specify either --token-secret or BSTDIO_NODE_SECRET") - } - } - if token == "" || secret == "" { - os.Exit(1) - } - - pullRunner, err := NewPullRunner(c, cliOpts, token, secret) - if err != nil { - fmt.Fprintf(os.Stderr, "Error encountered whilst initiating studio sync: %v\n", err) - os.Exit(1) - } - - sigCtx, done := context.WithCancel(context.Background()) - defer done() - - // TODO: Replace this with context.WithCancelCause once 1.20 is our - // minimum version. - var sigName string - var sigNameMut sync.Mutex - - go func() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - select { - case sig := <-sigChan: - sigNameMut.Lock() - switch sig { - case os.Interrupt: - sigName = "SIGINT" - case syscall.SIGTERM: - sigName = "SIGTERM" - default: - sigName = sig.String() - } - sigNameMut.Unlock() - done() - case <-sigCtx.Done(): - return - } - }() - - syncTicker := time.NewTicker(time.Second * 5) - defer syncTicker.Stop() - for { - // Wait for either a termination signal, or the next sync timer - select { - case <-syncTicker.C: - pullRunner.Sync(sigCtx) - case <-sigCtx.Done(): - sigNameMut.Lock() - pullRunner.logger.Info("Received signal %s, shutting down", sigName) - sigNameMut.Unlock() - if pullRunner.Stop(context.Background()) != nil { - os.Exit(1) - } - return nil - } - } - }, - } -} diff --git a/internal/cli/studio/pull_runner.go b/internal/cli/studio/pull_runner.go deleted file mode 100644 index 148e7cf507..0000000000 --- a/internal/cli/studio/pull_runner.go +++ /dev/null @@ -1,481 +0,0 @@ -package studio - -import ( - "context" - "errors" - "fmt" - "net/url" - "os" - "path" - "runtime/pprof" - "time" - - gonanoid "github.com/matoous/go-nanoid/v2" - "github.com/urfave/cli/v2" - - ibloblang "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/bundle/tracing" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/cli/studio/metrics" - stracing "github.com/benthosdev/benthos/v4/internal/cli/studio/tracing" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -type noopStopper struct{} - -func (n noopStopper) Stop(_ context.Context) error { - return nil -} - -// When a stream component (manager with resources or stream running a config) -// is instructed to shutdown this deadline determines the maximum amount of time -// we're willing to wait for it to be done gracefully when otherwise not -// configured. -const defaultCloseDeadline = time.Second * 30 - -// PullRunner encapsulates a component that runs a Benthos stream continuously -// by obtaining a deployment allocation from a Studio session, pulling the -// configs from that deployment, and then executing the configs in the -// background. -// -// Each time Sync is called the runner will poll the session for any deployment -// reallocations, or config changes and attempt to reflect those changes in the -// running stream. -type PullRunner struct { - confReaderSpec docs.FieldSpecs - confReader *config.Reader - sessionTracker *sessionTracker - - // Controls disabled deployment rotations - isDisabled bool - latestMainConf *stream.Config - - metricsFlushPeriod time.Duration - metrics *metrics.Tracker - mgr bundle.NewManagement - tracingSummary *tracing.Summary - stoppableMgr *common.StoppableManager - stoppableStream *common.SwappableStopper - logger *hotSwapLogger - - exitDelay time.Duration - exitTimeout time.Duration - - cliContext *cli.Context - cliOpts *common.CLIOpts - strictMode bool - version string - dateBuilt string - allowTraces bool - - nowFn func() time.Time -} - -// OptSetNowFn sets the function used to obtain a new time value representing -// now. By default time.Now is used. -func OptSetNowFn(fn func() time.Time) func(*PullRunner) { - return func(pr *PullRunner) { - pr.nowFn = fn - } -} - -// NewPullRunner creates a new PullRunner from a cli context, which is used for -// overriding a range of stream behaviours and settings various studio specific -// details such as the endpoint. The version, date stamps must be provided as -// well as a valid token and secret for the session that will be accessed. -// -// It's odd having to push a *cli.Context through here but I wanted to avoid -// needing to pass tens of parameters through for things like --set, -// --prefix-stream-endpoints, etc. Some of those customisation options are -// pushed deep into things like the manager constructor, and as cli options are -// expanded it'd be a drag to have to update every single constructor signature -// that calls into it. -func NewPullRunner(c *cli.Context, cliOpts *common.CLIOpts, token, secret string, opts ...func(p *PullRunner)) (*PullRunner, error) { - r := &PullRunner{ - confReaderSpec: cliOpts.MainConfigSpecCtor(), - metricsFlushPeriod: time.Second * 30, - stoppableStream: common.NewSwappableStopper(&noopStopper{}), - logger: &hotSwapLogger{}, - cliContext: c, - cliOpts: cliOpts, - strictMode: !c.Bool("chilled"), - version: cliOpts.Version, - dateBuilt: cliOpts.DateBuilt, - nowFn: time.Now, - allowTraces: c.Bool("send-traces"), - } - - for _, opt := range opts { - opt(r) - } - r.metrics = metrics.NewTracker(metrics.OptSetNowFn(r.nowFn)) - - nodeName := c.String("name") - if nodeName == "" { - var err error - if nodeName, err = gonanoid.New(); err != nil { - return nil, fmt.Errorf("failed to generate name: %w", err) - } - } - - baseURL, err := url.Parse(c.String("endpoint")) - if err != nil { - return nil, fmt.Errorf("failed to parse endpoint: %w", err) - } - baseURL.Path = path.Join(baseURL.Path, fmt.Sprintf("/api/v1/node/session/%v", c.String("session"))) - - // Logger is suuuuper primitive so we need to have one available before we - // bootstrap. In order to accommodate this we create a hot swappable logger - // that gets replaced each time a new config is loaded. - { - confPath, confResPaths, setSlice := c.String("config"), c.StringSlice("resources"), c.StringSlice("set") - tmpConf, _, localLints, err := config.NewReader(confPath, confResPaths, config.OptAddOverrides(setSlice...)).Read() - if err != nil { - return nil, fmt.Errorf("failed to create initial logger: %w", err) - } - - logger, err := common.CreateLogger(c, cliOpts, tmpConf, false) - if err != nil { - return nil, fmt.Errorf("failed to create initial logger: %w", err) - } - r.logger.swap(logger) - - if confPath != "" || len(confResPaths) > 0 || len(setSlice) > 0 { - r.logLints(localLints) - if r.strictMode && len(localLints) > 0 { - return nil, errors.New("linter errors were found in local configuration files, to ignore these errors run Benthos with --chilled") - } - - newSpec := cliOpts.MainConfigSpecCtor() - sanitObj, _ := tmpConf.GetRawSource().(map[string]any) - for _, k := range []string{ - "http", "input", "buffer", "output", "logger", "metrics", "tracer", - } { - if v, exists := sanitObj[k]; exists { - newSpec.SetDefault(v, k) - } - } - r.confReaderSpec = newSpec - } - } - - if r.sessionTracker, err = initSessionTracker(c.Context, r.nowFn, r.logger, nodeName, baseURL.String(), token, secret); err != nil { - return nil, fmt.Errorf("failed to initialise session connection: %w", err) - } - r.metricsFlushPeriod = r.sessionTracker.MetricsGuideFlushPeriod() - - err = r.bootstrapConfigReader(c.Context) - if err != nil { - r.logger.Error("Failed to run initial sync config: %v", err) - } - r.sessionTracker.SetRunError(err) - return r, nil -} - -func (r *PullRunner) logLints(lints []string) { - for _, lint := range lints { - if r.strictMode { - r.logger.With("lint", lint).Error("Config lint error") - } else { - r.logger.With("lint", lint).Warn("Config lint error") - } - } -} - -func (r *PullRunner) setStreamDisabled(ctx context.Context, toDisabled bool) error { - if r.isDisabled == toDisabled { - return nil // Already set - } - - return r.withExitContext(ctx, func(ctx context.Context) error { - if toDisabled { - if err := r.stoppableStream.Replace(ctx, func() (common.Stoppable, error) { - return &noopStopper{}, nil - }); err != nil { - return err - } - } else if r.latestMainConf != nil && r.mgr != nil { - if err := r.stoppableStream.Replace(ctx, func() (common.Stoppable, error) { - return stream.New(*r.latestMainConf, r.mgr) - }); err != nil { - return err - } - } - r.isDisabled = toDisabled - return nil - }) -} - -func (r *PullRunner) triggerStreamReset(ctx context.Context, conf *config.Type, mgr bundle.NewManagement) error { - r.latestMainConf = &conf.Config - if logger, err := common.CreateLogger(r.cliContext, r.cliOpts, *conf, false); err == nil { - r.logger.swap(logger) - } - - if r.isDisabled { - return nil - } - return r.withExitContext(ctx, func(ctx context.Context) error { - return r.stoppableStream.Replace(ctx, func() (common.Stoppable, error) { - return stream.New(conf.Config, mgr) - }) - }) -} - -func (r *PullRunner) bootstrapConfigReader(ctx context.Context) (bootstrapErr error) { - initMainFile := r.cliContext.String("config") - initResources := r.cliContext.StringSlice("resources") - initFiles := r.sessionTracker.Files() - if initFiles.MainConfig != nil { - initMainFile = initFiles.MainConfig.Name - } - for _, f := range initFiles.ResourceConfigs { - initResources = append(initResources, f.Name) - } - - sessFS := &sessionFS{ - tracker: r.sessionTracker, - backup: ifs.OS(), - } - - bloblEnv := ibloblang.GlobalEnvironment().WithCustomImporter(func(name string) ([]byte, error) { - return ifs.ReadFile(sessFS, name) - }) - - lintConf := docs.NewLintConfig(bundle.GlobalEnvironment) - lintConf.BloblangEnv = bloblang.XWrapEnvironment(bloblEnv).Deactivated() - - confReaderTmp := config.NewReader(initMainFile, initResources, - config.OptAddOverrides(r.cliContext.StringSlice("set")...), - config.OptTestSuffix("_benthos_test"), - config.OptUseFS(sessFS), - config.OptSetLintConfig(lintConf), - config.OptSetFullSpec(func() docs.FieldSpecs { - return r.confReaderSpec - }), - ) - - defer func() { - if bootstrapErr != nil { - _ = r.withExitContext(ctx, func(ctx context.Context) error { - return confReaderTmp.Close(ctx) - }) - } - }() - - conf, _, lints, err := confReaderTmp.Read() - if err != nil { - return fmt.Errorf("failed bootstrap config read: %w", err) - } - r.logLints(lints) - if r.strictMode && len(lints) > 0 { - return errors.New("found linting errors in config") - } - - tmpEnv, tmpTracingSummary := tracing.TracedBundle(bundle.GlobalEnvironment) - tmpTracingSummary.SetEnabled(false) - - stopMgrTmp, err := common.CreateManager( - r.cliContext, r.cliOpts, r.logger, false, conf, - manager.OptSetEnvironment(tmpEnv), - manager.OptSetBloblangEnvironment(bloblEnv), - manager.OptSetFS(sessFS)) - if err != nil { - return fmt.Errorf("failed to create manager from bootstrap config: %w", err) - } - defer func() { - if bootstrapErr != nil { - _ = r.withExitContext(ctx, func(ctx context.Context) error { - return stopMgrTmp.Stop(ctx) - }) - } - }() - - mgrTmp := stopMgrTmp.Manager().WithAddedMetrics(r.metrics) - if err := r.triggerStreamReset(ctx, &conf, mgrTmp); err != nil { - return fmt.Errorf("failed initial stream reset: %w", err) - } - - // Extract shutdown timeout values - var exitDelay time.Duration - if td := conf.SystemCloseDelay; td != "" { - var err error - if exitDelay, err = time.ParseDuration(td); err != nil { - return fmt.Errorf("failed to parse shutdown delay period string: %w", err) - } - } - - var exitTimeout time.Duration - if tout := conf.SystemCloseTimeout; tout != "" { - var err error - if exitTimeout, err = time.ParseDuration(tout); err != nil { - return fmt.Errorf("failed to parse shutdown timeout period string: %w", err) - } - } - - r.stoppableMgr = stopMgrTmp - r.mgr = mgrTmp - r.tracingSummary = tmpTracingSummary - r.confReader = confReaderTmp - r.exitDelay = exitDelay - r.exitTimeout = exitTimeout - - if err := confReaderTmp.SubscribeConfigChanges(func(conf *config.Type) error { - return r.triggerStreamReset(context.Background(), conf, mgrTmp) - }); err != nil { - return fmt.Errorf("failed to subscribe to config changes: %w", err) - } - return -} - -// Sync with the target session, obtaining new allocations, config changes, -// passing errors and metrics, etc. -func (r *PullRunner) Sync(ctx context.Context) { - var metricsOut *metrics.Observed - if r.nowFn().Sub(r.metrics.LastFlushed()) > r.metricsFlushPeriod { - metricsOut = r.metrics.Flush() - } - - // Pause traces (if previously enabled), and flush all events collected - // since the last sync. - var tracingOut *stracing.Observed - if r.tracingSummary != nil { - r.tracingSummary.SetEventLimit(0) - r.tracingSummary.SetEnabled(false) - if r.allowTraces { - tracingOut = stracing.FromInternal(r.tracingSummary) - } - } - - isDisabled, diff, requestedTraces, err := r.sessionTracker.Sync(ctx, metricsOut, tracingOut) - if err != nil { - r.logger.Error("Failed session sync: %v", err) - return - } - - if r.confReader == nil { - // We haven't bootstrapped yet, likely due to a bad config on - // our first and latest attempt. The latest sync may have fixed the - // issue so we can potentially try again but it's only worth it if there - // was a diff in the configs available compared to the last attempt. - if diff == nil { - return - } - - if isDisabled { - // Except the deployment is disabled now, so don't. - r.logger.Info("Deployment is disabled, so skipping bootstrap of initial config") - return - } - - err := r.bootstrapConfigReader(ctx) - if err != nil { - r.logger.Error("Failed to bootstrap initial config: %v", err) - } - r.sessionTracker.SetRunError(err) - return - } - - if err = r.setStreamDisabled(ctx, isDisabled); err != nil { - r.logger.Error("Failed to toggle deployment enablement: %v", err) - return - } - - var runErr error // TODO: Use new multi error - if diff != nil { - // We've already bootstrapped, and so we need to update our - // config reader of all changes. - for _, resName := range diff.RemoveResources { - if err := r.confReader.TriggerResourceDelete(r.mgr, resName); err != nil { - r.logger.Error("Failed to reflect resource file '%v' deletion: %v", r, err) - runErr = err - } - } - for _, res := range diff.AddResources { - if err := r.confReader.TriggerResourceUpdate(r.mgr, r.strictMode, res.Name); err != nil { - r.logger.Error("Failed to reflect resource file '%v' update: %v", res.Name, err) - runErr = err - } - } - if diff.MainConfig != nil { - if err := r.confReader.TriggerMainUpdate(r.mgr, r.strictMode, diff.MainConfig.Name); err != nil { - r.logger.Error("Failed to reflect main config file '%v' update: %v", diff.MainConfig.Name, err) - runErr = err - } - } - r.sessionTracker.SetRunError(runErr) - } - if runErr != nil { - return - } - - // Set a new trace limit and re-enable if appropriate, we want to do this if - // either the files we already have match the deployment, or after we've - // successfully followed the diff. - if r.allowTraces { - r.tracingSummary.SetEventLimit(requestedTraces) - r.tracingSummary.SetEnabled(requestedTraces > 0) - } -} - -func (r *PullRunner) withExitContext(ctx context.Context, fn func(context.Context) error) error { - tout := r.exitTimeout - if tout <= 0 { - tout = defaultCloseDeadline - } - ctx, done := context.WithTimeout(ctx, tout) - defer done() - return fn(ctx) -} - -// Stop any underlying stream and managers that may exist. -func (r *PullRunner) Stop(ctx context.Context) error { - { - // Use a shorter deadline for leaving as it's optional - leaveCtx := ctx - if dl, exists := ctx.Deadline(); !exists || dl.Sub(r.nowFn()) > time.Second { - var done func() - leaveCtx, done = context.WithTimeout(leaveCtx, time.Second) - defer done() - } - if err := r.sessionTracker.Leave(leaveCtx); err != nil { - r.logger.Warn("Failed to inform Studio session that we're shutting down: %v", err) - } - } - - if r.exitDelay > 0 { - r.logger.Info("Shutdown delay is in effect for %s", r.exitDelay) - if err := common.DelayShutdown(ctx, r.exitDelay); err != nil { - r.logger.Error("Shutdown delay failed: %s", err) - } - } - - return r.withExitContext(ctx, func(ctx context.Context) error { - if err := r.stoppableStream.Stop(ctx); err != nil { - r.logger.Warn( - "Service failed to close the running stream cleanly within allocated time: %v."+ - " Exiting forcefully and dumping stack trace to stderr\n", err, - ) - _ = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) - return err - } - if r.stoppableMgr == nil { - return nil - } - if err := r.stoppableMgr.Stop(ctx); err != nil { - r.logger.Warn( - "Service failed to close resources cleanly within allocated time: %v."+ - " Exiting forcefully and dumping stack trace to stderr\n", err, - ) - _ = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) - return err - } - return nil - }) -} diff --git a/internal/cli/studio/pull_runner_test.go b/internal/cli/studio/pull_runner_test.go deleted file mode 100644 index 5c9ba662b4..0000000000 --- a/internal/cli/studio/pull_runner_test.go +++ /dev/null @@ -1,1262 +0,0 @@ -package studio_test - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/nsf/jsondiff" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" - - icli "github.com/benthosdev/benthos/v4/internal/cli" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/cli/studio" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type validateRequestFn func(t *testing.T, w http.ResponseWriter, r *http.Request) - -type tExpectedRequest struct { - path string - fn validateRequestFn -} - -func expectedRequest(path string, fn validateRequestFn) tExpectedRequest { - return tExpectedRequest{path: path, fn: fn} -} - -func testServerForPullRunner( - t *testing.T, - nowFn func() time.Time, - args []string, - expectedRequests ...tExpectedRequest, -) (pr *studio.PullRunner, waitFn func(context.Context)) { - if nowFn == nil { - nowFn = time.Now - } - - doneChan := make(chan struct{}) - var closeOnce sync.Once - waitFn = func(ctx context.Context) { - select { - case <-ctx.Done(): - t.Error("requests never finished") - case <-doneChan: - } - } - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.NotEmpty(t, len(expectedRequests)) - - expReq := expectedRequests[0] - expectedRequests = expectedRequests[1:] - if len(expectedRequests) == 0 { - defer func() { - closeOnce.Do(func() { - close(doneChan) - }) - }() - } - - t.Logf("request: %v", expReq.path) - - require.Equal(t, expReq.path, r.URL.EscapedPath()) - - // Verify that our authorization tokens are punching through - assert.Equal(t, "aaa", r.Header.Get("X-Bstdio-Node-Id")) - assert.Equal(t, "Node bbb", r.Header.Get("Authorization")) - - expReq.fn(t, w, r) - })) - - injectedArgs := make([]string, 0, len(args)+2) - for _, a := range args { - injectedArgs = append(injectedArgs, a) - if a == "studio" { - injectedArgs = append(injectedArgs, "-e", testServer.URL) - } - } - - cliOpts := common.NewCLIOpts("1.2.3", "justnow") - - cliApp := icli.App(cliOpts) - for _, c := range cliApp.Commands { - if c.Name == "studio" { - for _, sc := range c.Subcommands { - if sc.Name == "pull" { - sc.Action = func(ctx *cli.Context) error { - var err error - pr, err = studio.NewPullRunner(ctx, cliOpts, "aaa", "bbb", studio.OptSetNowFn(nowFn)) - require.NoError(t, err) - return nil - } - } - } - } - } - require.NoError(t, cliApp.Run(injectedArgs)) - return -} - -type ( - obj = map[string]any - arr = []any -) - -func jsonRequestEqual(t *testing.T, r *http.Request, exp any) { - t.Helper() - - resBytes, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var act any - require.NoError(t, json.Unmarshal(resBytes, &act)) - - assert.Equal(t, exp, act) -} - -func jsonRequestSupersetMatch(t *testing.T, r *http.Request, exp any) { - t.Helper() - - resBytes, err := io.ReadAll(r.Body) - require.NoError(t, err) - - expBytes, err := json.Marshal(exp) - require.NoError(t, err) - - jdopts := jsondiff.DefaultConsoleOptions() - diff, explanation := jsondiff.Compare(resBytes, expBytes, &jdopts) - if diff != jsondiff.FullMatch && diff != jsondiff.SupersetMatch { - t.Errorf("json mismatch:\n%v", explanation) - } -} - -func jsonResponse(t *testing.T, w http.ResponseWriter, res any) { - t.Helper() - - resBytes, err := json.Marshal(res) - require.NoError(t, err) - - w.Header().Add("Content-Type", "application/json") - _, err = w.Write(resBytes) - require.NoError(t, err) -} - -func stringResponse(t *testing.T, w http.ResponseWriter, res string) { - t.Helper() - _, err := w.Write([]byte(res)) - require.NoError(t, err) -} - -func replacePaths(tmpDir, conf string) string { - return strings.NewReplacer("$DIR", tmpDir).Replace(conf) -} - -func TestPullRunnerHappyPath(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "main a.yaml", "modified": 1001}, - "resource_configs": arr{ - obj{"name": "resa.yaml", "modified": 1002}, - }, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/main%20a.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, ` -http: - enabled: false -input: - # Needs to keep generating across resource changes but not so much that we - # swamp the disk with data. - generate: - count: 300 - interval: 100ms - mapping: 'root.id = "first"' -output: - resource: aoutput -`) - }), - expectedRequest("/api/v1/node/session/foosession/download/resa.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "HEAD", r.Method) - }), - expectedRequest("/api/v1/node/session/foosession/download/resa.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -output_resources: - - label: aoutput - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestSupersetMatch(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "main a.yaml", "modified": 1001.0}, - "resource_configs": arr{ - obj{"name": "resa.yaml", "modified": 1002.0}, - }, - }) - jsonResponse(t, w, obj{ - "add_resources": arr{ - obj{"name": "resa.yaml", "modified": 1003}, - }, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/resa.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -output_resources: - - label: aoutput - file: - codec: lines - path: $DIR/outb.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outb.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*30, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerBadConfig(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - blahbluh: - nah: nope -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestSupersetMatch(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - "run_error": "failed bootstrap config read: maina.yaml: (5,1) unable to infer input type from candidates: [blahbluh]", - }) - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestSupersetMatch(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - "run_error": "failed bootstrap config read: maina.yaml: (5,1) unable to infer input type from candidates: [blahbluh]", - }) - jsonResponse(t, w, obj{ - "main_config": obj{"name": "maina.yaml", "modified": 1002}, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - pr.Sync(ctx) - pr.Sync(ctx) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*10, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerBlockedShutdown(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - retry: - output: - http_client: - url: http://example.com:1234 ] -shutdown_timeout: 2s -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - }) - jsonResponse(t, w, obj{ - "main_config": obj{"name": "maina.yaml", "modified": 1002}, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - time.Sleep(time.Millisecond * 100) - pr.Sync(ctx) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*10, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerSetOverride(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--set", `input.generate.mapping=root.id = "second"`, "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"second"}`) - }, time.Second*30, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerReassignment(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestSupersetMatch(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - }) - jsonResponse(t, w, obj{ - "reassignment": obj{ - "id": "depbid", - "name": "Deployment B", - }, - }) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depbid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestSupersetMatch(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - }) - jsonResponse(t, w, obj{ - "main_config": obj{"name": "mainb.yaml", "modified": 1002.0}, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/mainb.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "second"' -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"second"}`) - }, time.Second*30, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerBaseConfig(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "diskmain.yaml"), []byte(replacePaths(tmpDir, ` -http: - enabled: false -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)), 0o644)) - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "-c", filepath.Join(tmpDir, "diskmain.yaml"), "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*5, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerBaseConfigAndSet(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "diskmain.yaml"), []byte(replacePaths(tmpDir, ` -http: - enabled: false -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)), 0o644)) - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "-c", filepath.Join(tmpDir, "diskmain.yaml"), "--set", `input.generate.mapping=root.id = "second"`, "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -`)) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"second"}`) - }, time.Second*30, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerMetrics(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - tNow := time.Unix(1, 0) - var tNowMut sync.Mutex - - pr, waitFn := testServerForPullRunner(t, func() time.Time { - tNowMut.Lock() - defer tNowMut.Unlock() - return tNow - }, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 10, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 10 - interval: "" - mapping: 'root.id = "first"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - }) // No metrics yet - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - "metrics": obj{ - "input": obj{ - "": obj{"received": 10.0}, - }, - "output": obj{ - "aoutput": obj{ - "error": 0.0, - "sent": 10.0, - }, - }, - "processor": obj{}, - }, - }) // Metrics sent on flush - jsonResponse(t, w, obj{ - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 5 - interval: "" - mapping: 'root.id = "second"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outb.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - }) // No metrics again - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - "metrics": obj{ - "input": obj{ - "": obj{"received": 5.0}, - }, - "output": obj{ - "aoutput": obj{ - "error": 0.0, - "sent": 5.0, - }, - }, - "processor": obj{}, - }, - }) // Metrics sent on second flush - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - exp := strings.Repeat(`{"id":"first"}`+"\n", 10) - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return string(data) == exp - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) - - tNowMut.Lock() - tNow = time.Unix(12, 0) - tNowMut.Unlock() - pr.Sync(ctx) - - exp = strings.Repeat(`{"id":"second"}`+"\n", 5) - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outb.jsonl")) - return string(data) == exp - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) - - tNowMut.Lock() - tNow = time.Unix(23, 0) - tNowMut.Unlock() - pr.Sync(ctx) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerRateLimit(t *testing.T) { - t.Skip("This test blocks for 1s so dont run by default") - - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - tNow := time.Unix(1, 0) - var tNowMut sync.Mutex - - pr, waitFn := testServerForPullRunner(t, func() time.Time { - tNowMut.Lock() - defer tNowMut.Unlock() - return tNow - }, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 10, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - }) - w.Header().Add("Retry-After", "1") - w.WriteHeader(http.StatusTooManyRequests) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - }) - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - tStarted := time.Now() - pr.Sync(ctx) - tTaken := time.Since(tStarted) - - assert.Greater(t, tTaken, time.Second) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first"}`) - }, time.Second*30, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerTracesDisabled(t *testing.T) { - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - label: ainput - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - "metrics": obj{ - "input": obj{"ainput": obj{"received": 1.0}}, - "output": obj{"aoutput": obj{"error": 0.0, "sent": 1.0}}, - "processor": obj{}, - }, - }) // No traces yet - jsonResponse(t, w, obj{ - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - "requested_traces": 100, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - label: ainput - generate: - count: 10 - interval: "" - mapping: 'root.id = "second"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outb.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - "metrics": obj{ - "input": obj{"ainput": obj{"received": 10.0}}, - "output": obj{"aoutput": obj{"error": 0.0, "sent": 10.0}}, - "processor": obj{}, - }, - }) // Metrics sent on flush, still no traces as it's not enabled - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - exp := strings.Repeat(`{"id":"first"}`+"\n", 1) - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return string(data) == exp - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) // Includes new config and requests traces - - exp = strings.Repeat(`{"id":"second"}`+"\n", 10) - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outb.jsonl")) - return string(data) == exp - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) // Provides were requested above - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerTracesEnabled(t *testing.T) { - t.Skip("Traces are non-deterministic so we need to rework the sync response check") - - tmpDir := t.TempDir() - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession", "--send-traces"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - label: ainput - generate: - count: 1 - interval: "" - mapping: 'root.id = "first"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1001.0}, - "metrics": obj{ - "input": obj{"ainput": obj{"received": 1.0}}, - "output": obj{"aoutput": obj{"error": 0.0, "sent": 1.0}}, - "processor": obj{}, - }, - }) // No traces yet - jsonResponse(t, w, obj{ - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - "requested_traces": 100, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false -input: - label: ainput - generate: - count: 10 - interval: "" - mapping: 'root.id = "second"' -output: - label: aoutput - file: - codec: lines - path: $DIR/outb.jsonl -`)) - }), - expectedRequest("/api/v1/node/session/foosession/deployment/depaid/sync", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - "main_config": obj{"name": "maina.yaml", "modified": 1002.0}, - "metrics": obj{ - "input": obj{"ainput": obj{"received": 10.0}}, - "output": obj{"aoutput": obj{"error": 0.0, "sent": 10.0}}, - "processor": obj{}, - }, - "tracing": obj{ - "input_events": obj{ - "ainput": arr{ - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "PRODUCE"}, - }, - }, - "output_events": obj{ - "aoutput": arr{ - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - obj{"content": "{\"id\":\"second\"}", "metadata": obj{}, "type": "CONSUME"}, - }, - }, - }, - }) // Metrics sent on flush - jsonResponse(t, w, obj{}) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - exp := strings.Repeat(`{"id":"first"}`+"\n", 1) - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return string(data) == exp - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) // Includes new config and requests traces - - exp = strings.Repeat(`{"id":"second"}`+"\n", 10) - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outb.jsonl")) - return string(data) == exp - }, time.Second*30, time.Millisecond*10) - - pr.Sync(ctx) // Provides traces from above writes - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} - -func TestPullRunnerSharedMappings(t *testing.T) { - tmpDir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "a.blobl"), []byte(` -map a { - root.id = this.id + " and a" -} -`), 0o755)) - - ctx, done := context.WithTimeout(context.Background(), 30*time.Second) - defer done() - - pr, waitFn := testServerForPullRunner(t, nil, - []string{"benthos", "--log.level", "none", "studio", "pull", "--name", "foobarnode", "--session", "foosession"}, - expectedRequest("/api/v1/node/session/foosession/init", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - jsonRequestEqual(t, r, obj{ - "name": "foobarnode", - }) - jsonResponse(t, w, obj{ - "deployment_id": "depaid", - "deployment_name": "Deployment A", - "main_config": obj{"name": "maina.yaml", "modified": 1001}, - "metrics_guide_period_seconds": 300, - }) - }), - expectedRequest("/api/v1/node/session/foosession/download/maina.yaml", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, replacePaths(tmpDir, ` -http: - enabled: false - -input: - # Needs to keep generating across resource changes but not so much that we - # swamp the disk with data. - generate: - count: 300 - interval: 100ms - mapping: | - import "$DIR/a.blobl" - import "./b.blobl" - - root = {"id":"first"}.apply("a").apply("b") - -output: - file: - codec: lines - path: $DIR/outa.jsonl -`)) - }), - expectedRequest( - fmt.Sprintf("/api/v1/node/session/foosession/download/%v", url.PathEscape(filepath.Join(tmpDir, "a.blobl"))), - func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - http.Error(w, "Nah", http.StatusNotFound) - }), - expectedRequest("/api/v1/node/session/foosession/download/b.blobl", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, ` -map b { - root.id = this.id + " and b" -} -`) - }), - expectedRequest( - fmt.Sprintf("/api/v1/node/session/foosession/download/%v", url.PathEscape(filepath.Join(tmpDir, "a.blobl"))), - func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - http.Error(w, "Nah", http.StatusNotFound) - }), - expectedRequest("/api/v1/node/session/foosession/download/b.blobl", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "GET", r.Method) - stringResponse(t, w, ` -map b { - root.id = this.id + " and b" -} -`) - }), - expectedRequest("/api/v1/node/session/foosession/leave", func(t *testing.T, w http.ResponseWriter, r *http.Request) { - require.Equal(t, "POST", r.Method) - }), - ) - - assert.Eventually(t, func() bool { - data, _ := os.ReadFile(filepath.Join(tmpDir, "outa.jsonl")) - return strings.Contains(string(data), `{"id":"first and a and b"}`) - }, time.Second*10, time.Millisecond*10) - - require.NoError(t, pr.Stop(ctx)) - waitFn(ctx) -} diff --git a/internal/cli/studio/pull_session_fs.go b/internal/cli/studio/pull_session_fs.go deleted file mode 100644 index 62d5ae502a..0000000000 --- a/internal/cli/studio/pull_session_fs.go +++ /dev/null @@ -1,118 +0,0 @@ -package studio - -import ( - "context" - "errors" - "io/fs" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -// Implements ifs.FS around the Benthos Studio node APIs. -type sessionFS struct { - tracker *sessionTracker - backup ifs.FS -} - -func (s *sessionFS) Open(name string) (fs.File, error) { - f, err := s.tracker.ReadFile(context.Background(), name, false) - if err != nil && errors.Is(err, fs.ErrNotExist) && s.backup != nil { - f, err = s.backup.Open(name) - } - return f, err -} - -func (s *sessionFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - if flag == os.O_RDONLY { - return s.Open(name) - } - if s.backup == nil { - return nil, errors.New("not implemented") - } - return s.backup.OpenFile(name, flag, perm) -} - -func (s *sessionFS) Stat(name string) (fs.FileInfo, error) { - f, err := s.tracker.ReadFile(context.Background(), name, true) - if err != nil && errors.Is(err, fs.ErrNotExist) && s.backup != nil { - return s.backup.Stat(name) - } - if err != nil { - return nil, err - } - return f.Stat() -} - -func (s *sessionFS) Remove(name string) error { - if s.backup == nil { - return errors.New("not implemented") - } - return s.backup.Remove(name) -} - -func (s *sessionFS) MkdirAll(path string, perm fs.FileMode) error { - if s.backup == nil { - return errors.New("not implemented") - } - return s.backup.MkdirAll(path, perm) -} - -//------------------------------------------------------------------------------ - -type sessionFile struct { - res *http.Response - path string - modTime time.Time -} - -func (s *sessionFile) Stat() (fs.FileInfo, error) { - return &sessionFileInfo{ - path: s.path, - size: s.res.ContentLength, - modTime: s.modTime, - }, nil -} - -func (s *sessionFile) Read(b []byte) (int, error) { - return s.res.Body.Read(b) -} - -func (s *sessionFile) Close() error { - return s.res.Body.Close() -} - -//------------------------------------------------------------------------------ - -type sessionFileInfo struct { - path string - size int64 - modTime time.Time -} - -func (s *sessionFileInfo) Name() string { - return filepath.Base(s.path) -} - -func (s *sessionFileInfo) Size() int64 { - return s.size -} - -func (s *sessionFileInfo) Mode() fs.FileMode { - return fs.ModePerm -} - -func (s *sessionFileInfo) ModTime() time.Time { - return s.modTime -} - -func (s *sessionFileInfo) IsDir() bool { - return false -} - -func (s *sessionFileInfo) Sys() any { - return nil -} diff --git a/internal/cli/studio/pull_session_tracker.go b/internal/cli/studio/pull_session_tracker.go deleted file mode 100644 index c2fbf59d73..0000000000 --- a/internal/cli/studio/pull_session_tracker.go +++ /dev/null @@ -1,525 +0,0 @@ -package studio - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "net/http" - "net/url" - "path" - "strconv" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/cli/studio/metrics" - "github.com/benthosdev/benthos/v4/internal/cli/studio/tracing" -) - -// DeploymentConfigMeta describes a file that makes up part of a deployment. -type DeploymentConfigMeta struct { - Name string `json:"name"` - Modified int64 `json:"modified"` // Unix TS in milliseconds -} - -// DeploymentConfigDiff expresses config files that should be changed (removed, -// added, updated) in order to match the files of the deployment being synced -// to. -type DeploymentConfigDiff struct { - MainConfig *DeploymentConfigMeta `json:"main_config,omitempty"` - AddResources []DeploymentConfigMeta `json:"add_resources,omitempty"` - RemoveResources []string `json:"remove_resources,omitempty"` -} - -//------------------------------------------------------------------------------ - -func addAuthHeaders(token, secret string, req *http.Request) { - req.Header.Set("X-Bstdio-Node-Id", token) - req.Header.Set("Authorization", "Node "+secret) -} - -type sessionTracker struct { - logger *hotSwapLogger - baseURL string - nodeName string - token, secret string - - // Active state - mut sync.Mutex - rateLimitTo *time.Time - guideMetricsFlushPeriod time.Duration - deploymentID string - currentFiles pullConfigSummary - runErr error - - nowFn func() time.Time -} - -func initSessionTracker( - ctx context.Context, - nowFn func() time.Time, - logger *hotSwapLogger, - nodeName, baseURLStr, token, secret string, -) (*sessionTracker, error) { - tracker := &sessionTracker{ - logger: logger, - baseURL: baseURLStr, - nodeName: nodeName, - token: token, - secret: secret, - guideMetricsFlushPeriod: time.Second * 30, - nowFn: nowFn, - } - if err := tracker.init(ctx); err != nil { - return nil, err - } - return tracker, nil -} - -type studioAPIErr struct { - StatusCode int - BodyBytes []byte -} - -func (s *studioAPIErr) Error() string { - return fmt.Sprintf("request failed with status %v: %s", s.StatusCode, s.BodyBytes) -} - -func (s *sessionTracker) checkResponse(res *http.Response) (waitUntil *time.Time, abortReq bool, err error) { - if res.StatusCode == http.StatusOK { - return nil, false, nil - } - if res.StatusCode == http.StatusTooManyRequests { - if retAfterInt, err := strconv.ParseInt(res.Header.Get("Retry-After"), 10, 64); err == nil { - retUntil := s.nowFn().Add(time.Second * time.Duration(retAfterInt)) - waitUntil = &retUntil - } - } - _, abortReq = map[int]struct{}{ - http.StatusNotFound: {}, - }[res.StatusCode] - bodyBytes, _ := io.ReadAll(res.Body) - err = &studioAPIErr{StatusCode: res.StatusCode, BodyBytes: bodyBytes} - return -} - -func (s *sessionTracker) waitForRateLimit(ctx context.Context) error { - s.mut.Lock() - defer s.mut.Unlock() - if s.rateLimitTo == nil { - return nil - } - - waitFor := (*s.rateLimitTo).Sub(s.nowFn()) - if waitFor <= 0 { - s.rateLimitTo = nil - return nil - } - - select { - case <-time.After(waitFor): - s.rateLimitTo = nil - return nil - case <-ctx.Done(): - } - return nil -} - -type pullConfigSummary struct { - MainConfig *DeploymentConfigMeta `json:"main_config,omitempty"` - ResourceConfigs []DeploymentConfigMeta `json:"resource_configs,omitempty"` -} - -// Files returns the full summary of files belonging to the deployment currently -// syncing with. -func (s *sessionTracker) Files() pullConfigSummary { - s.mut.Lock() - defer s.mut.Unlock() - return s.currentFiles -} - -// SetRunError sets an error to display on the next sync that indicates a -// problem with running the latest received config. -func (s *sessionTracker) SetRunError(err error) { - s.mut.Lock() - defer s.mut.Unlock() - s.runErr = err -} - -//------------------------------------------------------------------------------ - -func (s *sessionTracker) doRateLimitedReq(ctx context.Context, reqFn func() (*http.Request, error)) (res *http.Response, err error) { - for { - if err = s.waitForRateLimit(ctx); err != nil { - return - } - - var req *http.Request - if req, err = reqFn(); err != nil { - return - } - - // Wait for one second after an error by default - nextWait := s.nowFn().Add(time.Second) - var abortReq bool - if res, err = http.DefaultClient.Do(req.WithContext(ctx)); err == nil { - // No request error, but also check the response status and rate - // limit suggestions. - var nextWaitTmp *time.Time - if nextWaitTmp, abortReq, err = s.checkResponse(res); nextWaitTmp != nil { - // The response has suggested a time to wait for, so we use - // that instead of our default. - nextWait = *nextWaitTmp - } - } - if err == nil || abortReq { - return - } - if ctxErr := ctx.Err(); ctxErr != nil { - err = ctxErr - return - } - - errFields := map[string]string{ - "host": req.URL.Host, - "path": req.URL.Path, - "error": err.Error(), - } - if res != nil { - errFields["status"] = res.Status - } - s.logger.WithFields(errFields).Error("Studio request failed") - - s.mut.Lock() - s.rateLimitTo = &nextWait - s.mut.Unlock() - } -} - -func (s *sessionTracker) init(ctx context.Context) error { - // NOTE: No locking actually needed here as this is exclusively called - // during construction and nowhere else. - - initURL, err := url.Parse(s.baseURL) - if err != nil { - return err - } - initURL.Path = path.Join(initURL.Path, "/init") - - requestBytes, err := json.Marshal(struct { - Name string `json:"name"` - }{ - Name: s.nodeName, - }) - if err != nil { - return err - } - - res, err := s.doRateLimitedReq(ctx, func() (*http.Request, error) { - req, err := http.NewRequest("POST", initURL.String(), bytes.NewReader(requestBytes)) - if err != nil { - return nil, err - } - addAuthHeaders(s.token, s.secret, req) - req.Header.Set("Content-Type", "application/json") - return req, err - }) - if err != nil { - return err - } - - defer res.Body.Close() - - response := struct { - DeploymentID string `json:"deployment_id"` - DeploymentName string `json:"deployment_name"` - MetricsGuidePeriodSeconds int64 `json:"metrics_guide_period_seconds"` - pullConfigSummary - }{} - responseDec := json.NewDecoder(res.Body) - if err := responseDec.Decode(&response); err != nil { - return err - } - - s.logger.WithFields(map[string]string{ - "deployment_id": response.DeploymentID, - "deployment_name": response.DeploymentName, - }).Info("Synced with session and preparing to load files from deployment") - - s.guideMetricsFlushPeriod = time.Second * time.Duration(response.MetricsGuidePeriodSeconds) - s.deploymentID = response.DeploymentID - s.currentFiles = response.pullConfigSummary - return nil -} - -// MetricsGuideFlushPeriod returns the period of time recommended by studio in -// which to wait between sending metrics data, this is obtained during the -// initialisation between this tracker and the server. -func (s *sessionTracker) MetricsGuideFlushPeriod() time.Duration { - return s.guideMetricsFlushPeriod -} - -// Leave marks this node session as being shut down. After a certain period of -// inactivity this will happen automatically on the server side but it's good -// practice to signal this as soon as possible. -func (s *sessionTracker) Leave(ctx context.Context) error { - if err := s.waitForRateLimit(ctx); err != nil { - return err - } - - leaveURL, err := url.Parse(s.baseURL) - if err != nil { - return err - } - leaveURL.Path = path.Join(leaveURL.Path, "/leave") - - requestBytes, err := json.Marshal(struct { - Name string `json:"name"` - }{ - Name: s.nodeName, - }) - if err != nil { - return err - } - - res, err := s.doRateLimitedReq(ctx, func() (*http.Request, error) { - req, err := http.NewRequest("POST", leaveURL.String(), bytes.NewReader(requestBytes)) - if err != nil { - return nil, err - } - addAuthHeaders(s.token, s.secret, req) - return req, err - }) - if err != nil { - return err - } - - defer res.Body.Close() - - return err -} - -// ReadFile attempts to read a given file from the session we're synced with. -func (s *sessionTracker) ReadFile(ctx context.Context, name string, headOnly bool) (fs.File, error) { - if err := s.waitForRateLimit(ctx); err != nil { - return nil, err - } - - var fileURLStr string - { - fileURL, err := url.Parse(s.baseURL) - if err != nil { - return nil, err - } - fileURL.Path = path.Join(fileURL.Path, "/download") - fileURLStr = fileURL.String() + "/" + url.PathEscape(path.Clean(name)) - } - - method := "GET" - if headOnly { - method = "HEAD" - } - - res, err := s.doRateLimitedReq(ctx, func() (*http.Request, error) { //nolint: bodyclose - req, err := http.NewRequest(method, fileURLStr, http.NoBody) - if err != nil { - return nil, err - } - addAuthHeaders(s.token, s.secret, req) - return req, err - }) - if err != nil { - var sErr *studioAPIErr - if errors.As(err, &sErr) && sErr.StatusCode == 404 { - return nil, fs.ErrNotExist - } - return nil, err - } - - modTimeMillis := 0 - if modifiedStr := res.Header.Get("X-Bstdio-Updated-Millis"); modifiedStr != "" { - modTimeMillis, _ = strconv.Atoi(modifiedStr) - } - if modTimeMillis > 0 { - modTimeI64 := int64(modTimeMillis) - - // Update our local modified note to avoid duplicate reads when the - // server file is updated after the sync. - s.mut.Lock() - if s.currentFiles.MainConfig != nil && s.currentFiles.MainConfig.Name == name && modTimeI64 > s.currentFiles.MainConfig.Modified { - s.currentFiles.MainConfig.Modified = modTimeI64 - } else { - for _, c := range s.currentFiles.ResourceConfigs { - if c.Name != name { - continue - } - if modTimeI64 > c.Modified { - c.Modified = modTimeI64 - } - break - } - } - s.mut.Unlock() - } - - return &sessionFile{ - res: res, - path: name, - modTime: time.UnixMilli(int64(modTimeMillis)), - }, nil -} - -//------------------------------------------------------------------------------ - -type pullSessionSyncRequest struct { - Name string `json:"name"` - pullConfigSummary - Metrics *metrics.Observed `json:"metrics,omitempty"` - Tracing *tracing.Observed `json:"tracing,omitempty"` - RunError string `json:"run_error,omitempty"` -} - -type reassignedDeploymentSummary struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type pullSessionSyncResponse struct { - // Reassignment information - Reassignment *reassignedDeploymentSummary `json:"reassignment,omitempty"` - - // True if your deployment is disabled/deleted but there's nothing to - // reassign to. - IsDisabled bool `json:"is_disabled"` - - // Diff information - DeploymentConfigDiff - - RequestedTraces int64 `json:"requested_traces"` -} - -// Sync sends a summary of this nodes execution up to this point along with the -// files (and updated timestamps) of the files we're running as part of our -// assigned deployment. The returned data contains the potential for deployment -// reassignment and/or a summary of files that are different and should be -// dropped, added or updated in our config reader. -func (s *sessionTracker) Sync( - ctx context.Context, - metrics *metrics.Observed, - tracing *tracing.Observed, -) (disabled bool, diff *DeploymentConfigDiff, requestsTraces int64, err error) { - if err = s.waitForRateLimit(ctx); err != nil { - return - } - - s.mut.Lock() - depID := s.deploymentID - syncReq := pullSessionSyncRequest{ - Name: s.nodeName, - pullConfigSummary: s.currentFiles, - Metrics: metrics, - Tracing: tracing, - } - if s.runErr != nil { - syncReq.RunError = s.runErr.Error() - } - s.mut.Unlock() - - var syncURL *url.URL - if syncURL, err = url.Parse(s.baseURL); err != nil { - return - } - syncURL.Path = path.Join(syncURL.Path, fmt.Sprintf("/deployment/%v/sync", depID)) - - var requestBytes []byte - if requestBytes, err = json.Marshal(syncReq); err != nil { - return - } - - var res *http.Response - if res, err = s.doRateLimitedReq(ctx, func() (*http.Request, error) { - req, err := http.NewRequest("POST", syncURL.String(), bytes.NewReader(requestBytes)) - if err != nil { - return nil, err - } - addAuthHeaders(s.token, s.secret, req) - return req, err - }); err != nil { - return - } - - defer res.Body.Close() - - var response pullSessionSyncResponse - responseDec := json.NewDecoder(res.Body) - if err = responseDec.Decode(&response); err != nil { - return - } - - if response.Reassignment != nil { - s.mut.Lock() - s.deploymentID = response.Reassignment.ID - s.mut.Unlock() - - s.logger.WithFields(map[string]string{ - "deployment_id": response.Reassignment.ID, - "deployment_name": response.Reassignment.Name, - }).Info("Synced with session and reassigned to a different deployment") - - // We will need to sync again in order to obtain the new deployment - // configs. We do this straight away as there's no sense in delaying, - // recursing is a bit of a risk but given we're protected by rate limits - // and the context we should be fine. - // - // Note: We also don't bother flushing the metrics again. - return s.Sync(ctx, nil, nil) - } - - disabled = response.IsDisabled - - s.logger.WithFields(map[string]string{ - "deployment_id": depID, - }).Info("Synced with session") - requestsTraces = response.RequestedTraces - - // Reflect the diff returned in our new summary of files. - summaryChanged := false - newFilesSummary := s.Files() - if response.MainConfig != nil { - summaryChanged = true - newFilesSummary.MainConfig = response.MainConfig - } - if len(response.RemoveResources) > 0 || len(response.AddResources) > 0 { - summaryChanged = true - removeNames := map[string]struct{}{} - for _, name := range response.RemoveResources { - removeNames[name] = struct{}{} - } - for _, res := range response.AddResources { - removeNames[res.Name] = struct{}{} - } - - var newResources []DeploymentConfigMeta - for _, res := range newFilesSummary.ResourceConfigs { - if _, skip := removeNames[res.Name]; skip { - continue - } - newResources = append(newResources, res) - } - newFilesSummary.ResourceConfigs = newResources - newFilesSummary.ResourceConfigs = append(newFilesSummary.ResourceConfigs, response.AddResources...) - } - if !summaryChanged { - return - } - - s.mut.Lock() - s.currentFiles = newFilesSummary - s.mut.Unlock() - - diff = &response.DeploymentConfigDiff - return -} diff --git a/internal/cli/studio/sync_schema.go b/internal/cli/studio/sync_schema.go deleted file mode 100644 index 69096d456b..0000000000 --- a/internal/cli/studio/sync_schema.go +++ /dev/null @@ -1,82 +0,0 @@ -package studio - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path" - - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/config/schema" -) - -func syncSchemaCommand(cliOpts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "sync-schema", - Usage: "Synchronizes the schema of this Benthos instance with a studio session", - Description: ` -This sync allows custom plugins and templates to be configured and linted -correctly within Benthos studio. - -In order to synchronize a single use token must be generated from the session -page within the studio application.`[1:], - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "session", - Aliases: []string{"s"}, - Required: true, - Value: "", - Usage: "The session ID to synchronize with.", - }, - &cli.StringFlag{ - Name: "token", - Aliases: []string{"t"}, - Required: true, - Value: "", - Usage: "The single use token used to authenticate the request.", - }, - }, - Action: func(c *cli.Context) error { - endpoint := c.String("endpoint") - sessionID := c.String("session") - tokenID := c.String("token") - - u, err := url.Parse(endpoint) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to parse endpoint: %v\n", err) - os.Exit(1) - } - u.Path = path.Join(u.Path, fmt.Sprintf("/api/v1/token/%v/session/%v/schema", tokenID, sessionID)) - - schema := schema.New(cliOpts.Version, cliOpts.DateBuilt) - schema.Config = cliOpts.MainConfigSpecCtor() - schema.Scrub() - schemaBytes, err := json.Marshal(schema) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to encode schema: %v\n", err) - os.Exit(1) - } - - res, err := http.Post(u.String(), "application/json", bytes.NewReader(schemaBytes)) - if err != nil { - fmt.Fprintf(os.Stderr, "Sync request failed: %v\n", err) - os.Exit(1) - } - - defer res.Body.Close() - - if res.StatusCode < 200 || res.StatusCode > 299 { - resBytes, _ := io.ReadAll(res.Body) - fmt.Fprintf(os.Stderr, "Sync request failed (%v): %v\n", res.StatusCode, string(resBytes)) - os.Exit(1) - } - return nil - }, - } -} diff --git a/internal/cli/studio/tracing/observed.go b/internal/cli/studio/tracing/observed.go deleted file mode 100644 index 8c1430cdb5..0000000000 --- a/internal/cli/studio/tracing/observed.go +++ /dev/null @@ -1,86 +0,0 @@ -package tracing - -import ( - "github.com/benthosdev/benthos/v4/internal/bundle/tracing" -) - -type ObservedSummary struct { - Input int `json:"input"` - Output int `json:"output"` - ProcessorErrors int `json:"processor_errors"` -} - -type ObservedEvent struct { - Type string `json:"type"` - Content string `json:"content"` - Metadata map[string]any `json:"metadata"` -} - -// Observed is a structured form of tracing events extracted from Benthos -// components during execution. This is entirely unrelated to Open Telemetry -// tracing concepts and is Benthos specific. -type Observed struct { - InputEvents map[string][]ObservedEvent `json:"input_events,omitempty"` - ProcessorEvents map[string][]ObservedEvent `json:"processor_events,omitempty"` - OutputEvents map[string][]ObservedEvent `json:"output_events,omitempty"` -} - -// FromInternal converts internal tracing events into a format we can serialise -// as JSON for Studio sync requests. A nil might be returned if no events were -// extracted. -func FromInternal(summary *tracing.Summary) *Observed { - inputEvents := map[string][]ObservedEvent{} - for k, v := range summary.InputEvents(true) { - var tEvents []ObservedEvent - for _, e := range v { - tEvents = append(tEvents, ObservedEvent{ - Type: string(e.Type), - Content: e.Content, - Metadata: e.Meta, - }) - } - if len(tEvents) > 0 { - inputEvents[k] = tEvents - } - } - - processorEvents := map[string][]ObservedEvent{} - for k, v := range summary.ProcessorEvents(true) { - var tEvents []ObservedEvent - for _, e := range v { - tEvents = append(tEvents, ObservedEvent{ - Type: string(e.Type), - Content: e.Content, - Metadata: e.Meta, - }) - } - if len(tEvents) > 0 { - processorEvents[k] = tEvents - } - } - - outputEvents := map[string][]ObservedEvent{} - for k, v := range summary.OutputEvents(true) { - var tEvents []ObservedEvent - for _, e := range v { - tEvents = append(tEvents, ObservedEvent{ - Type: string(e.Type), - Content: e.Content, - Metadata: e.Meta, - }) - } - if len(tEvents) > 0 { - outputEvents[k] = tEvents - } - } - - if len(inputEvents)+len(outputEvents)+len(processorEvents) == 0 { - return nil - } - - return &Observed{ - InputEvents: inputEvents, - ProcessorEvents: processorEvents, - OutputEvents: outputEvents, - } -} diff --git a/internal/cli/template/cli.go b/internal/cli/template/cli.go deleted file mode 100644 index e22f891cb6..0000000000 --- a/internal/cli/template/cli.go +++ /dev/null @@ -1,28 +0,0 @@ -package template - -import ( - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/cli/common" -) - -// CliCommand is a cli.Command definition for interacting with templates. -func CliCommand(opts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "template", - Usage: opts.ExecTemplate("Interact and generate {{.ProductName}} templates"), - Description: opts.ExecTemplate(` -EXPERIMENTAL: This subcommand, and templates in general, are experimental and -therefore are subject to change outside of major version releases. - -Allows linting and generating {{.ProductName}} templates. - - {{.BinaryName}} template lint ./path/to/templates/... - -For more information check out the docs at: -{{.DocumentationURL}}/configuration/templating`)[1:], - Subcommands: []*cli.Command{ - lintCliCommand(opts), - }, - } -} diff --git a/internal/cli/template/lint.go b/internal/cli/template/lint.go deleted file mode 100644 index ae52db54f3..0000000000 --- a/internal/cli/template/lint.go +++ /dev/null @@ -1,108 +0,0 @@ -package template - -import ( - "errors" - "fmt" - "os" - - "github.com/fatih/color" - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/docs" - ifilepath "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/template" -) - -var ( - red = color.New(color.FgRed).SprintFunc() - yellow = color.New(color.FgYellow).SprintFunc() -) - -type pathLint struct { - source string - lint docs.Lint -} - -func lintFile(path string) (pathLints []pathLint) { - conf, lints, err := template.ReadConfigFile(path) - if err != nil { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(1, docs.LintFailedRead, err), - }) - return - } - - for _, l := range lints { - pathLints = append(pathLints, pathLint{ - source: path, - lint: l, - }) - } - - testErrors, err := conf.Test() - if err != nil { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(1, docs.LintFailedRead, err), - }) - return - } - - for _, tErr := range testErrors { - pathLints = append(pathLints, pathLint{ - source: path, - lint: docs.NewLintError(1, docs.LintFailedRead, errors.New(tErr)), - }) - } - return -} - -func lintCliCommand(opts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "lint", - Usage: opts.ExecTemplate("Parse {{.ProductName}} templates and report any linting errors"), - Description: opts.ExecTemplate(` -Exits with a status code 1 if any linting errors are detected: - - {{.BinaryName}} template lint - {{.BinaryName}} template lint ./templates/*.yaml - {{.BinaryName}} template lint ./foo.yaml ./bar.yaml - {{.BinaryName}} template lint ./templates/... - -If a path ends with '...' then {{.ProductName}} will walk the target and lint any -files with the .yaml or .yml extension.`)[1:], - Action: func(c *cli.Context) error { - targets, err := ifilepath.GlobsAndSuperPaths(ifs.OS(), c.Args().Slice(), "yaml", "yml") - if err != nil { - fmt.Fprintf(os.Stderr, "Lint paths error: %v\n", err) - os.Exit(1) - } - var pathLints []pathLint - for _, target := range targets { - if target == "" { - continue - } - lints := lintFile(target) - if len(lints) > 0 { - pathLints = append(pathLints, lints...) - } - } - if len(pathLints) == 0 { - os.Exit(0) - } - for _, lint := range pathLints { - lintText := fmt.Sprintf("%v%v\n", lint.source, lint.lint.Error()) - if lint.lint.Type == docs.LintFailedRead { - fmt.Fprint(os.Stderr, red(lintText)) - } else { - fmt.Fprint(os.Stderr, yellow(lintText)) - } - } - os.Exit(1) - return nil - }, - } -} diff --git a/internal/cli/test/case.go b/internal/cli/test/case.go deleted file mode 100644 index bf4c9d2459..0000000000 --- a/internal/cli/test/case.go +++ /dev/null @@ -1,107 +0,0 @@ -package test - -import ( - "context" - "fmt" - "io/fs" - - iprocessor "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// CaseFailure encapsulates information about a failed test case. -type CaseFailure struct { - Name string - TestLine int - Reason string -} - -// String returns a string representation of the case failure. -func (c CaseFailure) String() string { - if c.TestLine == 0 { - return fmt.Sprintf("%v: %v", c.Name, c.Reason) - } - return fmt.Sprintf("%v [line %v]: %v", c.Name, c.TestLine, c.Reason) -} - -// ProcProvider returns compiled processors extracted from a Benthos config -// using a JSON Pointer. -type ProcProvider interface { - Provide(jsonPtr string, environment map[string]string, mocks map[string]any) ([]iprocessor.V1, error) - ProvideBloblang(path string) ([]iprocessor.V1, error) -} - -// ExecuteFrom executes a test case from the perspective of a given directory, -// which is used for obtaining relative condition file imports. -func ExecuteFrom(fs fs.FS, dir string, c test.Case, provider ProcProvider) (failures []CaseFailure, err error) { - var procSet []iprocessor.V1 - if c.TargetMapping != "" { - if procSet, err = provider.ProvideBloblang(c.TargetMapping); err != nil { - return nil, fmt.Errorf("failed to initialise Bloblang mapping '%v': %v", c.TargetMapping, err) - } - } else { - if procSet, err = provider.Provide(c.TargetProcessors, c.Environment, c.Mocks); err != nil { - return nil, fmt.Errorf("failed to initialise processors '%v': %v", c.TargetProcessors, err) - } - } - - reportFailure := func(reason string) { - failures = append(failures, CaseFailure{ - Name: c.Name, - TestLine: c.Line(), - Reason: reason, - }) - } - - var inputMsg []message.Batch - - for _, inputBatch := range c.InputBatches { - parts := make([]*message.Part, len(inputBatch)) - for i, v := range inputBatch { - if parts[i], err = v.ToMessage(fs, dir); err != nil { - err = fmt.Errorf("failed to create test input %v: %w", i, err) - return - } - } - - currentBatch := message.Batch(parts) - inputMsg = append(inputMsg, currentBatch) - - } - - outputBatches, result := iprocessor.ExecuteAll(context.Background(), procSet, inputMsg...) - if result != nil { - reportFailure(fmt.Sprintf("processors resulted in error: %v", result)) - } - - if lExp, lAct := len(c.OutputBatches), len(outputBatches); lAct < lExp { - reportFailure(fmt.Sprintf("wrong batch count, expected %v, got %v", lExp, lAct)) - } - - for i, v := range outputBatches { - if len(c.OutputBatches) <= i { - reportFailure(fmt.Sprintf("unexpected batch: %s", message.GetAllBytes(v))) - continue - } - expectedBatch := c.OutputBatches[i] - if lExp, lAct := len(expectedBatch), v.Len(); lExp != lAct { - reportFailure(fmt.Sprintf("mismatch of output batch %v message counts, expected %v, got %v", i, lExp, lAct)) - } - _ = v.Iter(func(i2 int, part *message.Part) error { - if len(expectedBatch) <= i2 { - reportFailure(fmt.Sprintf("unexpected message from batch %v: %s", i, part.AsBytes())) - return nil - } - condErrs := expectedBatch[i2].CheckAll(fs, dir, part) - for _, condErr := range condErrs { - reportFailure(fmt.Sprintf("batch %v message %v: %v", i, i2, condErr)) - } - if procErr := part.ErrorGet(); procErr != nil && len(condErrs) > 0 { - reportFailure(fmt.Sprintf("batch %v message %v: %v", i, i2, red(procErr))) - } - return nil - }) - } - return -} diff --git a/internal/cli/test/case_test.go b/internal/cli/test/case_test.go deleted file mode 100644 index 481e8e5165..0000000000 --- a/internal/cli/test/case_test.go +++ /dev/null @@ -1,405 +0,0 @@ -package test_test - -import ( - "errors" - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/fatih/color" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/cli/test" - "github.com/benthosdev/benthos/v4/internal/component/processor" - dtest "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -type mockProvider map[string][]processor.V1 - -func (m mockProvider) Provide(ptr string, env map[string]string, mocks map[string]any) ([]processor.V1, error) { - if procs, ok := m[ptr]; ok { - return procs, nil - } - return nil, errors.New("processors not found") -} - -func (m mockProvider) ProvideBloblang(name string) ([]processor.V1, error) { - if procs, ok := m[name]; ok { - return procs, nil - } - return nil, errors.New("mapping not found") -} - -func TestCase(t *testing.T) { - color.NoColor = true - - provider := mockProvider{} - - procConf := processor.NewConfig() - procConf.Type = "noop" - proc, err := mock.NewManager().NewProcessor(procConf) - if err != nil { - t.Fatal(err) - } - provider["/pipeline/processors"] = []processor.V1{proc} - - procConf = processor.NewConfig() - procConf.Type = "bloblang" - procConf.Plugin = `root = content().uppercase()` - if proc, err = mock.NewManager().NewProcessor(procConf); err != nil { - t.Fatal(err) - } - provider["/input/broker/inputs/0/processors"] = []processor.V1{proc} - - procConf = processor.NewConfig() - procConf.Type = "bloblang" - procConf.Plugin = `root = deleted()` - if proc, err = mock.NewManager().NewProcessor(procConf); err != nil { - t.Fatal(err) - } - provider["/input/broker/inputs/1/processors"] = []processor.V1{proc} - - procConf = processor.NewConfig() - procConf.Type = "bloblang" - procConf.Plugin = `root = if batch_index() == 0 { count("batch_id") }` - if proc, err = mock.NewManager().NewProcessor(procConf); err != nil { - t.Fatal(err) - } - provider["/input/broker/inputs/2/processors"] = []processor.V1{proc} - - type testCase struct { - name string - conf string - expected []test.CaseFailure - } - - tests := []testCase{ - { - name: "positive 1", - conf: ` -name: positive 1 -input_batch: -- content: foo bar -output_batches: -- - - content_equals: "foo bar" -`, - }, - { - name: "positive 2", - conf: ` -name: positive 2 -target_processors: /input/broker/inputs/0/processors -input_batch: -- content: foo bar -output_batches: -- - - content_equals: "FOO BAR" -`, - }, - { - name: "positive 3", - conf: ` -name: positive 3 -target_processors: /input/broker/inputs/1/processors -input_batch: -- content: foo bar -output_batches: []`, - }, - { - name: "json positive 4", - conf: ` -name: json positive 4 -input_batch: -- json_content: - foo: bar -output_batches: -- - - json_equals: - foo: bar -`, - }, - { - name: "positive 5", - conf: ` -name: positive 5 -input_batches: -- - - content: foo -- - - content: bar -output_batches: -- - - content_equals: "foo" -- - - content_equals: "bar" -`, - }, - { - name: "batch id 1", - conf: ` -name: batch id 1 -target_processors: /input/broker/inputs/2/processors -input_batches: -- - - content: foo -- - - content: foo - - content: bar -output_batches: -- - - content_equals: "1" -- - - content_equals: "2" - - content_equals: "bar" -`, - }, - { - name: "negative 1", - conf: ` -name: negative 1 -input_batch: -- content: foo bar -output_batches: -- - - content_equals: "foo baz" -`, - expected: []test.CaseFailure{ - { - Name: "negative 1", - TestLine: 2, - Reason: "batch 0 message 0: content_equals: content mismatch\n expected: foo baz\n received: foo bar", - }, - }, - }, - { - name: "negative 2", - conf: ` -name: negative 2 -input_batch: -- content: foo bar -- content: foo baz - metadata: - foo: baz -output_batches: -- - - content_equals: "foo bar" - - content_equals: "bar baz" - metadata_equals: - foo: bar -`, - expected: []test.CaseFailure{ - { - Name: "negative 2", - TestLine: 2, - Reason: "batch 0 message 1: content_equals: content mismatch\n expected: bar baz\n received: foo baz", - }, - { - Name: "negative 2", - TestLine: 2, - Reason: "batch 0 message 1: metadata_equals: metadata key 'foo' mismatch\n expected: bar\n received: baz", - }, - }, - }, - { - name: "negative batches count 1", - conf: ` -name: negative batches count 1 -input_batch: -- content: foo bar -output_batches: -- - - content_equals: "foo bar" -- - - content_equals: "foo bar" -`, - expected: []test.CaseFailure{ - { - Name: "negative batches count 1", - TestLine: 2, - Reason: "wrong batch count, expected 2, got 1", - }, - }, - }, - { - name: "unexpected batch 1", - conf: ` -name: unexpected batch 1 -input_batches: -- - - content: foo bar -- - - content: foo bar -- - - content: foo bar -output_batches: -- - - content_equals: "foo bar" -- - - content_equals: "foo bar" -`, - expected: []test.CaseFailure{ - { - Name: "unexpected batch 1", - TestLine: 2, - Reason: "unexpected batch: [foo bar]", - }, - }, - }, - } - - for _, testCase := range tests { - t.Run(testCase.name, func(tt *testing.T) { - node, err := docs.UnmarshalYAML([]byte(testCase.conf)) - require.NoError(t, err) - - c, err := dtest.CaseFromAny(node) - require.NoError(t, err) - - fails, err := test.ExecuteFrom(ifs.OS(), "", c, provider) - if err != nil { - tt.Fatal(err) - } - if exp, act := testCase.expected, fails; !reflect.DeepEqual(exp, act) { - tt.Errorf("Wrong results: %v != %v", act, exp) - } - }) - } -} - -func TestFileCaseInputs(t *testing.T) { - color.NoColor = true - - provider := mockProvider{} - procConf := processor.NewConfig() - - procConf.Type = "bloblang" - procConf.Plugin = `root = "hello world " + content().string()` - proc, err := mock.NewManager().NewProcessor(procConf) - require.NoError(t, err) - - provider["/pipeline/processors"] = []processor.V1{proc} - - tmpDir := t.TempDir() - - uppercasedPath := filepath.Join(tmpDir, "inner", "uppercased.txt") - notUppercasedPath := filepath.Join(tmpDir, "not_uppercased.txt") - - require.NoError(t, os.MkdirAll(filepath.Dir(uppercasedPath), 0o755)) - require.NoError(t, os.WriteFile(uppercasedPath, []byte(`FOO BAR BAZ`), 0o644)) - require.NoError(t, os.WriteFile(notUppercasedPath, []byte(`foo bar baz`), 0o644)) - - node, err := docs.UnmarshalYAML([]byte(` -name: uppercased -input_batch: - - file_content: ./inner/uppercased.txt -output_batches: -- - - content_equals: hello world FOO BAR BAZ -`)) - require.NoError(t, err) - - c, err := dtest.CaseFromAny(node) - require.NoError(t, err) - - fails, err := test.ExecuteFrom(ifs.OS(), tmpDir, c, provider) - require.NoError(t, err) - - assert.Equal(t, []test.CaseFailure(nil), fails) - - node, err = docs.UnmarshalYAML([]byte(` -name: not uppercased -input_batch: - - file_content: ./not_uppercased.txt -output_batches: -- - - content_equals: hello world FOO BAR BAZ -`)) - require.NoError(t, err) - - c, err = dtest.CaseFromAny(node) - require.NoError(t, err) - - fails, err = test.ExecuteFrom(ifs.OS(), tmpDir, c, provider) - require.NoError(t, err) - - assert.Equal(t, []test.CaseFailure{ - { - Name: "not uppercased", - TestLine: 2, - Reason: "batch 0 message 0: content_equals: content mismatch\n expected: hello world FOO BAR BAZ\n received: hello world foo bar baz", - }, - }, fails) -} - -func TestFileCaseConditions(t *testing.T) { - color.NoColor = true - - provider := mockProvider{} - procConf := processor.NewConfig() - - procConf.Type = "bloblang" - procConf.Plugin = `root = content().uppercase()` - proc, err := mock.NewManager().NewProcessor(procConf) - require.NoError(t, err) - - provider["/pipeline/processors"] = []processor.V1{proc} - - tmpDir := t.TempDir() - - uppercasedPath := filepath.Join(tmpDir, "inner", "uppercased.txt") - notUppercasedPath := filepath.Join(tmpDir, "not_uppercased.txt") - - require.NoError(t, os.MkdirAll(filepath.Dir(uppercasedPath), 0o755)) - require.NoError(t, os.WriteFile(uppercasedPath, []byte(`FOO BAR BAZ`), 0o644)) - require.NoError(t, os.WriteFile(notUppercasedPath, []byte(`foo bar baz`), 0o644)) - - node, err := docs.UnmarshalYAML([]byte(` -name: uppercased -input_batch: - - content: foo bar baz -output_batches: -- - - file_equals: "./inner/uppercased.txt" -`)) - require.NoError(t, err) - - c, err := dtest.CaseFromAny(node) - require.NoError(t, err) - - fails, err := test.ExecuteFrom(ifs.OS(), tmpDir, c, provider) - require.NoError(t, err) - - assert.Equal(t, []test.CaseFailure(nil), fails) - - node, err = docs.UnmarshalYAML([]byte(` -name: not uppercased -input_batch: - - content: foo bar baz -output_batches: -- - - file_equals: "./not_uppercased.txt" -`)) - require.NoError(t, err) - - c, err = dtest.CaseFromAny(node) - require.NoError(t, err) - - fails, err = test.ExecuteFrom(ifs.OS(), tmpDir, c, provider) - require.NoError(t, err) - - assert.Equal(t, []test.CaseFailure{ - { - Name: "not uppercased", - TestLine: 2, - Reason: "batch 0 message 0: file_equals: content mismatch\n expected: foo bar baz\n received: FOO BAR BAZ", - }, - }, fails) -} diff --git a/internal/cli/test/cli.go b/internal/cli/test/cli.go deleted file mode 100644 index 18c6487f28..0000000000 --- a/internal/cli/test/cli.go +++ /dev/null @@ -1,66 +0,0 @@ -package test - -import ( - "fmt" - "os" - - "github.com/urfave/cli/v2" - - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// CliCommand is a cli.Command definition for unit testing. -func CliCommand(cliOpts *common.CLIOpts) *cli.Command { - return &cli.Command{ - Name: "test", - Usage: cliOpts.ExecTemplate("Execute {{.ProductName}} unit tests"), - Description: cliOpts.ExecTemplate(` -Execute any number of {{.ProductName}} unit test definitions. If one or more tests -fail the process will report the errors and exit with a status code 1. - - {{.BinaryName}} test ./path/to/configs/... - {{.BinaryName}} test ./foo_configs/*.yaml ./bar_configs/*.yaml - {{.BinaryName}} test ./foo.yaml - -For more information check out the docs at: -{{.DocumentationURL}}/configuration/unit_testing`)[1:], - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "log", - Value: "", - Usage: "allow components to write logs at a provided level to stdout.", - }, - }, - Action: func(c *cli.Context) error { - if len(c.StringSlice("set")) > 0 { - fmt.Fprintln(os.Stderr, "Cannot override fields with --set (-s) during unit tests") - os.Exit(1) - } - resourcesPaths := c.StringSlice("resources") - var err error - if resourcesPaths, err = filepath.Globs(ifs.OS(), resourcesPaths); err != nil { - fmt.Printf("Failed to resolve resource glob pattern: %v\n", err) - os.Exit(1) - } - if logLevel := c.String("log"); logLevel != "" { - logConf := log.NewConfig() - logConf.LogLevel = logLevel - logger, err := log.New(os.Stdout, ifs.OS(), logConf) - if err != nil { - fmt.Printf("Failed to init logger: %v\n", err) - os.Exit(1) - } - if RunAll(c.Args().Slice(), cliOpts.MainConfigSpecCtor(), "_benthos_test", true, logger, resourcesPaths) { - os.Exit(0) - } - } else if RunAll(c.Args().Slice(), cliOpts.MainConfigSpecCtor(), "_benthos_test", true, log.Noop(), resourcesPaths) { - os.Exit(0) - } - os.Exit(1) - return nil - }, - } -} diff --git a/internal/cli/test/command.go b/internal/cli/test/command.go deleted file mode 100644 index eb43bdf0f7..0000000000 --- a/internal/cli/test/command.go +++ /dev/null @@ -1,199 +0,0 @@ -package test - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/fatih/color" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - ifilepath "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -var ( - green = color.New(color.FgGreen).SprintFunc() - red = color.New(color.FgRed).SprintFunc() - yellow = color.New(color.FgYellow).SprintFunc() -) - -//------------------------------------------------------------------------------ - -// GetPathPair returns the config path and expected accompanying test definition -// path for a given syntax and a path for either file. -func GetPathPair(fullPath, testSuffix string) (configPath, definitionPath string) { - path, file := filepath.Split(fullPath) - ext := filepath.Ext(file) - filename := strings.TrimSuffix(file, ext) - if strings.HasSuffix(filename, testSuffix) { - definitionPath = filepath.Clean(fullPath) - configPath = filepath.Join(path, strings.TrimSuffix(filename, testSuffix)+ext) - } else { - configPath = filepath.Clean(fullPath) - definitionPath = filepath.Join(path, filename+testSuffix+ext) - } - return -} - -func getDefinition(targetPath, definitionPath string) ([]test.Case, error) { - if _, err := ifs.OS().Stat(targetPath); err != nil { - return nil, fmt.Errorf("unable to access target config file '%v': %v", targetPath, err) - } - if _, err := ifs.OS().Stat(definitionPath); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return nil, fmt.Errorf("unable to access test definition file '%v': %v", definitionPath, err) - } - if !strings.HasSuffix(targetPath, ".yaml") && !strings.HasSuffix(targetPath, ".yml") { - return nil, nil - } - definitionPath = targetPath - } - defBytes, err := ifs.ReadFile(ifs.OS(), definitionPath) - if err != nil { - return nil, fmt.Errorf("failed to read test definition from '%v': %v", definitionPath, err) - } - - node, err := docs.UnmarshalYAML(defBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse test definition from '%v': %v", definitionPath, err) - } - - cases, err := test.FromAny(node) - if err != nil { - return nil, fmt.Errorf("failed to parse test definition from '%v': %v", definitionPath, err) - } - return cases, nil -} - -// GetTestTargets searches for test definition targets in a path with a given -// test suffix. -func GetTestTargets(targetPaths []string, testSuffix string) (map[string][]test.Case, error) { - targetPaths, err := ifilepath.GlobsAndSuperPaths(ifs.OS(), targetPaths, "yaml", "yml") - if err != nil { - return nil, err - } - - targetDefinitions := map[string][]test.Case{} - for _, tPath := range targetPaths { - configPath, definitionPath := GetPathPair(tPath, testSuffix) - def, err := getDefinition(configPath, definitionPath) - if err != nil { - return nil, err - } - if len(def) == 0 { - continue - } - targetDefinitions[filepath.Clean(configPath)] = def - } - return targetDefinitions, nil -} - -// Lints the config target of a test definition and either returns linting -// errors (false for failed) or returns an error. -func lintTarget(spec docs.FieldSpecs, path, testSuffix string) ([]docs.Lint, error) { - confPath, _ := GetPathPair(path, testSuffix) - - // This is necessary as each test case can provide a different set of - // environment variables, so in order to test env vars properly we would - // need to lint for each case. - skipEnvVarCheck := true - _, lints, err := config.ReadYAMLFileLinted(ifs.OS(), spec, confPath, skipEnvVarCheck, docs.NewLintConfig(bundle.GlobalEnvironment)) - if err != nil { - return nil, err - } - return lints, nil -} - -//------------------------------------------------------------------------------ - -// RunAll executes the test command for a slice of paths. The path can either be -// a config file, a config files test definition file, a directory, or the -// wildcard pattern './...'. -func RunAll(paths []string, spec docs.FieldSpecs, testSuffix string, lint bool, logger log.Modular, resourcesPaths []string) bool { - targets, err := GetTestTargets(paths, testSuffix) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to obtain test targets: %v\n", err) - return false - } - if len(targets) == 0 { - fmt.Printf("%v\n", yellow("No tests were found")) - return false - } - - type failedTarget struct { - target string - lints []docs.Lint - cases []CaseFailure - } - fails := []failedTarget{} - - targetPaths := make([]string, 0, len(targets)) - for k := range targets { - targetPaths = append(targetPaths, k) - } - sort.Strings(targetPaths) - - for _, target := range targetPaths { - var lints []docs.Lint - var failCases []CaseFailure - if lint { - if lints, err = lintTarget(spec, target, testSuffix); err != nil { - fmt.Fprintf(os.Stderr, "Failed to execute test target '%v': %v\n", target, err) - return false - } - } - if failCases, err = Execute(spec, targets[target], target, resourcesPaths, logger); err != nil { - fmt.Fprintf(os.Stderr, "Failed to execute test target '%v': %v\n", target, err) - return false - } - if len(lints) > 0 || len(failCases) > 0 { - fails = append(fails, failedTarget{ - target: target, - lints: lints, - cases: failCases, - }) - fmt.Printf("Test '%v' %v\n", target, red("failed")) - } else { - fmt.Printf("Test '%v' %v\n", target, green("succeeded")) - } - } - if len(fails) > 0 { - fmt.Printf("\nFailures:\n\n") - for i, fail := range fails { - if i > 0 { - fmt.Println("") - } - fmt.Printf("--- %v ---\n\n", fail.target) - for _, lint := range fail.lints { - fmt.Printf("Lint: %v\n", lint) - } - if len(fail.cases) > 0 { - if len(fail.lints) > 0 { - fmt.Println("") - } - var namePrev string - for i, fail := range fail.cases { - if namePrev != fail.Name { - if i > 0 { - fmt.Println("") - } - fmt.Printf("%v [line %v]:\n", fail.Name, fail.TestLine) - namePrev = fail.Name - } - fmt.Println(fail.Reason) - } - } - } - return false - } - return true -} diff --git a/internal/cli/test/command_test.go b/internal/cli/test/command_test.go deleted file mode 100644 index 58469c3016..0000000000 --- a/internal/cli/test/command_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package test_test - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/benthosdev/benthos/v4/internal/cli/test" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/log" -) - -func TestGetBothPaths(t *testing.T) { - type testCase struct { - input string - output [2]string - } - - tests := []testCase{ - { - input: "/foo/bar/baz.yaml", - output: [2]string{ - "/foo/bar/baz.yaml", - "/foo/bar/baz_benthos_test.yaml", - }, - }, - { - input: "baz.yaml", - output: [2]string{ - "baz.yaml", - "baz_benthos_test.yaml", - }, - }, - { - input: "./foo/bar/baz_benthos_test.yaml", - output: [2]string{ - "foo/bar/baz.yaml", - "foo/bar/baz_benthos_test.yaml", - }, - }, - { - input: "baz_benthos_test.yaml", - output: [2]string{ - "baz.yaml", - "baz_benthos_test.yaml", - }, - }, - { - input: "/foo/bar/baz.foo", - output: [2]string{ - "/foo/bar/baz.foo", - "/foo/bar/baz_benthos_test.foo", - }, - }, - { - input: "baz", - output: [2]string{ - "baz", - "baz_benthos_test", - }, - }, - { - input: "/foo/bar/baz_benthos_test.foo", - output: [2]string{ - "/foo/bar/baz.foo", - "/foo/bar/baz_benthos_test.foo", - }, - }, - { - input: "baz_benthos_test", - output: [2]string{ - "baz", - "baz_benthos_test", - }, - }, - } - - for i, testDef := range tests { - t.Run(fmt.Sprintf("Test case %v", i), func(tt *testing.T) { - cPath, dPath := test.GetPathPair(testDef.input, "_benthos_test") - if exp, act := testDef.output[0], cPath; exp != act { - tt.Errorf("Wrong config path: %v != %v", act, exp) - } - if exp, act := testDef.output[1], dPath; exp != act { - tt.Errorf("Wrong definition path: %v != %v", act, exp) - } - }) - } -} - -func TestGetTargetsSingle(t *testing.T) { - testDir, err := initTestFiles(t, map[string]string{ - "foo.yaml": `tests: [{name: ""}]`, - "foo_benthos_test.yaml": `tests: [{name: ""}]`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - paths, err := test.GetTestTargets([]string{filepath.Join(testDir, "foo.yaml")}, "_benthos_test") - if err != nil { - t.Fatal(err) - } - if exp, act := 1, len(paths); exp != act { - t.Fatalf("Wrong count of paths: %v != %v", act, exp) - } - if _, exists := paths[filepath.Join(testDir, "foo.yaml")]; !exists { - t.Errorf("Wrong path returned: %v does not contain foo.yaml", paths) - } - - paths, err = test.GetTestTargets([]string{filepath.Join(testDir, "foo_benthos_test.yaml")}, "_benthos_test") - if err != nil { - t.Fatal(err) - } - if exp, act := 1, len(paths); exp != act { - t.Fatalf("Wrong count of paths: %v != %v", act, exp) - } - if _, exists := paths[filepath.Join(testDir, "foo.yaml")]; !exists { - t.Errorf("Wrong path returned: %v does not contain foo.yaml", paths) - } -} - -func TestGetTargetsSingleError(t *testing.T) { - testDir, err := initTestFiles(t, map[string]string{ - "foo.yaml": `foobar: {}`, - "bar_benthos_test.yaml": `tests: [{}]`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - if _, err = test.GetTestTargets([]string{filepath.Join(testDir, "bar_benthos_test.yaml")}, "_benthos_test"); err == nil { - t.Error("Expected error") - } - if _, err = test.GetTestTargets([]string{"/does/not/exist/foo.yaml"}, "_benthos_test"); err == nil { - t.Error("Expected error") - } -} - -func TestGetTargetsDir(t *testing.T) { - testDir, err := initTestFiles(t, map[string]string{ - "foo.yaml": `foobar: {}`, - "foo_benthos_test.yaml": `tests: [{name: ""}]`, - "bar.yaml": `tests: [{name: ""}]`, - "not_a_yaml.txt": `foobar this isnt json or yaml`, - "nested/baz.yaml": `foobar: {}`, - "nested/baz_benthos_test.yaml": `tests: [{name: ""}]`, - "ignored.yaml": `foobar: {}`, - "nested/also_ignored.yaml": `foobar: {}`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - paths, err := test.GetTestTargets([]string{testDir + "/..."}, "_benthos_test") - if err != nil { - t.Fatal(err) - } - if exp, act := 3, len(paths); exp != act { - t.Fatalf("Wrong count of paths: %v != %v", act, exp) - } - if _, exists := paths[filepath.Join(testDir, "foo.yaml")]; !exists { - t.Errorf("Wrong path returned: %v does not contain foo.yaml", paths) - } - if _, exists := paths[filepath.Join(testDir, "bar.yaml")]; !exists { - t.Errorf("Wrong path returned: %v does not contain bar.yaml", paths) - } - if _, exists := paths[filepath.Join(testDir, "nested", "baz.yaml")]; !exists { - t.Errorf("Wrong path returned: %v does not contain nested/baz.yaml", paths) - } -} - -func TestGetTargetsDirError(t *testing.T) { - testDir, err := initTestFiles(t, map[string]string{ - "foo_benthos_test.yaml": `tests: [{}]`, - "bar.yaml": `foobar: {}`, - "bar_benthos_test.yaml": `tests: [{}]`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - if _, err = test.GetTestTargets([]string{testDir + "/..."}, "_benthos_test"); err == nil { - t.Error("Expected error") - } -} - -func TestGetTargetsDirRecurseError(t *testing.T) { - testDir, err := initTestFiles(t, map[string]string{ - "foo.yaml": `foobar: {}`, - "foo_benthos_test.yaml": `tests: [{}]`, - "bar.yaml": `foobar: {}`, - "bar_benthos_test.yaml": `tests: [{}]`, - "nested/baz_benthos_test.yaml": `tests: [{}]`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - if _, err = test.GetTestTargets([]string{testDir + "/..."}, "_benthos_test"); err == nil { - t.Error("Expected error") - } -} - -func TestCommandRunHappy(t *testing.T) { - testDir, err := initTestFiles(t, map[string]string{ - "foo.yaml": ` -pipeline: - meow: woof - processors: - - bloblang: 'root = content().uppercase()'`, - "foo_benthos_test.yaml": ` -tests: - - name: example test - target_processors: '/pipeline/processors' - environment: {} - input_batch: - - content: 'example content' - output_batches: - - - - content_equals: EXAMPLE CONTENT`, - "bar.yaml": ` -pipeline: - processors: - - bloblang: 'root = content().uppercase()'`, - "bar_benthos_test.yaml": ` -tests: - - name: example test - target_processors: '/pipeline/processors' - environment: {} - input_batch: - - content: 'example content' - output_batches: - - - - content_equals: example content`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - if !test.RunAll([]string{filepath.Join(testDir, "foo.yaml")}, config.Spec(), "_benthos_test", false, log.Noop(), nil) { - t.Error("Unexpected result") - } - - if test.RunAll([]string{filepath.Join(testDir, "foo.yaml")}, config.Spec(), "_benthos_test", true, log.Noop(), nil) { - t.Error("Unexpected result") - } - - if test.RunAll([]string{testDir}, config.Spec(), "_benthos_test", true, log.Noop(), nil) { - t.Error("Unexpected result") - } -} diff --git a/internal/cli/test/definition.go b/internal/cli/test/definition.go deleted file mode 100644 index d823a8ce4c..0000000000 --- a/internal/cli/test/definition.go +++ /dev/null @@ -1,37 +0,0 @@ -package test - -import ( - "fmt" - "path/filepath" - - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// Execute the test definition. -func Execute(confSpec docs.FieldSpecs, cases []test.Case, testFilePath string, resourcesPaths []string, logger log.Modular) ([]CaseFailure, error) { - procsProvider := NewProcessorsProvider( - testFilePath, - OptAddResourcesPaths(resourcesPaths), - OptProcessorsProviderSetLogger(logger), - OptSetConfigSpec(confSpec), - ) - - dir := filepath.Dir(testFilePath) - - var totalFailures []CaseFailure - for i, c := range cases { - cleanupEnv := setEnvironment(c.Environment) - failures, err := ExecuteFrom(ifs.OS(), dir, c, procsProvider) - if err != nil { - cleanupEnv() - return nil, fmt.Errorf("test case %v failed: %v", i, err) - } - totalFailures = append(totalFailures, failures...) - cleanupEnv() - } - - return totalFailures, nil -} diff --git a/internal/cli/test/definition_test.go b/internal/cli/test/definition_test.go deleted file mode 100644 index 75c68a1ab0..0000000000 --- a/internal/cli/test/definition_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package test_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/fatih/color" - - "github.com/benthosdev/benthos/v4/internal/cli/test" - "github.com/benthosdev/benthos/v4/internal/config" - dtest "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/log" -) - -func TestDefinitionFail(t *testing.T) { - color.NoColor = true - - testDir, err := initTestFiles(t, map[string]string{ - "config1.yaml": ` -pipeline: - processors: - - bloblang: 'root = content().uppercase()' -`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - def := []dtest.Case{ - { - Name: "foo test 1", - Environment: map[string]string{}, - TargetProcessors: "/pipeline/processors", - InputBatches: [][]dtest.InputConfig{ - { - { - Content: "foo bar baz", - Metadata: map[string]any{ - "key1": "value1", - }, - }, - { - Content: "one two three", - Metadata: map[string]any{ - "key1": "value2", - }, - }, - }, - }, - OutputBatches: [][]dtest.OutputConditionsMap{ - { - { - "content_equals": dtest.ContentEqualsCondition("FOO BAR baz"), - "metadata_equals": dtest.MetadataEqualsCondition{ - "key1": "value1", - }, - }, - { - "content_equals": dtest.ContentEqualsCondition("ONE TWO THREE"), - "metadata_equals": dtest.MetadataEqualsCondition{ - "key1": "value3", - }, - }, - }, - }, - }, - } - - failures, err := test.Execute(config.Spec(), def, filepath.Join(testDir, "config1.yaml"), nil, log.Noop()) - if err != nil { - t.Fatal(err) - } - - if exp, act := 2, len(failures); exp != act { - t.Fatalf("Wrong count of failures: %v != %v", act, exp) - } - if exp, act := "foo test 1: batch 0 message 0: content_equals: content mismatch\n expected: FOO BAR baz\n received: FOO BAR BAZ", failures[0].String(); exp != act { - t.Errorf("Mismatched fail message: %v != %v", act, exp) - } - if exp, act := "foo test 1: batch 0 message 1: metadata_equals: metadata key 'key1' mismatch\n expected: value3\n received: value2", failures[1].String(); exp != act { - t.Errorf("Mismatched fail message: %v != %v", act, exp) - } -} - -func TestDefinitionParallel(t *testing.T) { - color.NoColor = true - - testDir, err := initTestFiles(t, map[string]string{ - "config1.yaml": ` -pipeline: - processors: - - bloblang: 'root = content().uppercase()' -`, - }) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - def := []dtest.Case{ - { - Name: "foo test 1", - Environment: map[string]string{}, - TargetProcessors: "/pipeline/processors", - InputBatches: [][]dtest.InputConfig{ - { - { - Content: "foo bar baz", - Metadata: map[string]any{ - "key1": "value1", - }, - }, - }, - }, - OutputBatches: [][]dtest.OutputConditionsMap{ - { - { - "content_equals": dtest.ContentEqualsCondition("FOO BAR baz"), - "metadata_equals": dtest.MetadataEqualsCondition{ - "key1": "value1", - }, - }, - }, - }, - }, - { - Name: "foo test 2", - Environment: map[string]string{}, - TargetProcessors: "/pipeline/processors", - InputBatches: [][]dtest.InputConfig{ - { - { - Content: "one two three", - Metadata: map[string]any{ - "key1": "value2", - }, - }, - }, - }, - OutputBatches: [][]dtest.OutputConditionsMap{ - { - { - "content_equals": dtest.ContentEqualsCondition("ONE TWO THREE"), - "metadata_equals": dtest.MetadataEqualsCondition{ - "key1": "value3", - }, - }, - }, - }, - }, - } - - failures, err := test.Execute(config.Spec(), def, filepath.Join(testDir, "config1.yaml"), nil, log.Noop()) - if err != nil { - t.Fatal(err) - } - - if exp, act := 2, len(failures); exp != act { - t.Fatalf("Wrong count of failures: %v != %v", act, exp) - } - if exp, act := "foo test 1: batch 0 message 0: content_equals: content mismatch\n expected: FOO BAR baz\n received: FOO BAR BAZ", failures[0].String(); exp != act { - t.Errorf("Mismatched fail message: %v != %v", act, exp) - } - if exp, act := "foo test 2: batch 0 message 0: metadata_equals: metadata key 'key1' mismatch\n expected: value3\n received: value2", failures[1].String(); exp != act { - t.Errorf("Mismatched fail message: %v != %v", act, exp) - } -} diff --git a/internal/cli/test/processors_provider.go b/internal/cli/test/processors_provider.go deleted file mode 100644 index 5f125e5a76..0000000000 --- a/internal/cli/test/processors_provider.go +++ /dev/null @@ -1,414 +0,0 @@ -package test - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/Jeffail/gabs/v2" - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type cachedConfig struct { - mgr manager.ResourceConfig - procs []processor.Config -} - -// ProcessorsProvider consumes a Benthos config and, given a JSON Pointer, -// extracts and constructs the target processors from the config file. -type ProcessorsProvider struct { - targetPath string - resourcesPaths []string - cachedConfigs map[string]cachedConfig - - spec docs.FieldSpecs - logger log.Modular -} - -// NewProcessorsProvider returns a new processors provider aimed at a filepath. -func NewProcessorsProvider(targetPath string, opts ...func(*ProcessorsProvider)) *ProcessorsProvider { - p := &ProcessorsProvider{ - targetPath: targetPath, - cachedConfigs: map[string]cachedConfig{}, - spec: config.Spec(), - logger: log.Noop(), - } - for _, opt := range opts { - opt(p) - } - return p -} - -// OptSetConfigSpec sets the config spec used for linting. -func OptSetConfigSpec(spec docs.FieldSpecs) func(*ProcessorsProvider) { - return func(p *ProcessorsProvider) { - p.spec = spec - } -} - -// OptAddResourcesPaths adds paths to files where resources should be parsed. -func OptAddResourcesPaths(paths []string) func(*ProcessorsProvider) { - return func(p *ProcessorsProvider) { - p.resourcesPaths = paths - } -} - -// OptProcessorsProviderSetLogger sets the logger used by tested components. -func OptProcessorsProviderSetLogger(logger log.Modular) func(*ProcessorsProvider) { - return func(p *ProcessorsProvider) { - p.logger = logger - } -} - -//------------------------------------------------------------------------------ - -// Provide attempts to extract an array of processors from a Benthos config. -// Supports injected mocked components in the parsed config. If the JSON Pointer -// targets a single processor config it will be constructed and returned as an -// array of one element. -func (p *ProcessorsProvider) Provide(jsonPtr string, environment map[string]string, mocks map[string]any) ([]processor.V1, error) { - confs, err := p.getConfs(jsonPtr, environment, mocks) - if err != nil { - return nil, err - } - return p.initProcs(confs) -} - -// ProvideBloblang attempts to parse a Bloblang mapping and returns a processor -// slice that executes it. -func (p *ProcessorsProvider) ProvideBloblang(pathStr string) ([]processor.V1, error) { - if !filepath.IsAbs(pathStr) { - pathStr = filepath.Join(filepath.Dir(p.targetPath), pathStr) - } - - mappingBytes, err := ifs.ReadFile(ifs.OS(), pathStr) - if err != nil { - return nil, err - } - - pCtx := parser.GlobalContext().WithImporterRelativeToFile(pathStr) - exec, mapErr := parser.ParseMapping(pCtx, string(mappingBytes)) - if mapErr != nil { - return nil, mapErr - } - - return []processor.V1{ - processor.NewAutoObservedBatchedProcessor("bloblang", newBloblang(exec, p.logger), mock.NewManager()), - }, nil -} - -type bloblangProc struct { - exec *mapping.Executor - log log.Modular -} - -func newBloblang(exec *mapping.Executor, log log.Modular) processor.AutoObservedBatched { - return &bloblangProc{ - exec: exec, - log: log, - } -} - -func (b *bloblangProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - newParts := make([]*message.Part, 0, msg.Len()) - _ = msg.Iter(func(i int, part *message.Part) error { - p, err := b.exec.MapPart(i, msg) - if err != nil { - p = part.ShallowCopy() - ctx.OnError(err, i, p) - b.log.Error("%v\n", err) - } - if p != nil { - newParts = append(newParts, p) - } - return nil - }) - if len(newParts) == 0 { - return nil, nil - } - - newMsg := message.Batch(newParts) - return []message.Batch{newMsg}, nil -} - -func (b *bloblangProc) Close(context.Context) error { - return nil -} - -//------------------------------------------------------------------------------ - -func (p *ProcessorsProvider) initProcs(confs cachedConfig) ([]processor.V1, error) { - mgr, err := manager.New(confs.mgr, manager.OptSetLogger(p.logger)) - if err != nil { - return nil, fmt.Errorf("failed to initialise resources: %v", err) - } - - procs := make([]processor.V1, len(confs.procs)) - for i, conf := range confs.procs { - if procs[i], err = mgr.NewProcessor(conf); err != nil { - return nil, fmt.Errorf("failed to initialise processor index '%v': %v", i, err) - } - } - return procs, nil -} - -func confTargetID(jsonPtr string, environment map[string]string, mocks map[string]any) string { - mocksBytes, _ := json.Marshal(mocks) - return fmt.Sprintf("%v-%v-%s", jsonPtr, environment, mocksBytes) -} - -func setEnvironment(vars map[string]string) func() { - if vars == nil { - return func() {} - } - - // Set custom environment vars. - ogEnvVars := map[string]string{} - for k, v := range vars { - if ogV, exists := os.LookupEnv(k); exists { - ogEnvVars[k] = ogV - } - os.Setenv(k, v) - } - - // Reset env vars back to original values after config parse. - return func() { - for k := range vars { - if og, exists := ogEnvVars[k]; exists { - os.Setenv(k, og) - } else { - os.Unsetenv(k) - } - } - } -} - -func resolveProcessorsPointer(targetFile, jsonPtr string) (filePath, procPath string, err error) { - var u *url.URL - if u, err = url.Parse(jsonPtr); err != nil { - return - } - if u.Scheme != "" && u.Scheme != "file" { - err = fmt.Errorf("target processors '%v' contains non-path scheme value", jsonPtr) - return - } - - if u.Fragment != "" { - procPath = u.Fragment - filePath = filepath.Join(filepath.Dir(targetFile), u.Path) - } else { - procPath = u.Path - filePath = targetFile - } - if procPath == "" { - err = fmt.Errorf("failed to target processors '%v': reference URI must contain a path or fragment", jsonPtr) - } - return -} - -func setMock(confSpec docs.FieldSpecs, root *yaml.Node, mock any, pathSlice ...string) error { - var mockNode yaml.Node - if err := mockNode.Encode(mock); err != nil { - return fmt.Errorf("encode mock value: %w", err) - } - - labelPull := struct { - Label *string `yaml:"label"` - }{} - if err := mockNode.Decode(&labelPull); err != nil { - return fmt.Errorf("decode mock label: %w", err) - } - if labelPull.Label == nil { - if targetNode, _ := docs.GetYAMLPath(root, pathSlice...); targetNode != nil { - _ = targetNode.Decode(&labelPull) - } - } else { - labelPull.Label = nil - } - - if err := confSpec.SetYAMLPath(bundle.GlobalEnvironment, root, &mockNode, pathSlice...); err != nil { - return err - } - if labelPull.Label != nil { - var labelNode yaml.Node - if err := labelNode.Encode(labelPull.Label); err != nil { - return fmt.Errorf("encode mock label: %w", err) - } - if err := confSpec.SetYAMLPath(bundle.GlobalEnvironment, root, &labelNode, append(pathSlice, "label")...); err != nil { - return fmt.Errorf("set mock label: %w", err) - } - } - return nil -} - -func (p *ProcessorsProvider) getConfs(jsonPtr string, environment map[string]string, mocks map[string]any) (cachedConfig, error) { - cacheKey := confTargetID(jsonPtr, environment, mocks) - - confs, exists := p.cachedConfigs[cacheKey] - if exists { - return confs, nil - } - - targetPath, procPath, err := resolveProcessorsPointer(p.targetPath, jsonPtr) - if err != nil { - return confs, err - } - if targetPath == "" { - targetPath = p.targetPath - } - - // Set custom environment vars. - ogEnvVars := map[string]string{} - for k, v := range environment { - ogEnvVars[k] = os.Getenv(k) - os.Setenv(k, v) - } - - cleanupEnv := setEnvironment(environment) - defer cleanupEnv() - - envVarLookup := func(name string) (string, bool) { - if s, ok := environment[name]; ok { - return s, true - } - return os.LookupEnv(name) - } - - remainingMocks := map[string]any{} - for k, v := range mocks { - remainingMocks[k] = v - } - - configBytes, _, _, err := config.ReadFileEnvSwap(ifs.OS(), targetPath, envVarLookup) - if err != nil { - return confs, fmt.Errorf("failed to parse config file '%v': %v", targetPath, err) - } - - root, err := docs.UnmarshalYAML(configBytes) - if err != nil { - return confs, fmt.Errorf("failed to parse config file '%v': %v", targetPath, err) - } - - confSpec := p.spec - - // Replace mock components, starting with all absolute paths in JSON pointer - // form, then parsing remaining mock targets as label names. - for k, v := range remainingMocks { - if !strings.HasPrefix(k, "/") { - continue - } - mockPathSlice, err := gabs.JSONPointerToSlice(k) - if err != nil { - return confs, fmt.Errorf("failed to parse mock path '%v': %w", k, err) - } - if err = setMock(confSpec, root, &v, mockPathSlice...); err != nil { - return confs, fmt.Errorf("failed to set mock '%v': %w", k, err) - } - delete(remainingMocks, k) - } - - labelsToPaths := map[string][]string{} - if len(remainingMocks) > 0 { - confSpec.YAMLLabelsToPaths(bundle.GlobalEnvironment, root, labelsToPaths, nil) - for k, v := range remainingMocks { - mockPathSlice, exists := labelsToPaths[k] - if !exists { - return confs, fmt.Errorf("mock for label '%v' could not be applied as the label was not found in the test target file, it is not currently possible to mock resources imported separate to the test file", k) - } - if err = setMock(confSpec, root, &v, mockPathSlice...); err != nil { - return confs, fmt.Errorf("failed to set mock '%v': %w", k, err) - } - delete(remainingMocks, k) - } - } - - pConf, err := confSpec.ParsedConfigFromAny(root) - if err != nil { - return confs, fmt.Errorf("failed to parse config file '%v': %v", targetPath, err) - } - - mgrWrapper, err := manager.FromParsed(bundle.GlobalEnvironment, pConf) - if err != nil { - return confs, fmt.Errorf("failed to parse config file '%v': %v", targetPath, err) - } - - for _, path := range p.resourcesPaths { - resourceBytes, _, _, err := config.ReadFileEnvSwap(ifs.OS(), path, envVarLookup) - if err != nil { - return confs, fmt.Errorf("failed to parse resources config file '%v': %v", path, err) - } - - confNode, err := docs.UnmarshalYAML(resourceBytes) - if err != nil { - return confs, fmt.Errorf("failed to parse resources config file '%v': %v", path, err) - } - - extraMgrWrapper, err := manager.FromAny(bundle.GlobalEnvironment, confNode) - if err != nil { - return confs, fmt.Errorf("failed to parse resources config file '%v': %v", path, err) - } - if err = mgrWrapper.AddFrom(&extraMgrWrapper); err != nil { - return confs, fmt.Errorf("failed to merge resources from '%v': %v", path, err) - } - } - - // We can clear all input and output resources as they're not used by procs - // under any circumstances. - mgrWrapper.ResourceInputs = nil - mgrWrapper.ResourceOutputs = nil - - confs.mgr = mgrWrapper - - var pathSlice []string - if strings.HasPrefix(procPath, "/") { - if pathSlice, err = gabs.JSONPointerToSlice(procPath); err != nil { - return confs, fmt.Errorf("failed to parse case processors path '%v': %w", procPath, err) - } - } else { - if len(labelsToPaths) == 0 { - confSpec.YAMLLabelsToPaths(bundle.GlobalEnvironment, root, labelsToPaths, nil) - } - if pathSlice, exists = labelsToPaths[procPath]; !exists { - return confs, fmt.Errorf("target for label '%v' failed as the label was not found in the test target file, it is not currently possible to target resources imported separate to the test file", procPath) - } - } - - if root, err = docs.GetYAMLPath(root, pathSlice...); err != nil { - return confs, fmt.Errorf("failed to resolve case processors from '%v': %v", targetPath, err) - } - - if root.Kind == yaml.SequenceNode { - for _, n := range root.Content { - procConf, err := processor.FromAny(bundle.GlobalEnvironment, n) - if err != nil { - return confs, fmt.Errorf("failed to resolve case processors from '%v': %v", targetPath, err) - } - confs.procs = append(confs.procs, procConf) - } - } else { - procConf, err := processor.FromAny(bundle.GlobalEnvironment, root) - if err != nil { - return confs, fmt.Errorf("failed to resolve case processors from '%v': %v", targetPath, err) - } - confs.procs = append(confs.procs, procConf) - } - - p.cachedConfigs[cacheKey] = confs - return confs, nil -} diff --git a/internal/cli/test/processors_provider_test.go b/internal/cli/test/processors_provider_test.go deleted file mode 100644 index c575406937..0000000000 --- a/internal/cli/test/processors_provider_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package test_test - -import ( - "context" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/cli/test" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func initTestFiles(t *testing.T, files map[string]string) (string, error) { - testDir := t.TempDir() - - for k, v := range files { - fp := filepath.Join(testDir, k) - if err := os.MkdirAll(filepath.Dir(fp), 0o777); err != nil { - return "", err - } - if err := os.WriteFile(fp, []byte(v), 0o777); err != nil { - return "", err - } - } - - return testDir, nil -} - -func TestProcessorsProviderErrors(t *testing.T) { - files := map[string]string{ - "config1.yaml": ` -this isnt valid yaml - nah - what is even happening here?`, - "config2.yaml": ` -pipeline: - processors: - - bloblang: 'root = this'`, - "config3.yaml": ` -pipeline: - processors: - - type: doesnotexist`, - } - - testDir, err := initTestFiles(t, files) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - if _, err = test.NewProcessorsProvider(filepath.Join(testDir, "doesnotexist.yaml")).Provide("/pipeline/processors", nil, nil); err == nil { - t.Error("Expected error from bad filepath") - } - if _, err = test.NewProcessorsProvider(filepath.Join(testDir, "config1.yaml")).Provide("/pipeline/processors", nil, nil); err == nil { - t.Error("Expected error from bad config file") - } - if _, err = test.NewProcessorsProvider(filepath.Join(testDir, "config2.yaml")).Provide("/not/a/valid/path", nil, nil); err == nil { - t.Error("Expected error from bad processors path") - } - if _, err = test.NewProcessorsProvider(filepath.Join(testDir, "config3.yaml")).Provide("/pipeline/processors", nil, nil); err == nil { - t.Error("Expected error from bad processor type") - } -} - -func TestProcessorsProvider(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - files := map[string]string{ - "config1.yaml": ` -cache_resources: - - label: foocache - memory: {} - -pipeline: - processors: - - bloblang: 'meta foo = env("BAR_VAR").not_empty().catch("defaultvalue")' - - cache: - resource: foocache - operator: set - key: defaultkey - value: ${! meta("foo") } - - cache: - resource: foocache - operator: get - key: defaultkey - - bloblang: 'root = content().uppercase()'`, - } - - testDir, err := initTestFiles(t, files) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(testDir) - - provider := test.NewProcessorsProvider(filepath.Join(testDir, "config1.yaml")) - procs, err := provider.Provide("/pipeline/processors", nil, nil) - if err != nil { - t.Fatal(err) - } - if exp, act := 4, len(procs); exp != act { - t.Fatalf("Unexpected processor count: %v != %v", act, exp) - } - msgs, res := processor.ExecuteAll(tCtx, procs, message.QuickBatch([][]byte{[]byte("hello world")})) - require.NoError(t, res) - if exp, act := "DEFAULTVALUE", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } - - if procs, err = provider.Provide("/pipeline/processors", map[string]string{ - "BAR_VAR": "newvalue", - }, nil); err != nil { - t.Fatal(err) - } - if exp, act := 4, len(procs); exp != act { - t.Fatalf("Unexpected processor count: %v != %v", act, exp) - } - msgs, res = processor.ExecuteAll(tCtx, procs, message.QuickBatch([][]byte{[]byte("hello world")})) - require.NoError(t, res) - if exp, act := "NEWVALUE", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } -} - -func TestProcessorsProviderLabel(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - files := map[string]string{ - "config1.yaml": ` -pipeline: - processors: - - bloblang: 'meta foo = env("BAR_VAR").not_empty().catch("defaultvalue")' - - label: fooproc - bloblang: 'root = content().uppercase()'`, - } - - testDir, err := initTestFiles(t, files) - require.NoError(t, err) - defer os.RemoveAll(testDir) - - provider := test.NewProcessorsProvider(filepath.Join(testDir, "config1.yaml")) - procs, err := provider.Provide("fooproc", nil, nil) - require.NoError(t, err) - - assert.Len(t, procs, 1) - - msgs, res := processor.ExecuteAll(tCtx, procs, message.QuickBatch([][]byte{[]byte("hello world")})) - require.NoError(t, res) - if exp, act := "HELLO WORLD", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } -} - -func TestProcessorsProviderMocks(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - files := map[string]string{ - "config1.yaml": ` -pipeline: - processors: - - http: - url: http://example.com/foobar - verb: POST - - bloblang: 'root = content().string() + " first proc"' - - http: - url: http://example.com/barbaz - verb: POST - - bloblang: 'root = content().string() + " second proc"' -`, - } - - testDir, err := initTestFiles(t, files) - require.NoError(t, err) - - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - - mocks := map[string]any{} - require.NoError(t, yaml.Unmarshal([]byte(` -"/pipeline/processors/0": - bloblang: 'root = content().string() + " first mock"' -"/pipeline/processors/2": - bloblang: 'root = content().string() + " second mock"' -`), &mocks)) - - provider := test.NewProcessorsProvider(filepath.Join(testDir, "config1.yaml")) - procs, err := provider.Provide("/pipeline/processors", nil, mocks) - require.NoError(t, err) - - require.Len(t, procs, 4) - - msgs, res := processor.ExecuteAll(tCtx, procs, message.QuickBatch([][]byte{[]byte("starts with")})) - require.NoError(t, res) - require.Len(t, msgs, 1) - require.Equal(t, 1, msgs[0].Len()) - - assert.Equal(t, "starts with first mock first proc second mock second proc", string(msgs[0].Get(0).AsBytes())) -} - -func TestProcessorsProviderMocksFromLabel(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - files := map[string]string{ - "config1.yaml": ` -pipeline: - processors: - - label: first_http - http: - url: http://example.com/foobar - verb: POST - - bloblang: 'root = content().string() + " first proc"' - - label: second_http - http: - url: http://example.com/barbaz - verb: POST - - bloblang: 'root = content().string() + " second proc"' -`, - } - - testDir, err := initTestFiles(t, files) - require.NoError(t, err) - - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - - mocks := map[string]any{} - require.NoError(t, yaml.Unmarshal([]byte(` -"first_http": - bloblang: 'root = content().string() + " first mock"' -"second_http": - bloblang: 'root = content().string() + " second mock"' -`), &mocks)) - - provider := test.NewProcessorsProvider(filepath.Join(testDir, "config1.yaml")) - procs, err := provider.Provide("/pipeline/processors", nil, mocks) - require.NoError(t, err) - - require.Len(t, procs, 4) - - msgs, res := processor.ExecuteAll(tCtx, procs, message.QuickBatch([][]byte{[]byte("starts with")})) - require.NoError(t, res) - require.Len(t, msgs, 1) - require.Equal(t, 1, msgs[0].Len()) - - assert.Equal(t, "starts with first mock first proc second mock second proc", string(msgs[0].Get(0).AsBytes())) -} - -func TestProcessorsProviderMocksMixed(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - files := map[string]string{ - "config1.yaml": ` -pipeline: - processors: - - label: first_http - http: - url: http://example.com/foobar - verb: POST - - bloblang: 'root = content().string() + " first proc"' - - label: second_http - http: - url: http://example.com/barbaz - verb: POST - - bloblang: 'root = content().string() + " second proc"' -`, - } - - testDir, err := initTestFiles(t, files) - require.NoError(t, err) - - t.Cleanup(func() { - os.RemoveAll(testDir) - }) - - mocks := map[string]any{} - require.NoError(t, yaml.Unmarshal([]byte(` -"first_http": - bloblang: 'root = content().string() + " first mock"' -"/pipeline/processors/2": - bloblang: 'root = content().string() + " second mock"' -`), &mocks)) - - provider := test.NewProcessorsProvider(filepath.Join(testDir, "config1.yaml")) - procs, err := provider.Provide("/pipeline/processors", nil, mocks) - require.NoError(t, err) - - require.Len(t, procs, 4) - - msgs, res := processor.ExecuteAll(tCtx, procs, message.QuickBatch([][]byte{[]byte("starts with")})) - require.NoError(t, res) - require.Len(t, msgs, 1) - require.Equal(t, 1, msgs[0].Len()) - - assert.Equal(t, "starts with first mock first proc second mock second proc", string(msgs[0].Get(0).AsBytes())) -} - -func TestProcessorsExtraResources(t *testing.T) { - files := map[string]string{ - "resources1.yaml": ` -cache_resources: - - label: barcache - memory: {} -`, - "resources2.yaml": ` -cache_resources: - - label: bazcache - memory: {} -`, - "config1.yaml": ` -cache_resources: - - label: foocache - memory: {} - -pipeline: - processors: - - cache: - resource: foocache - operator: set - key: defaultkey - value: foo - - cache: - resource: barcache - operator: set - key: defaultkey - value: bar - - cache: - resource: bazcache - operator: set - key: defaultkey - value: bar -`, - } - - testDir, err := initTestFiles(t, files) - require.NoError(t, err) - defer os.RemoveAll(testDir) - - provider := test.NewProcessorsProvider( - filepath.Join(testDir, "config1.yaml"), - test.OptAddResourcesPaths([]string{ - filepath.Join(testDir, "resources1.yaml"), - filepath.Join(testDir, "resources2.yaml"), - }), - ) - procs, err := provider.Provide("/pipeline/processors", nil, nil) - require.NoError(t, err) - assert.Len(t, procs, 3) -} - -func TestProcessorsExtraResourcesError(t *testing.T) { - files := map[string]string{ - "resources1.yaml": ` -cache_resources: - - label: barcache - memory: {} -`, - "resources2.yaml": ` -cache_resources: - - label: barcache - memory: {} -`, - "config1.yaml": ` -cache_resources: - - label: foocache - memory: {} - -pipeline: - processors: - - cache: - resource: foocache - operator: set - key: defaultkey - value: foo - - cache: - resource: barcache - operator: set - key: defaultkey - value: bar -`, - } - - testDir, err := initTestFiles(t, files) - require.NoError(t, err) - defer os.RemoveAll(testDir) - - provider := test.NewProcessorsProvider( - filepath.Join(testDir, "config1.yaml"), - test.OptAddResourcesPaths([]string{ - filepath.Join(testDir, "resources1.yaml"), - filepath.Join(testDir, "resources2.yaml"), - }), - ) - _, err = provider.Provide("/pipeline/processors", nil, nil) - require.EqualError(t, err, "failed to initialise resources: cache resource label 'barcache' collides with a previously defined resource") -} diff --git a/internal/codec/reader.go b/internal/codec/reader.go deleted file mode 100644 index d8981a741e..0000000000 --- a/internal/codec/reader.go +++ /dev/null @@ -1,1179 +0,0 @@ -package codec - -import ( - "archive/tar" - "bufio" - "bytes" - "context" - "encoding/csv" - "errors" - "fmt" - "io" - "path/filepath" - "regexp" - "strconv" - "strings" - "sync" - - "github.com/klauspost/compress/gzip" - "github.com/klauspost/pgzip" - - goavro "github.com/linkedin/goavro/v2" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// ReaderDocs is a static field documentation for input codecs. -var ReaderDocs = NewReaderDocs("codec") - -func NewReaderDocs(name string) docs.FieldSpec { - return docs.FieldString( - name, "The way in which the bytes of a data source should be converted into discrete messages, codecs are useful for specifying how large files or continuous streams of data might be processed in small chunks rather than loading it all in memory. It's possible to consume lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. Codecs can be chained with `/`, for example a gzip compressed CSV file can be consumed with the codec `gzip/csv`.", "lines", "delim:\t", "delim:foobar", "gzip/csv", - ).HasAnnotatedOptions( - "auto", "EXPERIMENTAL: Attempts to derive a codec for each file based on information such as the extension. For example, a .tar.gz file would be consumed with the `gzip/tar` codec. Defaults to all-bytes.", - "all-bytes", "Consume the entire file as a single binary message.", - "avro-ocf:marshaler=x", "EXPERIMENTAL: Consume a stream of Avro OCF datum. The `marshaler` parameter is optional and has the options: `goavro` (default), `json`. Use `goavro` if OCF contains logical types.", - "chunker:x", "Consume the file in chunks of a given number of bytes.", - "csv", "Consume structured rows as comma separated values, the first row must be a header row.", - "csv:x", "Consume structured rows as values separated by a custom delimiter, the first row must be a header row. The custom delimiter must be a single character, e.g. the codec `\"csv:\\t\"` would consume a tab delimited file.", - "csv-safe", "Consume structured rows like `csv`, but sends messages with empty maps on failure to parse. Includes row number and parsing errors (if any) in the message's metadata.", - "csv-safe:x", "Consume structured rows like `csv:x` as values separated by a custom delimiter, but sends messages with empty maps on failure to parse. The custom delimiter must be a single character, e.g. the codec `\"csv-safe:\\t\"` would consume a tab delimited file. Includes row number and parsing errors (if any) in the message's metadata.", - "delim:x", "Consume the file in segments divided by a custom delimiter.", - "gzip", "Decompress a gzip file, this codec should precede another codec, e.g. `gzip/all-bytes`, `gzip/tar`, `gzip/csv`, etc.", - "pgzip", "Decompress a gzip file in parallel, this codec should precede another codec, e.g. `pgzip/all-bytes`, `pgzip/tar`, `pgzip/csv`, etc.", - "lines", "Consume the file in segments divided by linebreaks.", - "multipart", "Consumes the output of another codec and batches messages together. A batch ends when an empty message is consumed. For example, the codec `lines/multipart` could be used to consume multipart messages where an empty line indicates the end of each batch.", - "regex:(?m)^\\d\\d:\\d\\d:\\d\\d", "Consume the file in segments divided by regular expression.", - "skipbom", "Skip one or more byte order marks for each opened reader, this codec should precede another codec, e.g. `skipbom/csv`, etc.", - "tar", "Parse the file as a tar archive, and consume each file of the archive as a message.", - ).LinterBlobl("") -} - -//------------------------------------------------------------------------------ - -// ReaderConfig is a general configuration struct that covers all reader codecs. -type ReaderConfig struct { - MaxScanTokenSize int -} - -// NewReaderConfig creates a reader configuration with default values. -func NewReaderConfig() ReaderConfig { - return ReaderConfig{ - MaxScanTokenSize: bufio.MaxScanTokenSize, - } -} - -//------------------------------------------------------------------------------ - -// ReaderAckFn is a function provided to a reader codec that it should call once -// the underlying io.ReadCloser is fully consumed. -type ReaderAckFn func(context.Context, error) error - -func ackOnce(fn ReaderAckFn) ReaderAckFn { - var once sync.Once - return func(ctx context.Context, err error) error { - var ackErr error - once.Do(func() { - ackErr = fn(ctx, err) - }) - return ackErr - } -} - -// Reader is a codec type that reads message parts from a source. -type Reader interface { - Next(context.Context) ([]*message.Part, ReaderAckFn, error) - Close(context.Context) error -} - -type ioReaderConstructor func(string, io.ReadCloser) (io.ReadCloser, error) - -// ReaderConstructor creates a reader from a filename, an io.ReadCloser and an -// ack func which is called by the reader once the io.ReadCloser is finished -// with. The filename can be empty and is usually ignored, but might be -// necessary for certain codecs. -type ReaderConstructor func(string, io.ReadCloser, ReaderAckFn) (Reader, error) - -// readerReaderConstructor is a private constructor for readers that _must_ -// consume from other readers. -type readerReaderConstructor func(string, Reader) (Reader, error) - -func chainIOCtors(first, second ioReaderConstructor) ioReaderConstructor { - return func(s string, rc io.ReadCloser) (io.ReadCloser, error) { - r1, err := first(s, rc) - if err != nil { - return nil, err - } - r2, err := second(s, r1) - if err != nil { - r1.Close() - return nil, err - } - return r2, nil - } -} - -func chainIOIntoPartCtor(first ioReaderConstructor, second ReaderConstructor) ReaderConstructor { - return func(s string, rc io.ReadCloser, aFn ReaderAckFn) (Reader, error) { - r1, err := first(s, rc) - if err != nil { - return nil, err - } - r2, err := second(s, r1, aFn) - if err != nil { - r1.Close() - return nil, err - } - return r2, nil - } -} - -func chainPartIntoReaderCtor(first ReaderConstructor, second readerReaderConstructor) ReaderConstructor { - return func(s string, rc io.ReadCloser, aFn ReaderAckFn) (Reader, error) { - r1, err := first(s, rc, aFn) - if err != nil { - return nil, err - } - r2, err := second(s, r1) - if err != nil { - r1.Close(context.Background()) - return nil, err - } - return r2, nil - } -} - -func chainedReader(codec string, conf ReaderConfig) (ReaderConstructor, error) { - codecs := strings.Split(codec, "/") - - var ioCtor ioReaderConstructor - var partCtor ReaderConstructor - - for i, codec := range codecs { - if tmpIOCtor, ok := ioReader(codec, conf); ok { - if partCtor != nil { - return nil, fmt.Errorf("unable to follow codec '%v' with '%v'", codecs[i-1], codec) - } - if ioCtor != nil { - ioCtor = chainIOCtors(ioCtor, tmpIOCtor) - } else { - ioCtor = tmpIOCtor - } - continue - } - tmpPartCtor, ok, err := partReader(codec, conf) - if err != nil { - return nil, err - } - if ok { - if partCtor != nil { - return nil, fmt.Errorf("unable to follow codec '%v' with '%v'", codecs[i-1], codec) - } - if ioCtor != nil { - tmpPartCtor = chainIOIntoPartCtor(ioCtor, tmpPartCtor) - ioCtor = nil - } - partCtor = tmpPartCtor - continue - } - tmpReaderCtor, ok := readerReader(codec, conf) - if !ok { - return nil, fmt.Errorf("codec was not recognised: %v", codec) - } - if partCtor == nil { - return nil, fmt.Errorf("unable to codec '%v' must be preceded by a structured codec", codec) - } - partCtor = chainPartIntoReaderCtor(partCtor, tmpReaderCtor) - } - if partCtor == nil { - return nil, fmt.Errorf("codec was not recognised: %v", codecs) - } - return partCtor, nil -} - -func ioReader(codec string, conf ReaderConfig) (ioReaderConstructor, bool) { - switch codec { - case "gzip": - return func(_ string, r io.ReadCloser) (io.ReadCloser, error) { - g, err := gzip.NewReader(r) - if err != nil { - r.Close() - return nil, err - } - unzipped := ioReadCloserWrapper{Reader: g, underlying: r} - return &unzipped, nil - }, true - case "pgzip": - return func(_ string, r io.ReadCloser) (io.ReadCloser, error) { - g, err := pgzip.NewReader(r) - if err != nil { - r.Close() - return nil, err - } - unzipped := ioReadCloserWrapper{Reader: g, underlying: r} - return &unzipped, nil - }, true - case "skipbom": - return func(_ string, r io.ReadCloser) (io.ReadCloser, error) { - skipBom := ioReadCloserWrapper{Reader: skipBOM(r), underlying: r} - return &skipBom, nil - }, true - } - return nil, false -} - -func readerReader(codec string, conf ReaderConfig) (readerReaderConstructor, bool) { - if codec == "multipart" { - return func(_ string, r Reader) (Reader, error) { - return newMultipartReader(r) - }, true - } - return nil, false -} - -func partReader(codec string, conf ReaderConfig) (ReaderConstructor, bool, error) { - switch codec { - case "all-bytes": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return &allBytesReader{i: r, ack: fn, consumed: false}, nil - }, true, nil - case "avro-ocf": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newAvroOCFReader(conf, "goavro", r, fn) - }, true, nil - case "lines": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newLinesReader(conf, r, fn) - }, true, nil - case "csv": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newCSVReader(r, fn, nil) - }, true, nil - case "csv-safe": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newCSVSafeReader(r, fn, nil) - }, true, nil - case "tar": - return newTarReader, true, nil - } - - if strings.HasPrefix(codec, "avro-ocf:") { - jsonEncoder := strings.TrimPrefix(codec, "avro-ocf:") - switch jsonEncoder { - case "marshaler=json": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newAvroOCFReader(conf, "json", r, fn) - }, true, nil - case "marshaler=goavro": - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newAvroOCFReader(conf, "goavro", r, fn) - }, true, nil - default: - return nil, false, errors.New("avro-ocf codec requires a non-empty marshaler") - } - } - - if strings.HasPrefix(codec, "delim:") { - by := strings.TrimPrefix(codec, "delim:") - if by == "" { - return nil, false, errors.New("custom delimiter codec requires a non-empty delimiter") - } - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newCustomDelimReader(conf, r, by, fn) - }, true, nil - } - if strings.HasPrefix(codec, "csv:") { - by := strings.TrimPrefix(codec, "csv:") - if by == "" { - return nil, false, errors.New("csv codec requires a non-empty delimiter") - } - byRunes := []rune(by) - if len(byRunes) != 1 { - return nil, false, errors.New("csv codec requires a single character delimiter") - } - byRune := byRunes[0] - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newCSVReader(r, fn, &byRune) - }, true, nil - } - if strings.HasPrefix(codec, "csv-safe:") { - by := strings.TrimPrefix(codec, "csv-safe:") - if by == "" { - return nil, false, errors.New("csv-safe codec requires a non-empty delimiter") - } - byRunes := []rune(by) - if len(byRunes) != 1 { - return nil, false, errors.New("csv-safe codec requires a single character delimiter") - } - byRune := byRunes[0] - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newCSVSafeReader(r, fn, &byRune) - }, true, nil - } - if strings.HasPrefix(codec, "chunker:") { - chunkSize, err := strconv.ParseInt(strings.TrimPrefix(codec, "chunker:"), 10, 64) - if err != nil { - return nil, false, fmt.Errorf("invalid chunk size for chunker codec: %w", err) - } - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newChunkerReader(conf, r, chunkSize, fn) - }, true, nil - } - if strings.HasPrefix(codec, "regex:") { - by := strings.TrimPrefix(codec, "regex:") - if by == "" { - return nil, false, errors.New("regex codec requires a non-empty delimiter") - } - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - return newRexExpSplitReader(conf, r, by, fn) - }, true, nil - } - return nil, false, nil -} - -func convertDeprecatedCodec(codec string) string { - switch codec { - case "csv-gzip": - return "gzip/csv" - case "tar-gzip": - return "gzip/tar" - } - return codec -} - -// GetReader returns a constructor that creates reader codecs. -func GetReader(codec string, conf ReaderConfig) (ReaderConstructor, error) { - codec = convertDeprecatedCodec(codec) - if codec == "auto" { - return autoCodec(conf), nil - } - return chainedReader(codec, conf) -} - -func autoCodec(conf ReaderConfig) ReaderConstructor { - return func(path string, r io.ReadCloser, fn ReaderAckFn) (Reader, error) { - codec := "all-bytes" - switch filepath.Ext(path) { - case ".avro": - codec = "avro-ocf" - case ".csv": - codec = "csv" - case ".csv.gz", ".csv.gzip": - codec = "gzip/csv" - case ".tar": - codec = "tar" - case ".tgz": - codec = "gzip/tar" - } - if strings.HasSuffix(path, ".tar.gzip") { - codec = "gzip/tar" - } else if strings.HasSuffix(path, ".tar.gz") { - codec = "gzip/tar" - } - - ctor, err := GetReader(codec, conf) - if err != nil { - return nil, fmt.Errorf("failed to infer codec: %v", err) - } - return ctor(path, r, fn) - } -} - -// ioReadCloserWrapper is a helper that closes both the upper and underlying reader -// when you are creating some sort of wrapped reader where you want to ensure both -// are closed. -type ioReadCloserWrapper struct { - io.Reader - underlying io.ReadCloser -} - -func (w ioReadCloserWrapper) Close() error { - if rc, ok := w.Reader.(io.Closer); ok { - rc.Close() - } - return w.underlying.Close() -} - -//------------------------------------------------------------------------------ - -type allBytesReader struct { - i io.ReadCloser - ack ReaderAckFn - consumed bool -} - -func (a *allBytesReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - if a.consumed { - return nil, nil, io.EOF - } - a.consumed = true - b, err := io.ReadAll(a.i) - if err != nil { - _ = a.ack(ctx, err) - return nil, nil, err - } - p := message.NewPart(b) - return []*message.Part{p}, a.ack, nil -} - -func (a *allBytesReader) Close(ctx context.Context) error { - if !a.consumed { - _ = a.ack(ctx, errors.New("service shutting down")) - } - return a.i.Close() -} - -//------------------------------------------------------------------------------ - -type avroOCFReader struct { - ocf *goavro.OCFReader - r io.ReadCloser - avroCodec *goavro.Codec - decoder avroDecoder - logicalTypes bool - sourceAck ReaderAckFn - - mut sync.Mutex - finished bool - pending int32 -} - -type avroDecoder func(*avroOCFReader) (*message.Part, error) - -func newAvroOCFReader(conf ReaderConfig, marshaler string, r io.ReadCloser, ackFn ReaderAckFn) (Reader, error) { - br := bufio.NewReader(r) - ocf, err := goavro.NewOCFReader(br) - if err != nil { - return nil, err - } - ocfCodec := ocf.Codec() - ocfSchema := ocfCodec.Schema() - StandardJSONFullCodec, err := goavro.NewCodecForStandardJSONFull(ocfSchema) - if err != nil { - return nil, err - } - decoder := func(a *avroOCFReader) (*message.Part, error) { - datum, err := a.ocf.Read() - if err != nil { - return nil, err - } - a.pending++ - if !a.logicalTypes { - msg := message.NewPart(nil) - msg.SetStructuredMut(datum) - return msg, nil - } - jb, err := a.avroCodec.TextualFromNative(nil, datum) - if err != nil { - return nil, err - } - return message.NewPart(jb), nil - } - - var logicalTypes bool - switch marshaler { - case "json": - logicalTypes = false - case "goavro": - logicalTypes = true - } - - return &avroOCFReader{ - ocf: ocf, - r: r, - logicalTypes: logicalTypes, - decoder: decoder, - avroCodec: StandardJSONFullCodec, - sourceAck: ackOnce(ackFn), - }, nil -} - -func (a *avroOCFReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *avroOCFReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - scanned := a.ocf.Scan() - a.mut.Lock() - defer a.mut.Unlock() - - if scanned { - part, err := a.decoder(a) - if err != nil { - return nil, nil, err - } - return []*message.Part{part}, a.ack, nil - } - err := a.ocf.Err() - if err == nil { - err = io.EOF - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - } - return nil, nil, err -} - -func (a *avroOCFReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} - -//------------------------------------------------------------------------------ - -type linesReader struct { - buf *bufio.Scanner - r io.ReadCloser - sourceAck ReaderAckFn - - mut sync.Mutex - finished bool - pending int32 -} - -func newLinesReader(conf ReaderConfig, r io.ReadCloser, ackFn ReaderAckFn) (Reader, error) { - scanner := bufio.NewScanner(r) - if conf.MaxScanTokenSize != bufio.MaxScanTokenSize { - scanner.Buffer([]byte{}, conf.MaxScanTokenSize) - } - return &linesReader{ - buf: scanner, - r: r, - sourceAck: ackOnce(ackFn), - }, nil -} - -func (a *linesReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *linesReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - scanned := a.buf.Scan() - a.mut.Lock() - defer a.mut.Unlock() - - if scanned { - a.pending++ - bytesCopy := make([]byte, len(a.buf.Bytes())) - copy(bytesCopy, a.buf.Bytes()) - return []*message.Part{message.NewPart(bytesCopy)}, a.ack, nil - } - - err := a.buf.Err() - if err == nil { - err = io.EOF - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - } - return nil, nil, err -} - -func (a *linesReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} - -//------------------------------------------------------------------------------ - -type csvReader struct { - scanner *csv.Reader - r io.ReadCloser - sourceAck ReaderAckFn - - headers []string - - mut sync.Mutex - finished bool - pending int32 -} - -func newCSVReader(r io.ReadCloser, ackFn ReaderAckFn, customComma *rune) (Reader, error) { - scanner := csv.NewReader(r) - scanner.ReuseRecord = true - if customComma != nil { - scanner.Comma = *customComma - } - - headers, err := scanner.Read() - if err != nil { - return nil, err - } - - headersCopy := make([]string, len(headers)) - copy(headersCopy, headers) - - return &csvReader{ - scanner: scanner, - r: r, - sourceAck: ackOnce(ackFn), - headers: headersCopy, - }, nil -} - -func (a *csvReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *csvReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - records, err := a.scanner.Read() - - a.mut.Lock() - defer a.mut.Unlock() - - if err != nil { - if errors.Is(err, io.EOF) { - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - } - return nil, nil, err - } - - a.pending++ - - obj := make(map[string]any, len(records)) - for i, r := range records { - obj[a.headers[i]] = r - } - - part := message.NewPart(nil) - part.SetStructuredMut(obj) - - return []*message.Part{part}, a.ack, nil -} - -func (a *csvReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} - -//------------------------------------------------------------------------------ - -type csvSafeReader struct { - *csvReader - - rowCounter int32 -} - -func newCSVSafeReader(r io.ReadCloser, ackFn ReaderAckFn, customComma *rune) (Reader, error) { - baseCsvReader, err := newCSVReader(r, ackFn, customComma) - if err != nil { - return nil, err - } - - typedReader, _ := baseCsvReader.(*csvReader) - - return &csvSafeReader{ - csvReader: typedReader, - rowCounter: 1, // 1-indexed rows - }, nil -} - -func (a *csvSafeReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - records, err := a.scanner.Read() - - a.mut.Lock() - defer a.mut.Unlock() - - if errors.Is(err, io.EOF) { - a.finished = true - return nil, nil, err - } - - a.pending++ - a.rowCounter++ - - part := message.NewPart(nil) - part.MetaSetMut("row_number", a.rowCounter) - isEmpty := len(records) == 0 || strings.TrimSpace(records[0]) == "" - part.MetaSetMut("row_empty", isEmpty) - if err != nil { - part.SetStructuredMut(map[string]any{}) - part.MetaSetMut("row_parse_error", err.Error()) - } else { - obj := make(map[string]any, len(records)) - for i, r := range records { - obj[a.headers[i]] = r - } - part.SetStructuredMut(obj) - } - - return []*message.Part{part}, a.ack, nil -} - -//------------------------------------------------------------------------------ - -type customDelimReader struct { - buf *bufio.Scanner - r io.ReadCloser - sourceAck ReaderAckFn - - mut sync.Mutex - finished bool - pending int32 -} - -func newCustomDelimReader(conf ReaderConfig, r io.ReadCloser, delim string, ackFn ReaderAckFn) (Reader, error) { - scanner := bufio.NewScanner(r) - if conf.MaxScanTokenSize != bufio.MaxScanTokenSize { - scanner.Buffer([]byte{}, conf.MaxScanTokenSize) - } - - delimBytes := []byte(delim) - - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - if i := bytes.Index(data, delimBytes); i >= 0 { - // We have a full terminated line. - return i + len(delimBytes), data[0:i], nil - } - - // If we're at EOF, we have a final, non-terminated line. Return it. - if atEOF { - return len(data), data, nil - } - - // Request more data. - return 0, nil, nil - }) - - return &customDelimReader{ - buf: scanner, - r: r, - sourceAck: ackOnce(ackFn), - }, nil -} - -func (a *customDelimReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *customDelimReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - scanned := a.buf.Scan() - - a.mut.Lock() - defer a.mut.Unlock() - - if scanned { - a.pending++ - - bytesCopy := make([]byte, len(a.buf.Bytes())) - copy(bytesCopy, a.buf.Bytes()) - return []*message.Part{message.NewPart(bytesCopy)}, a.ack, nil - } - err := a.buf.Err() - if err == nil { - err = io.EOF - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - } - return nil, nil, err -} - -func (a *customDelimReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} - -//------------------------------------------------------------------------------ - -type chunkerReader struct { - chunkSize int64 - buf *bytes.Buffer - r io.ReadCloser - sourceAck ReaderAckFn - - mut sync.Mutex - finished bool - pending int32 -} - -func newChunkerReader(conf ReaderConfig, r io.ReadCloser, chunkSize int64, ackFn ReaderAckFn) (Reader, error) { - return &chunkerReader{ - chunkSize: chunkSize, - buf: bytes.NewBuffer(make([]byte, 0, chunkSize)), - r: r, - sourceAck: ackOnce(ackFn), - }, nil -} - -func (a *chunkerReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *chunkerReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - if a.finished { - return nil, nil, io.EOF - } - - _, err := io.CopyN(a.buf, a.r, a.chunkSize) - - a.mut.Lock() - defer a.mut.Unlock() - - if err != nil { - if errors.Is(err, io.EOF) { - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - return nil, nil, err - } - } - - if a.buf.Len() > 0 { - a.pending++ - - bytesCopy := make([]byte, a.buf.Len()) - copy(bytesCopy, a.buf.Bytes()) - - a.buf.Reset() - return []*message.Part{message.NewPart(bytesCopy)}, a.ack, nil - } - - return nil, nil, err -} - -func (a *chunkerReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} - -//------------------------------------------------------------------------------ - -type tarReader struct { - buf *tar.Reader - r io.ReadCloser - sourceAck ReaderAckFn - - mut sync.Mutex - finished bool - pending int32 -} - -func newTarReader(path string, r io.ReadCloser, ackFn ReaderAckFn) (Reader, error) { - return &tarReader{ - buf: tar.NewReader(r), - r: r, - sourceAck: ackOnce(ackFn), - }, nil -} - -func (a *tarReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *tarReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - _, err := a.buf.Next() - - a.mut.Lock() - defer a.mut.Unlock() - - if err == nil { - fileBuf := bytes.Buffer{} - if _, err = fileBuf.ReadFrom(a.buf); err != nil { - _ = a.sourceAck(ctx, err) - return nil, nil, err - } - a.pending++ - return []*message.Part{message.NewPart(fileBuf.Bytes())}, a.ack, nil - } - - if errors.Is(err, io.EOF) { - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - } - return nil, nil, err -} - -func (a *tarReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} - -//------------------------------------------------------------------------------ - -type multipartReader struct { - child Reader -} - -func newMultipartReader(r Reader) (Reader, error) { - return &multipartReader{ - child: r, - }, nil -} - -func isEmpty(p []*message.Part) bool { - if len(p) == 0 { - return true - } - if len(p) == 1 && len(p[0].AsBytes()) == 0 { - return true - } - return false -} - -func (m *multipartReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - var parts []*message.Part - var acks []ReaderAckFn - - ackFn := func(ctx context.Context, err error) error { - for _, fn := range acks { - _ = fn(ctx, err) - } - return nil - } - - for { - newParts, ack, err := m.child.Next(ctx) - if err != nil { - if errors.Is(err, io.EOF) && len(parts) > 0 { - return parts, ackFn, nil - } - return nil, nil, err - } - if isEmpty(newParts) { - _ = ack(ctx, nil) - if len(parts) > 0 { - // Empty message signals batch end. - return parts, ackFn, nil - } - } else { - parts = append(parts, newParts...) - acks = append(acks, ack) - } - } -} - -func (m *multipartReader) Close(ctx context.Context) error { - return m.child.Close(ctx) -} - -//------------------------------------------------------------------------------ - -type regexReader struct { - buf *bufio.Scanner - r io.ReadCloser - sourceAck ReaderAckFn - - mut sync.Mutex - finished bool - pending int32 -} - -func newRexExpSplitReader(conf ReaderConfig, r io.ReadCloser, regex string, ackFn ReaderAckFn) (Reader, error) { - scanner := bufio.NewScanner(r) - if conf.MaxScanTokenSize != bufio.MaxScanTokenSize { - scanner.Buffer([]byte{}, conf.MaxScanTokenSize) - } - - compiled, err := regexp.Compile(regex) - if err != nil { - return nil, err - } - - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - loc := compiled.FindAllIndex(data, 2) - if loc == nil { - if atEOF { - return len(data), data, nil - } - return 0, nil, nil - } - - if len(loc) == 1 { - if atEOF { - if loc[0][0] == 0 { - return len(data), data, nil - } - return loc[0][0], data[0:loc[0][0]], nil - } - return 0, nil, nil - } - if loc[0][0] == 0 { - return loc[1][0], data[0:loc[1][0]], nil - } - return loc[0][0], data[0:loc[0][0]], nil - }) - - return ®exReader{ - buf: scanner, - r: r, - sourceAck: ackOnce(ackFn), - }, nil -} - -func (a *regexReader) ack(ctx context.Context, err error) error { - a.mut.Lock() - a.pending-- - doAck := a.pending == 0 && a.finished - a.mut.Unlock() - - if err != nil { - return a.sourceAck(ctx, err) - } - if doAck { - return a.sourceAck(ctx, nil) - } - return nil -} - -func (a *regexReader) Next(ctx context.Context) ([]*message.Part, ReaderAckFn, error) { - scanned := a.buf.Scan() - - a.mut.Lock() - defer a.mut.Unlock() - - if scanned { - a.pending++ - - bytesCopy := make([]byte, len(a.buf.Bytes())) - copy(bytesCopy, a.buf.Bytes()) - return []*message.Part{message.NewPart(bytesCopy)}, a.ack, nil - } - err := a.buf.Err() - if err == nil { - err = io.EOF - a.finished = true - } else { - _ = a.sourceAck(ctx, err) - } - return nil, nil, err -} - -func (a *regexReader) Close(ctx context.Context) error { - a.mut.Lock() - defer a.mut.Unlock() - - if !a.finished { - _ = a.sourceAck(ctx, errors.New("service shutting down")) - } - if a.pending == 0 { - _ = a.sourceAck(ctx, nil) - } - return a.r.Close() -} diff --git a/internal/codec/reader_test.go b/internal/codec/reader_test.go deleted file mode 100644 index 560259de37..0000000000 --- a/internal/codec/reader_test.go +++ /dev/null @@ -1,927 +0,0 @@ -package codec - -import ( - "archive/tar" - "bytes" - "context" - "errors" - "fmt" - "io" - "sync" - "testing" - - "github.com/klauspost/compress/gzip" - "github.com/klauspost/pgzip" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -type noopCloser struct { - io.Reader - returnEOFOnRead bool -} - -func (n noopCloser) Read(p []byte) (int, error) { - byteCount, err := n.Reader.Read(p) - if err != nil { - return byteCount, err - } - - if n.returnEOFOnRead { - return byteCount, io.EOF - } - - return byteCount, err -} - -func (n noopCloser) Close() error { - return nil -} - -type microReader struct { - io.Reader -} - -func (n microReader) Read(p []byte) (int, error) { - // Only a max of 5 bytes at a time - if len(p) < 5 { - return n.Reader.Read(p) - } - - micro := make([]byte, 5) - byteCount, err := n.Reader.Read(micro) - if err != nil { - return byteCount, err - } - - _ = copy(p, micro) - return byteCount, nil -} - -func testReaderSuite(t *testing.T, codec, path string, data []byte, expected ...string) { - t.Run("close before reading", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - assert.NoError(t, r.Close(context.Background())) - assert.EqualError(t, ack, "service shutting down") - }) - - t.Run("returns all data even if EOF is encountered during the last read", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, &buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - for i, exp := range expected { - if i == len(expected)-1 { - buf.returnEOFOnRead = true - } - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, 1) - assert.Equal(t, exp, string(p[0].AsBytes())) - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("can consume micro flushes", func(t *testing.T) { - buf := noopCloser{microReader{bytes.NewReader(data)}, false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, 1) - assert.Equal(t, exp, string(p[0].AsBytes())) - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("acks ordered reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, 1) - assert.Equal(t, exp, string(p[0].AsBytes())) - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("acks unordered reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - var ackFns []ReaderAckFn - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.Len(t, p, 1) - ackFns = append(ackFns, ackFn) - assert.Equal(t, exp, string(p[0].AsBytes())) - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - assert.NoError(t, r.Close(context.Background())) - - for _, ackFn := range ackFns { - require.NoError(t, ackFn(context.Background(), nil)) - } - - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("acks parallel reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - wg := sync.WaitGroup{} - wg.Add(len(expected)) - - for _, exp := range expected { - exp := exp - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.Len(t, p, 1) - assert.Equal(t, exp, string(p[0].AsBytes())) - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - - go func() { - defer wg.Done() - require.NoError(t, ackFn(context.Background(), nil)) - }() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - wg.Wait() - assert.NoError(t, r.Close(context.Background())) - - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - if len(expected) > 0 { - t.Run("nacks unordered reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - exp := errors.New("real err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - var ackFns []ReaderAckFn - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.Len(t, p, 1) - ackFns = append(ackFns, ackFn) - assert.Equal(t, exp, string(p[0].AsBytes())) - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - assert.NoError(t, r.Close(context.Background())) - - for i, ackFn := range ackFns { - if i == 0 { - require.NoError(t, ackFn(context.Background(), exp)) - } else { - require.NoError(t, ackFn(context.Background(), nil)) - } - } - - assert.EqualError(t, ack, exp.Error()) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - } -} - -func TestLinesReader(t *testing.T) { - data := []byte("foo\nbar\nbaz") - testReaderSuite(t, "lines", "", data, "foo", "bar", "baz") - - data = []byte("") - testReaderSuite(t, "lines", "", data) -} - -func TestCSVReader(t *testing.T) { - data := []byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3") - testReaderSuite( - t, "csv", "", data, - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) - - data = []byte("col1,col2,col3") - testReaderSuite(t, "csv", "", data) -} - -func TestCSVSafeReader(t *testing.T) { - data := []byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3") - testReaderSuite( - t, "csv-safe", "", data, - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) - - data = []byte("col1,col2,col3") - testReaderSuite(t, "csv-safe", "", data) - - data = []byte("col1,col2,col3\nfoo1,bar1\nfoo2,bar2,baz2\nfoo3,bar3,baz3") - testReaderSuite( - t, "csv-safe", "", data, - `{}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) -} - -func TestCSVSafeCustomDelimiterReader(t *testing.T) { - data := []byte("col1|col2|col3\nfoo1|bar1|baz1\nfoo2|bar2|baz2\nfoo3|bar3|baz3") - testReaderSuite( - t, "csv-safe:|", "", data, - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) - - data = []byte("col1|col2|col3") - testReaderSuite(t, "csv-safe:|", "", data) - - data = []byte("col1|col2|col3\nfoo1|bar1\nfoo2|bar2|baz2\nfoo3|bar3|baz3") - testReaderSuite( - t, "csv-safe:|", "", data, - `{}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) -} - -func assertPartMetadataEqual[T any](t *testing.T, p *message.Part, key string, value T) { - rawVal, ok := p.MetaGetMut(key) - assert.True(t, ok) - typedVal, ok := rawVal.(T) - assert.True(t, ok) - assert.Equal(t, value, typedVal) -} - -func TestCsvSafeReaderMetadata(t *testing.T) { - data := []byte("col1,col2,col3\nfoo1,bar1,baz1\n \nfoo2,bar2\n") - expected := []string{ - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, // valid line - `{}`, // empty line - `{}`, // missing a column - } - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader("csv-safe", NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor("", buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - allReads := map[string][]byte{} - - for i, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, 1) - assert.Equal(t, exp, string(p[0].AsBytes())) - assertPartMetadataEqual(t, p[0], "row_number", int32(i+2)) // header row is row 1, and compensate for index-0 range - allReads[string(p[0].AsBytes())] = p[0].AsBytes() - if i == 1 { // empty row - assertPartMetadataEqual(t, p[0], "row_empty", true) - } else if i == 2 { // row with missing column - assertPartMetadataEqual(t, p[0], "row_parse_error", "record on line 4: wrong number of fields") - } - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) -} - -func TestPSVReader(t *testing.T) { - data := []byte("col1|col2|col3\nfoo1|bar1|baz1\nfoo2|bar2|baz2\nfoo3|bar3|baz3") - testReaderSuite( - t, "csv:|", "", data, - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) - - data = []byte("col1|col2|col3") - testReaderSuite(t, "csv:|", "", data) -} - -func TestAutoReader(t *testing.T) { - data := []byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3") - testReaderSuite( - t, "auto", "foo.csv", data, - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) - - data = []byte("col1,col2,col3") - testReaderSuite(t, "auto", "foo.csv", data) -} - -func TestCSVGzipReader(t *testing.T) { - var gzipBuf bytes.Buffer - zw := gzip.NewWriter(&gzipBuf) - _, _ = zw.Write([]byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3")) - zw.Close() - - testReaderSuite( - t, "gzip/csv", "", gzipBuf.Bytes(), - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) -} - -func TestCSVPGzipReader(t *testing.T) { - var gzipBuf bytes.Buffer - zw := pgzip.NewWriter(&gzipBuf) - _, _ = zw.Write([]byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3")) - zw.Close() - - testReaderSuite( - t, "pgzip/csv", "", gzipBuf.Bytes(), - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) -} - -func TestCSVGzipReaderOld(t *testing.T) { - var gzipBuf bytes.Buffer - zw := gzip.NewWriter(&gzipBuf) - _, _ = zw.Write([]byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3")) - zw.Close() - - testReaderSuite( - t, "csv-gzip", "", gzipBuf.Bytes(), - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) -} - -func TestCSVSkipBOMReader(t *testing.T) { - // https://en.wikipedia.org/wiki/Byte_order_mark - bom := []byte{0xef, 0xbb, 0xbf} - data := append(bom, []byte("col1,col2,col3\nfoo1,bar1,baz1\nfoo2,bar2,baz2\nfoo3,bar3,baz3")...) - testReaderSuite( - t, "skipbom/csv", "", data, - `{"col1":"foo1","col2":"bar1","col3":"baz1"}`, - `{"col1":"foo2","col2":"bar2","col3":"baz2"}`, - `{"col1":"foo3","col2":"bar3","col3":"baz3"}`, - ) - - data = []byte("col1,col2,col3") - testReaderSuite(t, "csv", "", data) -} - -func TestAllBytesReader(t *testing.T) { - data := []byte("foo\nbar\nbaz") - testReaderSuite(t, "all-bytes", "", data, "foo\nbar\nbaz") -} - -func TestDelimReader(t *testing.T) { - data := []byte("fooXbarXbaz") - testReaderSuite(t, "delim:X", "", data, "foo", "bar", "baz") - - data = []byte("") - testReaderSuite(t, "delim:X", "", data) -} - -func TestChunkerReader(t *testing.T) { - t.Run("with exact chunks", func(t *testing.T) { - data := []byte("foobarbaz") - testReaderSuite(t, "chunker:3", "", data, "foo", "bar", "baz") - }) - - t.Run("with remainder", func(t *testing.T) { - data := []byte("fooxbarybaz") - testReaderSuite(t, "chunker:3", "", data, "foo", "xba", "ryb", "az") - }) - - t.Run("tiny chunks", func(t *testing.T) { - data := []byte("") - testReaderSuite(t, "chunker:1", "", data) - }) - - t.Run("larger chunks", func(t *testing.T) { - data := []byte("hell1worldhell2worldhell3worldhell4worldhell5worldhell6world") - testReaderSuite( - t, "chunker:10", "", data, - "hell1world", "hell2world", "hell3world", - "hell4world", "hell5world", "hell6world", - ) - }) -} - -func TestTarReader(t *testing.T) { - input := []string{ - "first document", - "second document", - "third document", - } - - var tarBuf bytes.Buffer - tw := tar.NewWriter(&tarBuf) - for i := range input { - hdr := &tar.Header{ - Name: fmt.Sprintf("testfile%v", i), - Mode: 0o600, - Size: int64(len(input[i])), - } - - err := tw.WriteHeader(hdr) - require.NoError(t, err) - - _, err = tw.Write([]byte(input[i])) - require.NoError(t, err) - } - require.NoError(t, tw.Close()) - - testReaderSuite(t, "tar", "", tarBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tar", tarBuf.Bytes(), input...) -} - -func TestTarGzipReader(t *testing.T) { - input := []string{ - "first document", - "second document", - "third document", - } - - var gzipBuf bytes.Buffer - - zw := gzip.NewWriter(&gzipBuf) - tw := tar.NewWriter(zw) - for i := range input { - hdr := &tar.Header{ - Name: fmt.Sprintf("testfile%v", i), - Mode: 0o600, - Size: int64(len(input[i])), - } - - err := tw.WriteHeader(hdr) - require.NoError(t, err) - - _, err = tw.Write([]byte(input[i])) - require.NoError(t, err) - } - require.NoError(t, tw.Close()) - require.NoError(t, zw.Close()) - - testReaderSuite(t, "gzip/tar", "", gzipBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tar.gz", gzipBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tar.gzip", gzipBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tgz", gzipBuf.Bytes(), input...) -} - -func TestTarPGzipReader(t *testing.T) { - input := []string{ - "first document", - "second document", - "third document", - } - - var gzipBuf bytes.Buffer - - zw := pgzip.NewWriter(&gzipBuf) - tw := tar.NewWriter(zw) - for i := range input { - hdr := &tar.Header{ - Name: fmt.Sprintf("testfile%v", i), - Mode: 0o600, - Size: int64(len(input[i])), - } - - err := tw.WriteHeader(hdr) - require.NoError(t, err) - - _, err = tw.Write([]byte(input[i])) - require.NoError(t, err) - } - require.NoError(t, tw.Close()) - require.NoError(t, zw.Close()) - - testReaderSuite(t, "pgzip/tar", "", gzipBuf.Bytes(), input...) -} - -func TestTarGzipReaderOld(t *testing.T) { - input := []string{ - "first document", - "second document", - "third document", - } - - var gzipBuf bytes.Buffer - - zw := gzip.NewWriter(&gzipBuf) - tw := tar.NewWriter(zw) - for i := range input { - hdr := &tar.Header{ - Name: fmt.Sprintf("testfile%v", i), - Mode: 0o600, - Size: int64(len(input[i])), - } - - err := tw.WriteHeader(hdr) - require.NoError(t, err) - - _, err = tw.Write([]byte(input[i])) - require.NoError(t, err) - } - require.NoError(t, tw.Close()) - require.NoError(t, zw.Close()) - - testReaderSuite(t, "tar-gzip", "", gzipBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tar.gz", gzipBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tar.gzip", gzipBuf.Bytes(), input...) - testReaderSuite(t, "auto", "foo.tgz", gzipBuf.Bytes(), input...) -} - -func strsFromParts(ps []*message.Part) []string { - var strs []string - for _, part := range ps { - strs = append(strs, string(part.AsBytes())) - } - return strs -} - -func testMultipartReaderSuite(t *testing.T, codec, path string, data []byte, expected ...[]string) { - t.Run("close before reading", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - assert.NoError(t, r.Close(context.Background())) - assert.EqualError(t, ack, "service shutting down") - }) - - t.Run("returns all data even if EOF is encountered during the last read", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, &buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - for i, exp := range expected { - if i == len(expected)-1 { - buf.returnEOFOnRead = true - } - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, len(exp)) - assert.Equal(t, exp, strsFromParts(p)) - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - }) - - t.Run("acks ordered reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, len(exp)) - assert.Equal(t, exp, strsFromParts(p)) - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - }) - - t.Run("acks unordered reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - var ackFns []ReaderAckFn - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.Len(t, p, len(exp)) - ackFns = append(ackFns, ackFn) - assert.Equal(t, exp, strsFromParts(p)) - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - assert.NoError(t, r.Close(context.Background())) - - for _, ackFn := range ackFns { - require.NoError(t, ackFn(context.Background(), nil)) - } - - assert.NoError(t, ack) - }) - - t.Run("acks parallel reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(len(expected)) - - for _, exp := range expected { - exp := exp - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.Len(t, p, len(exp)) - assert.Equal(t, exp, strsFromParts(p)) - - go func() { - defer wg.Done() - require.NoError(t, ackFn(context.Background(), nil)) - }() - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - - wg.Wait() - assert.NoError(t, r.Close(context.Background())) - - assert.NoError(t, ack) - }) - - if len(expected) > 0 { - t.Run("nacks unordered reads", func(t *testing.T) { - buf := noopCloser{bytes.NewReader(data), false} - - ctor, err := GetReader(codec, NewReaderConfig()) - require.NoError(t, err) - - ack := errors.New("default err") - exp := errors.New("real err") - - r, err := ctor(path, buf, func(ctx context.Context, err error) error { - ack = err - return nil - }) - require.NoError(t, err) - - var ackFns []ReaderAckFn - for _, exp := range expected { - p, ackFn, err := r.Next(context.Background()) - require.NoError(t, err) - require.Len(t, p, len(exp)) - ackFns = append(ackFns, ackFn) - assert.Equal(t, exp, strsFromParts(p)) - } - - _, _, err = r.Next(context.Background()) - assert.EqualError(t, err, "EOF") - assert.NoError(t, r.Close(context.Background())) - - for i, ackFn := range ackFns { - if i == 0 { - require.NoError(t, ackFn(context.Background(), exp)) - } else { - require.NoError(t, ackFn(context.Background(), nil)) - } - } - - assert.EqualError(t, ack, exp.Error()) - }) - } -} - -func TestMultipartLinesReader(t *testing.T) { - data := []byte("foo\nbar\nbaz\n\nbuz\nqux\nquz\n") - testMultipartReaderSuite(t, "lines/multipart", "", data, []string{"foo", "bar", "baz"}, []string{"buz", "qux", "quz"}) - - data = []byte("") - testReaderSuite(t, "lines/multipart", "", data) -} - -func TestRegexpSplitReader(t *testing.T) { - data := []byte("foo\nbar\nbaz") - testReaderSuite(t, "regex:(?m)^", "", data, "foo\n", "bar\n", "baz") - - data = []byte("foo\nbar\nsplit\nbaz\nsplitsplit") - testReaderSuite(t, "regex:split", "", data, "foo\nbar\n", "split\nbaz\n", "split", "split") - - data = []byte("split") - testReaderSuite(t, "regex:\\n", "", data, "split") - testReaderSuite(t, "regex:split", "", data, "split") - - data = []byte("foo\nbar\nsplit\nbaz\nsplitsplit") - testReaderSuite(t, "regex:\\n", "", data, "foo", "\nbar", "\nsplit", "\nbaz", "\nsplitsplit") - - data = []byte("foo\nbar\nsplit\nbaz") - testReaderSuite(t, "regex:\\n", "", data, "foo", "\nbar", "\nsplit", "\nbaz") - - data = []byte("20:20:22 ERROR\nCode\n20:20:21 INFO\n20:20:21 INFO\n20:20:22 ERROR\nCode\n") - testReaderSuite(t, "regex:\\n\\d", "", data, "20:20:22 ERROR\nCode", "\n20:20:21 INFO", "\n20:20:21 INFO", "\n20:20:22 ERROR\nCode\n") - - data = []byte("20:20:22 ERROR\nCode\n20:20:21 INFO\n20:20:21 INFO\n20:20\n20:20:22 ERROR\nCode\n2022") - testReaderSuite(t, "regex:(?m)^\\d\\d:\\d\\d:\\d\\d", "", data, "20:20:22 ERROR\nCode\n", "20:20:21 INFO\n", "20:20:21 INFO\n20:20\n", "20:20:22 ERROR\nCode\n2022") - - data = []byte("") - testReaderSuite(t, "regex:split", "", data) -} diff --git a/internal/codec/skip_group_reader.go b/internal/codec/skip_group_reader.go deleted file mode 100644 index 97bdd0bb88..0000000000 --- a/internal/codec/skip_group_reader.go +++ /dev/null @@ -1,96 +0,0 @@ -package codec - -import ( - "io" - "sort" -) - -func skipBOM(r io.Reader) io.Reader { - return skipGroup(r, - []byte{0x00, 0x00, 0xFE, 0xFF}, // UTF32BigEndianBOM4 - []byte{0xFF, 0xFE, 0x00, 0x00}, // UTF32LittleEndianBOM4 - []byte{0xEF, 0xBB, 0xBF}, // UTF8BOM3 - []byte{0xFE, 0xFF}, // UTF16BigEndianBOM2 - []byte{0xFF, 0xFE}, // UTF16LittleEndianBOM2 - ) -} - -func skipGroup(rd io.Reader, groups ...[]byte) io.Reader { - if len(groups) == 0 { - return rd - } - - sort.Slice(groups, func(i, j int) bool { - return len(groups[i]) > len(groups[j]) - }) - - buf, err := readUpToMax(rd, len(groups[0])) - -groupLoop: - for _, g := range groups { - if len(buf) < len(g) { - continue - } - for i, b := range g { - if buf[i] != b { - continue groupLoop - } - } - if buf = buf[len(g):]; len(buf) == 0 { - buf = nil - } - break - } - - return &bufPriorityReader{ - rd: rd, - buf: buf, - err: err, - } -} - -func readUpToMax(r io.Reader, max int) (buf []byte, err error) { - if max == 0 { - return - } - - buf = make([]byte, max) - - var readLen int - for err == nil && readLen < max { - var n int - n, err = r.Read(buf[readLen:]) - readLen += n - } - buf = buf[:readLen] - return -} - -//------------------------------------------------------------------------------ - -// Reads from a buf and err as priority over the underlying io.Reader. -type bufPriorityReader struct { - rd io.Reader - buf []byte - err error -} - -func (r *bufPriorityReader) Read(p []byte) (n int, err error) { - if len(p) == 0 { - return - } - - if r.buf == nil { - if err = r.err; err != nil { - r.err = nil - return - } - return r.rd.Read(p) - } - - n = copy(p, r.buf) - if r.buf = r.buf[n:]; len(r.buf) == 0 { - r.buf = nil - } - return -} diff --git a/internal/codec/skip_group_reader_test.go b/internal/codec/skip_group_reader_test.go deleted file mode 100644 index 2303a73a95..0000000000 --- a/internal/codec/skip_group_reader_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package codec - -import ( - "bytes" - "fmt" - "io" - "testing" - "testing/iotest" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestReadUpTo(t *testing.T) { - for _, test := range []struct { - name string - input string - skips [][]byte - expected string - }{ - { - name: "no groups", - input: "foo", - expected: "foo", - }, - { - name: "no input", - input: "", - skips: [][]byte{ - []byte("hello "), - }, - expected: "", - }, - { - name: "an empty group", - input: "hello world", - skips: [][]byte{ - []byte("not this"), - []byte(""), - }, - expected: "hello world", - }, - { - name: "easy match", - input: "hello world", - expected: "world", - skips: [][]byte{ - []byte("hello "), - }, - }, - { - name: "exact skip match", - input: "foo", - expected: "", - skips: [][]byte{ - []byte("foo"), - }, - }, - { - name: "max is bigger", - input: "foa", - expected: "a", - skips: [][]byte{ - []byte("fo"), - []byte("what this is huge"), - }, - }, - { - name: "order doesnt matter", - input: "helloworld", - expected: "ld", - skips: [][]byte{ - []byte("hellowoa"), - []byte("hella"), - []byte("hello"), - []byte("hellowor"), - []byte("hea"), - }, - }, - } { - test := test - for _, readWrapper := range []struct { - name string - fn func(io.Reader) io.Reader - }{ - {"full", func(r io.Reader) io.Reader { return r }}, - {"one_byte", iotest.OneByteReader}, - } { - t.Run(fmt.Sprintf("%v_%v", test.name, readWrapper.name), func(t *testing.T) { - testReader := skipGroup(bytes.NewReader([]byte(test.input)), test.skips...) - output, err := io.ReadAll(testReader) - require.NoError(t, err) - assert.Equal(t, test.expected, string(output)) - }) - } - } -} diff --git a/internal/codec/writer.go b/internal/codec/writer.go deleted file mode 100644 index 0cb0bf9f5a..0000000000 --- a/internal/codec/writer.go +++ /dev/null @@ -1,63 +0,0 @@ -package codec - -import ( - "bytes" - "errors" - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -var WriterDocs = NewWriterDocs("codec") - -func NewWriterDocs(name string) docs.FieldSpec { - return docs.FieldString( - name, "The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter.", "lines", "delim:\t", "delim:foobar", - ).HasAnnotatedOptions( - "all-bytes", "Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted.", - "append", "Append each message to the output stream without any delimiter or special encoding.", - "lines", "Append each message to the output stream followed by a line break.", - "delim:x", "Append each message to the output stream followed by a custom delimiter.", - ).LinterBlobl("") -} - -//------------------------------------------------------------------------------ - -type SuffixFn func(data []byte) ([]byte, bool) - -type WriterConfig struct { - Append bool -} - -func GetWriter(codec string) (sFn SuffixFn, appendMode bool, err error) { - switch codec { - case "all-bytes": - return func(data []byte) ([]byte, bool) { return nil, false }, false, nil - case "append": - return customDelimSuffixFn(""), true, nil - case "lines": - return customDelimSuffixFn("\n"), true, nil - } - if strings.HasPrefix(codec, "delim:") { - by := strings.TrimPrefix(codec, "delim:") - if by == "" { - return nil, false, errors.New("custom delimiter codec requires a non-empty delimiter") - } - return customDelimSuffixFn(by), true, nil - } - return nil, false, fmt.Errorf("codec was not recognised: %v", codec) -} - -func customDelimSuffixFn(suffix string) SuffixFn { - suffixB := []byte(suffix) - return func(data []byte) ([]byte, bool) { - if len(suffixB) == 0 { - return nil, false - } - if !bytes.HasSuffix(data, suffixB) { - return suffixB, true - } - return nil, false - } -} diff --git a/internal/component/buffer/config.go b/internal/component/buffer/config.go deleted file mode 100644 index 74aab668aa..0000000000 --- a/internal/component/buffer/config.go +++ /dev/null @@ -1,68 +0,0 @@ -package buffer - -import ( - "fmt" - - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all buffer types. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -type Config struct { - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` -} - -// NewConfig returns a configuration struct fully populated with default values. -func NewConfig() Config { - return Config{ - Type: "none", - Plugin: nil, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeBuffer, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeBuffer, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/buffer/interface.go b/internal/component/buffer/interface.go deleted file mode 100644 index 2d80955ade..0000000000 --- a/internal/component/buffer/interface.go +++ /dev/null @@ -1,32 +0,0 @@ -package buffer - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Streamed is an interface implemented by all buffer types that provides stream -// based methods. -type Streamed interface { - // TransactionChan returns a channel used for consuming transactions from - // this type. Every transaction received must be resolved before another - // transaction will be sent. - TransactionChan() <-chan message.Transaction - - // Consume starts the type receiving transactions from a Transactor. - Consume(<-chan message.Transaction) error - - // TriggerStopConsuming instructs the buffer to cut off the producer it is - // consuming from. It will then enter a mode whereby messages can only be - // read, and when the buffer is empty it will shut down. - TriggerStopConsuming() - - // TriggerCloseNow triggers the shut down of this component but should not - // block the calling goroutine. - TriggerCloseNow() - - // WaitForClose is a blocking call to wait until the component has finished - // shutting down and cleaning up resources. - WaitForClose(ctx context.Context) error -} diff --git a/internal/component/buffer/memory_buffer_test.go b/internal/component/buffer/memory_buffer_test.go deleted file mode 100644 index fe9c8bf42f..0000000000 --- a/internal/component/buffer/memory_buffer_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package buffer - -import ( - "context" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type memoryBuffer struct { - messages chan message.Batch - endOfInputChan chan struct{} - closeOnce sync.Once -} - -func newMemoryBuffer(n int) *memoryBuffer { - return &memoryBuffer{ - messages: make(chan message.Batch, n), - endOfInputChan: make(chan struct{}), - } -} - -func (m *memoryBuffer) Read(ctx context.Context) (message.Batch, AckFunc, error) { - select { - case msg := <-m.messages: - return msg, func(c context.Context, e error) error { - return nil - }, nil - case <-ctx.Done(): - return nil, nil, ctx.Err() - case <-m.endOfInputChan: - // Input has ended, so return ErrEndOfBuffer if our buffer is empty. - select { - case msg := <-m.messages: - return msg, func(c context.Context, e error) error { - // YOLO: Drop messages that are nacked - return nil - }, nil - default: - return nil, nil, component.ErrTypeClosed - } - } -} - -func (m *memoryBuffer) Write(ctx context.Context, msg message.Batch, aFn AckFunc) error { - select { - case m.messages <- msg: - if err := aFn(context.Background(), nil); err != nil { - return err - } - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func (m *memoryBuffer) EndOfInput() { - m.closeOnce.Do(func() { - close(m.endOfInputChan) - }) -} - -func (m *memoryBuffer) Close(ctx context.Context) error { - // Nothing to clean up - return nil -} diff --git a/internal/component/buffer/stream.go b/internal/component/buffer/stream.go deleted file mode 100644 index 22ced36dd2..0000000000 --- a/internal/component/buffer/stream.go +++ /dev/null @@ -1,261 +0,0 @@ -package buffer - -import ( - "context" - "errors" - "sync" - "time" - - "go.opentelemetry.io/otel/trace" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/old/util/throttle" - "github.com/benthosdev/benthos/v4/internal/tracing" -) - -// AckFunc is a function used to acknowledge receipt of a message batch from a -// buffer. The provided error indicates whether the message batch was -// successfully delivered. Returns an error if the acknowledge was not -// propagated. -type AckFunc func(context.Context, error) error - -// ReaderWriter is a read/write interface implemented by buffers. -type ReaderWriter interface { - // Read the next oldest message batch. If the buffer has a persisted store - // the message is preserved until the returned AckFunc is called. Some - // temporal buffer implementations such as windowers will ignore the ack - // func. - Read(context.Context) (message.Batch, AckFunc, error) - - // Write a new message batch to the stack. - Write(context.Context, message.Batch, AckFunc) error - - // EndOfInput indicates to the buffer that the input has ended and that once - // the buffer is depleted it should return component.ErrTypeClosed from Read in - // order to gracefully shut down the pipeline. - // - // EndOfInput should be idempotent as it may be called more than once. - EndOfInput() - - // Close the buffer and all resources it has, messages should no longer be - // written or read by the implementation and it should clean up all - // resources. - Close(context.Context) error -} - -// Stream wraps a read/write buffer implementation with a channel based -// streaming component that satisfies the internal Benthos Consumer and Producer -// interfaces. -type Stream struct { - stats metrics.Type - log log.Modular - tracer trace.TracerProvider - typeStr string - - buffer ReaderWriter - - errThrottle *throttle.Type - shutSig *shutdown.Signaller - - messagesIn <-chan message.Transaction - messagesOut chan message.Transaction - - closedWG sync.WaitGroup -} - -// NewStream creates a new Producer/Consumer around a buffer. -func NewStream(typeStr string, buffer ReaderWriter, mgr component.Observability) Streamed { - m := Stream{ - typeStr: typeStr, - stats: mgr.Metrics(), - log: mgr.Logger(), - tracer: mgr.Tracer(), - buffer: buffer, - shutSig: shutdown.NewSignaller(), - messagesOut: make(chan message.Transaction), - } - m.errThrottle = throttle.New(throttle.OptCloseChan(m.shutSig.SoftStopChan())) - return &m -} - -//------------------------------------------------------------------------------ - -// inputLoop is an internal loop that brokers incoming messages to the buffer. -func (m *Stream) inputLoop() { - var ackGroup sync.WaitGroup - - defer func() { - m.buffer.EndOfInput() - ackGroup.Wait() - m.closedWG.Done() - }() - - var ( - mReceivedCount = m.stats.GetCounter("buffer_received") - mReceivedBatchCount = m.stats.GetCounter("buffer_batch_received") - ) - - closeAtLeisureCtx, doneLeisure := m.shutSig.SoftStopCtx(context.Background()) - defer doneLeisure() - - closeNowCtx, doneNow := m.shutSig.HardStopCtx(context.Background()) - defer doneNow() - - for { - var tr message.Transaction - var open bool - select { - case tr, open = <-m.messagesIn: - if !open { - return - } - case <-m.shutSig.SoftStopChan(): - return - } - - ackGroup.Add(1) - var ackOnce sync.Once - ackFunc := func(ctx context.Context, ackErr error) (err error) { - ackOnce.Do(func() { - err = tr.Ack(ctx, ackErr) - ackGroup.Done() - }) - return - } - - batchLen := tr.Payload.Len() - - writeBatch, _ := tracing.WithSiblingSpans(m.tracer, m.typeStr, tr.Payload) - err := m.buffer.Write(closeAtLeisureCtx, writeBatch, ackFunc) - if err == nil { - mReceivedCount.Incr(int64(batchLen)) - mReceivedBatchCount.Incr(1) - } else { - _ = ackFunc(closeNowCtx, err) - } - } -} - -// outputLoop is an internal loop brokers buffer messages to output pipe. -func (m *Stream) outputLoop() { - var ackGroup sync.WaitGroup - - closeNowCtx, done := m.shutSig.HardStopCtx(context.Background()) - defer done() - - defer func() { - ackGroup.Wait() - _ = m.buffer.Close(context.Background()) - close(m.messagesOut) - m.closedWG.Done() - }() - - var ( - mSent = m.stats.GetCounter("buffer_sent") - mSentBatch = m.stats.GetCounter("buffer_batch_sent") - mLatency = m.stats.GetTimer("buffer_latency_ns") - ) - - for { - msg, ackFunc, err := m.buffer.Read(closeNowCtx) - if err != nil { - if err != component.ErrTypeClosed && !errors.Is(err, context.Canceled) { - m.log.Error("Failed to read buffer: %v\n", err) - if !m.errThrottle.Retry() { - return - } - } else { - // If our buffer is closed then we exit. - return - } - continue - } - - // It's possible that the buffer wiped our previous root span. - tracing.InitSpans(m.tracer, m.typeStr, msg) - - batchLen := msg.Len() - - m.errThrottle.Reset() - resChan := make(chan error, 1) - select { - case m.messagesOut <- message.NewTransaction(msg, resChan): - case <-m.shutSig.HardStopChan(): - return - } - - startedAt := time.Now() - - mSent.Incr(int64(batchLen)) - mSentBatch.Incr(1) - ackGroup.Add(1) - - go func() { - defer ackGroup.Done() - select { - case res, open := <-resChan: - if !open { - return - } - mLatency.Timing(time.Since(startedAt).Nanoseconds()) - tracing.FinishSpans(msg) - if ackErr := ackFunc(closeNowCtx, res); ackErr != nil { - if ackErr != component.ErrTypeClosed { - m.log.Error("Failed to ack buffer message: %v\n", ackErr) - } - } - case <-m.shutSig.HardStopChan(): - return - } - }() - } -} - -// Consume assigns a messages channel for the output to read. -func (m *Stream) Consume(msgs <-chan message.Transaction) error { - if m.messagesIn != nil { - return component.ErrAlreadyStarted - } - m.messagesIn = msgs - - m.closedWG.Add(2) - go m.inputLoop() - go m.outputLoop() - go func() { - m.closedWG.Wait() - m.shutSig.TriggerHasStopped() - }() - return nil -} - -// TransactionChan returns the channel used for consuming messages from this -// buffer. -func (m *Stream) TransactionChan() <-chan message.Transaction { - return m.messagesOut -} - -// TriggerStopConsuming instructs the buffer to stop consuming messages and -// close once the buffer is empty. -func (m *Stream) TriggerStopConsuming() { - m.shutSig.TriggerSoftStop() -} - -// TriggerCloseNow shuts down the Stream and stops processing messages. -func (m *Stream) TriggerCloseNow() { - m.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the Stream output has closed down. -func (m *Stream) WaitForClose(ctx context.Context) error { - select { - case <-m.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/component/buffer/stream_test.go b/internal/component/buffer/stream_test.go deleted file mode 100644 index 1d3c82f1ea..0000000000 --- a/internal/component/buffer/stream_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package buffer - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestStreamMemoryBuffer(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - var incr, total uint8 = 100, 50 - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - b := NewStream("meow", newMemoryBuffer(int(total)), component.NoopObservability()) - require.NoError(t, b.Consume(tChan)) - - var i uint8 - - // Check correct flow no blocking - for ; i < total; i++ { - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)) - msgBytes[0][0] = i - - select { - // Send to buffer - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for unbuffered message %v send", i) - } - - // Instant response from buffer - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for unbuffered message %v response", i) - } - - // Receive on output - var outTr message.Transaction - select { - case outTr = <-b.TransactionChan(): - assert.Equal(t, i, outTr.Payload.Get(0).AsBytes()[0]) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for unbuffered message %v read", i) - } - - // Response from output - require.NoError(t, outTr.Ack(tCtx, nil)) - } - - for i = 0; i <= total; i++ { - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)) - msgBytes[0][0] = i - - select { - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v send", i) - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v response", i) - } - } - - // Should have reached limit here - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)+1) - - select { - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message send") - } - - // Response should block until buffer is relieved - select { - case res := <-resChan: - if res != nil { - t.Fatal(res) - } else { - t.Fatalf("Overflowed response returned before timeout") - } - case <-time.After(100 * time.Millisecond): - } - - var outTr message.Transaction - - // Extract last message - select { - case outTr = <-b.TransactionChan(): - assert.Equal(t, byte(0), outTr.Payload.Get(0).AsBytes()[0]) - require.NoError(t, outTr.Ack(tCtx, nil)) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message read") - } - - // Response from the last attempt should no longer be blocking - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(100 * time.Millisecond): - t.Errorf("Final buffered response blocked") - } - - // Extract all other messages - for i = 1; i <= total; i++ { - select { - case outTr = <-b.TransactionChan(): - assert.Equal(t, i, outTr.Payload.Get(0).AsBytes()[0]) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v read", i) - } - require.NoError(t, outTr.Ack(tCtx, nil)) - } - - // Get final message - select { - case outTr = <-b.TransactionChan(): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v read", i) - } - - require.NoError(t, outTr.Ack(tCtx, nil)) - - b.TriggerCloseNow() - require.NoError(t, b.WaitForClose(tCtx)) - - close(resChan) - close(tChan) -} - -func TestStreamBufferClosing(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - var incr, total uint8 = 100, 5 - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - b := NewStream("meow", newMemoryBuffer(int(total)), component.NoopObservability()) - require.NoError(t, b.Consume(tChan)) - - var i uint8 - - // Populate buffer with some messages - for i = 0; i < total; i++ { - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)) - msgBytes[0][0] = i - - select { - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v send", i) - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v response", i) - } - } - - // Close input, this should prompt the stack buffer to Flush(). - close(tChan) - - // Receive all of those messages from the buffer - for i = 0; i < total; i++ { - select { - case val := <-b.TransactionChan(): - assert.Equal(t, i, val.Payload.Get(0).AsBytes()[0]) - require.NoError(t, val.Ack(tCtx, nil)) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message read") - } - } - - // The buffer should now be closed, therefore so should our read channel. - select { - case _, open := <-b.TransactionChan(): - assert.False(t, open) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message read") - } - - // Should already be shut down. - assert.NoError(t, b.WaitForClose(tCtx)) -} - -//------------------------------------------------------------------------------ - -type readErrorBuffer struct { - readErrs chan error -} - -func (r *readErrorBuffer) Read(ctx context.Context) (message.Batch, AckFunc, error) { - select { - case err := <-r.readErrs: - return nil, nil, err - default: - } - return message.QuickBatch([][]byte{[]byte("hello world")}), func(c context.Context, e error) error { - return nil - }, nil -} - -func (r *readErrorBuffer) Write(ctx context.Context, msg message.Batch, aFn AckFunc) error { - return aFn(context.Background(), nil) -} - -func (r *readErrorBuffer) EndOfInput() { -} - -func (r *readErrorBuffer) Close(ctx context.Context) error { - return nil -} - -func TestStreamReadErrors(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - errBuf := &readErrorBuffer{ - readErrs: make(chan error, 2), - } - errBuf.readErrs <- errors.New("first error") - errBuf.readErrs <- errors.New("second error") - - b := NewStream("meow", errBuf, component.NoopObservability()) - require.NoError(t, b.Consume(tChan)) - - var tran message.Transaction - select { - case tran = <-b.TransactionChan(): - case <-time.After(time.Second * 5): - t.Fatal("timed out") - } - - require.Equal(t, 1, tran.Payload.Len()) - assert.Equal(t, "hello world", string(tran.Payload.Get(0).AsBytes())) - - require.NoError(t, tran.Ack(tCtx, nil)) - - b.TriggerCloseNow() - require.NoError(t, b.WaitForClose(tCtx)) - - close(resChan) - close(tChan) -} diff --git a/internal/component/cache/cache_metrics.go b/internal/component/cache/cache_metrics.go deleted file mode 100644 index 59892f1ae9..0000000000 --- a/internal/component/cache/cache_metrics.go +++ /dev/null @@ -1,137 +0,0 @@ -package cache - -import ( - "context" - "errors" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -type metricsCache struct { - c V1 - sig *shutdown.Signaller - - mGetNotFound metrics.StatCounter - mGetError metrics.StatCounter - mGetSuccess metrics.StatCounter - mGetLatency metrics.StatTimer - - mSetError metrics.StatCounter - mSetSuccess metrics.StatCounter - mSetLatency metrics.StatTimer - - mAddDupe metrics.StatCounter - mAddError metrics.StatCounter - mAddSuccess metrics.StatCounter - mAddLatency metrics.StatTimer - - mDelError metrics.StatCounter - mDelSuccess metrics.StatCounter - mDelLatency metrics.StatTimer -} - -// MetricsForCache wraps a cache with a struct that adds standard metrics over -// each method. -func MetricsForCache(c V1, stats metrics.Type) V1 { - cacheSuccess := stats.GetCounterVec("cache_success", "operation") - cacheError := stats.GetCounterVec("cache_error", "operation") - cacheLatency := stats.GetTimerVec("cache_latency_ns", "operation") - - return &metricsCache{ - c: c, sig: shutdown.NewSignaller(), - - mGetNotFound: stats.GetCounterVec("cache_not_found", "operation").With("get"), - mGetError: cacheError.With("get"), - mGetSuccess: cacheSuccess.With("get"), - mGetLatency: cacheLatency.With("get"), - - mSetError: cacheError.With("set"), - mSetSuccess: cacheSuccess.With("set"), - mSetLatency: cacheLatency.With("set"), - - mAddDupe: stats.GetCounterVec("cache_duplicate", "operation").With("add"), - mAddError: cacheError.With("add"), - mAddSuccess: cacheSuccess.With("add"), - mAddLatency: cacheLatency.With("add"), - - mDelError: cacheError.With("delete"), - mDelSuccess: cacheSuccess.With("delete"), - mDelLatency: cacheLatency.With("delete"), - } -} - -func (a *metricsCache) Get(ctx context.Context, key string) ([]byte, error) { - started := time.Now() - b, err := a.c.Get(ctx, key) - a.mGetLatency.Timing(int64(time.Since(started))) - if err != nil { - if errors.Is(err, component.ErrKeyNotFound) { - a.mGetNotFound.Incr(1) - } else { - a.mGetError.Incr(1) - } - } else { - a.mGetSuccess.Incr(1) - } - return b, err -} - -func (a *metricsCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - started := time.Now() - err := a.c.Set(ctx, key, value, ttl) - a.mSetLatency.Timing(int64(time.Since(started))) - if err != nil { - a.mSetError.Incr(1) - } else { - a.mSetSuccess.Incr(1) - } - return err -} - -func (a *metricsCache) SetMulti(ctx context.Context, items map[string]TTLItem) error { - started := time.Now() - err := a.c.SetMulti(ctx, items) - a.mSetLatency.Timing(int64(time.Since(started))) - if err != nil { - a.mSetError.Incr(int64(len(items))) - } else { - a.mSetSuccess.Incr(int64(len(items))) - } - return err -} - -func (a *metricsCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - started := time.Now() - err := a.c.Add(ctx, key, value, ttl) - a.mAddLatency.Timing(int64(time.Since(started))) - if err != nil { - if errors.Is(err, component.ErrKeyAlreadyExists) { - a.mAddDupe.Incr(1) - } else { - a.mAddError.Incr(1) - } - } else { - a.mAddSuccess.Incr(1) - } - return err -} - -func (a *metricsCache) Delete(ctx context.Context, key string) error { - started := time.Now() - err := a.c.Delete(ctx, key) - a.mDelLatency.Timing(int64(time.Since(started))) - if err != nil { - a.mDelError.Incr(1) - } else { - a.mDelSuccess.Incr(1) - } - return err -} - -func (a *metricsCache) Close(ctx context.Context) error { - return a.c.Close(ctx) -} diff --git a/internal/component/cache/cache_metrics_test.go b/internal/component/cache/cache_metrics_test.go deleted file mode 100644 index ae68b677a1..0000000000 --- a/internal/component/cache/cache_metrics_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package cache - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -type testCacheItem struct { - b []byte - ttl *time.Duration -} - -type closableCache struct { - m map[string]testCacheItem - err error - closed bool -} - -func (c *closableCache) Get(ctx context.Context, key string) ([]byte, error) { - if c.err != nil { - return nil, c.err - } - i, ok := c.m[key] - if !ok { - return nil, component.ErrKeyNotFound - } - return i.b, nil -} - -func (c *closableCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if c.err != nil { - return c.err - } - c.m[key] = testCacheItem{ - b: value, ttl: ttl, - } - return nil -} - -func (c *closableCache) SetMulti(ctx context.Context, keyValues map[string]TTLItem) error { - if c.err != nil { - return c.err - } - for k, v := range keyValues { - c.m[k] = testCacheItem{ - b: v.Value, ttl: v.TTL, - } - } - return nil -} - -func (c *closableCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if c.err != nil { - return c.err - } - if _, ok := c.m[key]; ok { - return component.ErrKeyAlreadyExists - } - c.m[key] = testCacheItem{ - b: value, ttl: ttl, - } - return nil -} - -func (c *closableCache) Delete(ctx context.Context, key string) error { - if c.err != nil { - return c.err - } - delete(c.m, key) - return nil -} - -func (c *closableCache) Close(ctx context.Context) error { - c.closed = true - return nil -} - -func TestCacheAirGapShutdown(t *testing.T) { - rl := &closableCache{} - agrl := MetricsForCache(rl, metrics.Noop()) - - err := agrl.Close(context.Background()) - assert.NoError(t, err) - assert.True(t, rl.closed) -} - -func TestCacheAirGapGet(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - }, - }, - } - agrl := MetricsForCache(rl, metrics.Noop()) - - b, err := agrl.Get(ctx, "foo") - assert.NoError(t, err) - assert.Equal(t, "bar", string(b)) - - _, err = agrl.Get(ctx, "not exist") - assert.Equal(t, err, component.ErrKeyNotFound) - assert.EqualError(t, err, "key does not exist") -} - -func TestCacheAirGapSet(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := MetricsForCache(rl, metrics.Noop()) - - err := agrl.Set(ctx, "foo", []byte("bar"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: nil, - }, - }, rl.m) - - err = agrl.Set(ctx, "foo", []byte("baz"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("baz"), - ttl: nil, - }, - }, rl.m) -} - -func TestCacheAirGapSetMultiWithTTL(t *testing.T) { - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - ctx := context.Background() - agrl := MetricsForCache(rl, metrics.Noop()) - - ttl1, ttl2 := time.Second, time.Millisecond - - err := agrl.SetMulti(ctx, map[string]TTLItem{ - "first": { - Value: []byte("bar"), - TTL: &ttl1, - }, - "second": { - Value: []byte("baz"), - TTL: &ttl2, - }, - }) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "first": { - b: []byte("bar"), - ttl: &ttl1, - }, - "second": { - b: []byte("baz"), - ttl: &ttl2, - }, - }, rl.m) -} - -func TestCacheAirGapSetWithTTL(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := MetricsForCache(rl, metrics.Noop()) - - ttl1, ttl2 := time.Second, time.Millisecond - err := agrl.Set(ctx, "foo", []byte("bar"), &ttl1) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: &ttl1, - }, - }, rl.m) - - err = agrl.Set(ctx, "foo", []byte("baz"), &ttl2) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("baz"), - ttl: &ttl2, - }, - }, rl.m) -} - -func TestCacheAirGapAdd(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := MetricsForCache(rl, metrics.Noop()) - - err := agrl.Add(ctx, "foo", []byte("bar"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: nil, - }, - }, rl.m) - - err = agrl.Add(ctx, "foo", []byte("baz"), nil) - assert.Equal(t, err, component.ErrKeyAlreadyExists) - assert.EqualError(t, err, "key already exists") -} - -func TestCacheAirGapAddWithTTL(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := MetricsForCache(rl, metrics.Noop()) - - ttl := time.Second - err := agrl.Add(ctx, "foo", []byte("bar"), &ttl) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: &ttl, - }, - }, rl.m) - - err = agrl.Add(ctx, "foo", []byte("baz"), nil) - assert.Equal(t, err, component.ErrKeyAlreadyExists) - assert.EqualError(t, err, "key already exists") -} - -func TestCacheAirGapDelete(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - }, - }, - } - agrl := MetricsForCache(rl, metrics.Noop()) - - err := agrl.Delete(ctx, "foo") - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{}, rl.m) -} diff --git a/internal/component/cache/config.go b/internal/component/cache/config.go deleted file mode 100644 index 5fed3d73dc..0000000000 --- a/internal/component/cache/config.go +++ /dev/null @@ -1,79 +0,0 @@ -package cache - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all cache types. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -type Config struct { - Label string `json:"label" yaml:"label"` - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` -} - -// NewConfig returns a configuration struct fully populated with default values. -func NewConfig() Config { - return Config{ - Label: "", - Type: "memory", - Plugin: nil, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeCache, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - conf.Label, _ = value["label"].(string) - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeCache, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - for i := 0; i < len(value.Content)-1; i += 2 { - if value.Content[i].Value == "label" { - conf.Label = value.Content[i+1].Value - break - } - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/cache/interface.go b/internal/component/cache/interface.go deleted file mode 100644 index 872bd2dc0a..0000000000 --- a/internal/component/cache/interface.go +++ /dev/null @@ -1,40 +0,0 @@ -package cache - -import ( - "context" - "time" -) - -// TTLItem contains a value to cache along with an optional TTL. -type TTLItem struct { - Value []byte - TTL *time.Duration -} - -// V1 Defines a common interface of cache implementations. -type V1 interface { - // Get attempts to locate and return a cached value by its key, returns an - // error if the key does not exist or if the command fails. - Get(ctx context.Context, key string) ([]byte, error) - - // Set attempts to set the value of a key, returns an error if the command - // fails. - Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error - - // SetMulti attempts to set the value of multiple keys, returns an error if - // any of the keys fail. - SetMulti(ctx context.Context, items map[string]TTLItem) error - - // Add attempts to set the value of a key only if the key does not already - // exist, returns an error if the key already exists or if the command - // fails. - Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error - - // Delete attempts to remove a key. Returns an error if a failure occurs. - Delete(ctx context.Context, key string) error - - // Close the component, blocks until either the underlying resources are - // cleaned up or the context is cancelled. Returns an error if the context - // is cancelled. - Close(ctx context.Context) error -} diff --git a/internal/component/errors.go b/internal/component/errors.go deleted file mode 100644 index 327ae3745c..0000000000 --- a/internal/component/errors.go +++ /dev/null @@ -1,81 +0,0 @@ -package component - -import ( - "errors" - "fmt" - "time" -) - -// ErrNotUnwrapped is returned in cases where a component was meant to be -// unwrapped either from the public packages or to the public packages but for -// some reason this did not happen. Unwrapping should only occur in times when -// it's guaranteed to succeed, so this error indicates that an assumption was -// incorrect during the migration of certain components which will need to be -// immediately addressed by maintainers. -var ErrNotUnwrapped = errors.New("something has gone wrong during the registering of this component, please open an issue https://github.com/benthosdev/benthos/issues/new to let us know") - -type errInvalidType struct { - typeStr string - tried string -} - -func (e *errInvalidType) Error() string { - return fmt.Sprintf("%v type of '%v' was not recognised", e.typeStr, e.tried) -} - -// ErrInvalidType creates an error that describes a component type being -// initialized with an unrecognised implementation. -func ErrInvalidType(typeStr, tried string) error { - return &errInvalidType{ - typeStr: typeStr, - tried: tried, - } -} - -// Errors used throughout the codebase. -var ( - ErrTimeout = errors.New("action timed out") - ErrTypeClosed = errors.New("type was closed") - - ErrNotConnected = errors.New("not connected to target source or sink") - - // ErrAlreadyStarted is returned when an input or output type gets started a - // second time. - ErrAlreadyStarted = errors.New("type has already been started") - - ErrNoAck = errors.New("failed to receive acknowledgement") - - ErrFailedSend = errors.New("message failed to reach a target destination") -) - -// ErrBackOff is an error returned that allows for a back off duration to be specified -type ErrBackOff struct { - Err error - Wait time.Duration -} - -// Error returns the Error string. -func (e *ErrBackOff) Error() string { - return e.Err.Error() -} - -//------------------------------------------------------------------------------ - -// Manager errors. -var ( - ErrInputNotFound = errors.New("input not found") - ErrCacheNotFound = errors.New("cache not found") - ErrProcessorNotFound = errors.New("processor not found") - ErrRateLimitNotFound = errors.New("rate limit not found") - ErrOutputNotFound = errors.New("output not found") - ErrKeyAlreadyExists = errors.New("key already exists") - ErrKeyNotFound = errors.New("key does not exist") - ErrPipeNotFound = errors.New("pipe was not found") -) - -//------------------------------------------------------------------------------ - -// Buffer errors. -var ( - ErrMessageTooLarge = errors.New("message body larger than buffer space") -) diff --git a/internal/component/input/async_cut_off.go b/internal/component/input/async_cut_off.go deleted file mode 100644 index f49133a150..0000000000 --- a/internal/component/input/async_cut_off.go +++ /dev/null @@ -1,89 +0,0 @@ -package input - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type asyncCutOffMsg struct { - msg message.Batch - ackFn AsyncAckFn -} - -// AsyncCutOff is a wrapper for input.Async implementations that exits from -// WaitForClose immediately. This is only useful when the underlying readable -// resource cannot be closed reliably and can block forever. -type AsyncCutOff struct { - msgChan chan asyncCutOffMsg - errChan chan error - - ctx context.Context - close func() - - r Async -} - -// NewAsyncCutOff returns a new AsyncCutOff wrapper around a input.Async. -func NewAsyncCutOff(r Async) *AsyncCutOff { - ctx, done := context.WithCancel(context.Background()) - return &AsyncCutOff{ - msgChan: make(chan asyncCutOffMsg), - errChan: make(chan error), - ctx: ctx, - close: done, - r: r, - } -} - -//------------------------------------------------------------------------------ - -// Connect attempts to establish a connection to the source, if -// unsuccessful returns an error. If the attempt is successful (or not -// necessary) returns nil. -func (c *AsyncCutOff) Connect(ctx context.Context) error { - return c.r.Connect(ctx) -} - -// ReadBatch attempts to read a new message from the source. -func (c *AsyncCutOff) ReadBatch(ctx context.Context) (message.Batch, AsyncAckFn, error) { - go func() { - msg, ackFn, err := c.r.ReadBatch(ctx) - if err == nil { - select { - case c.msgChan <- asyncCutOffMsg{ - msg: msg, - ackFn: ackFn, - }: - case <-ctx.Done(): - } - } else { - select { - case c.errChan <- err: - case <-ctx.Done(): - } - } - }() - select { - case m := <-c.msgChan: - return m.msg, m.ackFn, nil - case e := <-c.errChan: - return nil, nil, e - case <-ctx.Done(): - go func() { - _ = c.r.Close(context.Background()) - }() - case <-c.ctx.Done(): - } - return nil, nil, component.ErrTypeClosed -} - -// Close triggers the asynchronous closing of the reader. -func (c *AsyncCutOff) Close(ctx context.Context) error { - go func() { - _ = c.r.Close(context.Background()) - }() - c.close() - return nil -} diff --git a/internal/component/input/async_preserver.go b/internal/component/input/async_preserver.go deleted file mode 100644 index ea1ad180ba..0000000000 --- a/internal/component/input/async_preserver.go +++ /dev/null @@ -1,122 +0,0 @@ -package input - -import ( - "context" - "errors" - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/autoretry" - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// AsyncPreserver is a wrapper for input.Async implementations that keeps a -// buffer of sent messages until they are acknowledged. If an error occurs -// during message propagation the contents of the buffer will be resent instead -// of reading new messages until it is depleted. AsyncPreserver implements -// input.Async. -// -// Wrapping an input with this type is useful when your source of messages -// doesn't have a concept of a NoAck (like Kafka), and instead of "rejecting" -// messages we always intend to simply retry them until success. -type AsyncPreserver struct { - retryList *autoretry.List[message.Batch] - - inputClosed int32 - r Async -} - -// NewAsyncPreserver returns a new AsyncPreserver wrapper around a input.Async. -func NewAsyncPreserver(r Async) *AsyncPreserver { - return &AsyncPreserver{ - retryList: autoretry.NewList( - func(ctx context.Context) (message.Batch, autoretry.AckFunc, error) { - t, aFn, err := r.ReadBatch(ctx) - - // Make sure we're able to track the position of messages in - // order to reassociate them after a batch-wide error - // downstream. - _, t = message.NewSortGroup(t) - - return t, autoretry.AckFunc(aFn), err - }, - func(t message.Batch, err error) message.Batch { - var bErr *batch.Error - if len(t) == 0 || !errors.As(err, &bErr) || bErr.IndexedErrors() == 0 { - return t - } - - sortGroup := message.TopLevelSortGroup(t[0]) - if sortGroup == nil { - // We can't associate our source batch with the one that's associated - // with the batch error, therefore we fall back towards treating every - // message as if it was errored the same. - return t - } - - seenIndexes := map[int]struct{}{} - newBatch := make(message.Batch, 0, bErr.IndexedErrors()) - bErr.WalkPartsBySource(sortGroup, t, func(i int, p *message.Part, err error) bool { - if err == nil { - return true - } - if _, exists := seenIndexes[i]; exists { - return true - } - seenIndexes[i] = struct{}{} - newBatch = append(newBatch, p) - return true - }) - if len(newBatch) == 0 { - return t - } - return newBatch - }), - r: r, - } -} - -//------------------------------------------------------------------------------ - -// Connect attempts to establish a connection to the source, if -// unsuccessful returns an error. If the attempt is successful (or not -// necessary) returns nil. -func (p *AsyncPreserver) Connect(ctx context.Context) error { - err := p.r.Connect(ctx) - // If our source has finished but we still have messages in flight then - // we act like we're still open. Read will be called and we can either - // return the pending messages or wait for them. - if errors.Is(err, component.ErrTypeClosed) && !p.retryList.Exhausted() { - atomic.StoreInt32(&p.inputClosed, 1) - err = nil - } - return err -} - -// ReadBatch attempts to read a new message from the source. -func (p *AsyncPreserver) ReadBatch(ctx context.Context) (message.Batch, AsyncAckFn, error) { - batch, rAckFn, err := p.retryList.Shift(ctx, atomic.LoadInt32(&p.inputClosed) == 0) - if err != nil { - if errors.Is(err, autoretry.ErrExhausted) { - return nil, nil, component.ErrTypeClosed - } - if errors.Is(err, component.ErrTypeClosed) { - // Mark our input as being closed and trigger an immediate re-read - // in order to clear any pending retries. - atomic.StoreInt32(&p.inputClosed, 1) - return p.ReadBatch(ctx) - } - // Otherwise we have an unknown error from our reader that we should - // escalate, this is most likely an ErrNotConnected or ErrTimeout. - return nil, nil, err - } - return batch.ShallowCopy(), AsyncAckFn(rAckFn), nil -} - -// Close triggers the shut down of this component and blocks until completion or -// context cancellation. -func (p *AsyncPreserver) Close(ctx context.Context) error { - _ = p.retryList.Close(ctx) - return p.r.Close(ctx) -} diff --git a/internal/component/input/async_preserver_test.go b/internal/component/input/async_preserver_test.go deleted file mode 100644 index 6c1867639f..0000000000 --- a/internal/component/input/async_preserver_test.go +++ /dev/null @@ -1,1020 +0,0 @@ -package input_test - -import ( - "context" - "errors" - "reflect" - "sync" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func newMockAsyncReaderBlocked() *mockAsyncReader { - readerImpl := newMockAsyncReader() - readerImpl.unblockCloseAsyncChan = make(chan struct{}) - readerImpl.waitForCloseChan = make(chan error) - return readerImpl -} - -func TestAsyncPreserverClose(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - exp := errors.New("foo error") - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - if err := pres.Connect(ctx); err != nil { - t.Error(err) - } - assert.EqualError(t, pres.Close(ctx), "foo error") - wg.Done() - }() - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- exp: - case <-time.After(time.Second): - t.Error("Timed out") - } - - wg.Wait() -} - -func TestAsyncPreserverRetryPriority(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - readerImpl.msgsToSnd = []message.Batch{ - {message.NewPart([]byte("first msg"))}, - {message.NewPart([]byte("second msg"))}, - } - readerImpl.ackChan = make(chan error, 3) - for i := 0; i < 3; i++ { - readerImpl.ackChan <- nil - } - pres := input.NewAsyncPreserver(readerImpl) - - errFoo := errors.New("foo error") - - wg := sync.WaitGroup{} - wg.Add(1) - - readyToReadAgain := make(chan struct{}) - go func() { - require.NoError(t, pres.Connect(ctx)) - - // First message consumed, then nacked - msg, ackFn, err := pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - assert.Equal(t, "first msg", string(msg[0].AsBytes())) - - // This will block until either a nack or new message, we want to prove - // that the nack gets priority when the new message is blocking, so we - // nack after N time and return a new message after M time, where M > N. - go func() { - <-time.After(time.Millisecond * 500) - require.NoError(t, ackFn(ctx, errFoo)) - <-time.After(time.Second) - close(readyToReadAgain) - }() - - // Next message consumed, which is the nack, not the new message - var newAckFn input.AsyncAckFn - msg, newAckFn, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - assert.Equal(t, "first msg", string(msg[0].AsBytes())) - require.NoError(t, newAckFn(ctx, nil)) - - // Finally, the second message - msg, newAckFn, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - assert.Equal(t, "second msg", string(msg[0].AsBytes())) - require.NoError(t, newAckFn(ctx, nil)) - - require.NoError(t, pres.Close(ctx)) - wg.Done() - }() - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case <-readyToReadAgain: - case <-ctx.Done(): - t.Error("timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - wg.Wait() -} - -func TestAsyncPreserverNackThenClose(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - readerImpl.msgsToSnd = []message.Batch{ - message.QuickBatch([][]byte{[]byte("hello world")}), - } - pres := input.NewAsyncPreserver(readerImpl) - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - select { - case readerImpl.connChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- component.ErrTypeClosed: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.ackChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - }() - - err := pres.Connect(ctx) - assert.NoError(t, err) - - _, ackFn1, err := pres.ReadBatch(ctx) - assert.NoError(t, err) - assert.NoError(t, ackFn1(ctx, errors.New("rejected"))) - - _, ackFn2, err := pres.ReadBatch(ctx) - require.NoError(t, err) - assert.NoError(t, ackFn2(ctx, nil)) - - _, _, err = pres.ReadBatch(ctx) - assert.Equal(t, component.ErrTypeClosed, err) - - assert.NoError(t, pres.Close(ctx)) - wg.Wait() -} - -func TestAsyncPreserverCloseThenAck(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - readerImpl.msgsToSnd = []message.Batch{ - message.QuickBatch([][]byte{[]byte("hello world")}), - } - pres := input.NewAsyncPreserver(readerImpl) - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - select { - case readerImpl.connChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- component.ErrTypeClosed: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.ackChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - }() - - err := pres.Connect(ctx) - assert.NoError(t, err) - - _, ackFn1, err := pres.ReadBatch(ctx) - assert.NoError(t, err) - - go func() { - time.Sleep(time.Millisecond * 10) - assert.NoError(t, ackFn1(ctx, nil)) - }() - - _, _, err = pres.ReadBatch(ctx) - assert.Equal(t, component.ErrTypeClosed, err) - - assert.NoError(t, pres.Close(ctx)) - wg.Wait() -} - -func TestAsyncPreserverCloseThenNackThenAck(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - readerImpl.msgsToSnd = []message.Batch{ - message.QuickBatch([][]byte{[]byte("hello world")}), - } - pres := input.NewAsyncPreserver(readerImpl) - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - select { - case readerImpl.connChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- component.ErrTypeClosed: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.ackChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - }() - - err := pres.Connect(ctx) - assert.NoError(t, err) - - _, ackFn1, err := pres.ReadBatch(ctx) - assert.NoError(t, err) - assert.NoError(t, ackFn1(ctx, errors.New("huh"))) - - _, ackFn2, err := pres.ReadBatch(ctx) - require.NoError(t, err) - assert.NoError(t, ackFn2(ctx, nil)) - - _, _, err = pres.ReadBatch(ctx) - assert.Equal(t, component.ErrTypeClosed, err) - - assert.NoError(t, pres.Close(ctx)) - wg.Wait() -} - -func TestAsyncPreserverMutateThenNack(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - msg := message.NewPart(nil) - msg.SetStructuredMut(map[string]any{ - "hello": "world", - }) - - batch := message.Batch{msg} - - readerImpl := newMockAsyncReaderBlocked() - readerImpl.msgsToSnd = []message.Batch{batch} - pres := input.NewAsyncPreserver(readerImpl) - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - select { - case readerImpl.connChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- component.ErrTypeClosed: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.ackChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - }() - - err := pres.Connect(ctx) - assert.NoError(t, err) - - msgOne, ackFn1, err := pres.ReadBatch(ctx) - assert.NoError(t, err) - require.Equal(t, 1, msgOne.Len()) - - mStruct, err := msgOne.Get(0).AsStructuredMut() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - }, mStruct) - - _, err = gabs.Wrap(mStruct).Set("woof", "meow") - require.NoError(t, err) - assert.NoError(t, ackFn1(ctx, errors.New("huh"))) - - msgTwo, ackFn2, err := pres.ReadBatch(ctx) - require.NoError(t, err) - - mStruct, err = msgTwo.Get(0).AsStructuredMut() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - }, mStruct) - assert.NoError(t, ackFn2(ctx, nil)) - - _, _, err = pres.ReadBatch(ctx) - assert.Equal(t, component.ErrTypeClosed, err) - - assert.NoError(t, pres.Close(ctx)) - wg.Wait() -} - -func TestAsyncPreserverCloseViaConnectThenAck(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - readerImpl.msgsToSnd = []message.Batch{ - message.QuickBatch([][]byte{[]byte("hello world")}), - } - pres := input.NewAsyncPreserver(readerImpl) - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - select { - case readerImpl.connChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.readChan <- component.ErrNotConnected: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.ackChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.connChan <- component.ErrTypeClosed: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-ctx.Done(): - t.Error("Timed out") - } - - select { - case readerImpl.waitForCloseChan <- nil: - case <-ctx.Done(): - t.Error("Timed out") - } - }() - - err := pres.Connect(ctx) - assert.NoError(t, err) - - _, ackFn1, err := pres.ReadBatch(ctx) - assert.NoError(t, err) - - _, _, err = pres.ReadBatch(ctx) - assert.Equal(t, component.ErrNotConnected, err) - - assert.NoError(t, ackFn1(ctx, nil)) - - err = pres.Connect(ctx) - assert.Equal(t, component.ErrTypeClosed, err) - - assert.NoError(t, pres.Close(ctx)) - wg.Wait() -} - -func TestAsyncPreserverHappy(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - expParts := [][]byte{ - []byte("foo"), - } - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - for _, p := range expParts { - readerImpl.msgsToSnd = []message.Batch{message.QuickBatch([][]byte{p})} - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - }() - - if err := pres.Connect(ctx); err != nil { - t.Error(err) - } - - for _, exp := range expParts { - msg, _, err := pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := msg.Get(0).AsBytes(); !reflect.DeepEqual(act, exp) { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - } -} - -func TestAsyncPreserverErrorProp(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - expErr := errors.New("foo") - - go func() { - select { - case readerImpl.connChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.ackChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - if actErr := pres.Connect(ctx); expErr != actErr { - t.Errorf("Wrong error returned: %v != %v", actErr, expErr) - } - if _, _, actErr := pres.ReadBatch(ctx); expErr != actErr { - t.Errorf("Wrong error returned: %v != %v", actErr, expErr) - } - if _, aFn, actErr := pres.ReadBatch(ctx); actErr != nil { - t.Fatal(actErr) - } else if actErr = aFn(ctx, nil); expErr != actErr { - t.Errorf("Wrong error returned: %v != %v", actErr, expErr) - } -} - -func TestAsyncPreserverErrorBackoff(t *testing.T) { - t.Skip("Not liked by the race detector") - t.Parallel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.unblockCloseAsyncChan <- struct{}{}: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.waitForCloseChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) - defer cancel() - - require.NoError(t, pres.Connect(ctx)) - - i := 0 - for { - _, aFn, actErr := pres.ReadBatch(ctx) - if actErr != nil { - assert.ErrorIs(t, ctx.Err(), actErr) - break - } - require.NoError(t, aFn(ctx, errors.New("no thanks"))) - i++ - if i == 10 { - t.Error("Expected backoff to prevent this") - break - } - } - - assert.NoError(t, pres.Close(ctx)) -} - -func TestAsyncPreserverBatchError(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - readerImpl.msgsToSnd = []message.Batch{ - message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - []byte("bev"), - }), - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.ackChan <- errors.New("ack propagated"): - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - require.NoError(t, pres.Connect(ctx)) - - msg, ackFn, err := pres.ReadBatch(ctx) - require.NoError(t, err) - assert.Equal(t, [][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - []byte("bev"), - }, message.GetAllBytes(msg)) - - bErr := batch.NewError(msg, errors.New("first")) - bErr.Failed(1, errors.New("second")) - bErr.Failed(3, errors.New("third")) - - require.NoError(t, ackFn(ctx, bErr)) - - msg, ackFn, err = pres.ReadBatch(ctx) - require.NoError(t, err) - assert.Equal(t, [][]byte{ - []byte("bar"), - []byte("buz"), - }, message.GetAllBytes(msg)) - - require.EqualError(t, ackFn(ctx, nil), "ack propagated") -} - -func TestAsyncPreserverBatchErrorUnordered(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - readerImpl.msgsToSnd = []message.Batch{ - message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - []byte("bev"), - }), - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.ackChan <- errors.New("ack propagated"): - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - require.NoError(t, pres.Connect(ctx)) - - msg, ackFn, err := pres.ReadBatch(ctx) - require.NoError(t, err) - assert.Equal(t, [][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - []byte("bev"), - }, message.GetAllBytes(msg)) - - bMsg := message.Batch{ - msg.Get(1), - msg.Get(3), - msg.Get(0), - msg.Get(4), - msg.Get(2), - } - - bErr := batch.NewError(bMsg, errors.New("first")) - bErr.Failed(1, errors.New("second")) - bErr.Failed(2, errors.New("third")) - - require.NoError(t, ackFn(ctx, bErr)) - - msg, ackFn, err = pres.ReadBatch(ctx) - require.NoError(t, err) - assert.Equal(t, [][]byte{ - []byte("buz"), - []byte("foo"), - }, message.GetAllBytes(msg)) - - require.EqualError(t, ackFn(ctx, nil), "ack propagated") -} - -//------------------------------------------------------------------------------ - -func TestAsyncPreserverBuffer(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - sendMsg := func(content string) { - readerImpl.msgsToSnd = []message.Batch{message.QuickBatch( - [][]byte{[]byte(content)}, - )} - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - sendAck := func() { - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - // Send message normally. - exp := "msg 1" - exp2 := "msg 2" - exp3 := "msg 3" - - go sendMsg(exp) - msg, aFn, err := pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - - // Prime second message. - go sendMsg(exp2) - - // Fail previous message, expecting it to be resent. - _ = aFn(ctx, errors.New("failed")) - msg, aFn, err = pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - - // Read the primed message. - var aFn2 input.AsyncAckFn - msg, aFn2, err = pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := string(msg.Get(0).AsBytes()); exp2 != act { - t.Errorf("Wrong message returned: %v != %v", act, exp2) - } - - // Fail both messages, expecting them to be resent. - _ = aFn(ctx, errors.New("failed again")) - _ = aFn2(ctx, errors.New("failed again")) - - // Read both messages. - msg, aFn, err = pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - msg, aFn2, err = pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := string(msg.Get(0).AsBytes()); exp2 != act { - t.Errorf("Wrong message returned: %v != %v", act, exp2) - } - - // Prime a new message and also an acknowledgement. - go sendMsg(exp3) - go sendAck() - go sendAck() - - // Ack all messages. - _ = aFn(ctx, nil) - _ = aFn2(ctx, nil) - - msg, _, err = pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - if act := string(msg.Get(0).AsBytes()); exp3 != act { - t.Errorf("Wrong message returned: %v != %v", act, exp3) - } -} - -func TestAsyncPreserverBufferBatchedAcks(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockAsyncReaderBlocked() - pres := input.NewAsyncPreserver(readerImpl) - - sendMsg := func(content string) { - readerImpl.msgsToSnd = []message.Batch{message.QuickBatch( - [][]byte{[]byte(content)}, - )} - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - sendAck := func() { - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - messages := []string{ - "msg 1", - "msg 2", - "msg 3", - } - - ackFns := []input.AsyncAckFn{} - for _, exp := range messages { - go sendMsg(exp) - msg, aFn, err := pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - ackFns = append(ackFns, aFn) - if act := string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - } - - // Fail all messages, expecting them to be resent. - for _, aFn := range ackFns { - _ = aFn(ctx, errors.New("failed again")) - } - ackFns = []input.AsyncAckFn{} - - for _, exp := range messages { - msg, aFn, err := pres.ReadBatch(ctx) - if err != nil { - t.Fatal(err) - } - ackFns = append(ackFns, aFn) - if act := string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - } - - // Ack all messages. - go func() { - for _, aFn := range ackFns { - _ = aFn(ctx, nil) - } - }() - - for range ackFns { - sendAck() - } -} diff --git a/internal/component/input/async_reader.go b/internal/component/input/async_reader.go deleted file mode 100644 index bd5f1c24da..0000000000 --- a/internal/component/input/async_reader.go +++ /dev/null @@ -1,283 +0,0 @@ -package input - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "time" - - "github.com/cenkalti/backoff/v4" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" -) - -// AsyncReader is an input implementation that reads messages from an -// input.Async component. -type AsyncReader struct { - connected int32 - connBackoff backoff.BackOff - readBackoff backoff.BackOff - - typeStr string - reader Async - - mgr component.Observability - - transactions chan message.Transaction - shutSig *shutdown.Signaller -} - -// NewAsyncReader creates a new AsyncReader input type. -func NewAsyncReader( - typeStr string, - r Async, - mgr component.Observability, - opts ...func(a *AsyncReader), -) (Streamed, error) { - connBoff := backoff.NewExponentialBackOff() - connBoff.InitialInterval = time.Millisecond * 100 - connBoff.MaxInterval = time.Second - connBoff.MaxElapsedTime = 0 - - readBoff := backoff.NewExponentialBackOff() - readBoff.InitialInterval = time.Millisecond * 100 - readBoff.MaxInterval = time.Second - readBoff.MaxElapsedTime = 0 - - rdr := &AsyncReader{ - connBackoff: connBoff, - readBackoff: readBoff, - typeStr: typeStr, - reader: r, - mgr: mgr, - transactions: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - for _, opt := range opts { - opt(rdr) - } - - go rdr.loop() - return rdr, nil -} - -// AsyncReaderWithConnBackOff set the backoff used for limiting connection -// attempts. If the maximum number of retry attempts is reached then the input -// will gracefully stop. -func AsyncReaderWithConnBackOff(boff backoff.BackOff) func(a *AsyncReader) { - return func(a *AsyncReader) { - a.connBackoff = boff - } -} - -//------------------------------------------------------------------------------ - -func (r *AsyncReader) loop() { - // Metrics paths - var ( - mRcvd = r.mgr.Metrics().GetCounter("input_received") - mConn = r.mgr.Metrics().GetCounter("input_connection_up") - mFailedConn = r.mgr.Metrics().GetCounter("input_connection_failed") - mLostConn = r.mgr.Metrics().GetCounter("input_connection_lost") - mLatency = r.mgr.Metrics().GetTimer("input_latency_ns") - - traceName = "input_" + r.typeStr - ) - - closeAtLeisureCtx, calDone := r.shutSig.SoftStopCtx(context.Background()) - defer calDone() - - closeNowCtx, cnDone := r.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - defer func() { - _ = r.reader.Close(context.Background()) - - atomic.StoreInt32(&r.connected, 0) - - close(r.transactions) - r.shutSig.TriggerHasStopped() - }() - - pendingAcks := sync.WaitGroup{} - defer func() { - r.mgr.Logger().Debug("Waiting for pending acks to resolve before shutting down.") - pendingAcks.Wait() - r.mgr.Logger().Debug("Pending acks resolved.") - }() - - initConnection := func() bool { - for { - if r.shutSig.IsSoftStopSignalled() { - return false - } - if err := r.reader.Connect(closeAtLeisureCtx); err != nil { - if r.shutSig.IsSoftStopSignalled() || errors.Is(err, component.ErrTypeClosed) { - return false - } - r.mgr.Logger().Error("Failed to connect to %v: %v\n", r.typeStr, err) - mFailedConn.Incr(1) - - var nextBoff time.Duration - - var e *component.ErrBackOff - if errors.As(err, &e) { - nextBoff = e.Wait - } else { - nextBoff = r.connBackoff.NextBackOff() - } - - if nextBoff == backoff.Stop { - r.mgr.Logger().Error("Maximum number of connection attempt retries has been met, gracefully terminating input %v", r.typeStr) - return false - } - if sleepWithCancellation(closeAtLeisureCtx, nextBoff) != nil { - return false - } - } else { - r.connBackoff.Reset() - return true - } - } - } - if !initConnection() { - return - } - - r.mgr.Logger().Info("Input type %v is now active", r.typeStr) - mConn.Incr(1) - atomic.StoreInt32(&r.connected, 1) - - for { - msg, ackFn, err := r.reader.ReadBatch(closeAtLeisureCtx) - - // If our reader says it is not connected. - if errors.Is(err, component.ErrNotConnected) { - mLostConn.Incr(1) - atomic.StoreInt32(&r.connected, 0) - - // Continue to try to reconnect while still active. - if !initConnection() { - return - } - mConn.Incr(1) - atomic.StoreInt32(&r.connected, 1) - continue - } - - // Close immediately if our reader is closed. - if r.shutSig.IsSoftStopSignalled() || errors.Is(err, component.ErrTypeClosed) { - return - } - - if err != nil || len(msg) == 0 { - if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, component.ErrTimeout) && !errors.Is(err, component.ErrNotConnected) { - r.mgr.Logger().Error("Failed to read message: %v\n", err) - } - - nextBoff := r.readBackoff.NextBackOff() - if nextBoff == backoff.Stop { - r.mgr.Logger().Error("Maximum number of read attempt retries has been met, gracefully terminating input %v", r.typeStr) - return - } - select { - case <-time.After(nextBoff): - case <-r.shutSig.SoftStopChan(): - return - } - continue - } - - r.readBackoff.Reset() - mRcvd.Incr(int64(msg.Len())) - r.mgr.Logger().Trace("Consumed %v messages from '%v'.\n", msg.Len(), r.typeStr) - - startedAt := time.Now() - - resChan := make(chan error, 1) - tracing.InitSpans(r.mgr.Tracer(), traceName, msg) - select { - case r.transactions <- message.NewTransaction(msg, resChan): - case <-r.shutSig.SoftStopChan(): - return - } - - pendingAcks.Add(1) - go func( - m message.Batch, - aFn AsyncAckFn, - rChan chan error, - ) { - defer pendingAcks.Done() - - var res error - select { - case res = <-rChan: - case <-r.shutSig.HardStopChan(): - // Even if the pipeline is terminating we still want to attempt - // to propagate an acknowledgement from in-transit messages. - return - } - - mLatency.Timing(time.Since(startedAt).Nanoseconds()) - tracing.FinishSpans(m) - - if err = aFn(closeNowCtx, res); err != nil { - r.mgr.Logger().Error("Failed to acknowledge message: %v\n", err) - } - }(msg, ackFn, resChan) - } -} - -// TransactionChan returns a transactions channel for consuming messages from -// this input type. -func (r *AsyncReader) TransactionChan() <-chan message.Transaction { - return r.transactions -} - -// Connected returns a boolean indicating whether this input is currently -// connected to its target. -func (r *AsyncReader) Connected() bool { - return atomic.LoadInt32(&r.connected) == 1 -} - -// TriggerStopConsuming instructs the input to start shutting down resources -// once all pending messages are delivered and acknowledged. This call does -// not block. -func (r *AsyncReader) TriggerStopConsuming() { - r.shutSig.TriggerSoftStop() -} - -// TriggerCloseNow triggers the shut down of this component but should not block -// the calling goroutine. -func (r *AsyncReader) TriggerCloseNow() { - r.shutSig.TriggerHardStop() -} - -// WaitForClose is a blocking call to wait until the component has finished -// shutting down and cleaning up resources. -func (r *AsyncReader) WaitForClose(ctx context.Context) error { - select { - case <-r.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func sleepWithCancellation(ctx context.Context, d time.Duration) error { - t := time.NewTimer(d) - defer t.Stop() - - select { - case <-t.C: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} diff --git a/internal/component/input/async_reader_test.go b/internal/component/input/async_reader_test.go deleted file mode 100644 index a3c773196c..0000000000 --- a/internal/component/input/async_reader_test.go +++ /dev/null @@ -1,788 +0,0 @@ -package input_test - -import ( - "context" - "errors" - "fmt" - "reflect" - "sync" - "testing" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type mockAsyncReader struct { - msgsToSnd []message.Batch - ackRcvd []error - ackMut sync.Mutex - - connChan chan error - readChan chan error - ackChan chan error - closeAsyncChan chan struct{} - closeAsyncOnce sync.Once - - unblockCloseAsyncChan chan struct{} - waitForCloseChan chan error -} - -func newMockAsyncReader() *mockAsyncReader { - unblockCloseAsyncChan := make(chan struct{}) - close(unblockCloseAsyncChan) - - waitForCloseChan := make(chan error, 1) - waitForCloseChan <- nil - - return &mockAsyncReader{ - connChan: make(chan error), - readChan: make(chan error), - ackChan: make(chan error), - closeAsyncChan: make(chan struct{}), - unblockCloseAsyncChan: unblockCloseAsyncChan, - waitForCloseChan: waitForCloseChan, - } -} - -func (r *mockAsyncReader) Connect(ctx context.Context) error { - cerr, open := <-r.connChan - if !open { - return component.ErrNotConnected - } - return cerr -} - -func (r *mockAsyncReader) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - select { - case <-ctx.Done(): - return nil, nil, component.ErrTimeout - case err, open := <-r.readChan: - if !open { - return nil, nil, component.ErrNotConnected - } - if err != nil { - return nil, nil, err - } - } - r.ackMut.Lock() - r.ackRcvd = append(r.ackRcvd, errors.New("ack not received")) - i := len(r.ackRcvd) - 1 - r.ackMut.Unlock() - - nextMsg := message.Batch{message.NewPart(nil)} - if len(r.msgsToSnd) > 0 { - nextMsg = r.msgsToSnd[0] - r.msgsToSnd = r.msgsToSnd[1:] - } - - return nextMsg.DeepCopy(), func(ctx context.Context, res error) error { - r.ackMut.Lock() - r.ackRcvd[i] = res - r.ackMut.Unlock() - select { - case err := <-r.ackChan: - return err - case <-ctx.Done(): - } - return nil - }, nil -} - -func (r *mockAsyncReader) Close(ctx context.Context) error { - <-r.unblockCloseAsyncChan - r.closeAsyncOnce.Do(func() { - close(r.closeAsyncChan) - }) - return <-r.waitForCloseChan -} - -//------------------------------------------------------------------------------ - -type asyncReaderCantConnect struct{} - -func (r asyncReaderCantConnect) Connect(ctx context.Context) error { - return component.ErrNotConnected -} - -func (r asyncReaderCantConnect) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - return nil, nil, component.ErrNotConnected -} -func (r asyncReaderCantConnect) Close(ctx context.Context) error { return nil } - -func TestAsyncReaderCantConnect(t *testing.T) { - r, err := input.NewAsyncReader("foo", asyncReaderCantConnect{}, mock.NewManager()) - if err != nil { - t.Error(err) - return - } - - // We will fail to connect but should still exit immediately. - r.TriggerStopConsuming() - require.NoError(t, r.WaitForClose(context.Background())) -} - -//------------------------------------------------------------------------------ - -type asyncReaderCantRead struct { - connected int -} - -func (r *asyncReaderCantRead) Connect(ctx context.Context) error { - r.connected++ - return errors.New("nope") -} - -func (r *asyncReaderCantRead) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - return nil, nil, component.ErrNotConnected -} -func (r *asyncReaderCantRead) Close(ctx context.Context) error { return nil } - -func TestAsyncReaderCantRead(t *testing.T) { - readerImpl := &asyncReaderCantRead{} - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - r.TriggerStopConsuming() - require.NoError(t, r.WaitForClose(ctx)) - - assert.Less(t, readerImpl.connected, 2) -} - -//------------------------------------------------------------------------------ - -func TestAsyncReaderTypeClosedOnConn(t *testing.T) { - readerImpl := newMockAsyncReader() - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Error(err) - return - } - - go func() { - select { - case readerImpl.connChan <- component.ErrTypeClosed: - case <-time.After(time.Second): - } - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, r.WaitForClose(ctx)) -} - -func TestAsyncReaderTypeClosedOnReconn(t *testing.T) { - readerImpl := newMockAsyncReader() - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Error(err) - return - } - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.readChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - select { - case readerImpl.connChan <- component.ErrTypeClosed: - case <-time.After(time.Second): - } - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, r.WaitForClose(ctx)) -} - -func TestAsyncReaderTypeClosedOnReread(t *testing.T) { - readerImpl := newMockAsyncReader() - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.readChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.readChan <- component.ErrTypeClosed: - case <-time.After(time.Second): - } - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, r.WaitForClose(ctx)) -} - -//------------------------------------------------------------------------------ - -func TestAsyncReaderCanReconnect(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - readerImpl := newMockAsyncReader() - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Error(err) - return - } - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.readChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - } - }() - - var ts message.Transaction - var open bool - select { - case ts, open = <-r.TransactionChan(): - if !open { - t.Fatal("Closed early") - } - case <-time.After(time.Second): - t.Error("Timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - // We will be failing to send but should still exit immediately. - r.TriggerStopConsuming() - - go func() { - select { - case readerImpl.readChan <- nil: - case readerImpl.connChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - }() - - require.NoError(t, r.WaitForClose(tCtx)) -} - -func TestAsyncReaderFailsReconnect(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - readerImpl := newMockAsyncReader() - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Error(err) - return - } - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.readChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - select { - case readerImpl.connChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second * 2): - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - } - }() - - var ts message.Transaction - var open bool - select { - case ts, open = <-r.TransactionChan(): - if !open { - t.Fatal("Closed early") - } - case <-time.After(time.Second * 2): - t.Error("Timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - // We will be failing to send but should still exit immediately. - r.TriggerStopConsuming() - - go func() { - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - } - }() - - require.NoError(t, r.WaitForClose(tCtx)) -} - -func TestAsyncReaderCloseDuringReconnect(t *testing.T) { - readerImpl := newMockAsyncReader() - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - select { - case readerImpl.readChan <- component.ErrNotConnected: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - select { - case readerImpl.connChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - close(readerImpl.connChan) - }() - - // We will be failing to send but should still exit immediately. - r.TriggerStopConsuming() - close(readerImpl.readChan) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, r.WaitForClose(ctx)) -} - -func TestAsyncReaderHappyPath(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - exp := [][]byte{[]byte("foo"), []byte("bar")} - - readerImpl := newMockAsyncReader() - readerImpl.msgsToSnd = []message.Batch{message.QuickBatch(exp)} - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - } - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - } - }() - - var ts message.Transaction - var open bool - - select { - case ts, open = <-r.TransactionChan(): - if !open { - t.Fatal("Chan closed") - } - if act := message.GetAllBytes(ts.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Fatal("Timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - // We will be failing to send but should still exit immediately. - r.TriggerStopConsuming() - close(readerImpl.readChan) - close(readerImpl.connChan) - - require.NoError(t, r.WaitForClose(tCtx)) - - if readerImpl.ackRcvd[0] != nil { - t.Error(readerImpl.ackRcvd[0]) - } -} - -func TestAsyncReaderCloseWithPendingAcks(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - exp := [][]byte{[]byte("hello world")} - - readerImpl := newMockAsyncReader() - readerImpl.msgsToSnd = []message.Batch{message.QuickBatch(exp)} - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - require.NoError(t, err) - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - } - }() - - var ts message.Transaction - var open bool - - select { - case ts, open = <-r.TransactionChan(): - require.True(t, open) - assert.Equal(t, exp, message.GetAllBytes(ts.Payload)) - case <-time.After(time.Second): - t.Fatal("Timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - // Blocking the reader ack for now - r.TriggerStopConsuming() - - select { - case <-readerImpl.closeAsyncChan: - t.Fatal("reader closed early") - // case <-time.After(time.Millisecond * 100): - case <-time.After(time.Second): - } - - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - select { - case <-readerImpl.closeAsyncChan: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - if readerImpl.ackRcvd[0] != nil { - t.Error(readerImpl.ackRcvd[0]) - } -} - -func TestAsyncReaderSadPath(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - exp := [][]byte{[]byte("foo"), []byte("bar")} - expErr := errors.New("test error") - - readerImpl := newMockAsyncReader() - readerImpl.msgsToSnd = []message.Batch{message.QuickBatch(exp)} - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - for { - select { - case readerImpl.readChan <- nil: - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - } - return - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - } - } - }() - - var ts message.Transaction - var open bool - - select { - case ts, open = <-r.TransactionChan(): - if !open { - t.Fatal("Chan closed") - } - if act := message.GetAllBytes(ts.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Fatal("Timed out") - } - require.NoError(t, ts.Ack(tCtx, expErr)) - - // We will be failing to send but should still exit immediately. - r.TriggerStopConsuming() - close(readerImpl.readChan) - close(readerImpl.connChan) - - require.NoError(t, r.WaitForClose(tCtx)) - - if actErr := readerImpl.ackRcvd[0]; expErr != actErr { - t.Errorf("Wrong response received: %v != %v", actErr, expErr) - } -} - -func TestAsyncReaderParallel(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - expMsgs := []string{} - for i := 0; i < 10; i++ { - expMsgs = append(expMsgs, fmt.Sprintf("message: %v", i)) - } - readerImpl := newMockAsyncReader() - for _, str := range expMsgs { - readerImpl.msgsToSnd = append(readerImpl.msgsToSnd, message.QuickBatch([][]byte{[]byte(str)})) - } - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - for range expMsgs { - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - } - } - }() - - expErrs := []error{} - for i := range expMsgs { - expErrs = append(expErrs, fmt.Errorf("err %v", i)) - } - - resFns := make([]func(context.Context, error) error, len(expMsgs)) - for i, mStr := range expMsgs { - var ts message.Transaction - var open bool - select { - case ts, open = <-r.TransactionChan(): - if !open { - t.Fatal("Chan closed") - } - if act, exp := string(ts.Payload.Get(0).AsBytes()), mStr; exp != act { - t.Errorf("Wrong message returned: %v != %v", act, exp) - } - resFns[i] = ts.Ack - case <-time.After(time.Second): - t.Fatal("Timed out") - } - } - - go func() { - for range expErrs { - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - } - } - }() - - for i, e := range expErrs { - require.NoError(t, resFns[i](tCtx, e)) - } - - // We will be failing to send but should still exit immediately. - r.TriggerStopConsuming() - close(readerImpl.readChan) - close(readerImpl.connChan) - - require.NoError(t, r.WaitForClose(tCtx)) - - if exp, act := expErrs, readerImpl.ackRcvd; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected errors returned: %v != %v", act, exp) - } -} - -type asyncReaderCantReadOrConnect struct { - connected int -} - -func (r *asyncReaderCantReadOrConnect) Connect(ctx context.Context) error { - r.connected++ - return errors.New("sorry") -} - -func (r *asyncReaderCantReadOrConnect) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - return nil, nil, component.ErrNotConnected -} -func (r *asyncReaderCantReadOrConnect) Close(ctx context.Context) error { return nil } - -func TestAsyncReaderTypeConnMaxExceeded(t *testing.T) { - readerImpl := &asyncReaderCantReadOrConnect{} - - boff := backoff.NewExponentialBackOff() - boff.InitialInterval = time.Millisecond - boff.MaxInterval = time.Millisecond * 10 - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager(), input.AsyncReaderWithConnBackOff(backoff.WithMaxRetries(boff, 3))) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, r.WaitForClose(ctx)) - assert.Equal(t, 4, readerImpl.connected) -} - -//------------------------------------------------------------------------------ - -func BenchmarkAsyncReaderGenerateN1(b *testing.B) { - benchmarkAsyncReaderGenerateN(b, 1) -} - -func BenchmarkAsyncReaderGenerateN10(b *testing.B) { - benchmarkAsyncReaderGenerateN(b, 10) -} - -func BenchmarkAsyncReaderGenerateN100(b *testing.B) { - benchmarkAsyncReaderGenerateN(b, 100) -} - -func BenchmarkAsyncReaderGenerateN1000(b *testing.B) { - benchmarkAsyncReaderGenerateN(b, 1000) -} - -type mockStaticReader struct { - d []byte -} - -func (r *mockStaticReader) Connect(ctx context.Context) error { - return nil -} - -func (r *mockStaticReader) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - nextMsg := message.QuickBatch([][]byte{r.d}) - return nextMsg, func(ctx context.Context, res error) error { - return nil - }, nil -} - -func (r *mockStaticReader) Close(ctx context.Context) error { - return nil -} - -func benchmarkAsyncReaderGenerateN(b *testing.B, capacity int) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - readerImpl := &mockStaticReader{ - d: []byte(`root = "hello world"`), - } - - r, err := input.NewAsyncReader("foo", readerImpl, mock.NewManager()) - require.NoError(b, err) - - b.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - r.TriggerStopConsuming() - require.NoError(b, r.WaitForClose(ctx)) - }) - - resFns := make([]func(context.Context, error) error, capacity) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N/capacity; i++ { - for j := 0; j < capacity; j++ { - select { - case ts, open := <-r.TransactionChan(): - require.True(b, open) - resFns[j] = ts.Ack - case <-time.After(time.Second): - b.Fatal("Timed out") - } - } - - for j := 0; j < capacity; j++ { - require.NoError(b, resFns[j](tCtx, nil)) - } - } -} diff --git a/internal/component/input/batcher/batcher.go b/internal/component/input/batcher/batcher.go deleted file mode 100644 index 944b2f4516..0000000000 --- a/internal/component/input/batcher/batcher.go +++ /dev/null @@ -1,188 +0,0 @@ -package batcher - -import ( - "context" - "sync" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" -) - -// Impl wraps an input with a batch policy. -type Impl struct { - log log.Modular - - child input.Streamed - batcher *policy.Batcher - - messagesOut chan message.Transaction - - shutSig *shutdown.Signaller -} - -// New creates a new Batcher around an input. -func New(batcher *policy.Batcher, child input.Streamed, log log.Modular) input.Streamed { - b := Impl{ - log: log, - child: child, - batcher: batcher, - messagesOut: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - go b.loop() - return &b -} - -//------------------------------------------------------------------------------ - -func (m *Impl) loop() { - closeNowCtx, cnDone := m.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - defer func() { - m.child.TriggerCloseNow() - _ = m.child.WaitForClose(context.Background()) - - _ = m.batcher.Close(context.Background()) - - close(m.messagesOut) - m.shutSig.TriggerHasStopped() - }() - - var nextTimedBatchChan <-chan time.Time - if tNext := m.batcher.UntilNext(); tNext > 0 { - nextTimedBatchChan = time.After(tNext) - } - - pendingTrans := []*transaction.Tracked{} - pendingAcks := sync.WaitGroup{} - - flushBatchFn := func() { - sendMsg := m.batcher.Flush(closeNowCtx) - if sendMsg == nil { - return - } - - resChan := make(chan error) - select { - case m.messagesOut <- message.NewTransaction(sendMsg, resChan): - case <-m.shutSig.HardStopChan(): - return - } - - pendingAcks.Add(1) - go func(rChan <-chan error, aggregatedTransactions []*transaction.Tracked) { - defer pendingAcks.Done() - - select { - case <-m.shutSig.HardStopChan(): - return - case res, open := <-rChan: - if !open { - return - } - for _, c := range aggregatedTransactions { - if err := c.Ack(closeNowCtx, res); err != nil { - return - } - } - } - }(resChan, pendingTrans) - pendingTrans = nil - } - - defer func() { - // Final flush of remaining documents. - m.log.Debug("Flushing remaining messages of batch.") - flushBatchFn() - - // Wait for all pending acks to resolve. - m.log.Debug("Waiting for pending acks to resolve before shutting down.") - pendingAcks.Wait() - m.log.Debug("Pending acks resolved.") - }() - - for { - if nextTimedBatchChan == nil { - if tNext := m.batcher.UntilNext(); tNext > 0 { - nextTimedBatchChan = time.After(tNext) - } - } - - var flushBatch bool - select { - case tran, open := <-m.child.TransactionChan(): - if !open { - // If we're waiting for a timed batch then we will respect it. - if nextTimedBatchChan != nil { - select { - case <-nextTimedBatchChan: - case <-m.shutSig.SoftStopChan(): - } - } - flushBatchFn() - return - } - - trackedTran := transaction.NewTracked(tran.Payload, tran.Ack) - _ = trackedTran.Message().Iter(func(i int, p *message.Part) error { - if m.batcher.Add(p) { - flushBatch = true - } - return nil - }) - pendingTrans = append(pendingTrans, trackedTran) - case <-nextTimedBatchChan: - flushBatch = true - nextTimedBatchChan = nil - case <-m.shutSig.HardStopChan(): - return - } - - if flushBatch { - flushBatchFn() - } - } -} - -// Connected returns true if the underlying input is connected. -func (m *Impl) Connected() bool { - return m.child.Connected() -} - -// TransactionChan returns the channel used for consuming messages from this -// buffer. -func (m *Impl) TransactionChan() <-chan message.Transaction { - return m.messagesOut -} - -// TriggerStopConsuming instructs the input to start shutting down resources -// once all pending messages are delivered and acknowledged. This call does -// not block. -func (m *Impl) TriggerStopConsuming() { - m.shutSig.TriggerSoftStop() - m.child.TriggerStopConsuming() -} - -// TriggerCloseNow triggers the shut down of this component but should not block -// the calling goroutine. -func (m *Impl) TriggerCloseNow() { - m.shutSig.TriggerHardStop() -} - -// WaitForClose is a blocking call to wait until the component has finished -// shutting down and cleaning up resources. -func (m *Impl) WaitForClose(ctx context.Context) error { - select { - case <-m.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/component/input/batcher/batcher_test.go b/internal/component/input/batcher/batcher_test.go deleted file mode 100644 index dfa7e1a6e7..0000000000 --- a/internal/component/input/batcher/batcher_test.go +++ /dev/null @@ -1,364 +0,0 @@ -package batcher_test - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - ibatch "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/batch/policy/batchconfig" - "github.com/benthosdev/benthos/v4/internal/component/input/batcher" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestBatcherStandard(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockInput := &mock.Input{ - TChan: make(chan message.Transaction), - } - - batchConf := batchconfig.NewConfig() - batchConf.Count = 3 - - batchPol, err := policy.New(batchConf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - batcher := batcher.New(batchPol, mockInput, log.Noop()) - - testMsgs := []string{} - testResChans := []chan error{} - for i := 0; i < 8; i++ { - testMsgs = append(testMsgs, fmt.Sprintf("test%v", i)) - testResChans = append(testResChans, make(chan error)) - } - - resErrs := []error{} - doneWritesChan := make(chan struct{}) - doneReadsChan := make(chan struct{}) - go func() { - for i, m := range testMsgs { - mockInput.TChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(m)}), testResChans[i]) - } - close(doneWritesChan) - for _, rChan := range testResChans { - resErrs = append(resErrs, (<-rChan)) - } - close(doneReadsChan) - }() - - resFns := []func(context.Context, error) error{} - - var tran message.Transaction - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - { - tmpTran := tran - resFns = append(resFns, tmpTran.Ack) - } - - if exp, act := 3, tran.Payload.Len(); exp != act { - t.Errorf("Wrong batch size: %v != %v", act, exp) - } - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - if exp, act := fmt.Sprintf("test%v", i), string(part.AsBytes()); exp != act { - t.Errorf("Unexpected message part: %v != %v", act, exp) - } - return nil - }) - - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - { - tmpTran := tran - resFns = append(resFns, tmpTran.Ack) - } - - if exp, act := 3, tran.Payload.Len(); exp != act { - t.Errorf("Wrong batch size: %v != %v", act, exp) - } - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - if exp, act := fmt.Sprintf("test%v", i+3), string(part.AsBytes()); exp != act { - t.Errorf("Unexpected message part: %v != %v", act, exp) - } - return nil - }) - - select { - case <-batcher.TransactionChan(): - t.Error("Unexpected batch received") - default: - } - - select { - case <-doneWritesChan: - case <-time.After(time.Second): - t.Error("timed out") - } - batcher.TriggerStopConsuming() - - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - { - tmpTran := tran - resFns = append(resFns, tmpTran.Ack) - } - - if exp, act := 2, tran.Payload.Len(); exp != act { - t.Errorf("Wrong batch size: %v != %v", act, exp) - } - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - if exp, act := fmt.Sprintf("test%v", i+6), string(part.AsBytes()); exp != act { - t.Errorf("Unexpected message part: %v != %v", act, exp) - } - return nil - }) - - for i, rFn := range resFns { - require.NoError(t, rFn(tCtx, fmt.Errorf("testerr%v", i))) - } - - select { - case <-doneReadsChan: - case <-time.After(time.Second): - t.Error("timed out") - } - - for i, err := range resErrs { - exp := "testerr0" - if i >= 3 { - exp = "testerr1" - } - if i >= 6 { - exp = "testerr2" - } - if act := err.Error(); exp != act { - t.Errorf("Unexpected error returned: %v != %v", act, exp) - } - } - - if err := batcher.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestBatcherErrorTracking(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockInput := &mock.Input{ - TChan: make(chan message.Transaction), - } - - batchConf := batchconfig.NewConfig() - batchConf.Count = 3 - - batchPol, err := policy.New(batchConf, mock.NewManager()) - require.NoError(t, err) - - batcher := batcher.New(batchPol, mockInput, log.Noop()) - - testMsgs := []string{} - testResChans := []chan error{} - for i := 0; i < 3; i++ { - testMsgs = append(testMsgs, fmt.Sprintf("test%v", i)) - testResChans = append(testResChans, make(chan error)) - } - - resErrs := []error{} - doneReadsChan := make(chan struct{}) - go func() { - for i, m := range testMsgs { - mockInput.TChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(m)}), testResChans[i]) - } - for _, rChan := range testResChans { - resErrs = append(resErrs, (<-rChan)) - } - close(doneReadsChan) - }() - - var tran message.Transaction - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - assert.Equal(t, 3, tran.Payload.Len()) - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - assert.Equal(t, fmt.Sprintf("test%v", i), string(part.AsBytes())) - return nil - }) - - batchErr := ibatch.NewError(tran.Payload, errors.New("ignore this")) - batchErr.Failed(1, errors.New("message specific error")) - require.NoError(t, tran.Ack(tCtx, batchErr)) - - select { - case <-doneReadsChan: - case <-time.After(time.Second * 5): - t.Fatal("timed out") - } - - require.Len(t, resErrs, 3) - assert.NoError(t, resErrs[0]) - assert.EqualError(t, resErrs[1], "message specific error") - assert.NoError(t, resErrs[2]) - - mockInput.TriggerStopConsuming() - require.NoError(t, batcher.WaitForClose(tCtx)) -} - -func TestBatcherTiming(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockInput := &mock.Input{ - TChan: make(chan message.Transaction), - } - - batchConf := batchconfig.NewConfig() - batchConf.Count = 0 - batchConf.Period = "1ms" - - batchPol, err := policy.New(batchConf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - batcher := batcher.New(batchPol, mockInput, log.Noop()) - - resChan := make(chan error) - select { - case mockInput.TChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo1")}), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var tran message.Transaction - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if exp, act := 1, tran.Payload.Len(); exp != act { - t.Errorf("Wrong batch size: %v != %v", act, exp) - } - if exp, act := "foo1", string(tran.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected message part: %v != %v", act, exp) - } - - errSend := errors.New("this is a test error") - require.NoError(t, tran.Ack(tCtx, errSend)) - select { - case err := <-resChan: - if err != errSend { - t.Errorf("Unexpected error: %v != %v", err, errSend) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - select { - case mockInput.TChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo2")}), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if exp, act := 1, tran.Payload.Len(); exp != act { - t.Errorf("Wrong batch size: %v != %v", act, exp) - } - if exp, act := "foo2", string(tran.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected message part: %v != %v", act, exp) - } - - batcher.TriggerStopConsuming() - - require.NoError(t, tran.Ack(tCtx, errSend)) - select { - case err := <-resChan: - if err != errSend { - t.Errorf("Unexpected error: %v != %v", err, errSend) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if err := batcher.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestBatcherFinalFlush(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockInput := &mock.Input{ - TChan: make(chan message.Transaction), - } - - batchConf := batchconfig.NewConfig() - batchConf.Count = 10 - - batchPol, err := policy.New(batchConf, mock.NewManager()) - require.NoError(t, err) - - batcher := batcher.New(batchPol, mockInput, log.Noop()) - - resChan := make(chan error, 1) - select { - case mockInput.TChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo1")}), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - mockInput.TriggerStopConsuming() - - var tran message.Transaction - select { - case tran = <-batcher.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if exp, act := 1, tran.Payload.Len(); exp != act { - t.Errorf("Wrong batch size: %v != %v", act, exp) - } - if exp, act := "foo1", string(tran.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected message part: %v != %v", act, exp) - } - - batcher.TriggerStopConsuming() - require.NoError(t, tran.Ack(tCtx, nil)) - - if err := batcher.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} diff --git a/internal/component/input/config.go b/internal/component/input/config.go deleted file mode 100644 index a4c9e62689..0000000000 --- a/internal/component/input/config.go +++ /dev/null @@ -1,109 +0,0 @@ -package input - -import ( - "fmt" - - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all input types. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -type Config struct { - Label string `json:"label" yaml:"label"` - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` - Processors []processor.Config `json:"processors" yaml:"processors"` -} - -// NewConfig returns a configuration struct fully populated with default values. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -func NewConfig() Config { - return Config{ - Label: "", - Type: "stdin", - Plugin: nil, - Processors: []processor.Config{}, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeInput, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - conf.Label, _ = value["label"].(string) - - if procV, exists := value["processors"]; exists { - procArr, ok := procV.([]any) - if !ok { - err = fmt.Errorf("processors: unexpected value, expected array got %T", procV) - return - } - for i, pv := range procArr { - var tmpProc processor.Config - if tmpProc, err = processor.FromAny(prov, pv); err != nil { - err = fmt.Errorf("%v: %w", i, err) - return - } - conf.Processors = append(conf.Processors, tmpProc) - } - } - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeInput, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - for i := 0; i < len(value.Content)-1; i += 2 { - switch value.Content[i].Value { - case "label": - conf.Label = value.Content[i+1].Value - case "processors": - for i, n := range value.Content[i+1].Content { - var tmpProc processor.Config - if tmpProc, err = processor.FromAny(prov, n); err != nil { - err = fmt.Errorf("%v: %w", i, err) - return - } - conf.Processors = append(conf.Processors, tmpProc) - } - } - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/input/config/config.go b/internal/component/input/config/config.go deleted file mode 100644 index 6b0d2ea036..0000000000 --- a/internal/component/input/config/config.go +++ /dev/null @@ -1,76 +0,0 @@ -// Package config contains reusable config definitions and parsers for inputs -// defined via the public/service package. We could eventually consider moving -// this out into public/service for others to use. -package config - -import ( - "time" - - "github.com/cenkalti/backoff/v4" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/public/service" -) - -// Connection fields -const ( - fieldConn = "connection" - fieldConnMaxRetries = "max_retries" -) - -func connectionField() *service.ConfigField { - return service.NewObjectField(fieldConn, - service.NewIntField(fieldConnMaxRetries). - Description("An optional limit to the number of consecutive retry attempts that will be made before abandoning the connection altogether and gracefully terminating the input. When all inputs terminate in this way the service (or stream) will shut down. If set to zero connections will never be reattempted upon a failure. If set below zero this field is ignored (effectively unset)."). - Advanced(). - Example(-1). - Example(10). - Optional(), - ). - Description("Customise how websocket connection attempts are made."). - Optional(). - Advanced() -} - -func connectionBackOffFromParsed(conf *service.ParsedConfig) (boff backoff.BackOff, err error) { - { - eboff := backoff.NewExponentialBackOff() - eboff.InitialInterval = time.Millisecond * 100 - eboff.MaxInterval = time.Second - eboff.MaxElapsedTime = 0 - boff = eboff - } - - if conf.Contains(fieldConnMaxRetries) { - maxRetries, err := conf.FieldInt(fieldConnMaxRetries) - if err != nil { - return nil, err - } - if maxRetries >= 0 { - boff = backoff.WithMaxRetries(boff, uint64(maxRetries)) - } - } - return -} - -// AsyncOptsFields returns a list of config fields with the intended purpose of -// exposing AsyncReader configuration opts. -func AsyncOptsFields() []*service.ConfigField { - return []*service.ConfigField{ - connectionField(), - } -} - -// AsyncOptsFromParsed returns a slice of AsyncReader opt funcs from a parsed -// config, allowing users to customise behaviour such as connection retry back -// off. -func AsyncOptsFromParsed(conf *service.ParsedConfig) (opts []func(*input.AsyncReader), err error) { - if conf.Contains(fieldConn) { - var boff backoff.BackOff - if boff, err = connectionBackOffFromParsed(conf.Namespace(fieldConn)); err != nil { - return - } - opts = append(opts, input.AsyncReaderWithConnBackOff(boff)) - } - return -} diff --git a/internal/component/input/interface.go b/internal/component/input/interface.go deleted file mode 100644 index 81b6d3c681..0000000000 --- a/internal/component/input/interface.go +++ /dev/null @@ -1,57 +0,0 @@ -package input - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Streamed is a common interface implemented by inputs and provides channel -// based streaming APIs. -type Streamed interface { - // TransactionChan returns a channel used for consuming transactions from - // this type. Every transaction received must be resolved before another - // transaction will be sent. - TransactionChan() <-chan message.Transaction - - // Connected returns a boolean indicating whether this input is currently - // connected to its target. - Connected() bool - - // TriggerStopConsuming instructs the input to start shutting down resources - // once all pending messages are delivered and acknowledged. This call does - // not block. - TriggerStopConsuming() - - // TriggerCloseNow triggers the shut down of this component but should not - // block the calling goroutine. - TriggerCloseNow() - - // WaitForClose is a blocking call to wait until the component has finished - // shutting down and cleaning up resources. - WaitForClose(ctx context.Context) error -} - -// AsyncAckFn is a function used to acknowledge receipt of a message batch. The -// provided response indicates whether the message batch was successfully -// delivered. Returns an error if the acknowledge was not propagated. -type AsyncAckFn func(context.Context, error) error - -// Async is a type that reads Benthos messages from an external source and -// allows acknowledgements for a message batch to be propagated asynchronously. -type Async interface { - // Connect attempts to establish a connection to the source, if - // unsuccessful returns an error. If the attempt is successful (or not - // necessary) returns nil. - Connect(ctx context.Context) error - - // ReadBatch attempts to read a new message from the source. If - // successful a message is returned along with a function used to - // acknowledge receipt of the returned message. It's safe to process the - // returned message and read the next message asynchronously. - ReadBatch(ctx context.Context) (message.Batch, AsyncAckFn, error) - - // Close triggers the shut down of this component and blocks until - // completion or context cancellation. - Close(ctx context.Context) error -} diff --git a/internal/component/input/processors/append.go b/internal/component/input/processors/append.go deleted file mode 100644 index 7cb7b95099..0000000000 --- a/internal/component/input/processors/append.go +++ /dev/null @@ -1,45 +0,0 @@ -package processors - -import ( - "fmt" - "strconv" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -// AppendFromConfig takes a variant arg of pipeline constructor functions and -// returns a new slice of them where the processors of the provided input -// configuration will also be initialized. -func AppendFromConfig(conf input.Config, mgr bundle.NewManagement, pipelines ...processor.PipelineConstructorFunc) []processor.PipelineConstructorFunc { - if len(conf.Processors) > 0 { - pipelines = append([]processor.PipelineConstructorFunc{func() (processor.Pipeline, error) { - processors := make([]processor.V1, len(conf.Processors)) - for j, procConf := range conf.Processors { - newMgr := mgr.IntoPath("processors", strconv.Itoa(j)) - var err error - processors[j], err = newMgr.NewProcessor(procConf) - if err != nil { - return nil, fmt.Errorf("failed to create processor '%v': %v", procConf.Type, err) - } - } - return pipeline.NewProcessor(processors...), nil - }}, pipelines...) - } - return pipelines -} - -// WrapConstructor provides a way to define an input constructor without -// manually initializing processors of the config. -func WrapConstructor(fn func(input.Config, bundle.NewManagement) (input.Streamed, error)) bundle.InputConstructor { - return func(c input.Config, nm bundle.NewManagement) (input.Streamed, error) { - i, err := fn(c, nm) - if err != nil { - return nil, err - } - pcf := AppendFromConfig(c, nm) - return input.WrapWithPipelines(i, pcf...) - } -} diff --git a/internal/component/input/wrap_with_pipeline.go b/internal/component/input/wrap_with_pipeline.go deleted file mode 100644 index 3ba18407eb..0000000000 --- a/internal/component/input/wrap_with_pipeline.go +++ /dev/null @@ -1,80 +0,0 @@ -package input - -import ( - "context" - - iprocessor "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// WithPipeline is a type that wraps both an input type and a pipeline type -// by routing the input through the pipeline, and implements the input.Type -// interface in order to act like an ordinary input. -type WithPipeline struct { - in Streamed - pipe iprocessor.Pipeline -} - -// WrapWithPipeline routes an input directly into a processing pipeline and -// returns a type that manages both and acts like an ordinary input. -func WrapWithPipeline(in Streamed, pipeConstructor iprocessor.PipelineConstructorFunc) (*WithPipeline, error) { - pipe, err := pipeConstructor() - if err != nil { - return nil, err - } - - if err := pipe.Consume(in.TransactionChan()); err != nil { - return nil, err - } - return &WithPipeline{ - in: in, - pipe: pipe, - }, nil -} - -// WrapWithPipelines wraps an input with a variadic number of pipelines. -func WrapWithPipelines(in Streamed, pipeConstructors ...iprocessor.PipelineConstructorFunc) (Streamed, error) { - var err error - for _, ctor := range pipeConstructors { - if in, err = WrapWithPipeline(in, ctor); err != nil { - return nil, err - } - } - return in, nil -} - -//------------------------------------------------------------------------------ - -// TransactionChan returns the channel used for consuming transactions from this -// input. -func (i *WithPipeline) TransactionChan() <-chan message.Transaction { - return i.pipe.TransactionChan() -} - -// Connected returns a boolean indicating whether this input is currently -// connected to its target. -func (i *WithPipeline) Connected() bool { - return i.in.Connected() -} - -//------------------------------------------------------------------------------ - -// TriggerStopConsuming instructs the input to start shutting down resources -// once all pending messages are delivered and acknowledged. This call does -// not block. -func (i *WithPipeline) TriggerStopConsuming() { - i.in.TriggerStopConsuming() -} - -// TriggerCloseNow triggers the shut down of this component but should not block -// the calling goroutine. -func (i *WithPipeline) TriggerCloseNow() { - i.in.TriggerCloseNow() - i.pipe.TriggerCloseNow() -} - -// WaitForClose is a blocking call to wait until the component has finished -// shutting down and cleaning up resources. -func (i *WithPipeline) WaitForClose(ctx context.Context) error { - return i.pipe.WaitForClose(ctx) -} diff --git a/internal/component/input/wrap_with_pipeline_test.go b/internal/component/input/wrap_with_pipeline_test.go deleted file mode 100644 index c8d502e6b7..0000000000 --- a/internal/component/input/wrap_with_pipeline_test.go +++ /dev/null @@ -1,349 +0,0 @@ -package input_test - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - iprocessor "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/pipeline" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -type mockInput struct { - closeOnce sync.Once - ts chan message.Transaction -} - -func (m *mockInput) TransactionChan() <-chan message.Transaction { - return m.ts -} - -func (m *mockInput) Connected() bool { - return true -} - -func (m *mockInput) TriggerStopConsuming() { - m.closeOnce.Do(func() { - close(m.ts) - }) -} - -func (m *mockInput) TriggerCloseNow() { -} - -func (m *mockInput) WaitForClose(ctx context.Context) error { - return errors.New("wasnt expecting to ever see this tbh") -} - -//------------------------------------------------------------------------------ - -type mockPipe struct { - tsIn <-chan message.Transaction - ts chan message.Transaction -} - -func (m *mockPipe) Consume(ts <-chan message.Transaction) error { - m.tsIn = ts - return nil -} - -func (m *mockPipe) TransactionChan() <-chan message.Transaction { - return m.ts -} - -func (m *mockPipe) TriggerCloseNow() { - close(m.ts) -} - -func (m *mockPipe) WaitForClose(ctx context.Context) error { - return nil -} - -//------------------------------------------------------------------------------ - -func TestBasicWrapPipeline(t *testing.T) { - mockIn := &mockInput{ts: make(chan message.Transaction)} - mockPi := &mockPipe{ - ts: make(chan message.Transaction), - } - - _, err := input.WrapWithPipeline(mockIn, func() (iprocessor.Pipeline, error) { - return nil, errors.New("nope") - }) - - if err == nil { - t.Error("Expected error from back constructor") - } - - newInput, err := input.WrapWithPipeline(mockIn, func() (iprocessor.Pipeline, error) { - return mockPi, nil - }) - if err != nil { - t.Fatal(err) - } - - if newInput.TransactionChan() != mockPi.ts { - t.Error("Wrong transaction chan in new input type") - } - - if mockIn.ts != mockPi.tsIn { - t.Error("Wrong transactions chan in mock pipe") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - newInput.TriggerStopConsuming() - require.NoError(t, newInput.WaitForClose(ctx)) - - select { - case _, open := <-mockIn.ts: - if open { - t.Error("mock input is still open after close") - } - case _, open := <-mockPi.ts: - if open { - t.Error("mock pipe is still open after close") - } - default: - t.Error("neither type was closed") - } -} - -func TestWrapZeroPipelines(t *testing.T) { - mockIn := &mockInput{ts: make(chan message.Transaction)} - newInput, err := input.WrapWithPipelines(mockIn) - if err != nil { - t.Error(err) - } - - if newInput != mockIn { - t.Errorf("Wrong input obj returned: %v != %v", newInput, mockIn) - } -} - -func TestBasicWrapMultiPipelines(t *testing.T) { - mockIn := &mockInput{ts: make(chan message.Transaction)} - mockPi1 := &mockPipe{ - ts: make(chan message.Transaction), - } - mockPi2 := &mockPipe{ - ts: make(chan message.Transaction), - } - - _, err := input.WrapWithPipelines(mockIn, func() (iprocessor.Pipeline, error) { - return nil, errors.New("nope") - }) - if err == nil { - t.Error("Expected error from back constructor") - } - - newInput, err := input.WrapWithPipelines(mockIn, func() (iprocessor.Pipeline, error) { - return mockPi1, nil - }, func() (iprocessor.Pipeline, error) { - return mockPi2, nil - }) - if err != nil { - t.Fatal(err) - } - - if newInput.TransactionChan() != mockPi2.ts { - t.Error("Wrong message chan in new input type") - } - if mockPi2.tsIn != mockPi1.ts { - t.Error("Wrong message chan in mock pipe 2") - } - - if mockIn.ts != mockPi1.tsIn { - t.Error("Wrong messages chan in mock pipe 1") - } - if mockPi1.ts != mockPi2.tsIn { - t.Error("Wrong messages chan in mock pipe 2") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - newInput.TriggerStopConsuming() - require.NoError(t, newInput.WaitForClose(ctx)) - - select { - case _, open := <-mockIn.ts: - if open { - t.Error("mock input is still open after close") - } - case _, open := <-mockPi1.ts: - if open { - t.Error("mock pipe is still open after close") - } - case _, open := <-mockPi2.ts: - if open { - t.Error("mock pipe is still open after close") - } - default: - t.Error("neither type was closed") - } -} - -//------------------------------------------------------------------------------ - -type mockProc struct{} - -func (m mockProc) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - msgs := [1]message.Batch{msg} - return msgs[:], nil -} - -func (m mockProc) Close(ctx context.Context) error { - // Do nothing as our processor doesn't require resource cleanup. - return nil -} - -//------------------------------------------------------------------------------ - -func TestBasicWrapProcessors(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - mockIn := &mockInput{ts: make(chan message.Transaction)} - - pipe1 := pipeline.NewProcessor(mockProc{}) - pipe2 := pipeline.NewProcessor(mockProc{}) - - newInput, err := input.WrapWithPipelines(mockIn, func() (iprocessor.Pipeline, error) { - return pipe1, nil - }, func() (iprocessor.Pipeline, error) { - return pipe2, nil - }) - if err != nil { - t.Error(err) - } - - resChan := make(chan error) - - msg := message.QuickBatch([][]byte{[]byte("baz")}) - - select { - case mockIn.ts <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Error("action timed out") - } - - // Message should not be discarded - var ts message.Transaction - var open bool - select { - case res, open := <-resChan: - if !open { - t.Error("Channel was closed") - } - t.Errorf("Unexpected response: %v", res) - case ts, open = <-newInput.TransactionChan(): - if !open { - t.Error("channel was closed") - } else if exp, act := "baz", string(ts.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message received: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Error("action timed out") - } - - errFailed := errors.New("derp, failed") - - // Send error - go func() { - require.NoError(t, ts.Ack(tCtx, errFailed)) - }() - - // Receive again - select { - case res, open := <-resChan: - if !open { - t.Error("Channel was closed") - } - if res != errFailed { - t.Error(res) - } - case <-time.After(time.Second): - t.Error("action timed out") - } - - newInput.TriggerStopConsuming() - require.NoError(t, newInput.WaitForClose(tCtx)) -} - -func TestBasicWrapDoubleProcessors(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - mockIn := &mockInput{ts: make(chan message.Transaction)} - - pipe1 := pipeline.NewProcessor(mockProc{}, mockProc{}) - - newInput, err := input.WrapWithPipelines(mockIn, func() (iprocessor.Pipeline, error) { - return pipe1, nil - }) - if err != nil { - t.Error(err) - } - - resChan := make(chan error) - - msg := message.QuickBatch([][]byte{[]byte("baz")}) - - select { - case mockIn.ts <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Error("action timed out") - } - - // Message should not be discarded - var ts message.Transaction - var open bool - select { - case res, open := <-resChan: - if !open { - t.Error("Channel was closed") - } - t.Errorf("Unexpected response: %v", res) - case ts, open = <-newInput.TransactionChan(): - if !open { - t.Error("channel was closed") - } else if exp, act := "baz", string(ts.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message received: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Error("action timed out") - } - - errFailed := errors.New("derp, failed") - - // Send error - go func() { - require.NoError(t, ts.Ack(tCtx, errFailed)) - }() - - // Receive again - select { - case res, open := <-resChan: - if !open { - t.Error("Channel was closed") - } - if res != errFailed { - t.Error(res) - } - case <-time.After(time.Second): - t.Error("action timed out") - } - - newInput.TriggerStopConsuming() - require.NoError(t, newInput.WaitForClose(tCtx)) -} diff --git a/internal/component/interop/interop.go b/internal/component/interop/interop.go deleted file mode 100644 index 8b1cc1ed7b..0000000000 --- a/internal/component/interop/interop.go +++ /dev/null @@ -1,165 +0,0 @@ -package interop - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/public/service" -) - -// UnwrapOwnedInput attempts to unwrap a public owned component into an internal -// variant. This is useful in cases where we're migrating internal components to -// use the public configuration APIs but aren't quite ready to move the full -// implementation yet. -func UnwrapOwnedInput(o *service.OwnedInput) input.Streamed { - return o.XUnwrapper().(interface { - Unwrap() input.Streamed - }).Unwrap() -} - -// UnwrapInternalInput is a no-op implementation of an internal component that -// allows a public/service environment to unwrap it straight into the needed -// format during construction. This is useful in cases where we're migrating -// internal components to use the public configuration APIs but aren't quite -// ready to move the full implementation yet. -type UnwrapInternalInput struct { - s input.Streamed -} - -// NewUnwrapInternalInput returns wraps an internal component implementation. -func NewUnwrapInternalInput(s input.Streamed) *UnwrapInternalInput { - return &UnwrapInternalInput{s: s} -} - -// Unwrap in order to obtain the underlying Streamed implementation. -func (u *UnwrapInternalInput) Unwrap() input.Streamed { - return u.s -} - -// Connect does nothing, use Unwrap instead. -func (u *UnwrapInternalInput) Connect(ctx context.Context) error { - return component.ErrNotUnwrapped -} - -// ReadBatch does nothing, use Unwrap instead. -func (u *UnwrapInternalInput) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - return nil, nil, component.ErrNotUnwrapped -} - -// Close does nothing, use Unwrap instead. -func (u *UnwrapInternalInput) Close(ctx context.Context) error { - return component.ErrNotUnwrapped -} - -//------------------------------------------------------------------------------ - -// UnwrapOwnedProcessor attempts to unwrap a public owned component into an -// internal variant. This is useful in cases where we're migrating internal -// components to use the public configuration APIs but aren't quite ready to -// move the full implementation yet. -func UnwrapOwnedProcessor(o *service.OwnedProcessor) processor.V1 { - return o.XUnwrapper().(interface { - Unwrap() processor.V1 - }).Unwrap() -} - -// UnwrapInternalBatchProcessor is a no-op implementation of an internal -// component that allows a public/service environment to unwrap it straight into -// the needed format during construction. This is useful in cases where we're -// migrating internal components to use the public configuration APIs but aren't -// quite ready to move the full implementation yet. -type UnwrapInternalBatchProcessor struct { - s processor.V1 -} - -// NewUnwrapInternalBatchProcessor returns wraps an internal component -// implementation. -func NewUnwrapInternalBatchProcessor(s processor.V1) *UnwrapInternalBatchProcessor { - return &UnwrapInternalBatchProcessor{s: s} -} - -// Unwrap in order to obtain the underlying Streamed implementation. -func (u *UnwrapInternalBatchProcessor) Unwrap() processor.V1 { - return u.s -} - -// ProcessBatch does nothing, use Unwrap instead. -func (u *UnwrapInternalBatchProcessor) ProcessBatch(ctx context.Context, b service.MessageBatch) ([]service.MessageBatch, error) { - return nil, component.ErrNotUnwrapped -} - -// Close does nothing, use Unwrap instead. -func (u *UnwrapInternalBatchProcessor) Close(ctx context.Context) error { - return component.ErrNotUnwrapped -} - -//------------------------------------------------------------------------------ - -// UnwrapOwnedOutput attempts to unwrap a public owned component into an internal -// variant. This is useful in cases where we're migrating internal components to -// use the public configuration APIs but aren't quite ready to move the full -// implementation yet. -func UnwrapOwnedOutput(o *service.OwnedOutput) output.Streamed { - return o.XUnwrapper().(interface { - Unwrap() output.Streamed - }).Unwrap() -} - -// UnwrapInternalOutput is a no-op implementation of an internal component that -// allows a public/service environment to unwrap it straight into the needed -// format during construction. This is useful in cases where we're migrating -// internal components to use the public configuration APIs but aren't quite -// ready to move the full implementation yet. -type UnwrapInternalOutput struct { - s output.Streamed -} - -// NewUnwrapInternalOutput returns wraps an internal component implementation. -func NewUnwrapInternalOutput(s output.Streamed) *UnwrapInternalOutput { - return &UnwrapInternalOutput{s: s} -} - -// Unwrap in order to obtain the underlying Streamed implementation. -func (u *UnwrapInternalOutput) Unwrap() output.Streamed { - return u.s -} - -// Connect does nothing, use Unwrap instead. -func (u *UnwrapInternalOutput) Connect(ctx context.Context) error { - return component.ErrNotUnwrapped -} - -// WriteBatch does nothing, use Unwrap instead. -func (u *UnwrapInternalOutput) WriteBatch(ctx context.Context, b service.MessageBatch) error { - return component.ErrNotUnwrapped -} - -// Close does nothing, use Unwrap instead. -func (u *UnwrapInternalOutput) Close(ctx context.Context) error { - return component.ErrNotUnwrapped -} - -//------------------------------------------------------------------------------ - -// UnwrapManagement a public *service.Resources type into an internal -// bundle.NewManagement type. This solution will eventually be phased out as it -// is only used for migrating components. -func UnwrapManagement(r *service.Resources) bundle.NewManagement { - return r.XUnwrapper().(interface { - Unwrap() bundle.NewManagement - }).Unwrap() -} - -// UnwrapBatcher unwraps a public *service.Batcher type into an internal -// *policy.Batcher type. This solution will eventually be phased out as it is -// only used for migrating components. -func UnwrapBatcher(b *service.Batcher) *policy.Batcher { - return b.XUnwrapper().(interface { - Unwrap() *policy.Batcher - }).Unwrap() -} diff --git a/internal/component/metrics/combine.go b/internal/component/metrics/combine.go deleted file mode 100644 index 30dcef51e5..0000000000 --- a/internal/component/metrics/combine.go +++ /dev/null @@ -1,197 +0,0 @@ -package metrics - -import "net/http" - -type combinedWrapper struct { - t1 Type - t2 Type -} - -// Combine returns a Type implementation that feeds metrics into two underlying -// Type implementations. -func Combine(t1, t2 Type) Type { - return &combinedWrapper{ - t1: t1, - t2: t2, - } -} - -func unwrapMetric(t Type) Type { - u, ok := t.(interface { - Unwrap() Type - }) - if ok { - t = u.Unwrap() - } - return t -} - -// Unwrap to the underlying metrics type. -func (c *combinedWrapper) Unwrap() Type { - t1 := unwrapMetric(c.t1) - t2 := unwrapMetric(c.t2) - if t1 == t2 { - return t1 - } - return &combinedWrapper{ - t1: t1, - t2: t2, - } -} - -//------------------------------------------------------------------------------ - -type combinedCounter struct { - c1 StatCounter - c2 StatCounter -} - -func (c *combinedCounter) Incr(count int64) { - c.c1.Incr(count) - c.c2.Incr(count) -} - -func (c *combinedCounter) IncrFloat64(count float64) { - c.c1.IncrFloat64(count) - c.c2.IncrFloat64(count) -} - -type combinedTimer struct { - c1 StatTimer - c2 StatTimer -} - -func (c *combinedTimer) Timing(delta int64) { - c.c1.Timing(delta) - c.c2.Timing(delta) -} - -type combinedGauge struct { - c1 StatGauge - c2 StatGauge -} - -func (c *combinedGauge) Set(value int64) { - c.c1.Set(value) - c.c2.Set(value) -} - -func (c *combinedGauge) SetFloat64(value float64) { - c.c1.SetFloat64(value) - c.c2.SetFloat64(value) -} - -func (c *combinedGauge) Incr(count int64) { - c.c1.Incr(count) - c.c2.Incr(count) -} - -func (c *combinedGauge) IncrFloat64(count float64) { - c.c1.IncrFloat64(count) - c.c2.IncrFloat64(count) -} - -func (c *combinedGauge) Decr(count int64) { - c.c1.Decr(count) - c.c2.Decr(count) -} - -func (c *combinedGauge) DecrFloat64(count float64) { - c.c1.DecrFloat64(count) - c.c2.DecrFloat64(count) -} - -//------------------------------------------------------------------------------ - -type combinedCounterVec struct { - c1 StatCounterVec - c2 StatCounterVec -} - -func (c *combinedCounterVec) With(labelValues ...string) StatCounter { - return &combinedCounter{ - c1: c.c1.With(labelValues...), - c2: c.c2.With(labelValues...), - } -} - -type combinedTimerVec struct { - c1 StatTimerVec - c2 StatTimerVec -} - -func (c *combinedTimerVec) With(labelValues ...string) StatTimer { - return &combinedTimer{ - c1: c.c1.With(labelValues...), - c2: c.c2.With(labelValues...), - } -} - -type combinedGaugeVec struct { - c1 StatGaugeVec - c2 StatGaugeVec -} - -func (c *combinedGaugeVec) With(labelValues ...string) StatGauge { - return &combinedGauge{ - c1: c.c1.With(labelValues...), - c2: c.c2.With(labelValues...), - } -} - -//------------------------------------------------------------------------------ - -func (c *combinedWrapper) GetCounter(path string) StatCounter { - return &combinedCounter{ - c1: c.t1.GetCounter(path), - c2: c.t2.GetCounter(path), - } -} - -func (c *combinedWrapper) GetCounterVec(path string, n ...string) StatCounterVec { - return &combinedCounterVec{ - c1: c.t1.GetCounterVec(path, n...), - c2: c.t2.GetCounterVec(path, n...), - } -} - -func (c *combinedWrapper) GetTimer(path string) StatTimer { - return &combinedTimer{ - c1: c.t1.GetTimer(path), - c2: c.t2.GetTimer(path), - } -} - -func (c *combinedWrapper) GetTimerVec(path string, n ...string) StatTimerVec { - return &combinedTimerVec{ - c1: c.t1.GetTimerVec(path, n...), - c2: c.t2.GetTimerVec(path, n...), - } -} - -func (c *combinedWrapper) GetGauge(path string) StatGauge { - return &combinedGauge{ - c1: c.t1.GetGauge(path), - c2: c.t2.GetGauge(path), - } -} - -func (c *combinedWrapper) GetGaugeVec(path string, n ...string) StatGaugeVec { - return &combinedGaugeVec{ - c1: c.t1.GetGaugeVec(path, n...), - c2: c.t2.GetGaugeVec(path, n...), - } -} - -func (c *combinedWrapper) HandlerFunc() http.HandlerFunc { - if h := c.t1.HandlerFunc(); h != nil { - return h - } - return c.t2.HandlerFunc() -} - -func (c *combinedWrapper) Close() error { - c.t1.Close() - c.t2.Close() - return nil -} diff --git a/internal/component/metrics/config.go b/internal/component/metrics/config.go deleted file mode 100644 index 3c83c9833b..0000000000 --- a/internal/component/metrics/config.go +++ /dev/null @@ -1,78 +0,0 @@ -package metrics - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all metric output -// types. -type Config struct { - Type string `json:"type" yaml:"type"` - Mapping string `json:"mapping" yaml:"mapping"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` -} - -// NewConfig returns a configuration struct fully populated with default values. -func NewConfig() Config { - return Config{ - Type: "none", - Mapping: "", - Plugin: nil, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeMetrics, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - - conf.Mapping, _ = value["mapping"].(string) - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeMetrics, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - for i := 0; i < len(value.Content)-1; i += 2 { - if value.Content[i].Value == "mapping" { - conf.Mapping = value.Content[i+1].Value - break - } - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/metrics/config_test.go b/internal/component/metrics/config_test.go deleted file mode 100644 index 2d79c17493..0000000000 --- a/internal/component/metrics/config_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package metrics_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - - _ "github.com/benthosdev/benthos/v4/public/components/prometheus" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestMappingConfigAny(t *testing.T) { - conf, err := metrics.FromAny(bundle.GlobalEnvironment, map[string]any{ - "prometheus": map[string]any{}, - "mapping": `meta foo = "bar"`, - }) - require.NoError(t, err) - - ns, err := bundle.AllMetrics.Init(conf, mock.NewManager()) - require.NoError(t, err) - - ctrTwo := ns.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - ctrTwo.With("value3").IncrFloat64(10.452) - - body := getPage(t, ns.Child().HandlerFunc()) - - assert.Contains(t, body, "\ncountertwo{foo=\"bar\",label1=\"value1\"} 10") - assert.Contains(t, body, "\ncountertwo{foo=\"bar\",label1=\"value2\"} 11") - assert.Contains(t, body, "\ncountertwo{foo=\"bar\",label1=\"value3\"} 10.452") -} - -func TestMappingConfigYAML(t *testing.T) { - n, err := docs.UnmarshalYAML([]byte(` -prometheus: {} -mapping: 'meta foo = "bar"' -`)) - require.NoError(t, err) - - conf, err := metrics.FromAny(bundle.GlobalEnvironment, n) - require.NoError(t, err) - - ns, err := bundle.AllMetrics.Init(conf, mock.NewManager()) - require.NoError(t, err) - - ctrTwo := ns.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - ctrTwo.With("value3").IncrFloat64(10.452) - - body := getPage(t, ns.Child().HandlerFunc()) - - assert.Contains(t, body, "\ncountertwo{foo=\"bar\",label1=\"value1\"} 10") - assert.Contains(t, body, "\ncountertwo{foo=\"bar\",label1=\"value2\"} 11") - assert.Contains(t, body, "\ncountertwo{foo=\"bar\",label1=\"value3\"} 10.452") -} diff --git a/internal/component/metrics/dud_type.go b/internal/component/metrics/dud_type.go deleted file mode 100644 index 71b9a688b7..0000000000 --- a/internal/component/metrics/dud_type.go +++ /dev/null @@ -1,80 +0,0 @@ -package metrics - -import "net/http" - -// DudStat implements the Stat interface but doesn't actual do anything. -type DudStat struct{} - -// Incr does nothing. -func (d DudStat) Incr(count int64) {} - -// Decr does nothing. -func (d DudStat) Decr(count int64) {} - -// Timing does nothing. -func (d DudStat) Timing(delta int64) {} - -// Set does nothing. -func (d DudStat) Set(value int64) {} - -// SetFloat64 does nothing -func (d DudStat) SetFloat64(value float64) {} - -// IncrFloat64 does nothing -func (d DudStat) IncrFloat64(count float64) {} - -// DecrFloat64 does nothing -func (d DudStat) DecrFloat64(count float64) {} - -//------------------------------------------------------------------------------ - -var _ Type = DudType{} - -// DudType implements the Type interface but doesn't actual do anything. -type DudType struct { - ID int -} - -// GetCounter returns a DudStat. -func (d DudType) GetCounter(path string) StatCounter { - return DudStat{} -} - -// GetCounterVec returns a DudStat. -func (d DudType) GetCounterVec(path string, n ...string) StatCounterVec { - return FakeCounterVec(func(...string) StatCounter { - return DudStat{} - }) -} - -// GetTimer returns a DudStat. -func (d DudType) GetTimer(path string) StatTimer { - return DudStat{} -} - -// GetTimerVec returns a DudStat. -func (d DudType) GetTimerVec(path string, n ...string) StatTimerVec { - return FakeTimerVec(func(...string) StatTimer { - return DudStat{} - }) -} - -// GetGauge returns a DudStat. -func (d DudType) GetGauge(path string) StatGauge { - return DudStat{} -} - -// HandlerFunc returns nil. -func (d DudType) HandlerFunc() http.HandlerFunc { - return nil -} - -// GetGaugeVec returns a DudStat. -func (d DudType) GetGaugeVec(path string, n ...string) StatGaugeVec { - return FakeGaugeVec(func(...string) StatGauge { - return DudStat{} - }) -} - -// Close does nothing. -func (d DudType) Close() error { return nil } diff --git a/internal/component/metrics/local.go b/internal/component/metrics/local.go deleted file mode 100644 index 41564405b9..0000000000 --- a/internal/component/metrics/local.go +++ /dev/null @@ -1,275 +0,0 @@ -package metrics - -import ( - "net/http" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/rcrowley/go-metrics" -) - -// not sure if this is necessary yet. -var tagEncodingSeparator = "," - -// LocalStat is a representation of a single metric stat. Interactions with this -// stat are thread safe. -type LocalStat struct { - Value *int64 -} - -// Incr increments a metric by an int64 amount. -func (l *LocalStat) Incr(count int64) { - atomic.AddInt64(l.Value, count) -} - -// Decr decrements a metric by an amount. -func (l *LocalStat) Decr(count int64) { - atomic.AddInt64(l.Value, -count) -} - -// Set sets a gauge metric. -func (l *LocalStat) Set(value int64) { - atomic.StoreInt64(l.Value, value) -} - -func (l *LocalStat) IncrFloat64(count float64) { - l.Incr(int64(count)) -} - -func (l *LocalStat) DecrFloat64(count float64) { - l.Decr(int64(count)) -} - -func (l *LocalStat) SetFloat64(value float64) { - l.Set(int64(value)) -} - -// LocalTiming is a representation of a single metric timing. -type LocalTiming struct { - t metrics.Timer - lock sync.Mutex -} - -// Timing sets a timing metric. -func (l *LocalTiming) Timing(delta int64) { - l.lock.Lock() - l.t.Update(time.Duration(delta)) - l.lock.Unlock() -} - -//------------------------------------------------------------------------------ - -// Local is a metrics aggregator that stores metrics locally. -type Local struct { - flatCounters map[string]*LocalStat - flatTimings map[string]*LocalTiming - - mut sync.Mutex -} - -// NewLocal creates and returns a new Local aggregator. -func NewLocal() *Local { - return &Local{ - flatCounters: make(map[string]*LocalStat), - flatTimings: make(map[string]*LocalTiming), - } -} - -//------------------------------------------------------------------------------ - -// GetCounters returns a map of metric paths to counters. -func (l *Local) GetCounters() map[string]int64 { - return l.getCounters(false) -} - -// FlushCounters returns a map of the current state of the metrics paths to -// counters and then resets the counters to 0. -func (l *Local) FlushCounters() map[string]int64 { - return l.getCounters(true) -} - -// getCounters internal method that returns a copy of the counter maps before -// optionally reseting the counter as determined by the reset value passed in. -func (l *Local) getCounters(reset bool) map[string]int64 { - l.mut.Lock() - localFlatCounters := make(map[string]int64, len(l.flatCounters)) - for k := range l.flatCounters { - localFlatCounters[k] = atomic.LoadInt64(l.flatCounters[k].Value) - if reset { - atomic.StoreInt64(l.flatCounters[k].Value, 0) - } - } - l.mut.Unlock() - return localFlatCounters -} - -// GetTimings returns a map of metric paths to timers. -func (l *Local) GetTimings() map[string]metrics.Timer { - return l.getTimings(false) -} - -// FlushTimings returns a map of the current state of the metrics paths to -// timers and then resets the timers to 0. -func (l *Local) FlushTimings() map[string]metrics.Timer { - return l.getTimings(true) -} - -// getTimings returns a map of the current state of the metrics paths to -// timers before optionally reseting the timers as determined by the reset -// value passed in. -func (l *Local) getTimings(reset bool) map[string]metrics.Timer { - l.mut.Lock() - localFlatTimings := make(map[string]metrics.Timer, len(l.flatTimings)) - for k, v := range l.flatTimings { - v.lock.Lock() - localFlatTimings[k] = v.t.Snapshot() - if reset { - v.t.Stop() - v.t = metrics.NewTimer() - } - v.lock.Unlock() - } - l.mut.Unlock() - return localFlatTimings -} - -//------------------------------------------------------------------------------ - -func createLabelledPath(name string, tagNames, tagValues []string) string { - if len(tagNames) == 0 { - return name - } - - b := &strings.Builder{} - b.WriteString(name) - - if len(tagNames) == len(tagValues) { - tags := make(map[string]string, len(tagNames)) - for k, v := range tagNames { - tags[v] = tagValues[k] - } - - sortedTagNames := make([]string, len(tagNames)) - copy(sortedTagNames, tagNames) - sort.Strings(sortedTagNames) - - b.WriteByte('{') - for i, v := range sortedTagNames { - if i > 0 { - b.WriteString(tagEncodingSeparator) - } - b.WriteString(v) - b.WriteString("=") - b.WriteString(strconv.QuoteToASCII(tags[v])) - } - b.WriteByte('}') - } - return b.String() -} - -// ReverseLabelledPath extracts a name, tag names and tag values from a labelled -// metric name. -func ReverseLabelledPath(path string) (name string, tagNames, tagValues []string) { - if !strings.HasSuffix(path, "}") { - name = path - return - } - - labelsStart := strings.Index(path, "{") - if labelsStart == -1 { - name = path - return - } - - name = path[:labelsStart] - for _, tagKVStr := range strings.Split(path[labelsStart+1:len(path)-1], tagEncodingSeparator) { - tagKV := strings.Split(tagKVStr, "=") - if len(tagKV) != 2 { - continue - } - tagNames = append(tagNames, tagKV[0]) - tagValue, _ := strconv.Unquote(tagKV[1]) - tagValues = append(tagValues, tagValue) - } - return -} - -// GetCounter returns a stat counter object for a path. -func (l *Local) GetCounter(path string) StatCounter { - return l.GetCounterVec(path).With() -} - -// GetTimer returns a stat timer object for a path. -func (l *Local) GetTimer(path string) StatTimer { - return l.GetTimerVec(path).With() -} - -// GetGauge returns a stat gauge object for a path. -func (l *Local) GetGauge(path string) StatGauge { - return l.GetGaugeVec(path).With() -} - -// GetCounterVec returns a stat counter object for a path and records the -// labels and values. -func (l *Local) GetCounterVec(path string, k ...string) StatCounterVec { - return FakeCounterVec(func(v ...string) StatCounter { - newPath := createLabelledPath(path, k, v) - l.mut.Lock() - st, exists := l.flatCounters[newPath] - if !exists { - var i int64 - st = &LocalStat{Value: &i} - l.flatCounters[newPath] = st - } - l.mut.Unlock() - return st - }) -} - -// GetTimerVec returns a stat timer object for a path with the labels -// and values. -func (l *Local) GetTimerVec(path string, k ...string) StatTimerVec { - return FakeTimerVec(func(v ...string) StatTimer { - newPath := createLabelledPath(path, k, v) - l.mut.Lock() - st, exists := l.flatTimings[newPath] - if !exists { - st = &LocalTiming{t: metrics.NewTimer()} - l.flatTimings[newPath] = st - } - l.mut.Unlock() - return st - }) -} - -// GetGaugeVec returns a stat timer object for a path with the labels -// discarded. -func (l *Local) GetGaugeVec(path string, k ...string) StatGaugeVec { - return FakeGaugeVec(func(v ...string) StatGauge { - newPath := createLabelledPath(path, k, v) - l.mut.Lock() - st, exists := l.flatCounters[newPath] - if !exists { - var i int64 - st = &LocalStat{Value: &i} - l.flatCounters[newPath] = st - } - l.mut.Unlock() - return st - }) -} - -// HandlerFunc returns nil. -func (l *Local) HandlerFunc() http.HandlerFunc { - return nil -} - -// Close stops the Local object from aggregating metrics and cleans up -// resources. -func (l *Local) Close() error { - return nil -} diff --git a/internal/component/metrics/local_test.go b/internal/component/metrics/local_test.go deleted file mode 100644 index 2a437974ae..0000000000 --- a/internal/component/metrics/local_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package metrics - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCounter(t *testing.T) { - nm := NewLocal() - - ctr := nm.GetCounter("counterone") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.GetGauge("gaugeone") - gge.Set(12) - - tmr := nm.GetTimer("timerone") - tmr.Timing(13) - - ctrTwo := nm.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - - ggeTwo := nm.GetGaugeVec("gaugetwo", "label2") - ggeTwo.With("value3").Set(12) - - tmrTwo := nm.GetTimerVec("timertwo", "label3", "label4") - tmrTwo.With("value4", "value5").Timing(13) - - ggeTwo.With("value6").Set(24) - - expCounters := map[string]int64{ - "counterone": 21, - "countertwo{label1=\"value1\"}": 10, - "countertwo{label1=\"value2\"}": 11, - "gaugeone": 12, - "gaugetwo{label2=\"value3\"}": 12, - "gaugetwo{label2=\"value6\"}": 24, - } - assert.Equal(t, expCounters, nm.GetCounters()) - // Check twice to ensure we didn't flush - assert.Equal(t, expCounters, nm.GetCounters()) - - assert.Equal(t, expCounters, nm.FlushCounters()) - expCounters = map[string]int64{ - "counterone": 0, - "countertwo{label1=\"value1\"}": 0, - "countertwo{label1=\"value2\"}": 0, - "gaugeone": 0, - "gaugetwo{label2=\"value3\"}": 0, - "gaugetwo{label2=\"value6\"}": 0, - } - assert.Equal(t, expCounters, nm.GetCounters()) - - expTimingAvgs := map[string]float64{ - "timerone": 13, - "timertwo{label3=\"value4\",label4=\"value5\"}": 13, - } - actTimingAvgs := map[string]float64{} - for k, v := range nm.GetTimings() { - actTimingAvgs[k] = v.Mean() - } - - assert.Equal(t, expTimingAvgs, actTimingAvgs) - - // Check twice to ensure we didn't flush - actTimingAvgs = map[string]float64{} - for k, v := range nm.GetTimings() { - actTimingAvgs[k] = v.Mean() - } - assert.Equal(t, expTimingAvgs, actTimingAvgs) - - actTimingAvgs = map[string]float64{} - for k, v := range nm.FlushTimings() { - actTimingAvgs[k] = v.Mean() - } - assert.Equal(t, expTimingAvgs, actTimingAvgs) - - expTimingAvgs = map[string]float64{ - "timerone": 0, - "timertwo{label3=\"value4\",label4=\"value5\"}": 0, - } - - actTimingAvgs = map[string]float64{} - for k, v := range nm.GetTimings() { - actTimingAvgs[k] = v.Mean() - } - assert.Equal(t, expTimingAvgs, actTimingAvgs) -} - -func TestReverseName(t *testing.T) { - tests := map[string]struct { - input string - name string - tagNames []string - tagValues []string - }{ - "no labels": { - input: "hello world", - name: "hello world", - }, - "single label": { - input: `hello world{foo="bar"}`, - name: "hello world", - tagNames: []string{"foo"}, - tagValues: []string{"bar"}, - }, - "empty label": { - input: `hello world{foo=""}`, - name: "hello world", - tagNames: []string{"foo"}, - tagValues: []string{""}, - }, - "multiple labels": { - input: `hello world{foo="first",bar="second",baz="third"}`, - name: "hello world", - tagNames: []string{"foo", "bar", "baz"}, - tagValues: []string{"first", "second", "third"}, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - res, tagNames, tagValues := ReverseLabelledPath(test.input) - assert.Equal(t, test.name, res) - assert.Equal(t, test.tagNames, tagNames) - assert.Equal(t, test.tagValues, tagValues) - }) - } -} diff --git a/internal/component/metrics/mapping.go b/internal/component/metrics/mapping.go deleted file mode 100644 index 3553141015..0000000000 --- a/internal/component/metrics/mapping.go +++ /dev/null @@ -1,119 +0,0 @@ -package metrics - -import ( - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -// Mapping is a compiled Bloblang mapping used to rewrite metrics. -type Mapping struct { - m *mapping.Executor - logger log.Modular - staticVars map[string]any -} - -// NewMapping parses a Bloblang mapping and returns a metrics mapping. -func NewMapping(mapping string, logger log.Modular) (*Mapping, error) { - if mapping == "" { - return &Mapping{m: nil, logger: logger}, nil - } - m, err := bloblang.GlobalEnvironment().NewMapping(mapping) - if err != nil { - if perr, ok := err.(*parser.Error); ok { - return nil, fmt.Errorf("%v", perr.ErrorAtPosition([]rune(mapping))) - } - return nil, err - } - return &Mapping{m: m, logger: logger, staticVars: map[string]any{}}, nil -} - -// WithStaticVars adds a map of key/value pairs to the static variables of the -// metrics mapping. These are variables that will be made available to each -// invocation of the metrics mapping. -func (m *Mapping) WithStaticVars(kvs map[string]any) *Mapping { - newM := *m - - newM.staticVars = map[string]any{} - for k, v := range m.staticVars { - newM.staticVars[k] = v - } - for k, v := range kvs { - newM.staticVars[k] = v - } - - return &newM -} - -func (m *Mapping) mapPath(path string, labelNames, labelValues []string) (outPath string, outLabelNames, outLabelValues []string) { - if m == nil || m.m == nil { - return path, labelNames, labelValues - } - - part := message.NewPart(nil) - part.SetStructuredMut(path) - for i, v := range labelNames { - part.MetaSetMut(v, labelValues[i]) - } - msg := message.Batch{part} - - outPart := part.DeepCopy() - - var input any = path - vars := map[string]any{} - for k, v := range m.staticVars { - vars[k] = v - } - - var v any = value.Nothing(nil) - if err := m.m.ExecOnto(query.FunctionContext{ - Maps: m.m.Maps(), - Vars: vars, - MsgBatch: msg, - NewMeta: outPart, - NewValue: &v, - }.WithValue(input), mapping.AssignmentContext{ - Vars: vars, - Meta: outPart, - Value: &v, - }); err != nil { - m.logger.Error("Failed to apply path mapping on '%v': %v\n", path, err) - return path, nil, nil - } - - _ = outPart.MetaIterStr(func(k, v string) error { - outLabelNames = append(outLabelNames, k) - return nil - }) - if len(outLabelNames) > 0 { - sort.Strings(outLabelNames) - for _, k := range outLabelNames { - v := outPart.MetaGetStr(k) - m.logger.Trace("Metrics label '%v' created with static value '%v'.\n", k, v) - outLabelValues = append(outLabelValues, v) - } - } - - switch t := v.(type) { - case value.Delete: - m.logger.Trace("Deleting metrics path: %v\n", path) - return "", nil, nil - case value.Nothing: - m.logger.Trace("Metrics path '%v' registered unchanged.\n", path) - outPath = path - return - case string: - m.logger.Trace("Updated metrics path '%v' to: %v\n", path, t) - outPath = t - return - } - m.logger.Error("Path mapping returned invalid result, expected string, found %T\n", v) - return path, labelNames, labelValues -} diff --git a/internal/component/metrics/mapping_test.go b/internal/component/metrics/mapping_test.go deleted file mode 100644 index dbe450b407..0000000000 --- a/internal/component/metrics/mapping_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package metrics - -import ( - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/log" -) - -func TestPathMapping(t *testing.T) { - type testCase struct { - input string - inLabels []string - inValues []string - output string - labels []string - values []string - } - type test struct { - name string - mapping string - cases []testCase - } - tests := []test{ - { - name: "delete some", - mapping: `if this.contains("foo") { deleted() }`, - cases: []testCase{ - { - input: "foo", - output: "", - }, - { - input: "foo", - output: "", - }, - { - input: "hello foo world", - output: "", - }, - { - input: "hello world", - output: "hello world", - }, - { - input: "hello world", - inLabels: []string{"foo", "bar"}, - inValues: []string{"foo1", "bar1"}, - output: "hello world", - labels: []string{"bar", "foo"}, - values: []string{"bar1", "foo1"}, - }, - }, - }, - { - name: "throw an error", - mapping: `root = throw("nope")`, - cases: []testCase{{input: "foo", output: "foo"}}, - }, - { - name: "set a static label", - mapping: `root = this - meta foo = "bar"`, - cases: []testCase{ - { - input: "foo", output: "foo", - labels: []string{"foo"}, - values: []string{"bar"}, - }, - { - input: "foo", output: "foo", - inLabels: []string{"a", "b"}, - inValues: []string{"a1", "b1"}, - labels: []string{"a", "b", "foo"}, - values: []string{"a1", "b1", "bar"}, - }, - }, - }, - { - name: "set two static labels", - mapping: `root = this - meta foo = "bar" - meta bar = "baz"`, - cases: []testCase{ - { - input: "foo", output: "foo", - labels: []string{"bar", "foo"}, - values: []string{"baz", "bar"}, - }, - }, - }, - { - name: "replace foo with bar", - mapping: `this.replace_all("foo","bar")`, - cases: []testCase{ - {input: "foo", output: "bar"}, - {input: "hello foo world", output: "hello bar world"}, - {input: "hello world", output: "hello world"}, - }, - }, - { - name: "empty mapping", - mapping: ``, - cases: []testCase{ - {input: "foo", output: "foo"}, - {input: "hello world", output: "hello world"}, - }, - }, - { - name: "wrong value mapping", - mapping: `root = 10`, - cases: []testCase{ - {input: "foo", output: "foo"}, - {input: "hello world", output: "hello world"}, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - m, err := NewMapping(test.mapping, log.Noop()) - require.NoError(t, err) - for i, def := range test.cases { - out, labels, values := m.mapPath(def.input, def.inLabels, def.inValues) - assert.Equal(t, def.output, out, strconv.Itoa(i)) - assert.Equal(t, def.labels, labels, strconv.Itoa(i)) - assert.Equal(t, def.values, values, strconv.Itoa(i)) - } - }) - } -} - -func TestStaticVars(t *testing.T) { - mOne, err := NewMapping(` -meta from_a = $a | "nope" -meta from_b = $b | "nope" -meta from_c = $c | "nope" -`, log.Noop()) - require.NoError(t, err) - - mTwo := mOne.WithStaticVars(map[string]any{ - "a": "a value", - "b": "b value", - }) - - mThree := mTwo.WithStaticVars(map[string]any{ - "c": "c value", - }) - - out, labels, values := mOne.mapPath("hello world", nil, nil) - assert.Equal(t, "hello world", out) - assert.Equal(t, []string{"from_a", "from_b", "from_c"}, labels) - assert.Equal(t, []string{"nope", "nope", "nope"}, values) - - out, labels, values = mTwo.mapPath("hello world", nil, nil) - assert.Equal(t, "hello world", out) - assert.Equal(t, []string{"from_a", "from_b", "from_c"}, labels) - assert.Equal(t, []string{"a value", "b value", "nope"}, values) - - out, labels, values = mThree.mapPath("hello world", nil, nil) - assert.Equal(t, "hello world", out) - assert.Equal(t, []string{"from_a", "from_b", "from_c"}, labels) - assert.Equal(t, []string{"a value", "b value", "c value"}, values) -} diff --git a/internal/component/metrics/namespaced.go b/internal/component/metrics/namespaced.go deleted file mode 100644 index 267d5ce167..0000000000 --- a/internal/component/metrics/namespaced.go +++ /dev/null @@ -1,243 +0,0 @@ -package metrics - -import ( - "net/http" - "sort" -) - -// Namespaced wraps a child metrics exporter and exposes a Type API that -// adds namespacing labels and name prefixes to new. -type Namespaced struct { - labels map[string]string - mappings []*Mapping - child Type -} - -// NewNamespaced wraps a metrics exporter and adds prefixes and custom labels. -func NewNamespaced(child Type) *Namespaced { - return &Namespaced{ - child: child, - } -} - -// Noop returns a namespaced metrics aggregator with a noop child. -func Noop() *Namespaced { - return &Namespaced{ - child: DudType{}, - } -} - -// WithStats returns a namespaced metrics exporter with a different stats -// implementation. -func (n *Namespaced) WithStats(s Type) *Namespaced { - newNs := *n - newNs.child = s - return &newNs -} - -// WithLabels returns a namespaced metrics exporter with a new set of labels, -// which are added to any prior labels. -func (n *Namespaced) WithLabels(labels ...string) *Namespaced { - newLabels := map[string]string{} - for k, v := range n.labels { - newLabels[k] = v - } - for i := 0; i < len(labels)-1; i += 2 { - newLabels[labels[i]] = labels[i+1] - } - newNs := *n - newNs.labels = newLabels - return &newNs -} - -// WithMapping returns a namespaced metrics exporter with a new mapping. -// Mappings are applied _before_ the prefix and static labels are applied. -// Mappings already added are executed after this new mapping. -func (n *Namespaced) WithMapping(m *Mapping) *Namespaced { - newNs := *n - newMappings := make([]*Mapping, 0, len(n.mappings)+1) - newMappings = append(newMappings, m) - newMappings = append(newMappings, n.mappings...) - newNs.mappings = newMappings - return &newNs -} - -//------------------------------------------------------------------------------ - -// Child returns the underlying metrics type. -func (n *Namespaced) Child() Type { - return n.child -} - -// HandlerFunc returns the http handler of the child. -func (n *Namespaced) HandlerFunc() http.HandlerFunc { - return n.child.HandlerFunc() -} - -//------------------------------------------------------------------------------ - -func (n *Namespaced) getPathAndLabels(path string) (newPath string, labelKeys, labelValues []string) { - newPath = path - if n.labels != nil && len(n.labels) > 0 { - labelKeys = make([]string, 0, len(n.labels)) - for k := range n.labels { - labelKeys = append(labelKeys, k) - } - sort.Strings(labelKeys) - labelValues = make([]string, 0, len(n.labels)) - for _, k := range labelKeys { - labelValues = append(labelValues, n.labels[k]) - } - } - for _, mapping := range n.mappings { - if newPath, labelKeys, labelValues = mapping.mapPath(newPath, labelKeys, labelValues); newPath == "" { - return - } - } - return -} - -type counterVecWithStatic struct { - staticValues []string - child StatCounterVec -} - -func (c *counterVecWithStatic) With(values ...string) StatCounter { - newValues := make([]string, 0, len(c.staticValues)+len(values)) - newValues = append(newValues, c.staticValues...) - newValues = append(newValues, values...) - return c.child.With(newValues...) -} - -type timerVecWithStatic struct { - staticValues []string - child StatTimerVec -} - -func (c *timerVecWithStatic) With(values ...string) StatTimer { - newValues := make([]string, 0, len(c.staticValues)+len(values)) - newValues = append(newValues, c.staticValues...) - newValues = append(newValues, values...) - return c.child.With(newValues...) -} - -type gaugeVecWithStatic struct { - staticValues []string - child StatGaugeVec -} - -func (c *gaugeVecWithStatic) With(values ...string) StatGauge { - newValues := make([]string, 0, len(c.staticValues)+len(values)) - newValues = append(newValues, c.staticValues...) - newValues = append(newValues, values...) - return c.child.With(newValues...) -} - -//------------------------------------------------------------------------------ - -// GetCounter returns an editable counter stat for a given path. -func (n *Namespaced) GetCounter(path string) StatCounter { - path, labelKeys, labelValues := n.getPathAndLabels(path) - if path == "" { - return DudStat{} - } - if len(labelKeys) > 0 { - return n.child.GetCounterVec(path, labelKeys...).With(labelValues...) - } - return n.child.GetCounter(path) -} - -// GetCounterVec returns an editable counter stat for a given path with labels, -// these labels must be consistent with any other metrics registered on the same -// path. -func (n *Namespaced) GetCounterVec(path string, labelNames ...string) StatCounterVec { - path, staticKeys, staticValues := n.getPathAndLabels(path) - if path == "" { - return FakeCounterVec(func(...string) StatCounter { - return DudStat{} - }) - } - if len(staticKeys) > 0 { - newNames := make([]string, 0, len(staticKeys)+len(labelNames)) - newNames = append(newNames, staticKeys...) - newNames = append(newNames, labelNames...) - return &counterVecWithStatic{ - staticValues: staticValues, - child: n.child.GetCounterVec(path, newNames...), - } - } - return n.child.GetCounterVec(path, labelNames...) -} - -// GetTimer returns an editable timer stat for a given path. -func (n *Namespaced) GetTimer(path string) StatTimer { - path, labelKeys, labelValues := n.getPathAndLabels(path) - if path == "" { - return DudStat{} - } - if len(labelKeys) > 0 { - return n.child.GetTimerVec(path, labelKeys...).With(labelValues...) - } - return n.child.GetTimer(path) -} - -// GetTimerVec returns an editable timer stat for a given path with labels, -// these labels must be consistent with any other metrics registered on the same -// path. -func (n *Namespaced) GetTimerVec(path string, labelNames ...string) StatTimerVec { - path, staticKeys, staticValues := n.getPathAndLabels(path) - if path == "" { - return FakeTimerVec(func(...string) StatTimer { - return DudStat{} - }) - } - if len(staticKeys) > 0 { - newNames := make([]string, 0, len(staticKeys)+len(labelNames)) - newNames = append(newNames, staticKeys...) - newNames = append(newNames, labelNames...) - return &timerVecWithStatic{ - staticValues: staticValues, - child: n.child.GetTimerVec(path, newNames...), - } - } - return n.child.GetTimerVec(path, labelNames...) -} - -// GetGauge returns an editable gauge stat for a given path. -func (n *Namespaced) GetGauge(path string) StatGauge { - path, labelKeys, labelValues := n.getPathAndLabels(path) - if path == "" { - return DudStat{} - } - if len(labelKeys) > 0 { - return n.child.GetGaugeVec(path, labelKeys...).With(labelValues...) - } - return n.child.GetGauge(path) -} - -// GetGaugeVec returns an editable gauge stat for a given path with labels, -// these labels must be consistent with any other metrics registered on the same -// path. -func (n *Namespaced) GetGaugeVec(path string, labelNames ...string) StatGaugeVec { - path, staticKeys, staticValues := n.getPathAndLabels(path) - if path == "" { - return FakeGaugeVec(func(...string) StatGauge { - return DudStat{} - }) - } - if len(staticKeys) > 0 { - newNames := make([]string, 0, len(staticKeys)+len(labelNames)) - newNames = append(newNames, staticKeys...) - newNames = append(newNames, labelNames...) - return &gaugeVecWithStatic{ - staticValues: staticValues, - child: n.child.GetGaugeVec(path, newNames...), - } - } - return n.child.GetGaugeVec(path, labelNames...) -} - -// Close stops aggregating stats and cleans up resources. -func (n *Namespaced) Close() error { - return n.child.Close() -} diff --git a/internal/component/metrics/namespaced_test.go b/internal/component/metrics/namespaced_test.go deleted file mode 100644 index d31426a290..0000000000 --- a/internal/component/metrics/namespaced_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package metrics_test - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - - _ "github.com/benthosdev/benthos/v4/internal/impl/prometheus" -) - -func getTestProm(t *testing.T) (metrics.Type, http.HandlerFunc) { - t.Helper() - - conf := metrics.NewConfig() - conf.Type = "prometheus" - conf.Plugin = map[string]any{ - "use_histogram_timing": true, - } - - ns, err := bundle.AllMetrics.Init(conf, mock.NewManager()) - require.NoError(t, err) - - prom := ns.Child() - return prom, prom.HandlerFunc() -} - -func getPage(t *testing.T, handler http.HandlerFunc) string { - t.Helper() - - req := httptest.NewRequest(http.MethodGet, "http://example.com/foo", http.NoBody) - w := httptest.NewRecorder() - handler(w, req) - - body, err := io.ReadAll(w.Result().Body) - require.NoError(t, err) - - return string(body) -} - -func TestNamespacedNothing(t *testing.T) { - prom, handler := getTestProm(t) - - nm := metrics.NewNamespaced(prom) - - ctr := nm.GetCounter("counterone") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.GetGauge("gaugeone") - gge.Set(12) - - tmr := nm.GetTimer("timerone") - tmr.Timing(13) - - ctrTwo := nm.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - ctrTwo.With("value3").IncrFloat64(10.452) - - ggeTwo := nm.GetGaugeVec("gaugetwo", "label2") - ggeTwo.With("value3").Set(12) - - tmrTwo := nm.GetTimerVec("timertwo", "label3", "label4") - tmrTwo.With("value4", "value5").Timing(13) - - body := getPage(t, handler) - - assert.Contains(t, body, "\ncounterone 21") - assert.Contains(t, body, "\ngaugeone 12") - assert.Contains(t, body, "\ntimerone_sum 1.3e-08") - assert.Contains(t, body, "\ncountertwo{label1=\"value1\"} 10") - assert.Contains(t, body, "\ncountertwo{label1=\"value2\"} 11") - assert.Contains(t, body, "\ncountertwo{label1=\"value3\"} 10.452") - assert.Contains(t, body, "\ngaugetwo{label2=\"value3\"} 12") - assert.Contains(t, body, "\ntimertwo_sum{label3=\"value4\",label4=\"value5\"} 1.3e-08") -} - -func TestNamespacedPrefix(t *testing.T) { - prom, handler := getTestProm(t) - - nm := metrics.NewNamespaced(prom) - - ctr := nm.GetCounter("counterone") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.GetGauge("gaugeone") - gge.Set(12) - - tmr := nm.GetTimer("timerone") - tmr.Timing(13) - - ctrTwo := nm.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - - ggeTwo := nm.GetGaugeVec("gaugetwo", "label2") - ggeTwo.With("value3").Set(12) - - tmrTwo := nm.GetTimerVec("timertwo", "label3", "label4") - tmrTwo.With("value4", "value5").Timing(13) - - ctrThree := nm.GetCounter("counterthree") - ctrThree.Incr(22) - - body := getPage(t, handler) - - assert.Contains(t, body, "\ncounterone 21") - assert.Contains(t, body, "\ngaugeone 12") - assert.Contains(t, body, "\ntimerone_sum 1.3e-08") - assert.Contains(t, body, "\ncountertwo{label1=\"value1\"} 10") - assert.Contains(t, body, "\ncountertwo{label1=\"value2\"} 11") - assert.Contains(t, body, "\ngaugetwo{label2=\"value3\"} 12") - assert.Contains(t, body, "\ntimertwo_sum{label3=\"value4\",label4=\"value5\"} 1.3e-08") - assert.Contains(t, body, "\ncounterthree 22") -} - -func TestNamespacedPrefixStaticLabels(t *testing.T) { - prom, handler := getTestProm(t) - - nm := metrics.NewNamespaced(prom).WithLabels("static1", "svalue1") - - ctr := nm.GetCounter("counterone") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.GetGauge("gaugeone") - gge.Set(12) - - tmr := nm.GetTimer("timerone") - tmr.Timing(13) - - ctrTwo := nm.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - - ggeTwo := nm.GetGaugeVec("gaugetwo", "label2") - ggeTwo.With("value3").Set(12) - - tmrTwo := nm.GetTimerVec("timertwo", "label3", "label4") - tmrTwo.With("value4", "value5").Timing(13) - - nm2 := nm.WithLabels("static2", "svalue2") - - ctrThree := nm2.GetCounter("counterthree") - ctrThree.Incr(22) - - body := getPage(t, handler) - - assert.Contains(t, body, "\ncounterone{static1=\"svalue1\"} 21") - assert.Contains(t, body, "\ngaugeone{static1=\"svalue1\"} 12") - assert.Contains(t, body, "\ntimerone_sum{static1=\"svalue1\"} 1.3e-08") - assert.Contains(t, body, "\ncountertwo{label1=\"value1\",static1=\"svalue1\"} 10") - assert.Contains(t, body, "\ncountertwo{label1=\"value2\",static1=\"svalue1\"} 11") - assert.Contains(t, body, "\ngaugetwo{label2=\"value3\",static1=\"svalue1\"} 12") - assert.Contains(t, body, "\ntimertwo_sum{label3=\"value4\",label4=\"value5\",static1=\"svalue1\"} 1.3e-08") - assert.Contains(t, body, "\ncounterthree{static1=\"svalue1\",static2=\"svalue2\"} 22") -} - -func TestNamespacedPrefixStaticLabelsWithMappings(t *testing.T) { - prom, handler := getTestProm(t) - - mappingFooToBar, err := metrics.NewMapping(`root = this.replace_all("foo","bar")`, log.Noop()) - require.NoError(t, err) - - mappingBarToBaz, err := metrics.NewMapping(`root = this.replace_all("bar","baz")`, log.Noop()) - require.NoError(t, err) - - nm := metrics.NewNamespaced(prom).WithLabels("static1", "svalue1") - nm = nm.WithMapping(mappingBarToBaz) - nm = nm.WithMapping(mappingFooToBar) - - ctr := nm.GetCounter("counter") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.GetGauge("gauge") - gge.Set(12) - - tmr := nm.GetTimer("timer") - tmr.Timing(13) - - ctrTwo := nm.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - - ggeTwo := nm.GetGaugeVec("gaugetwo", "label2") - ggeTwo.With("value3").Set(12) - - tmrTwo := nm.GetTimerVec("timertwo", "label3", "label4") - tmrTwo.With("value4", "value5").Timing(13) - - body := getPage(t, handler) - - assert.Contains(t, body, "\ncounter{static1=\"svalue1\"} 21") - assert.Contains(t, body, "\ngauge{static1=\"svalue1\"} 12") - assert.Contains(t, body, "\ntimer_sum{static1=\"svalue1\"} 1.3e-08") - assert.Contains(t, body, "\ncountertwo{label1=\"value1\",static1=\"svalue1\"} 10") - assert.Contains(t, body, "\ncountertwo{label1=\"value2\",static1=\"svalue1\"} 11") - assert.Contains(t, body, "\ngaugetwo{label2=\"value3\",static1=\"svalue1\"} 12") - assert.Contains(t, body, "\ntimertwo_sum{label3=\"value4\",label4=\"value5\",static1=\"svalue1\"} 1.3e-08") -} - -func TestNamespacedPrefixStaticLabelsWithMappingLabels(t *testing.T) { - prom, handler := getTestProm(t) - - mappingFooToBar, err := metrics.NewMapping(`meta = meta().map_each(kv -> kv.value.replace_all("value","bar")) -meta extra1 = "extravalue1" -root = this.replace_all("foo","bar")`, log.Noop()) - require.NoError(t, err) - - mappingBarToBaz, err := metrics.NewMapping(`meta = meta().map_each(kv -> kv.value.replace_all("bar","baz")) -meta extra2 = "extravalue2" -root = this.replace_all("bar","baz")`, log.Noop()) - require.NoError(t, err) - - nm := metrics.NewNamespaced(prom).WithLabels("static1", "svalue1") - nm = nm.WithMapping(mappingBarToBaz) - nm = nm.WithMapping(mappingFooToBar) - - ctr := nm.GetCounter("counter") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.GetGauge("gauge") - gge.Set(12) - - tmr := nm.GetTimer("timer") - tmr.Timing(13) - - ctrTwo := nm.GetCounterVec("countertwo", "label1") - ctrTwo.With("value1").Incr(10) - ctrTwo.With("value2").Incr(11) - - ggeTwo := nm.GetGaugeVec("gaugetwo", "label2") - ggeTwo.With("value3").Set(12) - - tmrTwo := nm.GetTimerVec("timertwo", "label3", "label4") - tmrTwo.With("value4", "value5").Timing(13) - - body := getPage(t, handler) - - assert.Contains(t, body, "\ncounter{extra1=\"extravalue1\",extra2=\"extravalue2\",static1=\"sbaz1\"} 21") - assert.Contains(t, body, "\ngauge{extra1=\"extravalue1\",extra2=\"extravalue2\",static1=\"sbaz1\"} 12") - assert.Contains(t, body, "\ntimer_sum{extra1=\"extravalue1\",extra2=\"extravalue2\",static1=\"sbaz1\"} 1.3e-08") - assert.Contains(t, body, "\ncountertwo{extra1=\"extravalue1\",extra2=\"extravalue2\",label1=\"value1\",static1=\"sbaz1\"} 10") - assert.Contains(t, body, "\ncountertwo{extra1=\"extravalue1\",extra2=\"extravalue2\",label1=\"value2\",static1=\"sbaz1\"} 11") - assert.Contains(t, body, "\ngaugetwo{extra1=\"extravalue1\",extra2=\"extravalue2\",label2=\"value3\",static1=\"sbaz1\"} 12") - assert.Contains(t, body, "\ntimertwo_sum{extra1=\"extravalue1\",extra2=\"extravalue2\",label3=\"value4\",label4=\"value5\",static1=\"sbaz1\"} 1.3e-08") -} diff --git a/internal/component/metrics/type.go b/internal/component/metrics/type.go deleted file mode 100644 index 0155da66dc..0000000000 --- a/internal/component/metrics/type.go +++ /dev/null @@ -1,102 +0,0 @@ -package metrics - -import ( - "net/http" -) - -// StatCounter is a representation of a single counter metric stat. Interactions -// with this stat are thread safe. -type StatCounter interface { - // Incr increments a counter by an integer amount. - Incr(count int64) - - // IncrFloat64 increments a counter by a decimal amount. - IncrFloat64(count float64) -} - -// StatTimer is a representation of a single timer metric stat, timing values -// should be presented in nanoseconds for consistency. Interactions with this -// stat are thread safe. -type StatTimer interface { - // Timing sets a timing metric. - Timing(delta int64) -} - -// StatGauge is a representation of a single gauge metric stat. Interactions -// with this stat are thread safe. -type StatGauge interface { - // Set sets the integer value of a gauge metric. - Set(value int64) - - // Incr increments with an integer value a gauge by an amount. - Incr(count int64) - - // Decr decrements a gauge by an integer amount. - Decr(count int64) - - // SetFloat64 sets the value of a gauge metric. - SetFloat64(value float64) - - // IncrFloat64 increments a gauge by an amount. - IncrFloat64(count float64) - - // DecrFloat64 decrements a gauge by an amount. - DecrFloat64(count float64) -} - -//------------------------------------------------------------------------------ - -// StatCounterVec creates StatCounters with dynamic labels. -type StatCounterVec interface { - // With returns a StatCounter with a set of label values. - With(labelValues ...string) StatCounter -} - -// StatTimerVec creates StatTimers with dynamic labels. -type StatTimerVec interface { - // With returns a StatTimer with a set of label values. - With(labelValues ...string) StatTimer -} - -// StatGaugeVec creates StatGauges with dynamic labels. -type StatGaugeVec interface { - // With returns a StatGauge with a set of label values. - With(labelValues ...string) StatGauge -} - -//------------------------------------------------------------------------------ - -// Type is an interface for metrics aggregation. -type Type interface { - // GetCounter returns an editable counter stat for a given path. - GetCounter(path string) StatCounter - - // GetCounterVec returns an editable counter stat for a given path with labels, - // these labels must be consistent with any other metrics registered on the - // same path. - GetCounterVec(path string, labelNames ...string) StatCounterVec - - // GetTimer returns an editable timer stat for a given path. - GetTimer(path string) StatTimer - - // GetTimerVec returns an editable timer stat for a given path with labels, - // these labels must be consistent with any other metrics registered on the - // same path. - GetTimerVec(path string, labelNames ...string) StatTimerVec - - // GetGauge returns an editable gauge stat for a given path. - GetGauge(path string) StatGauge - - // GetGaugeVec returns an editable gauge stat for a given path with labels, - // these labels must be consistent with any other metrics registered on the - // same path. - GetGaugeVec(path string, labelNames ...string) StatGaugeVec - - // HandlerFunc returns an optional HTTP request handler that exposes metrics - // from the implementation. If nil is returned then no endpoint will be - // registered. - HandlerFunc() http.HandlerFunc - - // Close stops aggregating stats and cleans up resources. - Close() error -} diff --git a/internal/component/metrics/vector_util.go b/internal/component/metrics/vector_util.go deleted file mode 100644 index 07c5a0fb0d..0000000000 --- a/internal/component/metrics/vector_util.go +++ /dev/null @@ -1,50 +0,0 @@ -package metrics - -type fCounterVec struct { - f func(...string) StatCounter -} - -func (f *fCounterVec) With(labels ...string) StatCounter { - return f.f(labels...) -} - -// FakeCounterVec returns a counter vec implementation that ignores labels. -func FakeCounterVec(f func(...string) StatCounter) StatCounterVec { - return &fCounterVec{ - f: f, - } -} - -//------------------------------------------------------------------------------ - -type fTimerVec struct { - f func(...string) StatTimer -} - -func (f *fTimerVec) With(labels ...string) StatTimer { - return f.f(labels...) -} - -// FakeTimerVec returns a timer vec implementation that ignores labels. -func FakeTimerVec(f func(...string) StatTimer) StatTimerVec { - return &fTimerVec{ - f: f, - } -} - -//------------------------------------------------------------------------------ - -type fGaugeVec struct { - f func(...string) StatGauge -} - -func (f *fGaugeVec) With(labels ...string) StatGauge { - return f.f(labels...) -} - -// FakeGaugeVec returns a gauge vec implementation that ignores labels. -func FakeGaugeVec(f func(...string) StatGauge) StatGaugeVec { - return &fGaugeVec{ - f: f, - } -} diff --git a/internal/component/observability.go b/internal/component/observability.go deleted file mode 100644 index 2b5a44d8ad..0000000000 --- a/internal/component/observability.go +++ /dev/null @@ -1,38 +0,0 @@ -package component - -import ( - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// Observability is an interface implemented by components that provide a range -// of observability APIs to components. This is primarily done the service-wide -// managers. -type Observability interface { - Metrics() metrics.Type - Logger() log.Modular - Tracer() trace.TracerProvider -} - -type mockObs struct{} - -func (m mockObs) Metrics() metrics.Type { - return metrics.Noop() -} - -func (m mockObs) Logger() log.Modular { - return log.Noop() -} - -func (m mockObs) Tracer() trace.TracerProvider { - return noop.NewTracerProvider() -} - -// NoopObservability returns an implementation of Observability that does -// nothing. -func NoopObservability() Observability { - return mockObs{} -} diff --git a/internal/component/output/async_writer.go b/internal/component/output/async_writer.go deleted file mode 100644 index 1c7d6532ca..0000000000 --- a/internal/component/output/async_writer.go +++ /dev/null @@ -1,289 +0,0 @@ -package output - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "time" - - "github.com/cenkalti/backoff/v4" - "go.opentelemetry.io/otel/trace" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" -) - -// AsyncSink is a type that writes Benthos messages to a third party sink. If -// the protocol supports a form of acknowledgement then it will be returned by -// the call to Write. -type AsyncSink interface { - // Connect attempts to establish a connection to the sink, if - // unsuccessful returns an error. If the attempt is successful (or not - // necessary) returns nil. - Connect(ctx context.Context) error - - // WriteBatch should block until either the message is sent (and - // acknowledged) to a sink, or a transport specific error has occurred, or - // the Type is closed. - WriteBatch(ctx context.Context, msg message.Batch) error - - // Close is a blocking call to wait until the component has finished - // shutting down and cleaning up resources. - Close(ctx context.Context) error -} - -// AsyncWriter is an output type that writes messages to a writer.Type. -type AsyncWriter struct { - isConnected int32 - - typeStr string - maxInflight int - writer AsyncSink - - log log.Modular - stats metrics.Type - tracer trace.TracerProvider - - transactions <-chan message.Transaction - - shutSig *shutdown.Signaller -} - -// NewAsyncWriter creates a Streamed implementation around an AsyncSink. -func NewAsyncWriter(typeStr string, maxInflight int, w AsyncSink, mgr component.Observability) (Streamed, error) { - aWriter := &AsyncWriter{ - typeStr: typeStr, - maxInflight: maxInflight, - writer: w, - log: mgr.Logger(), - stats: mgr.Metrics(), - tracer: mgr.Tracer(), - transactions: nil, - shutSig: shutdown.NewSignaller(), - } - return aWriter, nil -} - -//------------------------------------------------------------------------------ - -func (w *AsyncWriter) latencyMeasuringWrite(ctx context.Context, msg message.Batch) (latencyNs int64, err error) { - t0 := time.Now() - err = w.writer.WriteBatch(ctx, msg) - if latencyNs = time.Since(t0).Nanoseconds(); latencyNs < 1 { - latencyNs = 1 - } - return latencyNs, err -} - -// loop is an internal loop that brokers incoming messages to output pipe. -func (w *AsyncWriter) loop() { - // Metrics paths - var ( - mSent = w.stats.GetCounter("output_sent") - mBatchSent = w.stats.GetCounter("output_batch_sent") - mError = w.stats.GetCounter("output_error") - mLatency = w.stats.GetTimer("output_latency_ns") - mConn = w.stats.GetCounter("output_connection_up") - mFailedConn = w.stats.GetCounter("output_connection_failed") - mLostConn = w.stats.GetCounter("output_connection_lost") - - traceName = "output_" + w.typeStr - ) - - defer func() { - _ = w.writer.Close(context.Background()) - - atomic.StoreInt32(&w.isConnected, 0) - w.shutSig.TriggerHasStopped() - }() - - connBackoff := backoff.NewExponentialBackOff() - connBackoff.InitialInterval = time.Millisecond * 500 - connBackoff.MaxInterval = time.Second - connBackoff.MaxElapsedTime = 0 - - closeLeisureCtx, done := w.shutSig.SoftStopCtx(context.Background()) - defer done() - - initConnection := func() bool { - for { - if err := w.writer.Connect(closeLeisureCtx); err != nil { - if w.shutSig.IsSoftStopSignalled() || errors.Is(err, component.ErrTypeClosed) { - return false - } - w.log.Error("Failed to connect to %v: %v\n", w.typeStr, err) - mFailedConn.Incr(1) - - var nextBoff time.Duration - - var ebo *component.ErrBackOff - if errors.As(err, &ebo) { - nextBoff = ebo.Wait - } else { - nextBoff = connBackoff.NextBackOff() - } - - if sleepWithCancellation(closeLeisureCtx, nextBoff) != nil { - return false - } - } else { - connBackoff.Reset() - return true - } - } - } - if !initConnection() { - return - } - - w.log.Info("Output type %v is now active", w.typeStr) - mConn.Incr(1) - atomic.StoreInt32(&w.isConnected, 1) - - wg := sync.WaitGroup{} - wg.Add(w.maxInflight) - - connectMut := sync.Mutex{} - connectLoop := func(msg message.Batch) (latency int64, err error) { - atomic.StoreInt32(&w.isConnected, 0) - - connectMut.Lock() - defer connectMut.Unlock() - - // If another goroutine got here first and we're able to send over the - // connection, then we gracefully accept defeat. - if atomic.LoadInt32(&w.isConnected) == 1 { - if latency, err = w.latencyMeasuringWrite(closeLeisureCtx, msg); err != component.ErrNotConnected { - return - } else if err != nil { - mError.Incr(1) - } - } - mLostConn.Incr(1) - - // Continue to try to reconnect while still active. - for { - if !initConnection() { - err = component.ErrTypeClosed - return - } - if latency, err = w.latencyMeasuringWrite(closeLeisureCtx, msg); err != component.ErrNotConnected { - atomic.StoreInt32(&w.isConnected, 1) - mConn.Incr(1) - return - } else if err != nil { - mError.Incr(1) - } - } - } - - writerLoop := func() { - defer wg.Done() - - for { - var ts message.Transaction - var open bool - select { - case ts, open = <-w.transactions: - if !open { - return - } - case <-w.shutSig.SoftStopChan(): - return - } - - w.log.Trace("Attempting to write %v messages to '%v'.\n", ts.Payload.Len(), w.typeStr) - _, spans := tracing.WithChildSpans(w.tracer, traceName, ts.Payload) - - latency, err := w.latencyMeasuringWrite(closeLeisureCtx, ts.Payload) - - // If our writer says it is not connected. - if errors.Is(err, component.ErrNotConnected) { - latency, err = connectLoop(ts.Payload) - } else if err != nil { - mError.Incr(1) - } - - // Close immediately if our writer is closed. - if errors.Is(err, component.ErrTypeClosed) { - return - } - - if err != nil { - if w.typeStr != "reject" { - // TODO: Maybe reintroduce a sleep here if we encounter a - // busy retry loop. - w.log.Error("Failed to send message to %v: %v\n", w.typeStr, err) - } else { - w.log.Debug("Rejecting message: %v\n", err) - } - } else { - mBatchSent.Incr(1) - mSent.Incr(int64(batch.MessageCollapsedCount(ts.Payload))) - mLatency.Timing(latency) - w.log.Trace("Successfully wrote %v messages to '%v'.\n", ts.Payload.Len(), w.typeStr) - } - - for _, s := range spans { - s.Finish() - } - - _ = ts.Ack(closeLeisureCtx, err) - } - } - - for i := 0; i < w.maxInflight; i++ { - go writerLoop() - } - wg.Wait() -} - -// Consume assigns a messages channel for the output to read. -func (w *AsyncWriter) Consume(ts <-chan message.Transaction) error { - if w.transactions != nil { - return component.ErrAlreadyStarted - } - w.transactions = ts - go w.loop() - return nil -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (w *AsyncWriter) Connected() bool { - return atomic.LoadInt32(&w.isConnected) == 1 -} - -// TriggerCloseNow shuts down the output and stops processing messages. -func (w *AsyncWriter) TriggerCloseNow() { - w.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the File output has closed down. -func (w *AsyncWriter) WaitForClose(ctx context.Context) error { - select { - case <-w.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func sleepWithCancellation(ctx context.Context, d time.Duration) error { - t := time.NewTimer(d) - defer t.Stop() - - select { - case <-t.C: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} diff --git a/internal/component/output/async_writer_test.go b/internal/component/output/async_writer_test.go deleted file mode 100644 index 6aed7caa66..0000000000 --- a/internal/component/output/async_writer_test.go +++ /dev/null @@ -1,637 +0,0 @@ -package output - -import ( - "context" - "errors" - "reflect" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type mockAsyncWriter struct { - msgsTotal uint64 - msgsRcvd sync.Map - connChan chan error - writeChan chan error -} - -func newAsyncMockWriter() *mockAsyncWriter { - return &mockAsyncWriter{ - connChan: make(chan error), - writeChan: make(chan error), - } -} - -func (w *mockAsyncWriter) Connect(ctx context.Context) error { - return <-w.connChan -} - -func (w *mockAsyncWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - w.msgsRcvd.Store(atomic.AddUint64(&w.msgsTotal, 1), msg) - return <-w.writeChan -} -func (w *mockAsyncWriter) Close(context.Context) error { return nil } - -type writerCantConnect struct{} - -func (w writerCantConnect) Connect(ctx context.Context) error { - return component.ErrNotConnected -} - -func (w writerCantConnect) WriteBatch(ctx context.Context, msg message.Batch) error { - return component.ErrNotConnected -} -func (w writerCantConnect) Close(context.Context) error { return nil } - -type writerCantSend struct { - connected int -} - -func (w *writerCantSend) Connect(ctx context.Context) error { - w.connected++ - return nil -} - -func (w *writerCantSend) WriteBatch(ctx context.Context, msg message.Batch) error { - return component.ErrNotConnected -} -func (w *writerCantSend) Close(context.Context) error { return nil } - -//------------------------------------------------------------------------------ - -func TestAsyncWriterCantConnect(t *testing.T) { - t.Parallel() - - w, err := NewAsyncWriter("foo", 1, writerCantConnect{}, component.NoopObservability()) - if err != nil { - t.Fatal(err) - } - - if err = w.Consume(make(chan message.Transaction)); err != nil { - t.Error(err) - } - if err = w.Consume(nil); err == nil { - t.Error("Expected error from duplicate receiver call") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - // We will fail to connect but should still exit immediately. - w.TriggerCloseNow() - require.NoError(t, w.WaitForClose(ctx)) -} - -//------------------------------------------------------------------------------ - -func TestAsyncWriterCantSendClosed(t *testing.T) { - t.Parallel() - - writerImpl := &writerCantSend{} - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - w.TriggerCloseNow() - require.NoError(t, w.WaitForClose(ctx)) -} - -func TestAsyncWriterCantSendClosedChan(t *testing.T) { - t.Parallel() - - writerImpl := &writerCantSend{} - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - close(msgChan) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, w.WaitForClose(ctx)) -} - -//------------------------------------------------------------------------------ - -func TestAsyncWriterStartClosed(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - select { - case writerImpl.connChan <- component.ErrTypeClosed: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, w.WaitForClose(ctx)) -} - -func TestAsyncWriterClosesOnReconn(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - select { - case writerImpl.writeChan <- component.ErrNotConnected: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case writerImpl.connChan <- component.ErrTypeClosed: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - select { - case msgChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, w.WaitForClose(ctx)) -} - -func TestAsyncWriterClosesOnResend(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - select { - case writerImpl.writeChan <- component.ErrNotConnected: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case writerImpl.writeChan <- component.ErrTypeClosed: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - select { - case msgChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, w.WaitForClose(ctx)) -} - -//------------------------------------------------------------------------------ - -func TestAsyncWriterCanReconnect(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - go func() { - select { - case writerImpl.writeChan <- component.ErrNotConnected: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case writerImpl.writeChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - select { - case msgChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case res, open := <-resChan: - if !open { - t.Error("Res chan closed") - } - if err := res; err != nil { - t.Error(err) - } - case <-time.After(time.Second): - t.Error("Timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - w.TriggerCloseNow() - require.NoError(t, w.WaitForClose(ctx)) -} - -func TestAsyncWriterCanReconnectAsync(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - w, err := NewAsyncWriter("foo", 2, writerImpl, component.NoopObservability()) - if err != nil { - t.Fatal(err) - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - resChan2 := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - doneChan := make(chan struct{}) - go func() { - defer close(doneChan) - select { - case writerImpl.writeChan <- component.ErrNotConnected: - case <-time.After(time.Second * 5): - t.Error("Timed out") - return - } - select { - case writerImpl.writeChan <- component.ErrNotConnected: - case <-time.After(time.Second * 5): - t.Error("Timed out") - return - } - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second * 5): - t.Error("Timed out") - return - } - go func() { - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second * 5): - } - }() - select { - case writerImpl.writeChan <- nil: - case <-time.After(time.Second * 5): - t.Error("Timed out") - return - } - select { - case writerImpl.writeChan <- nil: - case <-time.After(time.Second * 5): - t.Error("Timed out") - } - }() - - select { - case msgChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case msgChan <- message.NewTransaction(message.QuickBatch(nil), resChan2): - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case res, open := <-resChan: - if !open { - t.Error("Res chan closed") - } - if err := res; err != nil { - t.Error(err) - } - case <-time.After(time.Second * 5): - t.Error("Timed out") - } - select { - case res, open := <-resChan2: - if !open { - t.Error("Res chan closed") - } - if err := res; err != nil { - t.Error(err) - } - case <-time.After(time.Second * 5): - t.Error("Timed out") - } - <-doneChan - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - w.TriggerCloseNow() - require.NoError(t, w.WaitForClose(ctx)) -} - -func TestAsyncWriterCantReconnect(t *testing.T) { - t.Skip("Takes too long!") - t.Parallel() - - writerImpl := newAsyncMockWriter() - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - go func() { - select { - case msgChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - select { - case writerImpl.writeChan <- component.ErrNotConnected: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - select { - case writerImpl.connChan <- component.ErrNotConnected: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - // We will be failing to send but should still exit immediately. - w.TriggerCloseNow() - - go func() { - select { - case writerImpl.connChan <- component.ErrNotConnected: - case <-time.After(time.Second): - } - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, w.WaitForClose(ctx)) -} - -func TestAsyncWriterHappyPath(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - exp := [][]byte{[]byte("foo"), []byte("bar")} - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - go func() { - select { - case msgChan <- message.NewTransaction(message.QuickBatch(exp), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - select { - case writerImpl.writeChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - select { - case res, open := <-resChan: - require.True(t, open) - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - // We will be failing to send but should still exit immediately. - w.TriggerCloseNow() - require.NoError(t, w.WaitForClose(ctx)) - - msgRcvd, exists := writerImpl.msgsRcvd.Load(uint64(1)) - require.True(t, exists) - - if act := message.GetAllBytes(msgRcvd.(message.Batch)); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message sent: %v != %v", act, exp) - } -} - -func TestAsyncWriterSadPath(t *testing.T) { - t.Parallel() - - writerImpl := newAsyncMockWriter() - - exp := [][]byte{[]byte("foo"), []byte("bar")} - expErr := errors.New("message got lost or something") - - w, err := NewAsyncWriter("foo", 1, writerImpl, component.NoopObservability()) - if err != nil { - t.Error(err) - return - } - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = w.Consume(msgChan); err != nil { - t.Error(err) - } - - go func() { - select { - case msgChan <- message.NewTransaction(message.QuickBatch(exp), resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - select { - case writerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - select { - case writerImpl.writeChan <- expErr: - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - select { - case res, open := <-resChan: - if !open { - t.Fatal("Chan closed") - } - if actErr := res; expErr != actErr { - t.Errorf("Wrong response: %v != %v", actErr, expErr) - } - case <-time.After(time.Second): - t.Fatal("Timed out") - } - - // We will be failing to send but should still exit immediately. - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - w.TriggerCloseNow() - require.NoError(t, w.WaitForClose(ctx)) - - msgRcvd, exists := writerImpl.msgsRcvd.Load(uint64(1)) - require.True(t, exists) - - if act := message.GetAllBytes(msgRcvd.(message.Batch)); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message sent: %v != %v", act, exp) - } -} diff --git a/internal/component/output/batched_send.go b/internal/component/output/batched_send.go deleted file mode 100644 index 04e9df4d90..0000000000 --- a/internal/component/output/batched_send.go +++ /dev/null @@ -1,56 +0,0 @@ -package output - -import ( - "errors" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Returns true if the error should break a batch send loop. -func sendErrIsFatal(err error) bool { - if errors.Is(err, component.ErrTypeClosed) { - return true - } - if errors.Is(err, component.ErrNotConnected) { - return true - } - if errors.Is(err, component.ErrTimeout) { - return true - } - return false -} - -// IterateBatchedSend executes a closure fn on each message of a batch, where -// the closure is expected to attempt a send and return an error. If an error is -// returned then it is added to a batch error in order to support index specific -// error handling. -// -// However, if a fatal error is returned such as a connection loss or shut down -// then it is returned immediately. -func IterateBatchedSend(msg message.Batch, fn func(int, *message.Part) error) error { - if msg.Len() == 1 { - return fn(0, msg.Get(0)) - } - var batchErr *batch.Error - if err := msg.Iter(func(i int, p *message.Part) error { - tmpErr := fn(i, p) - if tmpErr != nil { - if sendErrIsFatal(tmpErr) { - return tmpErr - } - if batchErr == nil { - batchErr = batch.NewError(msg, tmpErr) - } - batchErr.Failed(i, tmpErr) - } - return nil - }); err != nil { - return err - } - if batchErr != nil { - return batchErr - } - return nil -} diff --git a/internal/component/output/batched_send_test.go b/internal/component/output/batched_send_test.go deleted file mode 100644 index 56fd69b43b..0000000000 --- a/internal/component/output/batched_send_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package output - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestBatchedSendHappy(t *testing.T) { - parts := []string{ - "foo", "bar", "baz", "buz", - } - - msg := message.QuickBatch(nil) - for _, p := range parts { - msg = append(msg, message.NewPart([]byte(p))) - } - - seen := []string{} - assert.NoError(t, IterateBatchedSend(msg, func(i int, p *message.Part) error { - assert.Len(t, seen, i) - seen = append(seen, string(p.AsBytes())) - return nil - })) - - assert.Equal(t, parts, seen) -} - -func TestBatchedSendALittleSad(t *testing.T) { - parts := []string{ - "foo", "bar", "baz", "buz", - } - - msg := message.QuickBatch(nil) - for _, p := range parts { - msg = append(msg, message.NewPart([]byte(p))) - } - - errFirst, errSecond := errors.New("first"), errors.New("second") - - seen := []string{} - err := IterateBatchedSend(msg, func(i int, p *message.Part) error { - assert.Len(t, seen, i) - seen = append(seen, string(p.AsBytes())) - if i == 1 { - return errFirst - } - if i == 3 { - return errSecond - } - return nil - }) - assert.Error(t, err) - - expErr := batch.NewError(msg, errFirst).Failed(1, errFirst).Failed(3, errSecond) - - assert.Equal(t, parts, seen) - assert.Equal(t, expErr, err) -} - -func TestBatchedSendFatal(t *testing.T) { - msg := message.QuickBatch(nil) - for _, p := range []string{ - "foo", "bar", "baz", "buz", - } { - msg = append(msg, message.NewPart([]byte(p))) - } - - seen := []string{} - err := IterateBatchedSend(msg, func(i int, p *message.Part) error { - assert.Len(t, seen, i) - seen = append(seen, string(p.AsBytes())) - if i == 1 { - return component.ErrTypeClosed - } - return nil - }) - assert.Error(t, err) - assert.EqualError(t, err, "type was closed") - assert.Equal(t, []string{"foo", "bar"}, seen) - - seen = []string{} - err = IterateBatchedSend(msg, func(i int, p *message.Part) error { - assert.Len(t, seen, i) - seen = append(seen, string(p.AsBytes())) - if i == 1 { - return component.ErrNotConnected - } - return nil - }) - assert.Error(t, err) - assert.EqualError(t, err, "not connected to target source or sink") - assert.Equal(t, []string{"foo", "bar"}, seen) - - seen = []string{} - err = IterateBatchedSend(msg, func(i int, p *message.Part) error { - assert.Len(t, seen, i) - seen = append(seen, string(p.AsBytes())) - if i == 1 { - return component.ErrTimeout - } - return nil - }) - assert.Error(t, err) - assert.EqualError(t, err, "action timed out") - assert.Equal(t, []string{"foo", "bar"}, seen) -} diff --git a/internal/component/output/batcher/batcher.go b/internal/component/output/batcher/batcher.go deleted file mode 100644 index 8e285a545f..0000000000 --- a/internal/component/output/batcher/batcher.go +++ /dev/null @@ -1,194 +0,0 @@ -package batcher - -import ( - "context" - "fmt" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/batch/policy/batchconfig" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" -) - -// Impl wraps an output with a batching policy. -type Impl struct { - stats metrics.Type - log log.Modular - - child output.Streamed - batcher *policy.Batcher - - messagesIn <-chan message.Transaction - messagesOut chan message.Transaction - - shutSig *shutdown.Signaller -} - -// NewFromConfig creates a new output preceded by a batching mechanism that -// enforces a given batching policy configuration. -func NewFromConfig(conf batchconfig.Config, child output.Streamed, mgr bundle.NewManagement) (output.Streamed, error) { - if !conf.IsNoop() { - policy, err := policy.New(conf, mgr.IntoPath("batching")) - if err != nil { - return nil, fmt.Errorf("failed to construct batch policy: %v", err) - } - child = New(policy, child, mgr) - } - return child, nil -} - -// New creates a new output preceded by a batching mechanism that enforces a -// given batching policy. -func New(batcher *policy.Batcher, child output.Streamed, mgr bundle.NewManagement) output.Streamed { - m := Impl{ - stats: mgr.Metrics(), - log: mgr.Logger(), - child: child, - batcher: batcher, - messagesOut: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - return &m -} - -//------------------------------------------------------------------------------ - -func (m *Impl) loop() { - closeNowCtx, cnDone := m.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - defer func() { - close(m.messagesOut) - - m.child.TriggerCloseNow() - _ = m.child.WaitForClose(context.Background()) - - _ = m.batcher.Close(context.Background()) - - m.shutSig.TriggerHasStopped() - }() - - var nextTimedBatchChan <-chan time.Time - if tNext := m.batcher.UntilNext(); tNext > 0 { - nextTimedBatchChan = time.After(tNext) - } - - var pendingTrans []*transaction.Tracked - for !m.shutSig.IsSoftStopSignalled() { - if nextTimedBatchChan == nil { - if tNext := m.batcher.UntilNext(); tNext > 0 { - nextTimedBatchChan = time.After(tNext) - } - } - - var flushBatch bool - select { - case tran, open := <-m.messagesIn: - if !open { - if flushBatch = m.batcher.Count() > 0; !flushBatch { - return - } - - // If we're waiting for a timed batch then we will respect it. - if nextTimedBatchChan != nil { - select { - case <-nextTimedBatchChan: - case <-m.shutSig.SoftStopChan(): - } - } - } else { - trackedTran := transaction.NewTracked(tran.Payload, tran.Ack) - _ = trackedTran.Message().Iter(func(i int, p *message.Part) error { - if m.batcher.Add(p) { - flushBatch = true - } - return nil - }) - pendingTrans = append(pendingTrans, trackedTran) - } - case <-nextTimedBatchChan: - flushBatch = true - nextTimedBatchChan = nil - case <-m.shutSig.SoftStopChan(): - flushBatch = true - } - - if !flushBatch { - continue - } - - sendMsg := m.batcher.Flush(closeNowCtx) - if sendMsg == nil { - continue - } - - resChan := make(chan error) - select { - case m.messagesOut <- message.NewTransaction(sendMsg, resChan): - case <-m.shutSig.SoftStopChan(): - return - } - - go func(rChan chan error, upstreamTrans []*transaction.Tracked) { - select { - case <-m.shutSig.SoftStopChan(): - return - case res, open := <-rChan: - if !open { - return - } - closeLeisureCtx, done := m.shutSig.SoftStopCtx(context.Background()) - for _, t := range upstreamTrans { - if err := t.Ack(closeLeisureCtx, res); err != nil { - done() - return - } - } - done() - } - }(resChan, pendingTrans) - pendingTrans = nil - } -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (m *Impl) Connected() bool { - return m.child.Connected() -} - -// Consume assigns a messages channel for the output to read. -func (m *Impl) Consume(msgs <-chan message.Transaction) error { - if m.messagesIn != nil { - return component.ErrAlreadyStarted - } - if err := m.child.Consume(m.messagesOut); err != nil { - return err - } - m.messagesIn = msgs - go m.loop() - return nil -} - -// TriggerCloseNow shuts down the Batcher and stops processing messages. -func (m *Impl) TriggerCloseNow() { - m.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the Batcher output has closed down. -func (m *Impl) WaitForClose(ctx context.Context) error { - select { - case <-m.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/component/output/batcher/batcher_test.go b/internal/component/output/batcher/batcher_test.go deleted file mode 100644 index 7c60e3751a..0000000000 --- a/internal/component/output/batcher/batcher_test.go +++ /dev/null @@ -1,401 +0,0 @@ -package batcher_test - -import ( - "context" - "errors" - "fmt" - "reflect" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - batchInternal "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/batch/policy/batchconfig" - "github.com/benthosdev/benthos/v4/internal/component/output/batcher" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestBatcherEarlyTermination(t *testing.T) { - tInChan := make(chan message.Transaction) - resChan := make(chan error) - - policyConf := batchconfig.NewConfig() - policyConf.Count = 10 - policyConf.Period = "50ms" - batchPol, err := policy.New(policyConf, mock.NewManager()) - require.NoError(t, err) - - out := &mock.OutputChanneled{} - - b := batcher.New(batchPol, out, mock.NewManager()) - require.NoError(t, b.Consume(tInChan)) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - done() - - require.Error(t, b.WaitForClose(ctx)) - - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo")}), resChan): - case <-time.After(time.Second): - t.Error("unexpected") - } - - ctx, done = context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.Error(t, b.WaitForClose(ctx)) -} - -func TestBatcherBasic(t *testing.T) { - tInChan := make(chan message.Transaction) - resChan := make(chan error) - - policyConf := batchconfig.NewConfig() - policyConf.Count = 4 - batchPol, err := policy.New(policyConf, mock.NewManager()) - require.NoError(t, err) - - out := &mock.OutputChanneled{} - - b := batcher.New(batchPol, out, mock.NewManager()) - require.NoError(t, b.Consume(tInChan)) - - tOutChan := out.TChan - - var firstBatchExpected [][]byte - var secondBatchExpected [][]byte - var finalBatchExpected [][]byte - for i := 0; i < 10; i++ { - inputBytes := []byte(fmt.Sprintf("foo %v", i)) - if i < 4 { - firstBatchExpected = append(firstBatchExpected, inputBytes) - } else if i < 8 { - secondBatchExpected = append(secondBatchExpected, inputBytes) - } else { - finalBatchExpected = append(finalBatchExpected, inputBytes) - } - } - - firstErr := errors.New("first error") - secondErr := errors.New("second error") - finalErr := errors.New("final error") - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - for _, batch := range firstBatchExpected { - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{batch}), resChan): - case <-time.After(time.Second): - t.Error("timed out") - } - } - for range firstBatchExpected { - select { - case actRes := <-resChan: - assert.Equal(t, firstErr, actRes) - case <-time.After(time.Second): - t.Error("timed out") - } - } - for _, batch := range secondBatchExpected { - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{batch}), resChan): - case <-time.After(time.Second): - t.Error("timed out") - } - } - for range secondBatchExpected { - select { - case actRes := <-resChan: - assert.Equal(t, secondErr, actRes) - case <-time.After(time.Second): - t.Error("timed out") - } - } - for _, batch := range finalBatchExpected { - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{batch}), resChan): - case <-time.After(time.Second): - t.Error("timed out") - } - } - close(tInChan) - for range finalBatchExpected { - select { - case actRes := <-resChan: - assert.Equal(t, finalErr, actRes) - case <-time.After(time.Second): - t.Error("timed out") - } - } - }() - - sendResponse := func(tran message.Transaction, err error) { - sCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - defer wg.Done() - require.NoError(t, tran.Ack(sCtx, err)) - } - - // Receive first batch on output - select { - case outTr := <-tOutChan: - if exp, act := firstBatchExpected, message.GetAllBytes(outTr.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result from batch: %s != %s", act, exp) - } - wg.Add(1) - go sendResponse(outTr, firstErr) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message read") - } - - // Receive second batch on output - select { - case outTr := <-tOutChan: - if exp, act := secondBatchExpected, message.GetAllBytes(outTr.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result from batch: %s != %s", act, exp) - } - wg.Add(1) - go sendResponse(outTr, secondErr) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message read") - } - - // Receive final batch on output - select { - case outTr := <-tOutChan: - if exp, act := finalBatchExpected, message.GetAllBytes(outTr.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result from batch: %s != %s", act, exp) - } - wg.Add(1) - go sendResponse(outTr, finalErr) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message read") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, b.WaitForClose(ctx)) - wg.Wait() -} - -func TestBatcherMaxInFlight(t *testing.T) { - timeOutCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tInChan := make(chan message.Transaction) - - policyConf := batchconfig.NewConfig() - policyConf.Count = 2 - batchPol, err := policy.New(policyConf, mock.NewManager()) - require.NoError(t, err) - - out := &mock.OutputChanneled{} - - b := batcher.New(batchPol, out, mock.NewManager()) - require.NoError(t, b.Consume(tInChan)) - - tOutChan := out.TChan - resChanOne, resChanTwo := make(chan error), make(chan error) - - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{ - []byte("hello world 1"), - []byte("hello world 2"), - []byte("hello world 3"), - []byte("hello world 4"), - }), resChanOne): - case <-timeOutCtx.Done(): - t.Fatal("timed out") - } - - var tranOne message.Transaction - select { - case tranOne = <-tOutChan: - case <-timeOutCtx.Done(): - t.Fatal("timed out") - } - - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{ - []byte("hello world 5"), - []byte("hello world 6"), - []byte("hello world 7"), - []byte("hello world 8"), - }), resChanTwo): - case <-timeOutCtx.Done(): - t.Fatal("timed out") - } - - var tranTwo message.Transaction - select { - case tranTwo = <-tOutChan: - case <-timeOutCtx.Done(): - t.Fatal("timed out") - } - - require.NoError(t, tranOne.Ack(timeOutCtx, nil)) - require.NoError(t, tranTwo.Ack(timeOutCtx, nil)) - - select { - case err := <-resChanOne: - require.NoError(t, err) - case <-timeOutCtx.Done(): - t.Fatal("timed out") - } - - select { - case err := <-resChanTwo: - require.NoError(t, err) - case <-timeOutCtx.Done(): - t.Fatal("timed out") - } - - b.TriggerCloseNow() - require.NoError(t, b.WaitForClose(timeOutCtx)) -} - -func TestBatcherBatchError(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tInChan := make(chan message.Transaction) - resChan := make(chan error) - - policyConf := batchconfig.NewConfig() - policyConf.Count = 4 - batchPol, err := policy.New(policyConf, mock.NewManager()) - require.NoError(t, err) - - out := &mock.OutputChanneled{} - - b := batcher.New(batchPol, out, mock.NewManager()) - require.NoError(t, b.Consume(tInChan)) - - tOutChan := out.TChan - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - firstErr := errors.New("first error") - thirdErr := errors.New("third error") - - // Receive first batch on output - var outTr message.Transaction - select { - case outTr = <-tOutChan: - case <-time.After(time.Second): - t.Error("Timed out waiting for message read") - } - assert.Equal(t, [][]byte{ - []byte("foo0"), - []byte("foo1"), - []byte("foo2"), - []byte("foo3"), - }, message.GetAllBytes(outTr.Payload)) - - batchErr := batchInternal.NewError(outTr.Payload, errors.New("foo")). - Failed(0, firstErr).Failed(2, thirdErr) - - require.NoError(t, outTr.Ack(tCtx, batchErr)) - }() - - for i := 0; i < 4; i++ { - data := []byte(fmt.Sprintf("foo%v", i)) - select { - case tInChan <- message.NewTransaction(message.QuickBatch([][]byte{data}), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - for i := 0; i < 4; i++ { - var act error - select { - case actRes := <-resChan: - act = actRes - case <-time.After(time.Second): - t.Fatal("timed out") - } - switch i { - case 0: - assert.EqualError(t, act, "first error") - case 2: - assert.EqualError(t, act, "third error") - default: - assert.NoError(t, act) - } - } - - close(tInChan) - b.TriggerCloseNow() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, b.WaitForClose(ctx)) - wg.Wait() -} - -func TestBatcherTimed(t *testing.T) { - tInChan := make(chan message.Transaction) - resChan := make(chan error) - - policyConf := batchconfig.NewConfig() - policyConf.Period = "100ms" - batchPol, err := policy.New(policyConf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - out := &mock.OutputChanneled{} - - b := batcher.New(batchPol, out, mock.NewManager()) - if err := b.Consume(tInChan); err != nil { - t.Fatal(err) - } - - tOutChan := out.TChan - - batchExpected := [][]byte{ - []byte("foo1"), - []byte("foo2"), - []byte("foo3"), - } - - select { - case tInChan <- message.NewTransaction(message.QuickBatch(batchExpected), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message send") - } - - // Receive first batch on output - var outTr message.Transaction - select { - case outTr = <-tOutChan: - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message read") - } - if exp, act := batchExpected, message.GetAllBytes(outTr.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result from batch: %s != %s", act, exp) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - close(tInChan) - b.TriggerCloseNow() - require.NoError(t, b.WaitForClose(ctx)) - - close(resChan) -} diff --git a/internal/component/output/config.go b/internal/component/output/config.go deleted file mode 100644 index 95d0fae31d..0000000000 --- a/internal/component/output/config.go +++ /dev/null @@ -1,109 +0,0 @@ -package output - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all output types. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -type Config struct { - Label string `json:"label" yaml:"label"` - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` - Processors []processor.Config `json:"processors" yaml:"processors"` -} - -// NewConfig returns a configuration struct fully populated with default values. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -func NewConfig() Config { - return Config{ - Label: "", - Type: "stdout", - Plugin: nil, - Processors: []processor.Config{}, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeOutput, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - conf.Label, _ = value["label"].(string) - - if procV, exists := value["processors"]; exists { - procArr, ok := procV.([]any) - if !ok { - err = fmt.Errorf("processors: unexpected value, expected array got %T", procV) - return - } - for i, pv := range procArr { - var tmpProc processor.Config - if tmpProc, err = processor.FromAny(prov, pv); err != nil { - err = fmt.Errorf("%v: %w", i, err) - return - } - conf.Processors = append(conf.Processors, tmpProc) - } - } - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeOutput, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - for i := 0; i < len(value.Content)-1; i += 2 { - switch value.Content[i].Value { - case "label": - conf.Label = value.Content[i+1].Value - case "processors": - for i, n := range value.Content[i+1].Content { - var tmpProc processor.Config - if tmpProc, err = processor.FromAny(prov, n); err != nil { - err = fmt.Errorf("%v: %w", i, err) - return - } - conf.Processors = append(conf.Processors, tmpProc) - } - } - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/output/interface.go b/internal/component/output/interface.go deleted file mode 100644 index 932d676b41..0000000000 --- a/internal/component/output/interface.go +++ /dev/null @@ -1,49 +0,0 @@ -package output - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Sync is a common interface implemented by outputs and provides synchronous -// based writing APIs. -type Sync interface { - // WriteTransaction attempts to write a transaction to an output. - WriteTransaction(context.Context, message.Transaction) error - - // Connected returns a boolean indicating whether this output is currently - // connected to its target. - Connected() bool - - // TriggerStopConsuming instructs the output to start shutting down - // resources once all pending messages are delivered and acknowledged. - TriggerStopConsuming() - - // TriggerCloseNow triggers the shut down of this component but should not - // block the calling goroutine. - TriggerCloseNow() - - // WaitForClose is a blocking call to wait until the component has finished - // shutting down and cleaning up resources. - WaitForClose(ctx context.Context) error -} - -// Streamed is a common interface implemented by outputs and provides channel -// based streaming APIs. -type Streamed interface { - // Consume starts the type receiving transactions from a Transactor. - Consume(<-chan message.Transaction) error - - // Connected returns a boolean indicating whether this output is currently - // connected to its target. - Connected() bool - - // TriggerCloseNow triggers the shut down of this component but should not - // block the calling goroutine. - TriggerCloseNow() - - // WaitForClose is a blocking call to wait until the component has finished - // shutting down and cleaning up resources. - WaitForClose(ctx context.Context) error -} diff --git a/internal/component/output/not_batched.go b/internal/component/output/not_batched.go deleted file mode 100644 index 91727b8fe2..0000000000 --- a/internal/component/output/not_batched.go +++ /dev/null @@ -1,167 +0,0 @@ -package output - -import ( - "context" - "errors" - "sync" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type notBatchedOutput struct { - out Streamed - - inChan <-chan message.Transaction - outChan chan message.Transaction - - shutSig *shutdown.Signaller -} - -// OnlySinglePayloads expands message batches into individual payloads, -// respecting the max in flight of the wrapped output. This is a more efficient -// way of feeding messages into an output that handles its own batching -// mechanism internally, or does not support batching at all. -func OnlySinglePayloads(out Streamed) Streamed { - n := ¬BatchedOutput{ - out: out, - outChan: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - return n -} - -//------------------------------------------------------------------------------ - -func (n *notBatchedOutput) breakMessageOut(msg message.Batch) error { - var wg sync.WaitGroup - - var batchErr *batch.Error - var batchErrMut sync.Mutex - addBatchErr := func(i int, err error) { - if err != nil { - batchErrMut.Lock() - if batchErr == nil { - batchErr = batch.NewError(msg, err) - } - batchErr.Failed(i, err) - batchErrMut.Unlock() - } - } - - if err := msg.Iter(func(i int, p *message.Part) error { - index := i - - tmpResChan := make(chan error, 1) - tmpMsg := message.Batch{p} - - select { - case n.outChan <- message.NewTransaction(tmpMsg, tmpResChan): - case <-n.shutSig.HardStopChan(): - if index == 0 { - return component.ErrTypeClosed - } - addBatchErr(index, component.ErrTypeClosed) - return nil - } - - wg.Add(1) - go func() { - defer wg.Done() - var err error - select { - case res := <-tmpResChan: - err = res - case <-n.shutSig.HardStopChan(): - err = component.ErrTypeClosed - } - addBatchErr(index, err) - }() - return nil - }); err != nil { - return err - } - - wg.Wait() - if batchErr != nil { - return batchErr - } - return nil -} - -func (n *notBatchedOutput) loop() { - ctx, done := n.shutSig.HardStopCtx(context.Background()) - defer done() - - defer func() { - close(n.outChan) - n.out.TriggerCloseNow() - _ = n.out.WaitForClose(ctx) - n.shutSig.TriggerHasStopped() - }() - - for { - var tran message.Transaction - var open bool - select { - case tran, open = <-n.inChan: - if !open { - return - } - case <-n.shutSig.SoftStopChan(): - return - } - - if tran.Payload.Len() == 1 { - select { - case n.outChan <- tran: - case <-n.shutSig.HardStopChan(): - return - } - } else { - var res error - if err := n.breakMessageOut(tran.Payload); err != nil { - if errors.Is(err, component.ErrTypeClosed) { - return - } - res = err - } - _ = tran.Ack(ctx, res) - } - } -} - -//------------------------------------------------------------------------------ - -func (n *notBatchedOutput) Consume(ts <-chan message.Transaction) error { - if n.inChan != nil { - return component.ErrAlreadyStarted - } - if err := n.out.Consume(n.outChan); err != nil { - return err - } - n.inChan = ts - go n.loop() - return nil -} - -func (n *notBatchedOutput) Connected() bool { - return n.out.Connected() -} - -func (n *notBatchedOutput) TriggerCloseNow() { - n.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the File output has closed down. -func (n *notBatchedOutput) WaitForClose(ctx context.Context) error { - select { - case <-n.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/component/output/not_batched_test.go b/internal/component/output/not_batched_test.go deleted file mode 100644 index 3d6b9e00df..0000000000 --- a/internal/component/output/not_batched_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package output - -import ( - "context" - "errors" - "fmt" - "sort" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type mockNBWriter struct { - t *testing.T - written []string - errorOn []string - closeCalled bool - closeChan chan error - mut sync.Mutex -} - -func (m *mockNBWriter) Connect(context.Context) error { - return nil -} - -func (m *mockNBWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - m.mut.Lock() - defer m.mut.Unlock() - - m.t.Helper() - assert.Equal(m.t, 1, msg.Len()) - return msg.Iter(func(i int, p *message.Part) error { - for _, eOn := range m.errorOn { - if eOn == string(p.AsBytes()) { - return errors.New("test err") - } - } - m.written = append(m.written, string(p.AsBytes())) - return nil - }) -} - -func (m *mockNBWriter) Close(ctx context.Context) error { - m.mut.Lock() - m.closeCalled = true - m.mut.Unlock() - if m.closeChan == nil { - return nil - } - return <-m.closeChan -} - -func TestNotBatchedSingleMessages(t *testing.T) { - msg := func(c string) message.Batch { - p := message.NewPart([]byte(c)) - msg := message.Batch{p} - return msg - } - - w := &mockNBWriter{t: t} - out, err := NewAsyncWriter("foo", 1, w, component.NoopObservability()) - require.NoError(t, err) - - nbOut := OnlySinglePayloads(out) - - resChan := make(chan error) - tChan := make(chan message.Transaction) - require.NoError(t, nbOut.Consume(tChan)) - - for i := 0; i < 5; i++ { - select { - case tChan <- message.NewTransaction(msg(fmt.Sprintf("foo%v", i)), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nbOut.TriggerCloseNow() - assert.NoError(t, nbOut.WaitForClose(ctx)) - assert.Equal(t, []string{ - "foo0", "foo1", "foo2", "foo3", "foo4", - }, w.written) -} - -func TestShutdown(t *testing.T) { - msg := func(c string) message.Batch { - p := message.NewPart([]byte(c)) - msg := message.Batch{p} - return msg - } - - w := &mockNBWriter{t: t, closeChan: make(chan error)} - out, err := NewAsyncWriter("foo", 1, w, component.NoopObservability()) - require.NoError(t, err) - - nbOut := OnlySinglePayloads(out) - - resChan := make(chan error) - tChan := make(chan message.Transaction) - require.NoError(t, nbOut.Consume(tChan)) - - select { - case tChan <- message.NewTransaction(msg("foo"), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - done() - - nbOut.TriggerCloseNow() - assert.EqualError(t, nbOut.WaitForClose(ctx), "context canceled") - - select { - case w.closeChan <- errors.New("custom err"): - case <-time.After(time.Second): - t.Error("timed out") - } - - ctx, done = context.WithTimeout(context.Background(), time.Second*30) - defer done() - - assert.NoError(t, nbOut.WaitForClose(ctx)) - assert.Equal(t, []string{"foo"}, w.written) - w.mut.Lock() - assert.True(t, w.closeCalled) - w.mut.Unlock() -} - -func TestNotBatchedBreakOutMessages(t *testing.T) { - msg := func(c ...string) message.Batch { - msg := message.QuickBatch(nil) - for _, str := range c { - msg = append(msg, message.NewPart([]byte(str))) - } - return msg - } - - w := &mockNBWriter{t: t} - out, err := NewAsyncWriter("foo", 1, w, component.NoopObservability()) - require.NoError(t, err) - - nbOut := OnlySinglePayloads(out) - - resChan := make(chan error) - tChan := make(chan message.Transaction) - require.NoError(t, nbOut.Consume(tChan)) - - select { - case tChan <- message.NewTransaction(msg( - "foo0", "foo1", "foo2", "foo3", "foo4", - ), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nbOut.TriggerCloseNow() - assert.NoError(t, nbOut.WaitForClose(ctx)) - assert.Equal(t, []string{ - "foo0", "foo1", "foo2", "foo3", "foo4", - }, w.written) -} - -func TestNotBatchedBreakOutMessagesErrors(t *testing.T) { - msg := func(c ...string) message.Batch { - msg := message.QuickBatch(nil) - for _, str := range c { - msg = append(msg, message.NewPart([]byte(str))) - } - return msg - } - - w := &mockNBWriter{t: t, errorOn: []string{"foo1", "foo3"}} - out, err := NewAsyncWriter("foo", 1, w, component.NoopObservability()) - require.NoError(t, err) - - nbOut := OnlySinglePayloads(out) - - resChan := make(chan error) - tChan := make(chan message.Transaction) - require.NoError(t, nbOut.Consume(tChan)) - - sourceMessage := msg("foo0", "foo1", "foo2", "foo3", "foo4") - sortGroup, sourceMessage := message.NewSortGroup(sourceMessage) - - select { - case tChan <- message.NewTransaction(sourceMessage, resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - select { - case res := <-resChan: - err := res - require.Error(t, err) - - var walkable *batch.Error - require.ErrorAs(t, err, &walkable, "expected error to be walkable batch error") - - errs := map[int]string{} - walkable.WalkPartsBySource(sortGroup, sourceMessage, func(i int, _ *message.Part, err error) bool { - if err != nil { - errs[i] = err.Error() - } - return true - }) - assert.Equal(t, map[int]string{ - 1: "test err", - 3: "test err", - }, errs) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nbOut.TriggerCloseNow() - assert.NoError(t, nbOut.WaitForClose(ctx)) - assert.Equal(t, []string{ - "foo0", "foo2", "foo4", - }, w.written) -} - -func TestNotBatchedBreakOutMessagesErrorsAsync(t *testing.T) { - msg := func(c ...string) message.Batch { - msg := message.QuickBatch(nil) - for _, str := range c { - msg = append(msg, message.NewPart([]byte(str))) - } - return msg - } - - w := &mockNBWriter{t: t, errorOn: []string{"foo1", "foo3"}} - out, err := NewAsyncWriter("foo", 5, w, component.NoopObservability()) - require.NoError(t, err) - - nbOut := OnlySinglePayloads(out) - - resChan := make(chan error) - tChan := make(chan message.Transaction) - require.NoError(t, nbOut.Consume(tChan)) - - sourceMessage := msg("foo0", "foo1", "foo2", "foo3", "foo4") - sortGroup, sourceMessage := message.NewSortGroup(sourceMessage) - - select { - case tChan <- message.NewTransaction(sourceMessage, resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - select { - case res := <-resChan: - err := res - require.Error(t, err) - - var walkable *batch.Error - require.ErrorAs(t, err, &walkable, "expected error to be walkable batch error") - - errs := map[int]string{} - walkable.WalkPartsBySource(sortGroup, sourceMessage, func(i int, _ *message.Part, err error) bool { - if err != nil { - errs[i] = err.Error() - } - return true - }) - assert.Equal(t, map[int]string{ - 1: "test err", - 3: "test err", - }, errs) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nbOut.TriggerCloseNow() - assert.NoError(t, nbOut.WaitForClose(ctx)) - sort.Strings(w.written) - assert.Equal(t, []string{ - "foo0", "foo2", "foo4", - }, w.written) -} diff --git a/internal/component/output/processors/append.go b/internal/component/output/processors/append.go deleted file mode 100644 index a3ec10aaf5..0000000000 --- a/internal/component/output/processors/append.go +++ /dev/null @@ -1,44 +0,0 @@ -package processors - -import ( - "strconv" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -// AppendFromConfig takes a variant arg of pipeline constructor functions and -// returns a new slice of them where the processors of the provided output -// configuration will also be initialized. -func AppendFromConfig(conf output.Config, mgr bundle.NewManagement, pipelines ...processor.PipelineConstructorFunc) []processor.PipelineConstructorFunc { - if len(conf.Processors) > 0 { - pipelines = append(pipelines, []processor.PipelineConstructorFunc{func() (processor.Pipeline, error) { - processors := make([]processor.V1, len(conf.Processors)) - for j, procConf := range conf.Processors { - var err error - pMgr := mgr.IntoPath("processors", strconv.Itoa(j)) - processors[j], err = pMgr.NewProcessor(procConf) - if err != nil { - return nil, err - } - } - return pipeline.NewProcessor(processors...), nil - }}...) - } - return pipelines -} - -// WrapConstructor provides a way to define an output constructor without -// manually initializing processors of the config. -func WrapConstructor(fn func(output.Config, bundle.NewManagement) (output.Streamed, error)) bundle.OutputConstructor { - return func(c output.Config, nm bundle.NewManagement, pcf ...processor.PipelineConstructorFunc) (output.Streamed, error) { - o, err := fn(c, nm) - if err != nil { - return nil, err - } - pcf = AppendFromConfig(c, nm, pcf...) - return output.WrapWithPipelines(o, pcf...) - } -} diff --git a/internal/component/output/wrap_with_pipeline.go b/internal/component/output/wrap_with_pipeline.go deleted file mode 100644 index 5ef0a33947..0000000000 --- a/internal/component/output/wrap_with_pipeline.go +++ /dev/null @@ -1,75 +0,0 @@ -package output - -import ( - "context" - - iprocessor "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// WithPipeline is a type that wraps both an output type and a pipeline type -// by routing the pipeline through the output, and implements the output.Type -// interface in order to act like an ordinary output. -type WithPipeline struct { - out Streamed - pipe iprocessor.Pipeline -} - -// WrapWithPipeline routes a processing pipeline directly into an output and -// returns a type that manages both and acts like an ordinary output. -func WrapWithPipeline(out Streamed, pipeConstructor iprocessor.PipelineConstructorFunc) (*WithPipeline, error) { - pipe, err := pipeConstructor() - if err != nil { - return nil, err - } - - if err := out.Consume(pipe.TransactionChan()); err != nil { - return nil, err - } - return &WithPipeline{ - out: out, - pipe: pipe, - }, nil -} - -// WrapWithPipelines wraps an output with a variadic number of pipelines. -func WrapWithPipelines(out Streamed, pipeConstructors ...iprocessor.PipelineConstructorFunc) (Streamed, error) { - var err error - for i := len(pipeConstructors) - 1; i >= 0; i-- { - if out, err = WrapWithPipeline(out, pipeConstructors[i]); err != nil { - return nil, err - } - } - return out, nil -} - -//------------------------------------------------------------------------------ - -// Consume starts the type listening to a message channel from a -// producer. -func (i *WithPipeline) Consume(tsChan <-chan message.Transaction) error { - return i.pipe.Consume(tsChan) -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (i *WithPipeline) Connected() bool { - return i.out.Connected() -} - -//------------------------------------------------------------------------------ - -// TriggerCloseNow triggers a closure of this object but does not block. -func (i *WithPipeline) TriggerCloseNow() { - i.pipe.TriggerCloseNow() - go func() { - _ = i.pipe.WaitForClose(context.Background()) - i.out.TriggerCloseNow() - }() -} - -// WaitForClose is a blocking call to wait until the object has finished closing -// down and cleaning up resources. -func (i *WithPipeline) WaitForClose(ctx context.Context) error { - return i.out.WaitForClose(ctx) -} diff --git a/internal/component/output/wrap_with_pipeline_test.go b/internal/component/output/wrap_with_pipeline_test.go deleted file mode 100644 index af28d77b87..0000000000 --- a/internal/component/output/wrap_with_pipeline_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package output_test - -import ( - "context" - "errors" - "reflect" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/pipeline" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -type mockOutput struct { - ts <-chan message.Transaction -} - -func (m *mockOutput) Consume(ts <-chan message.Transaction) error { - m.ts = ts - return nil -} - -func (m *mockOutput) Connected() bool { - return true -} - -func (m *mockOutput) TriggerCloseNow() { - // NOT EXPECTING TO HIT THIS -} - -func (m *mockOutput) WaitForClose(ctx context.Context) error { - select { - case _, open := <-m.ts: - if open { - return errors.New("messages chan still open") - } - case <-ctx.Done(): - return errors.New("timed out") - } - return nil -} - -//------------------------------------------------------------------------------ - -type mockPipe struct { - tsIn <-chan message.Transaction - ts chan message.Transaction - closeOnce sync.Once -} - -func (m *mockPipe) Consume(ts <-chan message.Transaction) error { - m.tsIn = ts - return nil -} - -func (m *mockPipe) TransactionChan() <-chan message.Transaction { - return m.ts -} - -func (m *mockPipe) TriggerCloseNow() { - m.closeOnce.Do(func() { - close(m.ts) - }) -} - -func (m *mockPipe) WaitForClose(ctx context.Context) error { - return errors.New("not expecting to see this") -} - -//------------------------------------------------------------------------------ - -func TestBasicWrapPipeline(t *testing.T) { - mockOut := &mockOutput{} - mockPi := &mockPipe{ - ts: make(chan message.Transaction), - } - - _, err := output.WrapWithPipeline(mockOut, func() (processor.Pipeline, error) { - return nil, errors.New("nope") - }) - if err == nil { - t.Error("expected error from back constructor") - } - - newOutput, err := output.WrapWithPipeline(mockOut, func() (processor.Pipeline, error) { - return mockPi, nil - }) - if err != nil { - t.Fatal(err) - } - - dudMsgChan := make(chan message.Transaction) - if err = newOutput.Consume(dudMsgChan); err != nil { - t.Error(err) - } - - if mockPi.tsIn != dudMsgChan { - t.Error("Wrong message chan in mock pipe") - } - - if mockOut.ts != mockPi.ts { - t.Error("Wrong messages chan in mock pipe") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - close(dudMsgChan) - mockPi.TriggerCloseNow() - newOutput.TriggerCloseNow() - require.NoError(t, newOutput.WaitForClose(ctx)) -} - -func TestBasicWrapPipelinesOrdering(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOut := &mockOutput{} - - firstProc, err := testutil.ProcessorFromYAML(` -insert_part: - content: foo - index: 0 -`) - require.NoError(t, err) - - secondProc, err := testutil.ProcessorFromYAML(` -select_parts: - parts: [ 0 ] -`) - require.NoError(t, err) - - conf := output.NewConfig() - conf.Processors = append(conf.Processors, firstProc) - - newOutput, err := output.WrapWithPipelines( - mockOut, - func() (processor.Pipeline, error) { - proc, err := mock.NewManager().NewProcessor(firstProc) - if err != nil { - return nil, err - } - return pipeline.NewProcessor(proc), nil - }, - func() (processor.Pipeline, error) { - proc, err := mock.NewManager().NewProcessor(secondProc) - if err != nil { - return nil, err - } - return pipeline.NewProcessor(proc), nil - }, - ) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - resChan := make(chan error) - if err = newOutput.Consume(tChan); err != nil { - t.Error(err) - } - - select { - case <-time.After(time.Second): - t.Fatal("timed out") - case tChan <- message.NewTransaction( - message.QuickBatch([][]byte{[]byte("bar")}), resChan, - ): - } - - var tran message.Transaction - select { - case <-time.After(time.Second): - t.Fatal("timed out") - case tran = <-mockOut.ts: - } - - exp := [][]byte{ - []byte("foo"), - } - if act := message.GetAllBytes(tran.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong contents: %s != %s", act, exp) - } - - go func() { - require.NoError(t, tran.Ack(ctx, nil)) - }() - - select { - case <-time.After(time.Second): - t.Fatal("timed out") - case res := <-resChan: - if res != nil { - t.Error(res) - } - } - - close(tChan) - newOutput.TriggerCloseNow() - require.NoError(t, newOutput.WaitForClose(ctx)) -} diff --git a/internal/component/processor/auto_observed.go b/internal/component/processor/auto_observed.go deleted file mode 100644 index 5f0f32c3a1..0000000000 --- a/internal/component/processor/auto_observed.go +++ /dev/null @@ -1,258 +0,0 @@ -package processor - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" -) - -// AutoObserved is a simpler processor interface to implement than V1 as it is -// not required to emit observability information within the implementation -// itself. -type AutoObserved interface { - // Process a message into one or more resulting messages, or return an error - // if one occurred during processing, in which case the message will - // continue unchanged except for having that error now affiliated with it. - // - // If zero messages are returned and the error is nil then the message is - // filtered. - Process(ctx context.Context, p *message.Part) ([]*message.Part, error) - - // Close the component, blocks until either the underlying resources are - // cleaned up or the context is cancelled. Returns an error if the context - // is cancelled. - Close(ctx context.Context) error -} - -// AutoObservedBatched is a simpler processor interface to implement than V1 as -// it is not required to emit observability information within the -// implementation itself. -type AutoObservedBatched interface { - // Process a batch of messages into one or more resulting batches, or return - // an error if one occurred during processing, in which case all messages - // will continue unchanged except for having that error now affiliated with - // them. - // - // In order to associate individual messages with an error please use - // ctx.OnError instead of msg.ErrorSet. They are similar, but using - // ctx.OnError ensures observability data is updated as well as the message - // being affiliated with the error. - // - // If zero message batches are returned and the error is nil then all - // messages are filtered. - ProcessBatch(ctx *BatchProcContext, b message.Batch) ([]message.Batch, error) - - // Close the component, blocks until either the underlying resources are - // cleaned up or the context is cancelled. Returns an error if the context - // is cancelled. - Close(ctx context.Context) error -} - -//------------------------------------------------------------------------------ - -// Implements V1. -type v2ToV1Processor struct { - typeStr string - p AutoObserved - mgr component.Observability - - mReceived metrics.StatCounter - mBatchReceived metrics.StatCounter - mSent metrics.StatCounter - mBatchSent metrics.StatCounter - mError metrics.StatCounter - mLatency metrics.StatTimer -} - -// NewAutoObservedProcessor wraps an AutoObserved processor with an -// implementation of V1 which handles observability information. -func NewAutoObservedProcessor(typeStr string, p AutoObserved, mgr component.Observability) V1 { - return &v2ToV1Processor{ - typeStr: typeStr, p: p, mgr: mgr, - - mReceived: mgr.Metrics().GetCounter("processor_received"), - mBatchReceived: mgr.Metrics().GetCounter("processor_batch_received"), - mSent: mgr.Metrics().GetCounter("processor_sent"), - mBatchSent: mgr.Metrics().GetCounter("processor_batch_sent"), - mError: mgr.Metrics().GetCounter("processor_error"), - mLatency: mgr.Metrics().GetTimer("processor_latency_ns"), - } -} - -func (a *v2ToV1Processor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - a.mReceived.Incr(int64(msg.Len())) - a.mBatchReceived.Incr(1) - - tStarted := time.Now() - - newParts := make([]*message.Part, 0, msg.Len()) - _ = msg.Iter(func(i int, part *message.Part) error { - _, span := tracing.WithChildSpan(a.mgr.Tracer(), a.typeStr, part) - - nextParts, err := a.p.Process(ctx, part) - if err != nil { - a.mError.Incr(1) - a.mgr.Logger().Debug("Processor failed: %v", err) - MarkErr(part, span, err) - nextParts = append(nextParts, part) - } - - span.Finish() - if len(nextParts) > 0 { - newParts = append(newParts, nextParts...) - } - return nil - }) - - a.mLatency.Timing(time.Since(tStarted).Nanoseconds()) - if len(newParts) == 0 { - return nil, nil - } - - a.mSent.Incr(int64(len(newParts))) - a.mBatchSent.Incr(1) - return []message.Batch{newParts}, nil -} - -func (a *v2ToV1Processor) Close(ctx context.Context) error { - return a.p.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// TestBatchProcContext creates a context for batch processors. It's safe to -// provide nil spans and parts functions for testing purposes. -func TestBatchProcContext(ctx context.Context, spans []*tracing.Span, parts []*message.Part) *BatchProcContext { - return &BatchProcContext{ - ctx: ctx, - spans: spans, - parts: parts, - } -} - -// BatchProcContext provides methods for triggering observability updates and -// accessing processor specific spans. -type BatchProcContext struct { - ctx context.Context - spans []*tracing.Span - parts []*message.Part - - mError metrics.StatCounter - logger log.Modular -} - -// Context returns the underlying processor context.Context. -func (b *BatchProcContext) Context() context.Context { - return b.ctx -} - -// Span returns a span created specifically for the invocation of the processor. -// This can be used in order to add context to what the processor did. -func (b *BatchProcContext) Span(index int) *tracing.Span { - if len(b.spans) <= index { - return nil - } - return b.spans[index] -} - -// OnError should be called when an individual message has encountered an error, -// this should be used instead of .ErrorSet() as it includes observability -// updates. -// -// This method can be called with index -1 in order to set generalised -// observability information without marking specific message errors. -func (b *BatchProcContext) OnError(err error, index int, p *message.Part) { - if b.mError != nil { - b.mError.Incr(1) - } - if b.logger != nil { - b.logger.Debug("Processor failed: %v", err) - } - - var span *tracing.Span - if len(b.spans) > index && index >= 0 { - span = b.spans[index] - } - if p == nil && len(b.parts) > index && index >= 0 { - p = b.parts[index] - } - MarkErr(p, span, err) -} - -// Implements types.Processor. -type v2BatchedToV1Processor struct { - typeStr string - p AutoObservedBatched - mgr component.Observability - - mReceived metrics.StatCounter - mBatchReceived metrics.StatCounter - mSent metrics.StatCounter - mBatchSent metrics.StatCounter - mError metrics.StatCounter - mLatency metrics.StatTimer -} - -// NewAutoObservedBatchedProcessor wraps an AutoObservedBatched processor with an -// implementation of V1 which handles observability information. -func NewAutoObservedBatchedProcessor(typeStr string, p AutoObservedBatched, mgr component.Observability) V1 { - return &v2BatchedToV1Processor{ - typeStr: typeStr, p: p, mgr: mgr, - - mReceived: mgr.Metrics().GetCounter("processor_received"), - mBatchReceived: mgr.Metrics().GetCounter("processor_batch_received"), - mSent: mgr.Metrics().GetCounter("processor_sent"), - mBatchSent: mgr.Metrics().GetCounter("processor_batch_sent"), - mError: mgr.Metrics().GetCounter("processor_error"), - mLatency: mgr.Metrics().GetTimer("processor_latency_ns"), - } -} - -func (a *v2BatchedToV1Processor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - a.mReceived.Incr(int64(msg.Len())) - a.mBatchReceived.Incr(1) - - tStarted := time.Now() - _, spans := tracing.WithChildSpans(a.mgr.Tracer(), a.typeStr, msg) - - outputBatches, err := a.p.ProcessBatch(&BatchProcContext{ - ctx: ctx, - spans: spans, - parts: msg, - mError: a.mError, - logger: a.mgr.Logger(), - }, msg) - if err != nil { - a.mError.Incr(int64(msg.Len())) - a.mgr.Logger().Debug("Processor failed: %v", err) - _ = msg.Iter(func(i int, p *message.Part) error { - MarkErr(p, spans[i], err) - return nil - }) - outputBatches = append(outputBatches, msg) - } - - for _, s := range spans { - s.Finish() - } - - a.mLatency.Timing(time.Since(tStarted).Nanoseconds()) - if len(outputBatches) == 0 { - return nil, nil - } - - for _, m := range outputBatches { - a.mSent.Incr(int64(m.Len())) - } - a.mBatchSent.Incr(int64(len(outputBatches))) - return outputBatches, nil -} - -func (a *v2BatchedToV1Processor) Close(ctx context.Context) error { - return a.p.Close(ctx) -} diff --git a/internal/component/processor/auto_observed_test.go b/internal/component/processor/auto_observed_test.go deleted file mode 100644 index 51c7d569a0..0000000000 --- a/internal/component/processor/auto_observed_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package processor - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fnProcessor struct { - fn func(context.Context, *message.Part) ([]*message.Part, error) - closed bool - - sync.Mutex -} - -func (p *fnProcessor) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - return p.fn(ctx, msg) -} - -func (p *fnProcessor) Close(ctx context.Context) error { - p.Lock() - p.closed = true - p.Unlock() - return nil -} - -func TestProcessorAirGapShutdown(t *testing.T) { - rp := &fnProcessor{} - agrp := NewAutoObservedProcessor("foo", rp, component.NoopObservability()) - - ctx, done := context.WithTimeout(context.Background(), time.Microsecond*5) - defer done() - - err := agrp.Close(ctx) - assert.NoError(t, err) - rp.Lock() - assert.True(t, rp.closed) - rp.Unlock() -} - -func TestProcessorAirGapOneToOne(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedProcessor("foo", &fnProcessor{ - fn: func(c context.Context, m *message.Part) ([]*message.Part, error) { - if b := m.AsBytes(); string(b) != "unchanged" { - return nil, errors.New("nope") - } - newPart := m.ShallowCopy() - newPart.SetBytes([]byte("changed")) - return []*message.Part{newPart}, nil - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "changed", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) -} - -func TestProcessorAirGapOneToError(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedProcessor("foo", &fnProcessor{ - fn: func(c context.Context, m *message.Part) ([]*message.Part, error) { - _, err := m.AsStructuredMut() - return nil, err - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{[]byte("not a structured doc")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.EqualError(t, msgs[0].Get(0).ErrorGet(), "invalid character 'o' in literal null (expecting 'u')") -} - -func TestProcessorAirGapOneToMany(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedProcessor("foo", &fnProcessor{ - fn: func(c context.Context, m *message.Part) ([]*message.Part, error) { - if b := m.AsBytes(); string(b) != "unchanged" { - return nil, errors.New("nope") - } - first := m.ShallowCopy() - second := m.ShallowCopy() - third := m.ShallowCopy() - first.SetBytes([]byte("changed 1")) - second.SetBytes([]byte("changed 2")) - third.SetBytes([]byte("changed 3")) - return []*message.Part{first, second, third}, nil - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 3, msgs[0].Len()) - assert.Equal(t, "changed 1", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "changed 2", string(msgs[0].Get(1).AsBytes())) - assert.Equal(t, "changed 3", string(msgs[0].Get(2).AsBytes())) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) -} - -//------------------------------------------------------------------------------ - -type fnBatchProcessor struct { - fn func(*BatchProcContext, message.Batch) ([]message.Batch, error) - closed bool -} - -func (p *fnBatchProcessor) ProcessBatch(ctx *BatchProcContext, batch message.Batch) ([]message.Batch, error) { - return p.fn(ctx, batch) -} - -func (p *fnBatchProcessor) Close(ctx context.Context) error { - p.closed = true - return nil -} - -func TestBatchProcessorAirGapShutdown(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Millisecond*5) - defer done() - - rp := &fnBatchProcessor{} - agrp := NewAutoObservedBatchedProcessor("foo", rp, component.NoopObservability()) - - err := agrp.Close(tCtx) - assert.NoError(t, err) - assert.True(t, rp.closed) -} - -func TestBatchProcessorAirGapOneToOne(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedBatchedProcessor("foo", &fnBatchProcessor{ - fn: func(c *BatchProcContext, msgs message.Batch) ([]message.Batch, error) { - if b := msgs.Get(0).AsBytes(); string(b) != "unchanged" { - return nil, errors.New("nope") - } - newMsg := msgs.Get(0).ShallowCopy() - newMsg.SetBytes([]byte("changed")) - return []message.Batch{{newMsg}}, nil - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "changed", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) -} - -func TestBatchProcessorAirGapOneToError(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedBatchedProcessor("foo", &fnBatchProcessor{ - fn: func(c *BatchProcContext, msgs message.Batch) ([]message.Batch, error) { - _, err := msgs.Get(0).AsStructuredMut() - return nil, err - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{[]byte("not a structured doc")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.EqualError(t, msgs[0].Get(0).ErrorGet(), "invalid character 'o' in literal null (expecting 'u')") -} - -func TestBatchProcessorAirGapOneToMany(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedBatchedProcessor("foo", &fnBatchProcessor{ - fn: func(c *BatchProcContext, msgs message.Batch) ([]message.Batch, error) { - if b := msgs.Get(0).AsBytes(); string(b) != "unchanged" { - return nil, errors.New("nope") - } - first := msgs.Get(0).ShallowCopy() - second := msgs.Get(0).ShallowCopy() - third := msgs.Get(0).ShallowCopy() - first.SetBytes([]byte("changed 1")) - second.SetBytes([]byte("changed 2")) - third.SetBytes([]byte("changed 3")) - - firstBatch := message.Batch{first, second} - secondBatch := message.Batch{third} - return []message.Batch{firstBatch, secondBatch}, nil - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 2) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) - - assert.Equal(t, 2, msgs[0].Len()) - assert.Equal(t, "changed 1", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "changed 2", string(msgs[0].Get(1).AsBytes())) - - assert.Equal(t, 1, msgs[1].Len()) - assert.Equal(t, "changed 3", string(msgs[1].Get(0).AsBytes())) -} - -func TestBatchProcessorAirGapIndividualErrors(t *testing.T) { - tCtx := context.Background() - - agrp := NewAutoObservedBatchedProcessor("foo", &fnBatchProcessor{ - fn: func(c *BatchProcContext, msgs message.Batch) ([]message.Batch, error) { - for i, m := range msgs { - if _, err := m.AsStructuredMut(); err != nil { - c.OnError(err, i, nil) - } - } - return []message.Batch{msgs}, nil - }, - }, component.NoopObservability()) - - msg := message.QuickBatch([][]byte{ - []byte("not a structured doc"), - []byte(`{"foo":"bar"}`), - []byte("abcdefg"), - }) - - msgs, err := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, err) - require.Len(t, msgs, 1) - require.Len(t, msgs[0], 3) - - assert.Equal(t, "not a structured doc", string(msgs[0][0].AsBytes())) - assert.Equal(t, `{"foo":"bar"}`, string(msgs[0][1].AsBytes())) - assert.Equal(t, "abcdefg", string(msgs[0][2].AsBytes())) - - assert.EqualError(t, msgs[0][0].ErrorGet(), "invalid character 'o' in literal null (expecting 'u')") - assert.NoError(t, msgs[0][1].ErrorGet()) - assert.EqualError(t, msgs[0][2].ErrorGet(), "invalid character 'a' looking for beginning of value") -} diff --git a/internal/component/processor/config.go b/internal/component/processor/config.go deleted file mode 100644 index 2376f1b24b..0000000000 --- a/internal/component/processor/config.go +++ /dev/null @@ -1,81 +0,0 @@ -package processor - -import ( - "fmt" - - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all processor types. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -type Config struct { - Label string `json:"label" yaml:"label"` - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` -} - -// NewConfig returns a configuration struct fully populated with default values. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -func NewConfig() Config { - return Config{ - Label: "", - Type: "bounds_check", - Plugin: nil, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeProcessor, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - conf.Label, _ = value["label"].(string) - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeProcessor, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - for i := 0; i < len(value.Content)-1; i += 2 { - if value.Content[i].Value == "label" { - conf.Label = value.Content[i+1].Value - break - } - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/processor/error.go b/internal/component/processor/error.go deleted file mode 100644 index 6934ee4f2f..0000000000 --- a/internal/component/processor/error.go +++ /dev/null @@ -1,28 +0,0 @@ -package processor - -import ( - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" -) - -// MarkErr marks a message part as having failed. This includes modifying -// metadata to contain this error as well as adding the error to a tracing span -// if the message has one. -func MarkErr(part *message.Part, span *tracing.Span, err error) { - if err == nil { - return - } - if part != nil { - part.ErrorSet(err) - } - if span == nil && part != nil { - span = tracing.GetActiveSpan(part) - } - if span != nil { - span.SetTag("error", "true") - span.LogKV( - "event", "error", - "type", err.Error(), - ) - } -} diff --git a/internal/component/processor/execute.go b/internal/component/processor/execute.go deleted file mode 100644 index 9004afbc68..0000000000 --- a/internal/component/processor/execute.go +++ /dev/null @@ -1,118 +0,0 @@ -package processor - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// ExecuteAll attempts to execute a slice of processors to a message. Returns -// N resulting messages or a response. The response may indicate either a NoAck -// in the event of the message being buffered or an unrecoverable error. -func ExecuteAll(ctx context.Context, procs []V1, msgs ...message.Batch) ([]message.Batch, error) { - resultMsgs := make([]message.Batch, len(msgs)) - copy(resultMsgs, msgs) - - for i := 0; len(resultMsgs) > 0 && i < len(procs); i++ { - var nextResultMsgs []message.Batch - for _, m := range resultMsgs { - rMsgs, err := procs[i].ProcessBatch(ctx, m) - if err != nil { - // We immediately return if a processor hits an unrecoverable - // error on a message. - return nil, err - } - if ctx.Err() != nil { - return nil, ctx.Err() - } - nextResultMsgs = append(nextResultMsgs, rMsgs...) - } - resultMsgs = nextResultMsgs - } - - return resultMsgs, nil -} - -// ExecuteTryAll attempts to execute a slice of processors to messages, if a -// message has failed a processing step it is prevented from being sent to -// subsequent processors. Returns N resulting messages or a response. The -// response may indicate either a NoAck in the event of the message being -// buffered or an unrecoverable error. -func ExecuteTryAll(ctx context.Context, procs []V1, msgs ...message.Batch) ([]message.Batch, error) { - resultMsgs := make([]message.Batch, len(msgs)) - copy(resultMsgs, msgs) - - for i := 0; len(resultMsgs) > 0 && i < len(procs); i++ { - var nextResultMsgs []message.Batch - for _, m := range resultMsgs { - // Skip messages that failed a prior stage. - if m.Get(0).ErrorGet() != nil { - nextResultMsgs = append(nextResultMsgs, m) - continue - } - rMsgs, err := procs[i].ProcessBatch(ctx, m) - if err != nil { - // We immediately return if a processor hits an unrecoverable - // error on a message. - return nil, err - } - if ctx.Err() != nil { - return nil, ctx.Err() - } - nextResultMsgs = append(nextResultMsgs, rMsgs...) - } - resultMsgs = nextResultMsgs - } - - return resultMsgs, nil -} - -type catchMessage struct { - batches []message.Batch - caught bool -} - -// ExecuteCatchAll attempts to execute a slice of processors to only messages -// that have failed a processing step. Returns N resulting messages or a -// response. -func ExecuteCatchAll(ctx context.Context, procs []V1, msgs ...message.Batch) ([]message.Batch, error) { - // Preserves the original order of messages before entering the catch block. - // Only processors that have failed a previous stage are "caught", and will - // remain caught until all catch processors are executed. - catchBatches := make([]catchMessage, len(msgs)) - for i, m := range msgs { - catchBatches[i] = catchMessage{ - batches: []message.Batch{m}, - caught: m.Get(0).ErrorGet() != nil, - } - } - - for i := 0; i < len(procs); i++ { - for j := 0; j < len(catchBatches); j++ { - if !catchBatches[j].caught || len(catchBatches[j].batches) == 0 { - continue - } - - var nextResultBatches []message.Batch - for _, m := range catchBatches[j].batches { - rMsgs, resultRes := procs[i].ProcessBatch(ctx, m) - if resultRes != nil { - // We immediately return if a processor hits an unrecoverable - // error on a message. - return nil, resultRes - } - if ctx.Err() != nil { - return nil, ctx.Err() - } - nextResultBatches = append(nextResultBatches, rMsgs...) - } - catchBatches[j].batches = nextResultBatches - } - } - - var resultBatches []message.Batch - for _, b := range catchBatches { - resultBatches = append(resultBatches, b.batches...) - } - return resultBatches, nil -} diff --git a/internal/component/processor/execute_test.go b/internal/component/processor/execute_test.go deleted file mode 100644 index 69964f0bc6..0000000000 --- a/internal/component/processor/execute_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package processor - -import ( - "context" - "errors" - "testing" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -type passthrough struct { - called int -} - -func (p *passthrough) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - p.called++ - return []message.Batch{msg}, nil -} - -func (p *passthrough) Close(ctx context.Context) error { - return nil -} - -func TestExecuteAllBasic(t *testing.T) { - procs := []V1{ - &passthrough{}, - &passthrough{}, - } - - tCtx := context.Background() - - msg := message.QuickBatch([][]byte{[]byte("test message")}) - msgs, res := ExecuteAll(tCtx, procs, msg) - if res != nil { - t.Fatal(res) - } - if exp, act := 1, len(msgs); exp != act { - t.Fatalf("Wrong count of messages: %v != %v", act, exp) - } - if exp, act := 1, msgs[0].Len(); exp != act { - t.Fatalf("Wrong count of message parts: %v != %v", act, exp) - } - if exp, act := "test message", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - for _, proc := range procs { - if exp, act := 1, proc.(*passthrough).called; exp != act { - t.Errorf("Wrong call count from processor: %v != %v", act, exp) - } - } -} - -func TestExecuteAllBasicBatch(t *testing.T) { - tCtx := context.Background() - - procs := []V1{ - &passthrough{}, - &passthrough{}, - } - - msg := message.QuickBatch([][]byte{ - []byte("test message 1"), - []byte("test message 2"), - []byte("test message 3"), - }) - msgs, res := ExecuteAll(tCtx, procs, msg) - if res != nil { - t.Fatal(res) - } - if exp, act := 1, len(msgs); exp != act { - t.Fatalf("Wrong count of messages: %v != %v", act, exp) - } - if exp, act := 3, msgs[0].Len(); exp != act { - t.Fatalf("Wrong count of message parts: %v != %v", act, exp) - } - if exp, act := "test message 1", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - for _, proc := range procs { - if exp, act := 1, proc.(*passthrough).called; exp != act { - t.Errorf("Wrong call count from processor: %v != %v", act, exp) - } - } -} - -func TestExecuteAllMulti(t *testing.T) { - tCtx := context.Background() - - procs := []V1{ - &passthrough{}, - &passthrough{}, - } - - msg1 := message.QuickBatch([][]byte{[]byte("test message 1")}) - msg2 := message.QuickBatch([][]byte{[]byte("test message 2")}) - msgs, res := ExecuteAll(tCtx, procs, msg1, msg2) - if res != nil { - t.Fatal(res) - } - if exp, act := 2, len(msgs); exp != act { - t.Fatalf("Wrong count of messages: %v != %v", act, exp) - } - if exp, act := 1, msgs[0].Len(); exp != act { - t.Fatalf("Wrong count of message parts: %v != %v", act, exp) - } - if exp, act := 1, msgs[1].Len(); exp != act { - t.Fatalf("Wrong count of message parts: %v != %v", act, exp) - } - if exp, act := "test message 1", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := "test message 2", string(msgs[1].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - for _, proc := range procs { - if exp, act := 2, proc.(*passthrough).called; exp != act { - t.Errorf("Wrong call count from processor: %v != %v", act, exp) - } - } -} - -type errored struct { - called int -} - -func (p *errored) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - p.called++ - return nil, errors.New("test error") -} - -func (p *errored) Close(ctx context.Context) error { - return nil -} - -func TestExecuteAllErrored(t *testing.T) { - tCtx := context.Background() - - procs := []V1{ - &passthrough{}, - &errored{}, - &passthrough{}, - } - - msg1 := message.QuickBatch([][]byte{[]byte("test message 1")}) - msg2 := message.QuickBatch([][]byte{[]byte("test message 2")}) - msgs, res := ExecuteAll(tCtx, procs, msg1, msg2) - if len(msgs) > 0 { - t.Fatal("received messages after drop") - } - if res == nil { - t.Fatal("received non noack response") - } - if exp, act := 2, procs[0].(*passthrough).called; exp != act { - t.Errorf("Wrong call count from processor: %v != %v", act, exp) - } - if exp, act := 1, procs[1].(*errored).called; exp != act { - t.Errorf("Wrong call count from processor: %v != %v", act, exp) - } - if exp, act := 0, procs[2].(*passthrough).called; exp != act { - t.Errorf("Wrong call count from processor: %v != %v", act, exp) - } -} diff --git a/internal/component/processor/interface.go b/internal/component/processor/interface.go deleted file mode 100644 index 2a73a68167..0000000000 --- a/internal/component/processor/interface.go +++ /dev/null @@ -1,65 +0,0 @@ -package processor - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// V1 is a common interface implemented by processors. The implementation of a -// V1 processor is responsible for all expected observability and error handling -// behaviour described within Benthos documentation. -type V1 interface { - // Process a batch of messages into one or more resulting batches, or return - // an error if the entire batch could not be processed, currently the only - // valid reason for returning an error is if the context was cancelled. - // - // If zero messages are returned and the error is nil then all messages are - // filtered. - ProcessBatch(ctx context.Context, b message.Batch) ([]message.Batch, error) - - // Close the component, blocks until either the underlying resources are - // cleaned up or the context is cancelled. Returns an error if the context - // is cancelled. - Close(ctx context.Context) error -} - -// Pipeline is an interface that implements channel based consumer and -// producer methods for streaming data through a processing pipeline. -type Pipeline interface { - // TransactionChan returns a channel used for consuming transactions from - // this type. Every transaction received must be resolved before another - // transaction will be sent. - TransactionChan() <-chan message.Transaction - - // Consume starts the type receiving transactions from a Transactor. - Consume(<-chan message.Transaction) error - - // TriggerCloseNow signals that the component should close immediately, - // messages in flight will be dropped. - TriggerCloseNow() - - // WaitForClose blocks until the component has closed down or the context is - // cancelled. Closing occurs either when the input transaction channel is - // closed and messages are flushed (and acked), or when CloseNowAsync is - // called. - WaitForClose(ctx context.Context) error -} - -// PipelineConstructorFunc is a constructor to be called for each parallel -// stream pipeline thread in order to construct a custom pipeline -// implementation. -type PipelineConstructorFunc func() (Pipeline, error) - -// Unwrap attempts to access a wrapped processor from the provided -// implementation where applicable, otherwise the provided processor is -// returned. This is necessary when access raw implementations that could have -// been wrapped in a tracing mechanism (or other). -func Unwrap(p V1) V1 { - if w, ok := p.(interface { - UnwrapProc() V1 - }); ok { - return Unwrap(w.UnwrapProc()) - } - return p -} diff --git a/internal/component/ratelimit/config.go b/internal/component/ratelimit/config.go deleted file mode 100644 index d172c4941d..0000000000 --- a/internal/component/ratelimit/config.go +++ /dev/null @@ -1,81 +0,0 @@ -package ratelimit - -import ( - "fmt" - - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Config is the all encompassing configuration struct for all cache types. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -type Config struct { - Label string `json:"label" yaml:"label"` - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` -} - -// NewConfig returns a configuration struct fully populated with default values. -// Deprecated: Do not add new components here. Instead, use the public plugin -// APIs. Examples can be found in: ./internal/impl. -func NewConfig() Config { - return Config{ - Label: "", - Type: "local", - Plugin: nil, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeRateLimit, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - conf.Label, _ = value["label"].(string) - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeRateLimit, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - for i := 0; i < len(value.Content)-1; i += 2 { - if value.Content[i].Value == "label" { - conf.Label = value.Content[i+1].Value - break - } - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/ratelimit/interface.go b/internal/component/ratelimit/interface.go deleted file mode 100644 index 7c4ff2822f..0000000000 --- a/internal/component/ratelimit/interface.go +++ /dev/null @@ -1,20 +0,0 @@ -package ratelimit - -import ( - "context" - "time" -) - -// V1 is a common interface implemented by rate limits. -type V1 interface { - // Access the rate limited resource. Returns a duration or an error if the - // rate limit check fails. The returned duration is either zero (meaning the - // resource may be accessed) or a reasonable length of time to wait before - // requesting again. - Access(ctx context.Context) (time.Duration, error) - - // Close the component, blocks until either the underlying resources are - // cleaned up or the context is cancelled. Returns an error if the context - // is cancelled. - Close(ctx context.Context) error -} diff --git a/internal/component/ratelimit/rate_limit_metrics.go b/internal/component/ratelimit/rate_limit_metrics.go deleted file mode 100644 index d0e6c61c85..0000000000 --- a/internal/component/ratelimit/rate_limit_metrics.go +++ /dev/null @@ -1,43 +0,0 @@ -package ratelimit - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -type metricsRateLimit struct { - r V1 - - mChecked metrics.StatCounter - mLimited metrics.StatCounter - mErr metrics.StatCounter -} - -// MetricsForRateLimit wraps a ratelimit.V2 with a struct that implements -// types.RateLimit. -func MetricsForRateLimit(r V1, stats metrics.Type) V1 { - return &metricsRateLimit{ - r: r, - - mChecked: stats.GetCounter("rate_limit_checked"), - mLimited: stats.GetCounter("rate_limit_triggered"), - mErr: stats.GetCounter("rate_limit_error"), - } -} - -func (r *metricsRateLimit) Access(ctx context.Context) (time.Duration, error) { - r.mChecked.Incr(1) - tout, err := r.r.Access(ctx) - if err != nil { - r.mErr.Incr(1) - } else if tout > 0 { - r.mLimited.Incr(1) - } - return tout, err -} - -func (r *metricsRateLimit) Close(ctx context.Context) error { - return r.r.Close(ctx) -} diff --git a/internal/component/ratelimit/rate_limit_metrics_test.go b/internal/component/ratelimit/rate_limit_metrics_test.go deleted file mode 100644 index d9b5c01197..0000000000 --- a/internal/component/ratelimit/rate_limit_metrics_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package ratelimit - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -type closableRateLimit struct { - closed bool -} - -func (c *closableRateLimit) Access(ctx context.Context) (time.Duration, error) { - return 0, nil -} - -func (c *closableRateLimit) Close(ctx context.Context) error { - c.closed = true - return nil -} - -func TestRateLimitAirGapShutdown(t *testing.T) { - rl := &closableRateLimit{} - agrl := MetricsForRateLimit(rl, metrics.Noop()) - - err := agrl.Close(context.Background()) - assert.NoError(t, err) - assert.True(t, rl.closed) -} diff --git a/internal/component/scanner/config.go b/internal/component/scanner/config.go deleted file mode 100644 index e7a5629569..0000000000 --- a/internal/component/scanner/config.go +++ /dev/null @@ -1,57 +0,0 @@ -package scanner - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -type Config struct { - Type string - Plugin any -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeScanner, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeScanner, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/component/scanner/interface.go b/internal/component/scanner/interface.go deleted file mode 100644 index 6a18d2ad12..0000000000 --- a/internal/component/scanner/interface.go +++ /dev/null @@ -1,32 +0,0 @@ -package scanner - -import ( - "context" - "io" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// AckFn is a function provided to a scanner that it should call once the -// derived io.ReadCloser is fully consumed. -type AckFn func(context.Context, error) error - -// Scanner is an interface implemented by all scanner implementations once a -// creator has instantiated it on a byte stream. -type Scanner interface { - Next(context.Context) (message.Batch, AckFn, error) - Close(context.Context) error -} - -// SourceDetails contains exclusively optional information which could be used -// by codec implementations in order to determine the underlying data format. -type SourceDetails struct { - Name string -} - -// Creator is an interface implemented by all scanners, which allows components -// to construct a scanner from an unbounded io.ReadCloser. -type Creator interface { - Create(rdr io.ReadCloser, aFn AckFn, details SourceDetails) (Scanner, error) - Close(context.Context) error -} diff --git a/internal/component/scanner/testutil/testutil.go b/internal/component/scanner/testutil/testutil.go deleted file mode 100644 index baa6811c07..0000000000 --- a/internal/component/scanner/testutil/testutil.go +++ /dev/null @@ -1,263 +0,0 @@ -package testutil - -import ( - "bytes" - "context" - "errors" - "io" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -type microReader struct { - io.Reader -} - -func (n microReader) Read(p []byte) (int, error) { - // Only a max of 5 bytes at a time - if len(p) < 5 { - return n.Reader.Read(p) - } - - micro := make([]byte, 5) - byteCount, err := n.Reader.Read(micro) - if err != nil { - return byteCount, err - } - - _ = copy(p, micro) - return byteCount, nil -} - -func ScannerTestSuite(t *testing.T, codec *service.OwnedScannerCreator, details *service.ScannerSourceDetails, data []byte, expected ...string) { - if details == nil { - details = &service.ScannerSourceDetails{} - } - - t.Run("close before reading", func(t *testing.T) { - buf := io.NopCloser(bytes.NewReader(data)) - - ack := errors.New("default err") - - r, err := codec.Create(buf, func(ctx context.Context, err error) error { - ack = err - return nil - }, details) - require.NoError(t, err) - - assert.NoError(t, r.Close(context.Background())) - assert.EqualError(t, ack, "service shutting down") - }) - - t.Run("can consume micro flushes", func(t *testing.T) { - buf := io.NopCloser(microReader{bytes.NewReader(data)}) - - ack := errors.New("default err") - - r, err := codec.Create(buf, func(ctx context.Context, err error) error { - ack = err - return nil - }, details) - require.NoError(t, err) - - allReads := map[string][]byte{} - - for _, exp := range expected { - p, ackFn, err := r.NextBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, 1) - - mBytes, err := p[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(mBytes)) - allReads[string(mBytes)] = mBytes - } - - _, _, err = r.NextBatch(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("acks ordered reads", func(t *testing.T) { - buf := io.NopCloser(bytes.NewReader(data)) - - ack := errors.New("default err") - - r, err := codec.Create(buf, func(ctx context.Context, err error) error { - ack = err - return nil - }, details) - require.NoError(t, err) - - allReads := map[string][]byte{} - - for _, exp := range expected { - p, ackFn, err := r.NextBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, ackFn(context.Background(), nil)) - require.Len(t, p, 1) - - mBytes, err := p[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(mBytes)) - allReads[string(mBytes)] = mBytes - } - - _, _, err = r.NextBatch(context.Background()) - assert.EqualError(t, err, "EOF") - - assert.NoError(t, r.Close(context.Background())) - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("acks unordered reads", func(t *testing.T) { - buf := io.NopCloser(bytes.NewReader(data)) - - ack := errors.New("default err") - - r, err := codec.Create(buf, func(ctx context.Context, err error) error { - ack = err - return nil - }, details) - require.NoError(t, err) - - allReads := map[string][]byte{} - - var ackFns []service.AckFunc - for _, exp := range expected { - p, ackFn, err := r.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, p, 1) - ackFns = append(ackFns, ackFn) - - mBytes, err := p[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(mBytes)) - allReads[string(mBytes)] = mBytes - } - - _, _, err = r.NextBatch(context.Background()) - assert.EqualError(t, err, "EOF") - assert.NoError(t, r.Close(context.Background())) - - for _, ackFn := range ackFns { - require.NoError(t, ackFn(context.Background(), nil)) - } - - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - t.Run("acks parallel reads", func(t *testing.T) { - buf := io.NopCloser(bytes.NewReader(data)) - - ack := errors.New("default err") - - r, err := codec.Create(buf, func(ctx context.Context, err error) error { - ack = err - return nil - }, details) - require.NoError(t, err) - - allReads := map[string][]byte{} - - wg := sync.WaitGroup{} - wg.Add(len(expected)) - - for _, exp := range expected { - exp := exp - p, ackFn, err := r.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, p, 1) - - mBytes, err := p[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(mBytes)) - allReads[string(mBytes)] = mBytes - - go func() { - defer wg.Done() - require.NoError(t, ackFn(context.Background(), nil)) - }() - } - - _, _, err = r.NextBatch(context.Background()) - assert.EqualError(t, err, "EOF") - - wg.Wait() - assert.NoError(t, r.Close(context.Background())) - - assert.NoError(t, ack) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - - if len(expected) > 0 { - t.Run("nacks unordered reads", func(t *testing.T) { - buf := io.NopCloser(bytes.NewReader(data)) - - ack := errors.New("default err") - exp := errors.New("real err") - - r, err := codec.Create(buf, func(ctx context.Context, err error) error { - ack = err - return nil - }, details) - require.NoError(t, err) - - allReads := map[string][]byte{} - - var ackFns []service.AckFunc - for _, exp := range expected { - p, ackFn, err := r.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, p, 1) - ackFns = append(ackFns, ackFn) - - mBytes, err := p[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(mBytes)) - allReads[string(mBytes)] = mBytes - } - - _, _, err = r.NextBatch(context.Background()) - assert.EqualError(t, err, "EOF") - assert.NoError(t, r.Close(context.Background())) - - for i, ackFn := range ackFns { - if i == 0 { - require.NoError(t, ackFn(context.Background(), exp)) - } else { - require.NoError(t, ackFn(context.Background(), nil)) - } - } - - assert.EqualError(t, ack, exp.Error()) - - for k, v := range allReads { - assert.Equal(t, k, string(v), "Must not corrupt previous reads") - } - }) - } -} diff --git a/internal/component/testutil/from_yaml.go b/internal/component/testutil/from_yaml.go deleted file mode 100644 index 91a1257bc0..0000000000 --- a/internal/component/testutil/from_yaml.go +++ /dev/null @@ -1,123 +0,0 @@ -package testutil - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/tracer" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -func BufferFromYAML(confStr string, args ...any) (buffer.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return buffer.Config{}, err - } - return buffer.FromAny(bundle.GlobalEnvironment, node) -} - -func CacheFromYAML(confStr string, args ...any) (cache.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return cache.Config{}, err - } - return cache.FromAny(bundle.GlobalEnvironment, node) -} - -func InputFromYAML(confStr string, args ...any) (input.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return input.Config{}, err - } - return input.FromAny(bundle.GlobalEnvironment, node) -} - -func MetricsFromYAML(confStr string, args ...any) (metrics.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return metrics.Config{}, err - } - return metrics.FromAny(bundle.GlobalEnvironment, node) -} - -func OutputFromYAML(confStr string, args ...any) (output.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return output.Config{}, err - } - return output.FromAny(bundle.GlobalEnvironment, node) -} - -func ProcessorFromYAML(confStr string, args ...any) (processor.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return processor.Config{}, err - } - return processor.FromAny(bundle.GlobalEnvironment, node) -} - -func RateLimitFromYAML(confStr string, args ...any) (ratelimit.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return ratelimit.Config{}, err - } - return ratelimit.FromAny(bundle.GlobalEnvironment, node) -} - -func TracerFromYAML(confStr string, args ...any) (tracer.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return tracer.Config{}, err - } - return tracer.FromAny(bundle.GlobalEnvironment, node) -} - -func ManagerFromYAML(confStr string, args ...any) (manager.ResourceConfig, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return manager.ResourceConfig{}, err - } - return manager.FromAny(bundle.GlobalEnvironment, node) -} - -func StreamFromYAML(confStr string, args ...any) (stream.Config, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return stream.Config{}, err - } - - var rawSource any - _ = node.Decode(&rawSource) - - pConf, err := stream.Spec().ParsedConfigFromAny(node) - if err != nil { - return stream.Config{}, err - } - return stream.FromParsed(bundle.GlobalEnvironment, pConf, rawSource) -} - -func ConfigFromYAML(confStr string, args ...any) (config.Type, error) { - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, confStr, args...)) - if err != nil { - return config.Type{}, err - } - - var rawSource any - _ = node.Decode(&rawSource) - - pConf, err := config.Spec().ParsedConfigFromAny(node) - if err != nil { - return config.Type{}, err - } - return config.FromParsed(bundle.GlobalEnvironment, pConf, rawSource) -} diff --git a/internal/component/tracer/config.go b/internal/component/tracer/config.go deleted file mode 100644 index d76e7871d0..0000000000 --- a/internal/component/tracer/config.go +++ /dev/null @@ -1,73 +0,0 @@ -package tracer - -import ( - "fmt" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func init() { - // TODO: I'm so confused, these APIs are a nightmare. - otel.SetTextMapPropagator(propagation.TraceContext{}) -} - -// Config is the all encompassing configuration struct for all tracer types. -type Config struct { - Type string `json:"type" yaml:"type"` - Plugin any `json:"plugin,omitempty" yaml:"plugin,omitempty"` -} - -// NewConfig returns a configuration struct fully populated with default values. -func NewConfig() Config { - return Config{ - Type: "none", - Plugin: nil, - } -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, value map[string]any) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromMap(prov, docs.TypeTracer, value); err != nil { - err = docs.NewLintError(0, docs.LintComponentNotFound, err) - return - } - - if p, exists := value[conf.Type]; exists { - conf.Plugin = p - } else if p, exists := value["plugin"]; exists { - conf.Plugin = p - } - return -} - -func fromYAML(prov docs.Provider, value *yaml.Node) (conf Config, err error) { - if conf.Type, _, err = docs.GetInferenceCandidateFromYAML(prov, docs.TypeTracer, value); err != nil { - err = docs.NewLintError(value.Line, docs.LintComponentNotFound, err) - return - } - - pluginNode, err := docs.GetPluginConfigYAML(conf.Type, value) - if err != nil { - err = docs.NewLintError(value.Line, docs.LintFailedRead, err) - return - } - - conf.Plugin = &pluginNode - return -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index c819a1f641..0000000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package config_test - -import ( - "bytes" - "context" - "os" - "path/filepath" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/stream" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func testConfToAny(t testing.TB, conf any) any { - var node yaml.Node - err := node.Encode(conf) - require.NoError(t, err) - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - err = config.Spec().SanitiseYAML(&node, sanitConf) - require.NoError(t, err) - - var v any - require.NoError(t, node.Decode(&v)) - return v -} - -func TestSetOverridesOnNothing(t *testing.T) { - rdr := config.NewReader("", nil, config.OptAddOverrides( - "input.type=generate", - "input.generate.mapping=this.foo", - "output.type=drop", - )) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - assert.Empty(t, lints) - - v := gabs.Wrap(testConfToAny(t, conf)) - - assert.Equal(t, "this.foo", v.S("input", "generate", "mapping").Data()) - assert.Equal(t, map[string]any{}, v.S("output", "drop").Data()) -} - -func TestSetOverrideErrors(t *testing.T) { - tests := []struct { - name string - input string - err string - }{ - { - name: "no value", - input: "input.type=", - err: "invalid set expression 'input.type='", - }, - { - name: "no equals", - input: "input.type", - err: "invalid set expression 'input.type'", - }, - { - name: "completely empty", - input: "", - err: "invalid set expression ''", - }, - { - name: "cant set that", - input: "input=meow", - err: "invalid type !!str, expected object", - }, - } - - for _, test := range tests { - rdr := config.NewReader("", nil, config.OptAddOverrides(test.input)) - - _, _, _, err := rdr.Read() - assert.Contains(t, err.Error(), test.err) - } -} - -func TestSetOverridesOfFile(t *testing.T) { - dir := t.TempDir() - - fullPath := filepath.Join(dir, "main.yaml") - require.NoError(t, os.WriteFile(fullPath, []byte(` -input: - generate: - count: 10 - mapping: 'root = "meow"' -`), 0o644)) - - rdr := config.NewReader(fullPath, nil, config.OptAddOverrides( - "input.generate.count=5", - "input.generate.interval=10s", - "output.type=drop", - )) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - assert.Empty(t, lints) - - v := gabs.Wrap(testConfToAny(t, conf)) - - assert.Equal(t, `root = "meow"`, v.S("input", "generate", "mapping").Data()) - assert.Equal(t, `10s`, v.S("input", "generate", "interval").Data()) - assert.Equal(t, 5, v.S("input", "generate", "count").Data()) - - oMap := v.S("output").ChildrenMap() - assert.Len(t, oMap, 2) - assert.Contains(t, oMap, "drop") - assert.Contains(t, oMap, "label") -} - -func TestResources(t *testing.T) { - dir := t.TempDir() - - fullPath := filepath.Join(dir, "main.yaml") - require.NoError(t, os.WriteFile(fullPath, []byte(` -input: - generate: - count: 5 - mapping: 'root = "meow"' -output: - drop: {} -`), 0o644)) - - resourceOnePath := filepath.Join(dir, "res1.yaml") - require.NoError(t, os.WriteFile(resourceOnePath, []byte(` -cache_resources: - - label: foo - memory: - default_ttl: 12s - -tests: - - name: huh -`), 0o644)) - - resourceTwoPath := filepath.Join(dir, "res2.yaml") - require.NoError(t, os.WriteFile(resourceTwoPath, []byte(` -cache_resources: - - label: bar - memory: - default_ttl: 13s -`), 0o644)) - - resourceThreePath := filepath.Join(dir, "res3.yaml") - require.NoError(t, os.WriteFile(resourceThreePath, []byte(` -tests: - - name: whut -`), 0o644)) - - rdr := config.NewReader(fullPath, []string{resourceOnePath, resourceTwoPath, resourceThreePath}) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - assert.Empty(t, lints) - - v := gabs.Wrap(testConfToAny(t, conf)) - - assert.Equal(t, `root = "meow"`, v.S("input", "generate", "mapping").Data()) - - require.Len(t, v.S("cache_resources").Data(), 2) - - assert.Equal(t, "foo", v.S("cache_resources", "0", "label").Data()) - assert.Equal(t, "12s", v.S("cache_resources", "0", "memory", "default_ttl").Data()) - - assert.Equal(t, "bar", v.S("cache_resources", "1", "label").Data()) - assert.Equal(t, "13s", v.S("cache_resources", "1", "memory", "default_ttl").Data()) -} - -func TestLints(t *testing.T) { - dir := t.TempDir() - - fullPath := filepath.Join(dir, "main.yaml") - require.NoError(t, os.WriteFile(fullPath, []byte(` -input: - meow1: not this - generate: - count: 5 - mapping: 'root = "meow"' - -output: - drop: {} -`), 0o644)) - - resourceOnePath := filepath.Join(dir, "res1.yaml") - require.NoError(t, os.WriteFile(resourceOnePath, []byte(` -cache_resources: - - label: foo - memory: - meow2: or this - default_ttl: 12s -`), 0o644)) - - resourceTwoPath := filepath.Join(dir, "res2.yaml") - require.NoError(t, os.WriteFile(resourceTwoPath, []byte(` -cache_resources: - - label: bar - memory: - meow3: or also this - default_ttl: 13s -`), 0o644)) - - rdr := config.NewReader(fullPath, []string{resourceOnePath, resourceTwoPath}) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Len(t, lints, 3) - assert.Contains(t, lints[0], "/main.yaml(3,1) field meow1 ") - assert.Contains(t, lints[1], "/res1.yaml(5,1) field meow2 ") - assert.Contains(t, lints[2], "/res2.yaml(5,1) field meow3 ") - - v := gabs.Wrap(testConfToAny(t, conf)) - - assert.Equal(t, `root = "meow"`, v.S("input", "generate", "mapping").Data()) - - require.Len(t, v.S("cache_resources").Data(), 2) - - assert.Equal(t, "foo", v.S("cache_resources", "0", "label").Data()) - assert.Equal(t, "12s", v.S("cache_resources", "0", "memory", "default_ttl").Data()) - - assert.Equal(t, "bar", v.S("cache_resources", "1", "label").Data()) - assert.Equal(t, "13s", v.S("cache_resources", "1", "memory", "default_ttl").Data()) -} - -func TestDefaultBasedOverridesWithYAML(t *testing.T) { - tmpDir := t.TempDir() - outFile := filepath.Join(tmpDir, "foo.txt") - - var v yaml.Node - require.NoError(t, v.Encode(map[string]any{ - "file": map[string]any{ - "path": outFile, - }, - })) - - spec := config.Spec() - spec.SetDefault(&v, "output") - - pConf, err := spec.ParsedConfigFromAny(map[string]any{ - "input": map[string]any{ - "generate": map[string]any{ - "mapping": `root.foo = "bar"`, - "count": 1, - "interval": "1us", - }, - }, - }) - require.NoError(t, err) - - c, err := config.FromParsed(bundle.GlobalEnvironment, pConf, nil) - require.NoError(t, err) - - s, err := stream.New(c.Config, mock.NewManager()) - require.NoError(t, err) - - assert.Eventually(t, func() bool { - fBytes, _ := os.ReadFile(outFile) - return bytes.Contains(fBytes, []byte(`{"foo":"bar"}`)) - }, time.Second, time.Millisecond*10) - - require.NoError(t, s.Stop(context.Background())) -} - -func TestDefaultBasedOverridesWithAny(t *testing.T) { - tmpDir := t.TempDir() - outFile := filepath.Join(tmpDir, "foo.txt") - - spec := config.Spec() - spec.SetDefault(map[string]any{ - "file": map[string]any{ - "path": outFile, - }, - }, "output") - - node, err := docs.UnmarshalYAML([]byte(` -input: - generate: - mapping: 'root.foo = "bar"' - count: 1 - interval: 1us -`)) - require.NoError(t, err) - - pConf, err := spec.ParsedConfigFromAny(node) - require.NoError(t, err) - - c, err := config.FromParsed(bundle.GlobalEnvironment, pConf, nil) - require.NoError(t, err) - - s, err := stream.New(c.Config, mock.NewManager()) - require.NoError(t, err) - - assert.Eventually(t, func() bool { - fBytes, _ := os.ReadFile(outFile) - return bytes.Contains(fBytes, []byte(`{"foo":"bar"}`)) - }, time.Second, time.Millisecond*10) - - require.NoError(t, s.Stop(context.Background())) -} - -func TestDefaultBasedOverridesWithExplicit(t *testing.T) { - tmpDir := t.TempDir() - outFile := filepath.Join(tmpDir, "foo.txt") - - outConf := output.Config{ - Type: "file", - Plugin: map[string]any{ - "path": outFile, - }, - } - - spec := config.Spec() - spec.SetDefault(outConf, "output") - - node, err := docs.UnmarshalYAML([]byte(` -input: - generate: - mapping: 'root.foo = "bar"' - count: 1 - interval: 1us -`)) - require.NoError(t, err) - - pConf, err := spec.ParsedConfigFromAny(node) - require.NoError(t, err) - - c, err := config.FromParsed(bundle.GlobalEnvironment, pConf, nil) - require.NoError(t, err) - - s, err := stream.New(c.Config, mock.NewManager()) - require.NoError(t, err) - - assert.Eventually(t, func() bool { - fBytes, _ := os.ReadFile(outFile) - return bytes.Contains(fBytes, []byte(`{"foo":"bar"}`)) - }, time.Second, time.Millisecond*10) - - require.NoError(t, s.Stop(context.Background())) -} diff --git a/internal/config/env_vars.go b/internal/config/env_vars.go deleted file mode 100644 index e67543d783..0000000000 --- a/internal/config/env_vars.go +++ /dev/null @@ -1,75 +0,0 @@ -package config - -import ( - "bytes" - "fmt" - "regexp" - "strings" -) - -var ( - envRegex = regexp.MustCompile(`\${[0-9A-Za-z_.]+(:((\${[^}]+})|[^}])*)?}`) - escapedEnvRegex = regexp.MustCompile(`\${({[0-9A-Za-z_.]+(:((\${[^}]+})|[^}])*)?})}`) -) - -// ErrMissingEnvVars is returned when attempting environment variable -// interpolations where the referenced environment variables are missing. -type ErrMissingEnvVars struct { - Variables []string - - // Our best attempt at parsing the config that's missing variables by simply - // inserting an empty string. There's a good chance this is still a valid - // config! :) - BestAttempt []byte -} - -// Error returns a rather sweet error message. -func (e *ErrMissingEnvVars) Error() string { - // TODO: Deduplicate the variables as they might be repeated. - return fmt.Sprintf("required environment variables were not set: %v", e.Variables) -} - -// ReplaceEnvVariables will search a blob of data for the pattern `${FOO:bar}`, -// where `FOO` is an environment variable name and `bar` is a default value. The -// `bar` section (including the colon) can be left out if there is no -// appropriate default value for the field. -// -// For each aforementioned pattern found in the blob the contents of the -// respective environment variable will be read and will replace the pattern. If -// the environment variable is empty or does not exist then either the default -// value is used or the field will be left empty. -func ReplaceEnvVariables(inBytes []byte, lookupFn func(string) (string, bool)) (replaced []byte, err error) { - var missingVarsErr ErrMissingEnvVars - - replaced = envRegex.ReplaceAllFunc(inBytes, func(content []byte) []byte { - var value string - var ok bool - if len(content) > 3 { - if colonIndex := bytes.IndexByte(content, ':'); colonIndex == -1 { - varName := string(content[2 : len(content)-1]) - if value, ok = lookupFn(varName); !ok { - missingVarsErr.Variables = append(missingVarsErr.Variables, varName) - } - } else { - targetVar := content[2:colonIndex] - defaultVal := content[colonIndex+1 : len(content)-1] - value, _ = lookupFn(string(targetVar)) - if value == "" { - value = string(defaultVal) - } - } - // Escape newlines, otherwise there's no way that they would work - // within a config. - value = strings.ReplaceAll(value, "\n", "\\n") - } - return []byte(value) - }) - replaced = escapedEnvRegex.ReplaceAll(replaced, []byte("$$$1")) - - if len(missingVarsErr.Variables) > 0 { - missingVarsErr.BestAttempt = replaced - err = &missingVarsErr - replaced = nil - } - return -} diff --git a/internal/config/env_vars_test.go b/internal/config/env_vars_test.go deleted file mode 100644 index a8e90e3b4f..0000000000 --- a/internal/config/env_vars_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEnvSwapping(t *testing.T) { - envFn := func(s string) (string, bool) { - switch s { - case "BENTHOS_TEST_FOO": - return "", true - case "BENTHOS.TEST.FOO": - return "testfoo", true - case "BENTHOS.TEST.BAR": - return "test\nbar", true - } - return "", false - } - - tests := map[string]struct { - result string - errContains string - }{ - "foo ${DOES_NOT_EXIST:} baz": {result: "foo baz"}, - "${DOES_NOT_EXIST:}": {result: ""}, - "${BENTHOS_TEST_FOO:}": {result: ""}, - "${BENTHOS.TEST.FOO:}": {result: "testfoo"}, - "foo ${BENTHOS_TEST_FOO:bar} baz": {result: "foo bar baz"}, - "foo ${BENTHOS.TEST.FOO:bar} baz": {result: "foo testfoo baz"}, - "foo ${BENTHOS.TEST.FOO} baz": {result: "foo testfoo baz"}, - "foo ${BENTHOS_TEST_FOO:http://bar.com} baz": {result: "foo http://bar.com baz"}, - "foo ${BENTHOS_TEST_FOO:http://bar.com?wat=nuh} baz": {result: "foo http://bar.com?wat=nuh baz"}, - "foo ${BENTHOS_TEST_FOO:http://bar.com#wat} baz": {result: "foo http://bar.com#wat baz"}, - "foo ${BENTHOS_TEST_FOO:tcp://*:2020} baz": {result: "foo tcp://*:2020 baz"}, - "foo ${BENTHOS_TEST_FOO:bar} http://bar.com baz": {result: "foo bar http://bar.com baz"}, - "foo ${BENTHOS_TEST_FOO} http://bar.com baz": {result: "foo http://bar.com baz"}, - "foo ${BENTHOS_TEST_FOO:wat@nuh.com} baz": {result: "foo wat@nuh.com baz"}, - "foo ${} baz": {result: "foo ${} baz"}, - "foo ${BENTHOS_TEST_FOO:foo,bar} baz": {result: "foo foo,bar baz"}, - "foo ${BENTHOS_TEST_FOO} baz": {result: "foo baz"}, - "foo ${BENTHOS_TEST_FOO:${!metadata:foo}} baz": {result: "foo ${!metadata:foo} baz"}, - "foo ${BENTHOS_TEST_FOO:${!metadata:foo}${!metadata:bar}} baz": {result: "foo ${!metadata:foo}${!metadata:bar} baz"}, - "foo ${BENTHOS_TEST_FOO:${!count:foo}-${!timestamp_unix_nano}.tar.gz} baz": {result: "foo ${!count:foo}-${!timestamp_unix_nano}.tar.gz baz"}, - "foo ${{BENTHOS_TEST_FOO:bar}} baz": {result: "foo ${BENTHOS_TEST_FOO:bar} baz"}, - "foo ${{BENTHOS_TEST_FOO}} baz": {result: "foo ${BENTHOS_TEST_FOO} baz"}, - "foo ${BENTHOS.TEST.BAR} baz": {result: "foo test\\nbar baz"}, - "foo ${BENTHOS_TEST_THIS_DOESNT_EXIST_LOL} baz": {errContains: "required environment variables were not set: [BENTHOS_TEST_THIS_DOESNT_EXIST_LOL]"}, - "foo ${BENTHOS_TEST_NOPE_A} baz ${BENTHOS_TEST_NOPE_B} buz": {errContains: "required environment variables were not set: [BENTHOS_TEST_NOPE_A BENTHOS_TEST_NOPE_B]"}, - "foo ${DOES_NOT_EXIST::} baz": {result: "foo : baz"}, - } - - for in, exp := range tests { - out, err := ReplaceEnvVariables([]byte(in), envFn) - if exp.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), exp.errContains) - } else { - require.NoError(t, err) - assert.Equal(t, exp.result, string(out)) - } - } -} diff --git a/internal/config/lint.go b/internal/config/lint.go deleted file mode 100644 index 0268398373..0000000000 --- a/internal/config/lint.go +++ /dev/null @@ -1,111 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "io" - "io/fs" - "os" - "time" - "unicode/utf8" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -// ReadYAMLFileLinted will attempt to read a configuration file path into a -// structure. Returns an array of lint messages or an error. -func ReadYAMLFileLinted(fs ifs.FS, spec docs.FieldSpecs, path string, skipEnvVarCheck bool, lConf docs.LintConfig) (Type, []docs.Lint, error) { - configBytes, lints, _, err := ReadFileEnvSwap(fs, path, os.LookupEnv) - if err != nil { - return Type{}, nil, err - } - - if skipEnvVarCheck { - var newLints []docs.Lint - for _, l := range lints { - if l.Type != docs.LintMissingEnvVar { - newLints = append(newLints, l) - } - } - lints = newLints - } - - cNode, err := docs.UnmarshalYAML(configBytes) - if err != nil { - return Type{}, nil, err - } - - var rawSource any - _ = cNode.Decode(&rawSource) - - var pConf *docs.ParsedConfig - if pConf, err = spec.ParsedConfigFromAny(cNode); err != nil { - return Type{}, nil, err - } - - conf, err := FromParsed(lConf.DocsProvider, pConf, rawSource) - if err != nil { - return Type{}, nil, err - } - - if !bytes.HasPrefix(configBytes, []byte("# BENTHOS LINT DISABLE")) { - lints = append(lints, spec.LintYAML(docs.NewLintContext(lConf), cNode)...) - } - return conf, lints, nil -} - -// LintYAMLBytes attempts to report errors within a user config. Returns a slice of -// lint results. -func LintYAMLBytes(lintConf docs.LintConfig, rawBytes []byte) ([]docs.Lint, error) { - if bytes.HasPrefix(rawBytes, []byte("# BENTHOS LINT DISABLE")) { - return nil, nil - } - - rawNode, err := docs.UnmarshalYAML(rawBytes) - if err != nil { - return nil, err - } - - return Spec().LintYAML(docs.NewLintContext(lintConf), rawNode), nil -} - -// ReadFileEnvSwap reads a file and replaces any environment variable -// interpolations before returning the contents. Linting errors are returned if -// the file has an unexpected higher level format, such as invalid utf-8 -// encoding. -// -// An modTime timestamp is returned if the modtime of the file is available. -func ReadFileEnvSwap(store ifs.FS, path string, lookupEnvFn func(name string) (string, bool)) (configBytes []byte, lints []docs.Lint, modTime time.Time, err error) { - var configFile fs.File - if configFile, err = store.Open(path); err != nil { - return - } - - if info, ierr := configFile.Stat(); ierr == nil { - modTime = info.ModTime() - } - - if configBytes, err = io.ReadAll(configFile); err != nil { - return - } - - if !utf8.Valid(configBytes) { - lints = append(lints, docs.NewLintError( - 1, docs.LintFailedRead, - errors.New("detected invalid utf-8 encoding in config, this may result in interpolation functions not working as expected"), - )) - } - - if configBytes, err = ReplaceEnvVariables(configBytes, lookupEnvFn); err != nil { - var errEnvMissing *ErrMissingEnvVars - if errors.As(err, &errEnvMissing) { - configBytes = errEnvMissing.BestAttempt - lints = append(lints, docs.NewLintError(1, docs.LintMissingEnvVar, err)) - err = nil - } else { - return - } - } - return -} diff --git a/internal/config/reader.go b/internal/config/reader.go deleted file mode 100644 index a3711a15d5..0000000000 --- a/internal/config/reader.go +++ /dev/null @@ -1,409 +0,0 @@ -package config - -import ( - "bytes" - "context" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - "time" - - "github.com/Jeffail/gabs/v2" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -const ( - defaultChangeFlushPeriod = 50 * time.Millisecond - defaultChangeDelayPeriod = time.Second - defaultFilesRefreshPeriod = time.Second -) - -type streamFileInfo struct { - id string -} - -type fileWatcher interface { - Close() error -} - -// Reader provides utilities for parsing a Benthos config as a main file with -// a collection of resource files, and options such as overrides. -type Reader struct { - // The suffix given to unit test definition files, this is used in order to - // exclude unit tests from being run in streams mode with arbitrary - // directory walking. - testSuffix string - - // The filesystem used for reading config files. - fs ifs.FS - - // Specs for various config types. - specFullConfig docs.FieldSpecs - specStreamOnly docs.FieldSpecs - specObservability docs.FieldSpecs - specResources docs.FieldSpecs - - // Used for linting configs - lintConf docs.LintConfig - - mainPath string - resourcePaths []string - streamsPaths []string - overrides []string - - modTimeLastRead map[string]time.Time - - // Controls whether the main config should include input, output, etc. - streamsMode bool - - // Tracks the details of the config file when we last read it. - configFileInfo resourceFileInfo - - // Tracks the details of stream config files when we last read them. - streamFileInfo map[string]streamFileInfo - - // Tracks the details of resource config files when we last read them, - // including information such as the specific resources that were created - // from it. - resourceFileInfo map[string]resourceFileInfo - resourceSources *resourceSourceInfo - - mainUpdateFn MainUpdateFunc - streamUpdateFn StreamUpdateFunc - watcher fileWatcher - - changeFlushPeriod time.Duration - changeDelayPeriod time.Duration - filesRefreshPeriod time.Duration -} - -// NewReader creates a new config reader. -func NewReader(mainPath string, resourcePaths []string, opts ...OptFunc) *Reader { - if mainPath != "" { - mainPath = filepath.Clean(mainPath) - } - r := &Reader{ - testSuffix: "_benthos_test", - fs: ifs.OS(), - lintConf: docs.NewLintConfig(bundle.GlobalEnvironment), - mainPath: mainPath, - resourcePaths: resourcePaths, - modTimeLastRead: map[string]time.Time{}, - streamFileInfo: map[string]streamFileInfo{}, - resourceFileInfo: map[string]resourceFileInfo{}, - resourceSources: newResourceSourceInfo(), - changeFlushPeriod: defaultChangeFlushPeriod, - changeDelayPeriod: defaultChangeDelayPeriod, - filesRefreshPeriod: defaultFilesRefreshPeriod, - - specFullConfig: Spec(), - specStreamOnly: stream.Spec(), - specObservability: SpecWithoutStream(Spec()), - specResources: manager.Spec(), - } - for _, opt := range opts { - opt(r) - } - return r -} - -//------------------------------------------------------------------------------ - -// OptFunc is an opt function that changes the behaviour of a config reader. -type OptFunc func(*Reader) - -// OptSetFullSpec overrides the default general config spec with the provided -// one. -func OptSetFullSpec(spec func() docs.FieldSpecs) OptFunc { - return func(r *Reader) { - r.specFullConfig = spec() - r.specObservability = SpecWithoutStream(spec()) - } -} - -// OptTestSuffix configures the suffix given to unit test definition files, this -// is used in order to exclude unit tests from being run in streams mode with -// arbitrary directory walking. -func OptTestSuffix(suffix string) OptFunc { - return func(r *Reader) { - r.testSuffix = suffix - } -} - -// OptAddOverrides adds one or more override expressions to the config reader, -// each of the form `path=value`. -func OptAddOverrides(overrides ...string) OptFunc { - return func(r *Reader) { - r.overrides = append(r.overrides, overrides...) - } -} - -// OptSetLintConfig sets the config used for linting files. -func OptSetLintConfig(lConf docs.LintConfig) OptFunc { - return func(r *Reader) { - r.lintConf = lConf - } -} - -// OptSetStreamPaths marks this config reader as operating in streams mode, and -// adds a list of paths to obtain individual stream configs from. -func OptSetStreamPaths(streamsPaths ...string) OptFunc { - return func(r *Reader) { - r.streamsPaths = streamsPaths - r.streamsMode = true - } -} - -// OptUseFS sets the ifs.FS implementation for the reader to use. By default the -// OS filesystem is used, and when overridden it is no longer possible to use -// BeginFileWatching. -func OptUseFS(fs ifs.FS) OptFunc { - return func(r *Reader) { - r.fs = fs - } -} - -//------------------------------------------------------------------------------ - -func (r *Reader) lintCtx() docs.LintContext { - return docs.NewLintContext(r.lintConf) -} - -// Read a Benthos config from the files and options specified. -func (r *Reader) Read() (conf Type, pConf *docs.ParsedConfig, lints []string, err error) { - if conf, pConf, lints, err = r.readMain(r.mainPath); err != nil { - return - } - r.configFileInfo = resInfoFromConfig(&conf.ResourceConfig) - r.resourceSources.populateFrom(r.mainPath, &r.configFileInfo) - - var rLints []string - if rLints, err = r.readResources(&conf.ResourceConfig); err != nil { - return - } - lints = append(lints, rLints...) - return -} - -// ReadStreams attempts to read Benthos stream configs from one or more paths. -// Stream configs are extracted and added to a provided map, where the id is -// derived from the path of the stream config file. -func (r *Reader) ReadStreams(confs map[string]stream.Config) (lints []string, err error) { - return r.readStreamFiles(confs) -} - -// MainUpdateFunc is a closure function called whenever a main config has been -// updated. If an error is returned then the attempt will be made again after a -// grace period. -type MainUpdateFunc func(conf *Type) error - -// SubscribeConfigChanges registers a closure function to be called whenever the -// main configuration file is updated. -// -// The provided closure should return true if the stream was successfully -// replaced. -func (r *Reader) SubscribeConfigChanges(fn MainUpdateFunc) error { - if r.watcher != nil { - return errors.New("a file watcher has already been started") - } - - r.mainUpdateFn = fn - return nil -} - -// StreamUpdateFunc is a closure function called whenever a stream config has -// been updated. If an error is returned then the attempt will be made again -// after a grace period. -// -// When the provided config is nil it is a signal that the stream has been -// deleted, and it is expected that the provided update func should shut that -// stream down. -type StreamUpdateFunc func(id string, conf *stream.Config) error - -// SubscribeStreamChanges registers a closure to be called whenever the -// configuration of a stream is updated. -// -// The provided closure should return true if the stream was successfully -// replaced. -func (r *Reader) SubscribeStreamChanges(fn StreamUpdateFunc) error { - if r.watcher != nil { - return errors.New("a file watcher has already been started") - } - - r.streamUpdateFn = fn - return nil -} - -// Close the reader, when this method exits all reloading will be stopped. -func (r *Reader) Close(ctx context.Context) error { - if r.watcher != nil { - return r.watcher.Close() - } - return nil -} - -//------------------------------------------------------------------------------ - -func applyOverrides(specs docs.FieldSpecs, root *yaml.Node, overrides ...string) error { - for _, override := range overrides { - eqIndex := strings.Index(override, "=") - if eqIndex == -1 { - return fmt.Errorf("invalid set expression '%v': expected foo=bar syntax", override) - } - - path := override[:eqIndex] - value := override[eqIndex+1:] - if path == "" || value == "" { - return fmt.Errorf("invalid set expression '%v': expected foo=bar syntax", override) - } - - valNode := yaml.Node{ - Kind: yaml.ScalarNode, - Value: value, - } - if err := specs.SetYAMLPath(bundle.GlobalEnvironment, root, &valNode, gabs.DotPathToSlice(path)...); err != nil { - return fmt.Errorf("failed to set config field override: %w", err) - } - } - return nil -} - -func (r *Reader) readMain(mainPath string) (conf Type, pConf *docs.ParsedConfig, lints []string, err error) { - defer func() { - if err != nil && mainPath != "" { - err = fmt.Errorf("%v: %w", mainPath, err) - } - }() - - var rawNode *yaml.Node - var confBytes []byte - if mainPath != "" { - var dLints []docs.Lint - var modTime time.Time - if confBytes, dLints, modTime, err = ReadFileEnvSwap(r.fs, mainPath, os.LookupEnv); err != nil { - return - } - for _, l := range dLints { - lints = append(lints, l.Error()) - } - r.modTimeLastRead[mainPath] = modTime - - if rawNode, err = docs.UnmarshalYAML(confBytes); err != nil { - return - } - } else { - var tmpNode yaml.Node - if err = tmpNode.Encode(map[string]any{}); err != nil { - return - } - rawNode = &tmpNode - } - - confSpec := r.specFullConfig - if r.streamsMode { - // Spec is limited to just non-stream fields when in streams mode (no - // input, output, etc) - confSpec = r.specObservability - } - if err = applyOverrides(confSpec, rawNode, r.overrides...); err != nil { - return - } - - if !bytes.HasPrefix(confBytes, []byte("# BENTHOS LINT DISABLE")) { - lintFilePrefix := mainPath - for _, lint := range confSpec.LintYAML(r.lintCtx(), rawNode) { - lints = append(lints, fmt.Sprintf("%v%v", lintFilePrefix, lint.Error())) - } - } - - var rawSource any - _ = rawNode.Decode(&rawSource) - - if pConf, err = confSpec.ParsedConfigFromAny(rawNode); err != nil { - return - } - - if r.streamsMode { - conf.rawSource = rawSource - err = noStreamFromParsed(r.lintConf.DocsProvider, pConf, &conf) - } else { - conf, err = FromParsed(r.lintConf.DocsProvider, pConf, rawSource) - } - return -} - -// TriggerMainUpdate attempts to re-read the main configuration file, trigger -// the provided main update func, and apply changes to resources to the provided -// manager as appropriate. -func (r *Reader) TriggerMainUpdate(mgr bundle.NewManagement, strict bool, newPath string) error { - conf, _, lints, err := r.readMain(newPath) - if errors.Is(err, fs.ErrNotExist) { - if r.mainPath != newPath { - mgr.Logger().Error("Failed to read changed main config: %v", err) - return noReread(err) - } - // Ignore main file deletes for now - return nil - } - if err != nil { - if r.mainPath != newPath { - mgr.Logger().Error("Failed to read new main config %v: %v", newPath, err) - } else { - mgr.Logger().Error("Failed to read updated config: %v", err) - } - - // Rejecting due to invalid file means we do not want to try again. - return noReread(err) - } - if r.mainPath != newPath { - mgr.Logger().Info("Main config changed to %v, attempting to update pipeline.", newPath) - } else { - mgr.Logger().Info("Main config updated, attempting to update pipeline.") - } - - lintlog := mgr.Logger() - for _, lint := range lints { - lintlog.Info(lint) - } - if strict && len(lints) > 0 { - mgr.Logger().Error("Rejecting updated main config due to linter errors, to allow linting errors run Benthos with --chilled") - - // Rejecting from linters means we do not want to try again. - return noReread(errors.New("file contained linting errors and is running in strict mode")) - } - - // If the main config file has been changed then we remove all resources - // under the old name first. - if r.mainPath != newPath { - if err := r.applyResourceChanges(r.mainPath, mgr, resInfoEmpty(), r.configFileInfo); err != nil { - return err - } - r.mainPath = newPath - r.configFileInfo = resInfoEmpty() - } - - // Update any resources within the file. - newInfo := resInfoFromConfig(&conf.ResourceConfig) - if err := r.applyResourceChanges(r.mainPath, mgr, newInfo, r.configFileInfo); err != nil { - return err - } - r.configFileInfo = newInfo - - if r.mainUpdateFn != nil { - if err := r.mainUpdateFn(&conf); err != nil { - mgr.Logger().Error("Failed to apply updated config: %v", err) - return err - } - mgr.Logger().Info("Updated main config") - } - return nil -} diff --git a/internal/config/reader_test.go b/internal/config/reader_test.go deleted file mode 100644 index fe3250235a..0000000000 --- a/internal/config/reader_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package config - -import ( - "errors" - "io/fs" - "testing" - "testing/fstest" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -func newDummyReader(confFilePath string, resourcePaths []string, opts ...OptFunc) *Reader { - rdr := NewReader(confFilePath, resourcePaths, opts...) - rdr.changeDelayPeriod = 1 * time.Millisecond - rdr.changeFlushPeriod = 1 * time.Millisecond - rdr.filesRefreshPeriod = 1 * time.Millisecond - return rdr -} - -type testFS struct { - m fstest.MapFS -} - -func (fs testFS) Open(name string) (fs.File, error) { - return fs.m.Open(name) -} - -func (fs testFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - return fs.m.Open(name) -} - -func (fs testFS) Stat(name string) (fs.FileInfo, error) { - return fs.m.Stat(name) -} - -func (fs testFS) MkdirAll(name string, perm fs.FileMode) error { - return errors.New("not implemented") -} - -func (fs testFS) Remove(name string) error { - return errors.New("not implemented") -} - -func TestCustomFileSync(t *testing.T) { - testFS := &testFS{m: fstest.MapFS{ - "foo_main.yaml": &fstest.MapFile{ - Data: []byte(` -input: - label: fooin - inproc: foo - -output: - label: fooout - inproc: bar -`), - }, - "a.yaml": &fstest.MapFile{ - Data: []byte(` -processor_resources: - - label: a - mapping: 'root = content() + " a1"' - - label: b - mapping: 'root = content() + " b1"' -`), - }, - "b.yaml": &fstest.MapFile{ - Data: []byte(` -processor_resources: - - label: c - mapping: 'root = content() + " c1"' - - label: d - mapping: 'root = content() + " d1"' -`), - }, - }} - rdr := newDummyReader("foo_main.yaml", []string{"a.yaml", "b.yaml"}, OptUseFS(testFS)) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - assert.Equal(t, "fooin", conf.Input.Label) - assert.Equal(t, "fooout", conf.Output.Label) - - assert.Len(t, conf.ResourceProcessors, 4) - assert.Equal(t, "a", conf.ResourceProcessors[0].Label) - assert.Equal(t, "b", conf.ResourceProcessors[1].Label) - assert.Equal(t, "c", conf.ResourceProcessors[2].Label) - assert.Equal(t, "d", conf.ResourceProcessors[3].Label) -} - -func TestCustomFileChangeMain(t *testing.T) { - testFS := &testFS{m: fstest.MapFS{ - "foo_main.yaml": &fstest.MapFile{ - Data: []byte(` -input: - label: fooin - inproc: foo - -output: - label: fooout - inproc: bar - -processor_resources: - - label: a - mapping: 'root = content() + " a1"' - - label: b - mapping: 'root = content() + " b1"' -`), - }, - "bar_main.yaml": &fstest.MapFile{ - Data: []byte(` -input: - label: foointwo - inproc: foo - -output: - label: fooouttwo - inproc: bar - -processor_resources: - - label: c - mapping: 'root = content() + " c1"' - - label: d - mapping: 'root = content() + " d1"' -`), - }, - }} - rdr := newDummyReader("foo_main.yaml", nil, OptUseFS(testFS)) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - assert.Equal(t, "fooin", conf.Input.Label) - assert.Equal(t, "fooout", conf.Output.Label) - - assert.Len(t, conf.ResourceProcessors, 2) - assert.Equal(t, "a", conf.ResourceProcessors[0].Label) - assert.Equal(t, "b", conf.ResourceProcessors[1].Label) - - // Watch for configuration changes - testMgr, err := manager.New(conf.ResourceConfig) - require.NoError(t, err) - - changeChan := make(chan struct{}) - var updatedConf stream.Config - require.NoError(t, rdr.SubscribeConfigChanges(func(conf *Type) error { - updatedConf = conf.Config - close(changeChan) - return nil - })) - - assert.True(t, testMgr.ProbeProcessor("a")) - assert.True(t, testMgr.ProbeProcessor("b")) - assert.False(t, testMgr.ProbeProcessor("c")) - assert.False(t, testMgr.ProbeProcessor("d")) - - require.NoError(t, rdr.TriggerMainUpdate(testMgr, true, "bar_main.yaml")) - - // Wait for the config watcher to reload the config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - require.FailNow(t, "Expected a config change to be triggered") - } - - assert.Equal(t, "foointwo", updatedConf.Input.Label) - assert.Equal(t, "fooouttwo", updatedConf.Output.Label) - - assert.False(t, testMgr.ProbeProcessor("a")) - assert.False(t, testMgr.ProbeProcessor("b")) - assert.True(t, testMgr.ProbeProcessor("c")) - assert.True(t, testMgr.ProbeProcessor("d")) -} - -func TestCustomFileStartEmpty(t *testing.T) { - testFS := &testFS{m: fstest.MapFS{ - "foo_main.yaml": &fstest.MapFile{ - Data: []byte(` -input: - label: fooin - inproc: foo - -output: - label: fooout - inproc: bar -`), - }, - "a.yaml": &fstest.MapFile{ - Data: []byte(` -processor_resources: - - label: a - mapping: 'root = content() + " a1"' - - label: b - mapping: 'root = content() + " b1"' -`), - }, - "b.yaml": &fstest.MapFile{ - Data: []byte(` -processor_resources: - - label: c - mapping: 'root = content() + " c1"' - - label: d - mapping: 'root = content() + " d1"' -`), - }, - }} - - rdr := newDummyReader("", nil, OptUseFS(testFS)) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - - changeChan := make(chan struct{}) - var conf stream.Config - require.NoError(t, rdr.SubscribeConfigChanges(func(c *Type) error { - conf = c.Config - close(changeChan) - return nil - })) - - require.NoError(t, rdr.TriggerResourceUpdate(testMgr, true, "a.yaml")) - require.NoError(t, rdr.TriggerResourceUpdate(testMgr, true, "b.yaml")) - - require.NoError(t, rdr.TriggerMainUpdate(testMgr, true, "foo_main.yaml")) - - assert.Equal(t, "fooin", conf.Input.Label) - assert.Equal(t, "fooout", conf.Output.Label) - - assert.True(t, testMgr.ProbeProcessor("a")) - assert.True(t, testMgr.ProbeProcessor("b")) - assert.True(t, testMgr.ProbeProcessor("c")) - assert.True(t, testMgr.ProbeProcessor("d")) -} diff --git a/internal/config/resource_reader.go b/internal/config/resource_reader.go deleted file mode 100644 index 39e3926a49..0000000000 --- a/internal/config/resource_reader.go +++ /dev/null @@ -1,388 +0,0 @@ -package config - -import ( - "bytes" - "context" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "time" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - ifilepath "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/manager" -) - -// Keeps track of which resource file provided a given resource type, this is -// important when removing resources that have been deleted from a file, as it's -// possible it was moved to a new file and that update was reflected before this -// one. -type resourceSourceInfo struct { - inputs map[string]string - processors map[string]string - outputs map[string]string - caches map[string]string - rateLimits map[string]string -} - -func newResourceSourceInfo() *resourceSourceInfo { - return &resourceSourceInfo{ - inputs: map[string]string{}, - processors: map[string]string{}, - outputs: map[string]string{}, - caches: map[string]string{}, - rateLimits: map[string]string{}, - } -} - -func (r *resourceSourceInfo) populateFrom(path string, info *resourceFileInfo) { - for k := range info.caches { - r.caches[k] = path - } - for k := range info.inputs { - r.inputs[k] = path - } - for k := range info.outputs { - r.outputs[k] = path - } - for k := range info.processors { - r.processors[k] = path - } - for k := range info.rateLimits { - r.rateLimits[k] = path - } -} - -func (r *resourceSourceInfo) removeOwnedCache(ctx context.Context, label, path string, mgr bundle.NewManagement) { - if r.caches[label] == path { - if err := mgr.RemoveCache(ctx, label); err != nil { - mgr.Logger().Error("Failed to remove deleted resource %v: %v", label, err) - } else { - delete(r.caches, label) - } - } -} - -func (r *resourceSourceInfo) removeOwnedInput(ctx context.Context, label, path string, mgr bundle.NewManagement) { - if r.inputs[label] == path { - if err := mgr.RemoveInput(ctx, label); err != nil { - mgr.Logger().Error("Failed to remove deleted resource %v: %v", label, err) - } else { - delete(r.inputs, label) - } - } -} - -func (r *resourceSourceInfo) removeOwnedOutput(ctx context.Context, label, path string, mgr bundle.NewManagement) { - if r.outputs[label] == path { - if err := mgr.RemoveOutput(ctx, label); err != nil { - mgr.Logger().Error("Failed to remove deleted resource %v: %v", label, err) - } else { - delete(r.outputs, label) - } - } -} - -func (r *resourceSourceInfo) removeOwnedProcessor(ctx context.Context, label, path string, mgr bundle.NewManagement) { - if r.processors[label] == path { - if err := mgr.RemoveProcessor(ctx, label); err != nil { - mgr.Logger().Error("Failed to remove deleted resource %v: %v", label, err) - } else { - delete(r.processors, label) - } - } -} - -func (r *resourceSourceInfo) removeOwnedRateLimit(ctx context.Context, label, path string, mgr bundle.NewManagement) { - if r.rateLimits[label] == path { - if err := mgr.RemoveRateLimit(ctx, label); err != nil { - mgr.Logger().Error("Failed to remove deleted resource %v: %v", label, err) - } else { - delete(r.rateLimits, label) - } - } -} - -// Keeps track of which resources came from a file in its last read, if configs -// are changed, added or missing we need to reflect that. -type resourceFileInfo struct { - inputs map[string]*input.Config - processors map[string]*processor.Config - outputs map[string]*output.Config - caches map[string]*cache.Config - rateLimits map[string]*ratelimit.Config -} - -func resInfoEmpty() resourceFileInfo { - return resourceFileInfo{ - inputs: map[string]*input.Config{}, - processors: map[string]*processor.Config{}, - outputs: map[string]*output.Config{}, - caches: map[string]*cache.Config{}, - rateLimits: map[string]*ratelimit.Config{}, - } -} - -func resInfoFromConfig(conf *manager.ResourceConfig) resourceFileInfo { - resInfo := resInfoEmpty() - - // New style - for _, c := range conf.ResourceInputs { - c := c - resInfo.inputs[c.Label] = &c - } - for _, c := range conf.ResourceProcessors { - c := c - resInfo.processors[c.Label] = &c - } - for _, c := range conf.ResourceOutputs { - c := c - resInfo.outputs[c.Label] = &c - } - for _, c := range conf.ResourceCaches { - c := c - resInfo.caches[c.Label] = &c - } - for _, c := range conf.ResourceRateLimits { - c := c - resInfo.rateLimits[c.Label] = &c - } - - return resInfo -} - -func (r *Reader) resourcePathsExpanded() ([]string, error) { - resourcePaths, err := ifilepath.Globs(r.fs, r.resourcePaths) - if err != nil { - return nil, fmt.Errorf("failed to resolve resource glob pattern: %w", err) - } - for i, v := range resourcePaths { - resourcePaths[i] = filepath.Clean(v) - } - return resourcePaths, nil -} - -func (r *Reader) readResources(conf *manager.ResourceConfig) (lints []string, err error) { - resourcesPaths, err := r.resourcePathsExpanded() - if err != nil { - return nil, err - } - for _, path := range resourcesPaths { - var rconf manager.ResourceConfig - var rLints []string - if rconf, rLints, err = r.readResource(path); err != nil { - return - } - lints = append(lints, rLints...) - - resInfo := resInfoFromConfig(&rconf) - r.resourceFileInfo[path] = resInfo - r.resourceSources.populateFrom(path, &resInfo) - - if err = conf.AddFrom(&rconf); err != nil { - err = fmt.Errorf("%v: %w", path, err) - return - } - } - return -} - -func (r *Reader) readResource(path string) (conf manager.ResourceConfig, lints []string, err error) { - defer func() { - if err != nil { - err = fmt.Errorf("%v: %w", path, err) - } - }() - - var confBytes []byte - var dLints []docs.Lint - var modTime time.Time - if confBytes, dLints, modTime, err = ReadFileEnvSwap(r.fs, path, os.LookupEnv); err != nil { - return - } - for _, l := range dLints { - lints = append(lints, l.Error()) - } - r.modTimeLastRead[path] = modTime - - var rawNode *yaml.Node - if rawNode, err = docs.UnmarshalYAML(confBytes); err != nil { - return - } - - spec := append(docs.FieldSpecs{ - test.ConfigSpec(), - }, r.specResources...) - if !bytes.HasPrefix(confBytes, []byte("# BENTHOS LINT DISABLE")) { - for _, lint := range spec.LintYAML(r.lintCtx(), rawNode) { - lints = append(lints, fmt.Sprintf("%v%v", path, lint.Error())) - } - } - - var pConf *docs.ParsedConfig - if pConf, err = spec.ParsedConfigFromAny(rawNode); err != nil { - return - } - - conf, err = manager.FromParsed(r.lintConf.DocsProvider, pConf) - return -} - -// TriggerResourceUpdate attempts to re-read a resource configuration file and -// apply changes to the provided manager as appropriate. -func (r *Reader) TriggerResourceUpdate(mgr bundle.NewManagement, strict bool, path string) error { - newResConf, lints, err := r.readResource(path) - if errors.Is(err, fs.ErrNotExist) { - return r.TriggerResourceDelete(mgr, path) - } - if err != nil { - mgr.Logger().Error("Failed to read updated resources config: %v", err) - return noReread(err) - } - - prevInfo, exists := r.resourceFileInfo[path] - if exists { - mgr.Logger().Info("Resource %v config updated, attempting to update resources.", path) - } else { - prevInfo = resInfoEmpty() - mgr.Logger().Info("Resource %v config created, attempting to add resources.", path) - } - - lintlog := mgr.Logger() - for _, lint := range lints { - lintlog.Info(lint) - } - if strict && len(lints) > 0 { - mgr.Logger().Error("Rejecting updated resource config due to linter errors, to allow linting errors run Benthos with --chilled") - return noReread(errors.New("file contained linting errors and is running in strict mode")) - } - - newInfo := resInfoFromConfig(&newResConf) - if err := r.applyResourceChanges(path, mgr, newInfo, prevInfo); err != nil { - return err - } - - r.resourceFileInfo[path] = newInfo - return nil -} - -// TriggerResourceDelete attempts to remove all resources that originated from a -// given file. -func (r *Reader) TriggerResourceDelete(mgr bundle.NewManagement, path string) error { - prevInfo, exists := r.resourceFileInfo[path] - if !exists { - return nil - } - mgr.Logger().Info("Resource file %v deleted, attempting to remove resources.", path) - - newInfo := resInfoEmpty() - if err := r.applyResourceChanges(path, mgr, newInfo, prevInfo); err != nil { - return err - } - delete(r.resourceFileInfo, path) - return nil -} - -func (r *Reader) applyResourceChanges(path string, mgr bundle.NewManagement, currentInfo, prevInfo resourceFileInfo) error { - // Kind of arbitrary, but I feel better about having some sort of timeout. - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) - defer cancel() - - // WARNING: The order here is actually kind of important, we want to start - // with components that could be dependencies of other components. This is - // a "best attempt", so not all edge cases need to be accounted for. - - unaccounted := map[string]struct{}{} - for k := range prevInfo.rateLimits { - unaccounted[k] = struct{}{} - } - for k, v := range currentInfo.rateLimits { - delete(unaccounted, k) - if err := mgr.StoreRateLimit(ctx, k, *v); err != nil { - mgr.Logger().Error("Failed to update resource %v: %v", k, err) - return fmt.Errorf("resource %v: %w", k, err) - } - mgr.Logger().Info("Updated resource %v config from file.", k) - } - for k := range unaccounted { - r.resourceSources.removeOwnedRateLimit(ctx, k, path, mgr) - } - - unaccounted = map[string]struct{}{} - for k := range prevInfo.caches { - unaccounted[k] = struct{}{} - } - for k, v := range currentInfo.caches { - delete(unaccounted, k) - if err := mgr.StoreCache(ctx, k, *v); err != nil { - mgr.Logger().Error("Failed to update resource %v: %v", k, err) - return fmt.Errorf("resource %v: %w", k, err) - } - mgr.Logger().Info("Updated resource %v config from file.", k) - } - for k := range unaccounted { - r.resourceSources.removeOwnedCache(ctx, k, path, mgr) - } - - unaccounted = map[string]struct{}{} - for k := range prevInfo.processors { - unaccounted[k] = struct{}{} - } - for k, v := range currentInfo.processors { - delete(unaccounted, k) - if err := mgr.StoreProcessor(ctx, k, *v); err != nil { - mgr.Logger().Error("Failed to update resource %v: %v", k, err) - return fmt.Errorf("resource %v: %w", k, err) - } - mgr.Logger().Info("Updated resource %v config from file.", k) - } - for k := range unaccounted { - r.resourceSources.removeOwnedProcessor(ctx, k, path, mgr) - } - - unaccounted = map[string]struct{}{} - for k := range prevInfo.inputs { - unaccounted[k] = struct{}{} - } - for k, v := range currentInfo.inputs { - delete(unaccounted, k) - if err := mgr.StoreInput(ctx, k, *v); err != nil { - mgr.Logger().Error("Failed to update resource %v: %v", k, err) - return fmt.Errorf("resource %v: %w", k, err) - } - mgr.Logger().Info("Updated resource %v config from file.", k) - } - for k := range unaccounted { - r.resourceSources.removeOwnedInput(ctx, k, path, mgr) - } - - unaccounted = map[string]struct{}{} - for k := range prevInfo.outputs { - unaccounted[k] = struct{}{} - } - for k, v := range currentInfo.outputs { - delete(unaccounted, k) - if err := mgr.StoreOutput(ctx, k, *v); err != nil { - mgr.Logger().Error("Failed to update resource %v: %v", k, err) - return fmt.Errorf("resource %v: %w", k, err) - } - mgr.Logger().Info("Updated resource %v config from file.", k) - } - for k := range unaccounted { - r.resourceSources.removeOwnedOutput(ctx, k, path, mgr) - } - - r.resourceSources.populateFrom(path, ¤tInfo) - return nil -} diff --git a/internal/config/resource_reader_test.go b/internal/config/resource_reader_test.go deleted file mode 100644 index 496c872618..0000000000 --- a/internal/config/resource_reader_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package config - -import ( - "context" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestReaderResourceFileReading(t *testing.T) { - confDir := t.TempDir() - - mainFilePath := filepath.Join(confDir, "main.yaml") - require.NoError(t, os.WriteFile(mainFilePath, []byte(` -input: - inproc: meow - -output: - drop: {} -`), 0o644)) - - require.NoError(t, os.WriteFile(filepath.Join(confDir, "a_res.yaml"), []byte(` -processor_resources: - - label: fooproc - mapping: | - root = content().uppercase() - - label: barproc - mapping: | - root = content() + " and bar" -`), 0o644)) - - require.NoError(t, os.WriteFile(filepath.Join(confDir, "b_res.yaml"), []byte(` -processor_resources: - - label: bazproc - mapping: | - root = content() + " and baz" -`), 0o644)) - - rdr := NewReader(mainFilePath, []string{confDir + "/*_res.yaml"}) - rdr.changeDelayPeriod = 1 * time.Millisecond - rdr.changeFlushPeriod = 1 * time.Millisecond - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - require.NoError(t, rdr.SubscribeConfigChanges(func(conf *Type) error { - return nil - })) - - // Watch for configuration changes. - testMgr, err := manager.New(conf.ResourceConfig) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - assertProc := func(name, input, output string) { - require.NoError(t, testMgr.AccessProcessor(tCtx, name, func(p processor.V1) { - res, err := p.ProcessBatch(tCtx, message.Batch{ - message.NewPart([]byte(input)), - }) - require.NoError(t, err) - require.Len(t, res, 1) - require.Len(t, res[0], 1) - assert.Equal(t, output, string(res[0][0].AsBytes())) - })) - } - - assertProc("fooproc", "hello world", "HELLO WORLD") - assertProc("barproc", "hello world", "hello world and bar") - assertProc("bazproc", "hello world", "hello world and baz") - - // Update foo, remove bar. - require.NoError(t, os.WriteFile(filepath.Join(confDir, "a_res.yaml"), []byte(` -processor_resources: - - label: fooproc - mapping: | - root = content().uppercase() + "!!!" -`), 0o644)) - - checkProc := func(name, input string) (output string) { - _ = testMgr.AccessProcessor(tCtx, name, func(p processor.V1) { - res, err := p.ProcessBatch(tCtx, message.Batch{ - message.NewPart([]byte(input)), - }) - if err != nil || len(res) != 1 || len(res[0]) != 1 { - return - } - output = string(res[0][0].AsBytes()) - }) - return - } - - require.Eventually(t, func() bool { - return checkProc("fooproc", "hello world") == "HELLO WORLD!!!" && - testMgr.AccessProcessor(tCtx, "barproc", func(v processor.V1) {}) != nil - }, time.Second, time.Millisecond*10) - - assertProc("fooproc", "hello world", "HELLO WORLD!!!") - require.EqualError(t, testMgr.AccessProcessor(tCtx, "barproc", func(v processor.V1) {}), "unable to locate resource: barproc") - assertProc("bazproc", "hello world", "hello world and baz") - - // Update baz, add new bar. - require.NoError(t, os.WriteFile(filepath.Join(confDir, "b_res.yaml"), []byte(` -processor_resources: - - label: bazproc - mapping: | - root = content() + " and a new baz" - - label: barproc - mapping: | - root = content() + " and a replaced bar" -`), 0o644)) - - require.Eventually(t, func() bool { - return checkProc("barproc", "hello world") == "hello world and a replaced bar" && - checkProc("bazproc", "hello world") == "hello world and a new baz" - }, time.Second, time.Millisecond*10) - - assertProc("fooproc", "hello world", "HELLO WORLD!!!") - assertProc("barproc", "hello world", "hello world and a replaced bar") - assertProc("bazproc", "hello world", "hello world and a new baz") -} - -func TestReaderResourceMovedToNewFile(t *testing.T) { - confDir := t.TempDir() - - mainFilePath := filepath.Join(confDir, "main.yaml") - require.NoError(t, os.WriteFile(mainFilePath, []byte(` -input: - inproc: meow - -output: - drop: {} -`), 0o644)) - - require.NoError(t, os.WriteFile(filepath.Join(confDir, "a_res.yaml"), []byte(` -processor_resources: - - label: fooproc - mapping: | - root = content().uppercase() - - label: barproc - mapping: | - root = content() + " and bar" -`), 0o644)) - - require.NoError(t, os.WriteFile(filepath.Join(confDir, "b_res.yaml"), []byte(` -processor_resources: - - label: bazproc - mapping: | - root = content() + " and baz" -`), 0o644)) - - rdr := NewReader(mainFilePath, []string{confDir + "/*_res.yaml"}) - rdr.changeDelayPeriod = 1 * time.Millisecond - rdr.changeFlushPeriod = 1 * time.Millisecond - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - require.NoError(t, rdr.SubscribeConfigChanges(func(conf *Type) error { - return nil - })) - - // Watch for configuration changes. - testMgr, err := manager.New(conf.ResourceConfig) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - assertProc := func(name, input, output string) { - require.NoError(t, testMgr.AccessProcessor(tCtx, name, func(p processor.V1) { - res, err := p.ProcessBatch(tCtx, message.Batch{ - message.NewPart([]byte(input)), - }) - require.NoError(t, err) - require.Len(t, res, 1) - require.Len(t, res[0], 1) - assert.Equal(t, output, string(res[0][0].AsBytes())) - })) - } - - assertProc("fooproc", "hello world", "HELLO WORLD") - assertProc("barproc", "hello world", "hello world and bar") - assertProc("bazproc", "hello world", "hello world and baz") - - // Update baz, add new bar. - require.NoError(t, os.WriteFile(filepath.Join(confDir, "b_res.yaml"), []byte(` -processor_resources: - - label: bazproc - mapping: | - root = content() + " and a new baz" - - label: barproc - mapping: | - root = content() + " and a replaced bar" -`), 0o644)) - - checkProc := func(name, input string) (output string) { - require.NoError(t, testMgr.AccessProcessor(tCtx, name, func(p processor.V1) { - res, err := p.ProcessBatch(tCtx, message.Batch{ - message.NewPart([]byte(input)), - }) - if err != nil || len(res) != 1 || len(res[0]) != 1 { - return - } - output = string(res[0][0].AsBytes()) - })) - return - } - - require.Eventually(t, func() bool { - return checkProc("barproc", "hello world") == "hello world and a replaced bar" && - checkProc("bazproc", "hello world") == "hello world and a new baz" - }, time.Second, time.Millisecond*10) - - assertProc("fooproc", "hello world", "HELLO WORLD") - assertProc("barproc", "hello world", "hello world and a replaced bar") - assertProc("bazproc", "hello world", "hello world and a new baz") - - // Update foo, remove bar - require.NoError(t, os.WriteFile(filepath.Join(confDir, "a_res.yaml"), []byte(` -processor_resources: - - label: fooproc - mapping: | - root = content().uppercase() + "!!!" -`), 0o644)) - - require.Eventually(t, func() bool { - return checkProc("fooproc", "hello world") == "HELLO WORLD!!!" - }, time.Second, time.Millisecond*10) - - // Bar should still exist because it was moved to a new file. - assertProc("fooproc", "hello world", "HELLO WORLD!!!") - assertProc("barproc", "hello world", "hello world and a replaced bar") - assertProc("bazproc", "hello world", "hello world and a new baz") -} diff --git a/internal/config/schema.go b/internal/config/schema.go deleted file mode 100644 index c66eadb67e..0000000000 --- a/internal/config/schema.go +++ /dev/null @@ -1,154 +0,0 @@ -package config - -import ( - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/tracer" - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -const ( - fieldHTTP = "http" - fieldLogger = "logger" - fieldMetrics = "metrics" - fieldTracer = "tracer" - fieldSystemCloseDelay = "shutdown_delay" - fieldSystemCloseTimeout = "shutdown_timeout" - fieldTests = "tests" -) - -// Type is the Benthos service configuration struct. -type Type struct { - HTTP api.Config `yaml:"http"` - stream.Config `yaml:",inline"` - manager.ResourceConfig `yaml:",inline"` - Logger log.Config `yaml:"logger"` - Metrics metrics.Config `yaml:"metrics"` - Tracer tracer.Config `yaml:"tracer"` - SystemCloseDelay string `yaml:"shutdown_delay"` - SystemCloseTimeout string `yaml:"shutdown_timeout"` - Tests []any `yaml:"tests"` - - rawSource any -} - -func (t *Type) GetRawSource() any { - return t.rawSource -} - -var httpField = docs.FieldObject(fieldHTTP, "Configures the service-wide HTTP server.").WithChildren(api.Spec()...) - -func observabilityFields() docs.FieldSpecs { - defaultMetrics := "none" - if _, exists := bundle.GlobalEnvironment.GetDocs("prometheus", docs.TypeMetrics); exists { - defaultMetrics = "prometheus" - } - return docs.FieldSpecs{ - docs.FieldObject(fieldLogger, "Describes how operational logs should be emitted.").WithChildren(log.Spec()...), - docs.FieldMetrics(fieldMetrics, "A mechanism for exporting metrics.").HasDefault(map[string]any{ - "mapping": "", - defaultMetrics: map[string]any{}, - }), - docs.FieldTracer(fieldTracer, "A mechanism for exporting traces.").HasDefault(map[string]any{ - "none": map[string]any{}, - }), - docs.FieldString(fieldSystemCloseDelay, "A period of time to wait for metrics and traces to be pulled or pushed from the process.").HasDefault("0s"), - docs.FieldString(fieldSystemCloseTimeout, "The maximum period of time to wait for a clean shutdown. If this time is exceeded Benthos will forcefully close.").HasDefault("20s"), - } -} - -// Spec returns a docs.FieldSpec for an entire Benthos configuration. -func Spec() docs.FieldSpecs { - fields := docs.FieldSpecs{httpField} - fields = append(fields, stream.Spec()...) - fields = append(fields, manager.Spec()...) - fields = append(fields, observabilityFields()...) - fields = append(fields, test.ConfigSpec().Advanced()) - return fields -} - -// SpecWithoutStream describes a stream config without the core stream fields. -func SpecWithoutStream(spec docs.FieldSpecs) docs.FieldSpecs { - streamFields := map[string]struct{}{} - for _, f := range stream.Spec() { - streamFields[f.Name] = struct{}{} - } - - var fields docs.FieldSpecs - for _, f := range spec { - if _, exists := streamFields[f.Name]; exists { - continue - } - fields = append(fields, f) - } - return fields -} - -func FromParsed(prov docs.Provider, pConf *docs.ParsedConfig, rawSource any) (conf Type, err error) { - conf.rawSource = rawSource - if conf.Config, err = stream.FromParsed(prov, pConf, nil); err != nil { - return - } - if conf.ResourceConfig, err = manager.FromParsed(prov, pConf); err != nil { - return - } - err = noStreamFromParsed(prov, pConf, &conf) - return -} - -func noStreamFromParsed(prov docs.Provider, pConf *docs.ParsedConfig, conf *Type) (err error) { - if pConf.Contains(fieldHTTP) { - if conf.HTTP, err = api.FromParsed(pConf.Namespace(fieldHTTP)); err != nil { - return - } - } else { - conf.HTTP = api.NewConfig() - } - if pConf.Contains(fieldLogger) { - if conf.Logger, err = log.FromParsed(pConf.Namespace(fieldLogger)); err != nil { - return - } - } else { - conf.Logger = log.NewConfig() - } - if ga, _ := pConf.FieldAny(fieldMetrics); ga != nil { - if conf.Metrics, err = metrics.FromAny(prov, ga); err != nil { - return - } - } else { - conf.Metrics = metrics.NewConfig() - } - if ga, _ := pConf.FieldAny(fieldTracer); ga != nil { - if conf.Tracer, err = tracer.FromAny(prov, ga); err != nil { - return - } - } else { - conf.Tracer = tracer.NewConfig() - } - if pConf.Contains(fieldSystemCloseDelay) { - if conf.SystemCloseDelay, err = pConf.FieldString(fieldSystemCloseDelay); err != nil { - return - } - } - if pConf.Contains(fieldSystemCloseTimeout) { - if conf.SystemCloseTimeout, err = pConf.FieldString(fieldSystemCloseTimeout); err != nil { - return - } - } - if pConf.Contains(fieldTests) { - var tmpTests []*docs.ParsedConfig - if tmpTests, err = pConf.FieldAnyList(fieldTests); err != nil { - return - } - for _, v := range tmpTests { - t, _ := v.FieldAny() - conf.Tests = append(conf.Tests, t) - } - } - return -} diff --git a/internal/config/schema/schema.go b/internal/config/schema/schema.go deleted file mode 100644 index cb5222220f..0000000000 --- a/internal/config/schema/schema.go +++ /dev/null @@ -1,196 +0,0 @@ -package schema - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// Full represents the entirety of the Benthos instances configuration spec and -// all plugins. -type Full struct { - Version string `json:"version"` - Date string `json:"date"` - Config docs.FieldSpecs `json:"config,omitempty"` - Buffers []docs.ComponentSpec `json:"buffers,omitempty"` - Caches []docs.ComponentSpec `json:"caches,omitempty"` - Inputs []docs.ComponentSpec `json:"inputs,omitempty"` - Outputs []docs.ComponentSpec `json:"outputs,omitempty"` - Processors []docs.ComponentSpec `json:"processors,omitempty"` - RateLimits []docs.ComponentSpec `json:"rate-limits,omitempty"` - Metrics []docs.ComponentSpec `json:"metrics,omitempty"` - Tracers []docs.ComponentSpec `json:"tracers,omitempty"` - Scanners []docs.ComponentSpec `json:"scanners,omitempty"` - BloblangFunctions []query.FunctionSpec `json:"bloblang-functions,omitempty"` - BloblangMethods []query.MethodSpec `json:"bloblang-methods,omitempty"` -} - -// New walks all registered Benthos components and creates a full schema -// definition of it. -func New(version, date string) Full { - s := Full{ - Version: version, - Date: date, - Config: config.Spec(), - Buffers: bundle.AllBuffers.Docs(), - Caches: bundle.AllCaches.Docs(), - Inputs: bundle.AllInputs.Docs(), - Outputs: bundle.AllOutputs.Docs(), - Processors: bundle.AllProcessors.Docs(), - RateLimits: bundle.AllRateLimits.Docs(), - Metrics: bundle.AllMetrics.Docs(), - Tracers: bundle.AllTracers.Docs(), - Scanners: bundle.AllScanners.Docs(), - BloblangFunctions: query.FunctionDocs(), - BloblangMethods: query.MethodDocs(), - } - return s -} - -func ofStatus(status string, components []docs.ComponentSpec) []docs.ComponentSpec { - var newComps []docs.ComponentSpec - for _, c := range components { - if c.Status == docs.Status(status) { - newComps = append(newComps, c) - } - } - return newComps -} - -// ReduceToStatus reduces the components in the schema to only those matching -// the given stability status. -func (f *Full) ReduceToStatus(status string) { - f.Buffers = ofStatus(status, f.Buffers) - f.Caches = ofStatus(status, f.Caches) - f.Inputs = ofStatus(status, f.Inputs) - f.Outputs = ofStatus(status, f.Outputs) - f.Processors = ofStatus(status, f.Processors) - f.RateLimits = ofStatus(status, f.RateLimits) - f.Metrics = ofStatus(status, f.Metrics) - f.Tracers = ofStatus(status, f.Tracers) - f.Scanners = ofStatus(status, f.Scanners) - - var newFuncs []query.FunctionSpec - for _, s := range f.BloblangFunctions { - if s.Status == query.Status(status) { - newFuncs = append(newFuncs, s) - } - } - f.BloblangFunctions = newFuncs - - var newMethods []query.MethodSpec - for _, s := range f.BloblangMethods { - if s.Status == query.Status(status) { - newMethods = append(newMethods, s) - } - } - f.BloblangMethods = newMethods -} - -func justNames(components []docs.ComponentSpec) []string { - names := []string{} - for _, c := range components { - if c.Status != docs.StatusDeprecated { - names = append(names, c.Name) - } - } - return names -} - -func justNamesBloblFuncs(fns []query.FunctionSpec) []string { - names := []string{} - for _, c := range fns { - if c.Status != query.StatusDeprecated { - names = append(names, c.Name) - } - } - return names -} - -func justNamesBloblMethods(fns []query.MethodSpec) []string { - names := []string{} - for _, c := range fns { - if c.Status != query.StatusDeprecated { - names = append(names, c.Name) - } - } - return names -} - -// Flattened returns a flattened representation of all registered plugin types -// and names. -func (f *Full) Flattened() map[string][]string { - return map[string][]string{ - "buffers": justNames(f.Buffers), - "caches": justNames(f.Caches), - "inputs": justNames(f.Inputs), - "outputs": justNames(f.Outputs), - "processors": justNames(f.Processors), - "rate-limits": justNames(f.RateLimits), - "metrics": justNames(f.Metrics), - "tracers": justNames(f.Tracers), - "scanners": justNames(f.Scanners), - "bloblang-functions": justNamesBloblFuncs(f.BloblangFunctions), - "bloblang-methods": justNamesBloblMethods(f.BloblangMethods), - } -} - -// Scrub walks the schema and removes all descriptions and other long-form -// documentation, reducing the overall size. -func (f *Full) Scrub() { - scrubFieldSpecs(f.Config) - scrubComponentSpecs(f.Buffers) - scrubComponentSpecs(f.Caches) - scrubComponentSpecs(f.Inputs) - scrubComponentSpecs(f.Outputs) - scrubComponentSpecs(f.Processors) - scrubComponentSpecs(f.RateLimits) - scrubComponentSpecs(f.Metrics) - scrubComponentSpecs(f.Tracers) - scrubComponentSpecs(f.Scanners) - - for i := range f.BloblangFunctions { - f.BloblangFunctions[i].Description = "" - f.BloblangFunctions[i].Examples = nil - scrubParams(f.BloblangFunctions[i].Params.Definitions) - } - for i := range f.BloblangMethods { - f.BloblangMethods[i].Description = "" - f.BloblangMethods[i].Examples = nil - f.BloblangMethods[i].Categories = nil - scrubParams(f.BloblangMethods[i].Params.Definitions) - } -} - -func scrubParams(p []query.ParamDefinition) { - for i := range p { - p[i].Description = "" - } -} - -func scrubFieldSpecs(fs []docs.FieldSpec) { - for i := range fs { - fs[i].Description = "" - fs[i].Examples = nil - for j := range fs[i].AnnotatedOptions { - fs[i].AnnotatedOptions[j][1] = "" - } - scrubFieldSpecs(fs[i].Children) - } -} - -func scrubFieldSpec(fs *docs.FieldSpec) { - fs.Description = "" - scrubFieldSpecs(fs.Children) -} - -func scrubComponentSpecs(cs []docs.ComponentSpec) { - for i := range cs { - cs[i].Description = "" - cs[i].Summary = "" - cs[i].Footnotes = "" - cs[i].Examples = nil - scrubFieldSpec(&cs[i].Config) - } -} diff --git a/internal/config/stream_reader.go b/internal/config/stream_reader.go deleted file mode 100644 index e3e7463f21..0000000000 --- a/internal/config/stream_reader.go +++ /dev/null @@ -1,240 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - "time" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - ifilepath "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -// inferStreamID attempts to infer a stream identifier from a file path and -// containing directory. If the dir field is non-empty then the identifier will -// include all sub-directories in the path as an id prefix, this means loading -// streams with the same file name from different branches are still given -// unique names. -func inferStreamID(dir, path string) (string, error) { - var id string - if dir != "" { - var err error - if id, err = filepath.Rel(dir, path); err != nil { - return "", err - } - } else { - id = filepath.Base(path) - } - - id = strings.Trim(id, string(filepath.Separator)) - id = strings.TrimSuffix(id, ".yaml") - id = strings.TrimSuffix(id, ".yml") - id = strings.ReplaceAll(id, string(filepath.Separator), "_") - - return id, nil -} - -func (r *Reader) readStreamFileConfig(path string) (conf stream.Config, lints []string, err error) { - var confBytes []byte - var dLints []docs.Lint - var modTime time.Time - if confBytes, dLints, modTime, err = ReadFileEnvSwap(r.fs, path, os.LookupEnv); err != nil { - return - } - for _, l := range dLints { - lints = append(lints, l.Error()) - } - r.modTimeLastRead[path] = modTime - - var rawNode *yaml.Node - if rawNode, err = docs.UnmarshalYAML(confBytes); err != nil { - return - } - - var rawSource any - _ = rawNode.Decode(&rawSource) - - confSpec := append(docs.FieldSpecs{}, r.specStreamOnly...) - confSpec = append(confSpec, test.ConfigSpec()) - - if !bytes.HasPrefix(confBytes, []byte("# BENTHOS LINT DISABLE")) { - for _, lint := range confSpec.LintYAML(r.lintCtx(), rawNode) { - lints = append(lints, fmt.Sprintf("%v%v", path, lint.Error())) - } - } - - var pConf *docs.ParsedConfig - if pConf, err = confSpec.ParsedConfigFromAny(rawNode); err != nil { - return - } - - conf, err = stream.FromParsed(r.lintConf.DocsProvider, pConf, rawSource) - return -} - -func (r *Reader) readStreamFile(id, path string, confs map[string]stream.Config) ([]string, error) { - if id == "" { - return nil, fmt.Errorf("stream id could not be inferred from file: %v", path) - } - if _, exists := confs[id]; exists { - return nil, fmt.Errorf("stream id (%v) collision from file: %v", id, path) - } - - conf, lints, err := r.readStreamFileConfig(path) - if err != nil { - return nil, err - } - - confs[id] = conf - return lints, nil -} - -func (r *Reader) streamPathsExpanded() ([]string, error) { - streamsPaths, err := ifilepath.Globs(r.fs, r.streamsPaths) - if err != nil { - return nil, fmt.Errorf("failed to resolve stream glob pattern: %w", err) - } - - var paths []string - for _, target := range streamsPaths { - target = filepath.Clean(target) - - if info, err := r.fs.Stat(target); err != nil { - return nil, err - } else if !info.IsDir() { - id, err := inferStreamID("", target) - if err != nil { - return nil, err - } - - if _, exists := r.streamFileInfo[target]; !exists { - r.streamFileInfo[target] = streamFileInfo{id: id} - } - paths = append(paths, target) - continue - } - - if err := fs.WalkDir(r.fs, target, func(path string, info fs.DirEntry, werr error) error { - if werr != nil { - return werr - } - if info.IsDir() || - (!strings.HasSuffix(info.Name(), ".yaml") && - !strings.HasSuffix(info.Name(), ".yml")) { - return nil - } - - id, err := inferStreamID(target, path) - if err != nil { - return err - } - - // TODO: This is quite lazy and might run into issues e.g. the path - // `foo/bar.yaml` would collide with a test suffix of `_bar`. - if r.testSuffix != "" && strings.HasSuffix(id, r.testSuffix) { - return nil - } - - path = filepath.Clean(path) - if _, exists := r.streamFileInfo[path]; !exists { - r.streamFileInfo[path] = streamFileInfo{id: id} - } - paths = append(paths, path) - return nil - }); err != nil { - return nil, err - } - } - return paths, nil -} - -func (r *Reader) readStreamFiles(streamMap map[string]stream.Config) (pathLints []string, err error) { - var streamsPaths []string - if streamsPaths, err = r.streamPathsExpanded(); err != nil { - return nil, err - } - - for _, target := range streamsPaths { - tmpPathLints, err := r.readStreamFile(r.streamFileInfo[target].id, target, streamMap) - if err != nil { - return nil, fmt.Errorf("failed to load config '%v': %v", target, err) - } - pathLints = append(pathLints, tmpPathLints...) - } - return -} - -func (r *Reader) findStreamPathWalkedDir(streamPath string) (dir string) { - for _, p := range r.streamsPaths { - if strings.HasPrefix(streamPath, p) && len(p) > len(dir) { - dir = p - } - } - return -} - -// TriggerStreamUpdate attempts to re-read a stream configuration file, and -// trigger the provided stream update func. -func (r *Reader) TriggerStreamUpdate(mgr bundle.NewManagement, strict bool, path string) error { - if r.streamUpdateFn == nil { - return nil - } - - conf, lints, err := r.readStreamFileConfig(path) - if errors.Is(err, fs.ErrNotExist) { - info, exists := r.streamFileInfo[path] - if !exists { - return nil - } - mgr.Logger().Info("Stream %v config deleted, attempting to remove stream.", info.id) - - if err := r.streamUpdateFn(info.id, nil); err != nil { - mgr.Logger().Error("Failed to remove deleted stream %v config: %v", info.id, err) - return err - } - mgr.Logger().Info("Removed stream %v.", info.id) - return nil - } - if err != nil { - mgr.Logger().Error("Failed to read updated stream config: %v", err) - return noReread(err) - } - - info, exists := r.streamFileInfo[path] - if exists { - mgr.Logger().Info("Stream %v config updated, attempting to update stream.", info.id) - } else { - id, err := inferStreamID(r.findStreamPathWalkedDir(path), path) - if err != nil { - return err - } - info = streamFileInfo{id: id} - r.streamFileInfo[path] = info - mgr.Logger().Info("Stream %v config added, attempting to create stream.", info.id) - } - - lintlog := mgr.Logger() - for _, lint := range lints { - lintlog.Info(lint) - } - if strict && len(lints) > 0 { - mgr.Logger().Error("Rejecting updated stream %v config due to linter errors, to allow linting errors run Benthos with --chilled.", info.id) - return noReread(errors.New("file contained linting errors and is running in strict mode")) - } - - if err := r.streamUpdateFn(info.id, &conf); err != nil { - mgr.Logger().Error("Failed to apply updated stream %v config: %v", info.id, err) - return err - } - mgr.Logger().Info("Updated stream %v config from file.", info.id) - return nil -} diff --git a/internal/config/stream_reader_test.go b/internal/config/stream_reader_test.go deleted file mode 100644 index e3e853766c..0000000000 --- a/internal/config/stream_reader_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package config_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/stream" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestStreamsLints(t *testing.T) { - dir := t.TempDir() - - generalConfPath := filepath.Join(dir, "main.yaml") - require.NoError(t, os.WriteFile(generalConfPath, []byte(` -logger: - level: ALL -`), 0o644)) - - streamOnePath := filepath.Join(dir, "first.yaml") - require.NoError(t, os.WriteFile(streamOnePath, []byte(` -input: - meow1: not this - generate: - count: 10 - mapping: 'root = "meow"' - -output: - drop: {} -`), 0o644)) - - streamTwoPath := filepath.Join(dir, "second.yaml") - require.NoError(t, os.WriteFile(streamTwoPath, []byte(` -pipeline: - processors: - - bloblang: 'root = this.lowercase()' - -cache_resources: - - label: this_shouldnt_be_here - memory: - ttl: 13 -`), 0o644)) - - rdr := config.NewReader(generalConfPath, nil, config.OptSetStreamPaths(streamOnePath, streamTwoPath)) - - _, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - streamConfs := map[string]stream.Config{} - lints, err = rdr.ReadStreams(streamConfs) - require.NoError(t, err) - - require.Len(t, lints, 2) - assert.Contains(t, lints[0], "/first.yaml(3,1) field meow1 ") - assert.Contains(t, lints[1], "/second.yaml(6,1) field cache_resources not recognised") - - require.Len(t, streamConfs, 2) - - firstAny := gabs.Wrap(testConfToAny(t, streamConfs["first"])) - - assert.Equal(t, "generate", streamConfs["first"].Input.Type) - assert.Equal(t, `root = "meow"`, firstAny.S("input", "generate", "mapping").Data()) -} - -func TestStreamsDirectoryWalk(t *testing.T) { - dir := t.TempDir() - - streamOnePath := filepath.Join(dir, "first.yaml") - require.NoError(t, os.WriteFile(streamOnePath, []byte(` -pipeline: - processors: - - bloblang: 'root = "first"' -`), 0o644)) - - require.NoError(t, os.MkdirAll(filepath.Join(dir, "nested", "inner"), 0o755)) - - streamTwoPath := filepath.Join(dir, "nested", "inner", "second.yaml") - require.NoError(t, os.WriteFile(streamTwoPath, []byte(` -pipeline: - processors: - - bloblang: 'root = "second"' -`), 0o644)) - - streamThreePath := filepath.Join(dir, "nested", "inner", "third.yaml") - require.NoError(t, os.WriteFile(streamThreePath, []byte(` -pipeline: - processors: - - bloblang: 'root = "third"' -`), 0o644)) - - rdr := config.NewReader("", nil, config.OptSetStreamPaths(streamOnePath, filepath.Join(dir, "nested"))) - - _, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - streamConfs := map[string]stream.Config{} - lints, err = rdr.ReadStreams(streamConfs) - require.NoError(t, err) - require.Empty(t, lints) - - require.Len(t, streamConfs, 3) - require.Contains(t, streamConfs, "first") - require.Contains(t, streamConfs, "inner_second") - require.Contains(t, streamConfs, "inner_third") - - assert.Equal(t, `root = "first"`, gabs.Wrap(testConfToAny(t, streamConfs["first"])).S("pipeline", "processors", "0", "bloblang").Data()) - assert.Equal(t, `root = "second"`, gabs.Wrap(testConfToAny(t, streamConfs["inner_second"])).S("pipeline", "processors", "0", "bloblang").Data()) - assert.Equal(t, `root = "third"`, gabs.Wrap(testConfToAny(t, streamConfs["inner_third"])).S("pipeline", "processors", "0", "bloblang").Data()) -} diff --git a/internal/config/test/case.go b/internal/config/test/case.go deleted file mode 100644 index f2a3725295..0000000000 --- a/internal/config/test/case.go +++ /dev/null @@ -1,161 +0,0 @@ -package test - -import ( - "github.com/benthosdev/benthos/v4/internal/docs" -) - -const ( - fieldCaseName = "name" - fieldCaseEnvironment = "environment" - fieldCaseTargetProcessors = "target_processors" - fieldCaseTargetMapping = "target_mapping" - fieldCaseMocks = "mocks" - fieldCaseInputBatch = "input_batch" - fieldCaseInputBatches = "input_batches" - fieldCaseOutputBatches = "output_batches" -) - -type Case struct { - Name string - Environment map[string]string - TargetProcessors string - TargetMapping string - Mocks map[string]any - InputBatches [][]InputConfig - OutputBatches [][]OutputConditionsMap - - line int -} - -func (c *Case) Line() int { - return c.line -} - -func caseFields() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString(fieldCaseName, "The name of the test, this should be unique and give a rough indication of what behavior is being tested."), - docs.FieldString(fieldCaseEnvironment, "An optional map of environment variables to set for the duration of the test.").Map().Optional(), - docs.FieldString(fieldCaseTargetProcessors, ` -A [JSON Pointer][json-pointer] that identifies the specific processors which should be executed by the test. The target can either be a single processor or an array of processors. Alternatively a resource label can be used to identify a processor. - -It is also possible to target processors in a separate file by prefixing the target with a path relative to the test file followed by a # symbol. -`, - "foo_processor", - "/pipeline/processors/0", - "target.yaml#/pipeline/processors", - "target.yaml#/pipeline/processors", - ).HasDefault("/pipeline/processors"), - docs.FieldString(fieldCaseTargetMapping, - "A file path relative to the test definition path of a Bloblang file to execute as an alternative to testing processors with the `target_processors` field. This allows you to define unit tests for Bloblang mappings directly.", - ).HasDefault(""), - docs.FieldAnything(fieldCaseMocks, - "An optional map of processors to mock. Keys should contain either a label or a JSON pointer of a processor that should be mocked. Values should contain a processor definition, which will replace the mocked processor. Most of the time you'll want to use a [`mapping` processor][processors.mapping] here, and use it to create a result that emulates the target processor.", - map[string]any{ - "get_foobar_api": map[string]any{ - "mapping": "root = content().string() + \" this is some mock content\"", - }, - }, - map[string]any{ - "/pipeline/processors/1": map[string]any{ - "mapping": "root = content().string() + \" this is some mock content\"", - }, - }, - ).Map().Optional(), - docs.FieldObject(fieldCaseInputBatch, "Define a batch of messages to feed into your test, specify either an `input_batch` or a series of `input_batches`."). - Array().Optional().WithChildren(inputFields()...), - docs.FieldObject(fieldCaseInputBatches, "Define a series of batches of messages to feed into your test, specify either an `input_batch` or a series of `input_batches`."). - ArrayOfArrays().Optional().WithChildren(inputFields()...), - docs.FieldObject(fieldCaseOutputBatches, "List of output batches."). - ArrayOfArrays().Optional().WithChildren(outputFields()...), - } -} - -func CaseFromAny(v any) (Case, error) { - pConf, err := caseFields().ParsedConfigFromAny(v) - if err != nil { - return Case{}, err - } - return CaseFromParsed(pConf) -} - -func CaseFromParsed(pConf *docs.ParsedConfig) (c Case, err error) { - c.line, _ = pConf.Line() - if c.Name, err = pConf.FieldString(fieldCaseName); err != nil { - return - } - if pConf.Contains(fieldCaseEnvironment) { - if c.Environment, err = pConf.FieldStringMap(fieldCaseEnvironment); err != nil { - return - } - } - if c.TargetProcessors, err = pConf.FieldString(fieldCaseTargetProcessors); err != nil { - return - } - if c.TargetMapping, err = pConf.FieldString(fieldCaseTargetMapping); err != nil { - return - } - - if pConf.Contains(fieldCaseMocks) { - var tmpMocksAny map[string]*docs.ParsedConfig - if tmpMocksAny, err = pConf.FieldAnyMap(fieldCaseMocks); err != nil { - return - } - c.Mocks = map[string]any{} - for k, v := range tmpMocksAny { - if c.Mocks[k], err = v.FieldAny(); err != nil { - return - } - } - } - - if pConf.Contains(fieldCaseInputBatches) { - var iBListOfList [][]*docs.ParsedConfig - if iBListOfList, err = pConf.FieldObjectListOfLists(fieldCaseInputBatches); err != nil { - return - } - for _, ol := range iBListOfList { - tmpList := make([]InputConfig, len(ol)) - for i, il := range ol { - if tmpList[i], err = InputFromParsed(il); err != nil { - return - } - } - c.InputBatches = append(c.InputBatches, tmpList) - } - } - - if pConf.Contains(fieldCaseInputBatch) { - var iBList []*docs.ParsedConfig - if iBList, err = pConf.FieldObjectList(fieldCaseInputBatch); err != nil { - return - } - if len(iBList) > 0 { - var tmpList []InputConfig - for _, icp := range iBList { - var inputTmp InputConfig - if inputTmp, err = InputFromParsed(icp); err != nil { - return - } - tmpList = append(tmpList, inputTmp) - } - c.InputBatches = append(c.InputBatches, tmpList) - } - } - - if pConf.Contains(fieldCaseOutputBatches) { - var oBListOfList [][]*docs.ParsedConfig - if oBListOfList, err = pConf.FieldObjectListOfLists(fieldCaseOutputBatches); err != nil { - return - } - for _, ol := range oBListOfList { - tmpList := make([]OutputConditionsMap, len(ol)) - for i, il := range ol { - if tmpList[i], err = OutputConditionsFromParsed(il); err != nil { - return - } - } - c.OutputBatches = append(c.OutputBatches, tmpList) - } - } - return -} diff --git a/internal/config/test/docs.adoc b/internal/config/test/docs.adoc deleted file mode 100644 index be870e5f3c..0000000000 --- a/internal/config/test/docs.adoc +++ /dev/null @@ -1,301 +0,0 @@ -= Unit Testing -:json-pointer-url: https://tools.ietf.org/html/rfc6901 -:bloblang-url: xref:guides:bloblang/about.adoc -:logger-url: xref:components:logger/about.adoc -:processors-mapping-url: xref:components:processors/mapping.adoc - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the contents of: - internal/config/test/docs.adoc -//// - -The {page-component-title} service offers a command `benthos test` for running unit tests on sections of a configuration file. This makes it easy to protect your config files from regressions over time. - -== Writing a test - -Let's imagine we have a configuration file `foo.yaml` containing some processors: - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - -pipeline: - processors: - - mapping: '"%vend".format(content().uppercase().string())' - -output: - aws_s3: - bucket: TODO - path: '${! meta("kafka_topic") }/${! json("message.id") }.json' -``` - -One way to write our unit tests for this config is to accompany it with a file of the same name and extension but suffixed with `_benthos_test`, which in this case would be `foo_benthos_test.yaml`. - -```yml -tests: - - name: example test - target_processors: '/pipeline/processors' - environment: {} - input_batch: - - content: 'example content' - metadata: - example_key: example metadata value - output_batches: - - - - content_equals: EXAMPLE CONTENTend - metadata_equals: - example_key: example metadata value -``` - -Under `tests` we have a list of any number of unit tests to execute for the config file. Each test is run in complete isolation, including any resources defined by the config file. Tests should be allocated a unique `name` that identifies the feature being tested. - -The field `target_processors` is either the label of a processor to test, or a {json-pointer-url}[JSON Pointer] that identifies the position of a processor, or list of processors, within the file which should be executed by the test. For example a value of `foo` would target a processor with the label `foo`, and a value of `/input/processors` would target all processors within the input section of the config. - -The field `environment` allows you to define an object of key/value pairs that set environment variables to be evaluated during the parsing of the target config file. These are unique to each test, allowing you to test different environment variable interpolation combinations. - -The field `input_batch` lists one or more messages to be fed into the targeted processors as a batch. Each message of the batch may have its raw content defined as well as metadata key/value pairs. - -For the common case where the messages are in JSON format, you can use `json_content` instead of `content` to specify the message structurally rather than verbatim. - -The field `output_batches` lists any number of batches of messages which are expected to result from the target processors. Each batch lists any number of messages, each one defining <> to describe the expected contents of the message. - -If the number of batches defined does not match the resulting number of batches the test will fail. If the number of messages defined in each batch does not match the number in the resulting batches the test will fail. If any condition of a message fails then the test fails. - -=== Inline tests - -Sometimes it's more convenient to define your tests within the config being tested. This is fine, simply add the `tests` field to the end of the config being tested. - -=== Bloblang tests - -Sometimes when working with large {bloblang-url}[Bloblang mappings] it's preferred to have the full mapping in a separate file to your {page-component-title} configuration. In this case it's possible to write unit tests that target and execute the mapping directly with the field `target_mapping`, which when specified is interpreted as either an absolute path or a path relative to the test definition file that points to a file containing only a Bloblang mapping. - -For example, if we were to have a file `cities.blobl` containing a mapping: - -```coffeescript -root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -``` - -We can accompany it with a test file `cities_test.yaml` containing a regular test definition: - -```yml -tests: - - name: test cities mapping - target_mapping: './cities.blobl' - environment: {} - input_batch: - - content: | - { - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] - } - output_batches: - - - - json_equals: {"Cities": "Bellevue, Olympia, Seattle"} -``` - -And execute this test the same way we execute other {page-component-title} tests (`benthos test ./dir/cities_test.yaml`, `benthos test ./dir/...`, etc). - -=== Fragmented tests - -Sometimes the number of tests you need to define in order to cover a config file is so vast that it's necessary to split them across multiple test definition files. This is possible but {page-component-title} still requires a way to detect the configuration file being targeted by these fragmented test definition files. In order to do this we must prefix our `target_processors` field with the path of the target relative to the definition file. - -The syntax of `target_processors` in this case is a full {json-pointer-url}[JSON Pointer] that should look something like `target.yaml#/pipeline/processors`. For example, if we saved our test definition above in an arbitrary location like `./tests/first.yaml` and wanted to target our original `foo.yaml` config file, we could do that with the following: - -```yml -tests: - - name: example test - target_processors: '../foo.yaml#/pipeline/processors' - environment: {} - input_batch: - - content: 'example content' - metadata: - example_key: example metadata value - output_batches: - - - - content_equals: EXAMPLE CONTENTend - metadata_equals: - example_key: example metadata value -``` - -== Input Definitions - -=== `content` - -Sets the raw content of the message. - -=== `json_content` - -```yml -json_content: - foo: foo value - bar: [ element1, 10 ] -``` - -Sets the raw content of the message to a JSON document matching the structure of the value. - -=== `file_content` - -```yml -file_content: ./foo/bar.txt -``` - -Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. - -=== `metadata` - -A map of key/value pairs that sets the metadata values of the message. - -== Output Conditions - -=== `bloblang` - -```yml -bloblang: 'this.age > 10 && @foo.length() > 0' -``` - -Executes a {bloblang-url}[Bloblang expression] on a message, if the result is anything other than a boolean equalling `true` the test fails. - -=== `content_equals` - -```yml -content_equals: example content -``` - -Checks the full raw contents of a message against a value. - -=== `content_matches` - -```yml -content_matches: "^foo [a-z]+ bar$" -``` - -Checks whether the full raw contents of a message matches a regular expression (re2). - -=== `metadata_equals` - -```yml -metadata_equals: - example_key: example metadata value -``` - -Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. - -=== `file_equals` - -```yml -file_equals: ./foo/bar.txt -``` - -Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. - -=== `file_json_equals` - -```yml -file_json_equals: ./foo/bar.json -``` - -Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. - -=== `json_equals` - -```yml -json_equals: { "key": "value" } -``` - -Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. - -You can also structure the condition content as YAML and it will be converted to the equivalent JSON document for testing: - -```yml -json_equals: - key: value -``` - -=== `json_contains` - -```yml -json_contains: { "key": "value" } -``` - -Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. - -== Running tests - -Executing tests for a specific config can be done by pointing the subcommand `test` at either the config to be tested or its test definition, e.g. `benthos test ./config.yaml` and `benthos test ./config_benthos_test.yaml` are equivalent. - -The `test` subcommand also supports wildcard patterns e.g. `benthos test ./foo/*.yaml` will execute all tests within matching files. In order to walk a directory tree and execute all tests found you can use the shortcut `./...`, e.g. `benthos test ./...` will execute all tests found in the current directory, any child directories, and so on. - -If you want to allow components to write logs at a provided level to stdout when running the tests, you can use -`benthos test --log `. Please consult the {logger-url}[logger docs] for further details. - -== Mocking processors - -BETA: This feature is currently in a BETA phase, which means breaking changes could be made if a fundamental issue with the feature is found. - -Sometimes you'll want to write tests for a series of processors, where one or more of them are networked (or otherwise stateful). Rather than creating and managing mocked services you can define mock versions of those processors in the test definition. For example, if we have a config with the following processors: - -```yaml -pipeline: - processors: - - mapping: 'root = "simon says: " + content()' - - label: get_foobar_api - http: - url: http://example.com/foobar - verb: GET - - mapping: 'root = content().uppercase()' -``` - -Rather than create a fake service for the `http` processor to interact with we can define a mock in our test definition that replaces it with a {processors-mapping-url}[`mapping` processor]. Mocks are configured as a map of labels that identify a processor to replace and the config to replace it with: - -```yaml -tests: - - name: mocks the http proc - target_processors: '/pipeline/processors' - mocks: - get_foobar_api: - mapping: 'root = content().string() + " this is some mock content"' - input_batch: - - content: "hello world" - output_batches: - - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" -``` - -With the above test definition the `http` processor will be swapped out for `mapping: 'root = content().string() + " this is some mock content"'`. For the purposes of mocking it is recommended that you use a {processors-mapping-url}[`mapping` processor] that simply mutates the message in a way that you would expect the mocked processor to. - -NOTE: It's not currently possible to mock components that are imported as separate resource files (using `--resource`/`-r`). It is recommended that you mock these by maintaining separate definitions for test purposes (`-r "./test/*.yaml"`). - -=== More granular mocking - -It is also possible to target specific fields within the test config by {json-pointer-url}[JSON pointers] as an alternative to labels. The following test definition would create the same mock as the previous: - -```yaml -tests: - - name: mocks the http proc - target_processors: '/pipeline/processors' - mocks: - /pipeline/processors/1: - mapping: 'root = content().string() + " this is some mock content"' - input_batch: - - content: "hello world" - output_batches: - - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" -``` - -== Fields - -The schema of a template file is as follows: - -{{template "field_docs" . -}} diff --git a/internal/config/test/docs.go b/internal/config/test/docs.go deleted file mode 100644 index 4222046e24..0000000000 --- a/internal/config/test/docs.go +++ /dev/null @@ -1,90 +0,0 @@ -package test - -import ( - "bytes" - "fmt" - "text/template" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" - - _ "embed" -) - -const fieldTests = "tests" - -//go:embed docs.adoc -var testDocs string - -type testContext struct { - Fields []docs.FieldSpecCtx -} - -// DocsMarkdown returns a markdown document for the templates documentation. -func DocsMarkdown() ([]byte, error) { - testDocsTemplate := docs.FieldsTemplate(false) + testDocs - - var buf bytes.Buffer - err := template.Must(template.New("tests").Parse(testDocsTemplate)).Execute(&buf, testContext{ - Fields: docs.FieldObject("", "").WithChildren(ConfigSpec()).FlattenChildrenForDocs(), - }) - - return buf.Bytes(), err -} - -// ConfigSpec returns a configuration spec for a template. -func ConfigSpec() docs.FieldSpec { - return docs.FieldObject(fieldTests, "A list of one or more unit tests to execute.").Array().WithChildren(caseFields()...).Optional() -} - -func FromAny(v any) ([]Case, error) { - if t, ok := v.(*yaml.Node); ok { - var tmp struct { - Tests []yaml.Node - } - if err := t.Decode(&tmp); err != nil { - return nil, err - } - var cases []Case - for i, v := range tmp.Tests { - pConf, err := caseFields().ParsedConfigFromAny(&v) - if err != nil { - return nil, fmt.Errorf("%v: %w", i, err) - } - c, err := CaseFromParsed(pConf) - if err != nil { - return nil, fmt.Errorf("%v: %w", i, err) - } - cases = append(cases, c) - } - return cases, nil - } - - pConf, err := ConfigSpec().ParsedConfigFromAny(v) - if err != nil { - return nil, err - } - return FromParsed(pConf) -} - -func FromParsed(pConf *docs.ParsedConfig) ([]Case, error) { - if !pConf.Contains(fieldTests) { - return nil, nil - } - - oList, err := pConf.FieldObjectList(fieldTests) - if err != nil { - return nil, err - } - - var cases []Case - for i, pc := range oList { - c, err := CaseFromParsed(pc) - if err != nil { - return nil, fmt.Errorf("%v: %w", i, err) - } - cases = append(cases, c) - } - return cases, nil -} diff --git a/internal/config/test/input.go b/internal/config/test/input.go deleted file mode 100644 index 9532a6dc5a..0000000000 --- a/internal/config/test/input.go +++ /dev/null @@ -1,93 +0,0 @@ -package test - -import ( - "encoding/json" - "io/fs" - "path/filepath" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/message" -) - -const ( - fieldInputContent = "content" - fieldInputJSONContent = "json_content" - fieldInputFileContent = "file_content" - fieldInputMetadata = "metadata" -) - -func inputFields() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString(fieldInputContent, "The raw content of the input message.").Optional(), - docs.FieldAnything(fieldInputJSONContent, "Sets the raw content of the message to a JSON document matching the structure of the value.", - map[string]any{ - "foo": "foo value", - "bar": []any{"element1", 10}, - }, - ).Optional(), - docs.FieldString(fieldInputFileContent, "Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file.", "./foo/bar.txt").Optional(), - docs.FieldAnything(fieldInputMetadata, "A map of metadata key/values to add to the input message.").Map().Optional(), - } -} - -type InputConfig struct { - Content string - Path string - Metadata map[string]any -} - -func (i InputConfig) ToMessage(fs fs.FS, dir string) (*message.Part, error) { - msgContent := []byte(i.Content) - if i.Path != "" { - relPath := filepath.Join(dir, i.Path) - rawBytes, err := ifs.ReadFile(fs, relPath) - if err != nil { - return nil, err - } - msgContent = rawBytes - } - - msg := message.NewPart(msgContent) - for k, v := range i.Metadata { - msg.MetaSetMut(k, v) - } - return msg, nil -} - -func InputFromParsed(pConf *docs.ParsedConfig) (conf InputConfig, err error) { - if pConf.Contains(fieldInputContent) { - if conf.Content, err = pConf.FieldString(fieldInputContent); err != nil { - return - } - } - if pConf.Contains(fieldInputJSONContent) { - var v any - if v, err = pConf.FieldAny(fieldInputJSONContent); err != nil { - return - } - var jBytes []byte - if jBytes, err = json.Marshal(v); err != nil { - return - } - conf.Content = string(jBytes) - } - if pConf.Contains(fieldInputFileContent) { - if conf.Path, err = pConf.FieldString(fieldInputFileContent); err != nil { - return - } - } - if pConf.Contains(fieldInputMetadata) { - conf.Metadata = map[string]any{} - var tmpMap map[string]*docs.ParsedConfig - if tmpMap, err = pConf.FieldAnyMap(fieldInputMetadata); err != nil { - return - } - for k, v := range tmpMap { - if conf.Metadata[k], err = v.FieldAny(); err != nil { - return - } - } - } - return -} diff --git a/internal/config/test/output.go b/internal/config/test/output.go deleted file mode 100644 index 14864bf8df..0000000000 --- a/internal/config/test/output.go +++ /dev/null @@ -1,316 +0,0 @@ -package test - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/fs" - "path/filepath" - "regexp" - "sort" - - "github.com/fatih/color" - "github.com/nsf/jsondiff" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -var ( - red = color.New(color.FgRed).SprintFunc() - blue = color.New(color.FgBlue).SprintFunc() -) - -const ( - fieldOutputBloblang = "bloblang" - fieldOutputContentEquals = "content_equals" - fieldOutputContentMatches = "content_matches" - fieldOutputMetadataEquals = "metadata_equals" - fieldOutputFileEquals = "file_equals" - fieldOutputFileJSONEquals = "file_json_equals" - fieldOutputFileJSONContains = "file_json_contains" - fieldOutputJSONEquals = "json_equals" - fieldOutputJSONContains = "json_contains" -) - -func outputFields() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldBloblang(fieldOutputBloblang, "Executes a Bloblang mapping on the output message, if the result is anything other than a boolean equalling `true` the test fails.", - "this.age > 10 && @foo.length() > 0", - ).Optional(), - docs.FieldString(fieldOutputContentEquals, "Checks the full raw contents of a message against a value.").Optional(), - docs.FieldString(fieldOutputContentMatches, "Checks whether the full raw contents of a message matches a regular expression (re2).", "^foo [a-z]+ bar$").Optional(), - docs.FieldAnything(fieldOutputMetadataEquals, "Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail.", - map[string]any{ - "example_key": "example metadata value", - }, - ).Map().Optional(), - docs.FieldString(fieldOutputFileEquals, "Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file.", - "./foo/bar.txt", - ).Optional(), - docs.FieldString(fieldOutputFileJSONEquals, "Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file.", - "./foo/bar.json", - ).Optional(), - docs.FieldAnything(fieldOutputJSONEquals, "Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences.", - map[string]any{"key": "value"}, - ).Optional(), - docs.FieldAnything(fieldOutputJSONContains, "Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition.", - map[string]any{"key": "value"}, - ).Optional(), - docs.FieldString(fieldOutputFileJSONContains, "Checks that both the message and the file contents are valid JSON documents, and that the message is a superset of the condition. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file.", - "./foo/bar.json", - ).Optional(), - } -} - -type OutputCondition interface { - Check(fs fs.FS, dir string, part *message.Part) error -} - -type OutputConditionsMap map[string]OutputCondition - -func (c OutputConditionsMap) CheckAll(fs fs.FS, dir string, part *message.Part) (errs []error) { - condTypes := []string{} - for k := range c { - condTypes = append(condTypes, k) - } - sort.Strings(condTypes) - for _, k := range condTypes { - if err := c[k].Check(fs, dir, part); err != nil { - errs = append(errs, fmt.Errorf("%v: %v", k, err)) - } - } - return -} - -func OutputConditionsFromParsed(pConf *docs.ParsedConfig) (m OutputConditionsMap, err error) { - m = OutputConditionsMap{} - if pConf.Contains(fieldOutputBloblang) { - var tmpStr string - if tmpStr, err = pConf.FieldString(fieldOutputBloblang); err != nil { - return - } - var bloblCond *BloblangCondition - if bloblCond, err = parseBloblangCondition(tmpStr); err != nil { - err = fmt.Errorf(fieldOutputBloblang+": %w", err) - return - } - m[fieldOutputBloblang] = bloblCond - } - - if pConf.Contains(fieldOutputContentEquals) { - var tmpStr string - if tmpStr, err = pConf.FieldString(fieldOutputContentEquals); err != nil { - return - } - m[fieldOutputContentEquals] = ContentEqualsCondition(tmpStr) - } - - if pConf.Contains(fieldOutputContentMatches) { - var tmpStr string - if tmpStr, err = pConf.FieldString(fieldOutputContentMatches); err != nil { - return - } - m[fieldOutputContentMatches] = ContentMatchesCondition(tmpStr) - } - - if pConf.Contains(fieldOutputMetadataEquals) { - var tmpMap map[string]*docs.ParsedConfig - if tmpMap, err = pConf.FieldAnyMap(fieldOutputMetadataEquals); err != nil { - return - } - metaMap := MetadataEqualsCondition{} - for k, v := range tmpMap { - if metaMap[k], err = v.FieldAny(); err != nil { - return - } - } - m[fieldOutputMetadataEquals] = metaMap - } - - if pConf.Contains(fieldOutputFileEquals) { - var tmpStr string - if tmpStr, err = pConf.FieldString(fieldOutputFileEquals); err != nil { - return - } - m[fieldOutputFileEquals] = FileEqualsCondition(tmpStr) - } - - if pConf.Contains(fieldOutputFileJSONEquals) { - var tmpStr string - if tmpStr, err = pConf.FieldString(fieldOutputFileJSONEquals); err != nil { - return - } - m[fieldOutputFileJSONEquals] = FileJSONEqualsCondition(tmpStr) - } - - if pConf.Contains(fieldOutputFileJSONContains) { - var tmpStr string - if tmpStr, err = pConf.FieldString(fieldOutputFileJSONContains); err != nil { - return - } - m[fieldOutputFileJSONContains] = FileJSONContainsCondition(tmpStr) - } - - if pConf.Contains(fieldOutputJSONEquals) { - var tmpAny any - if tmpAny, err = pConf.FieldAny(fieldOutputJSONEquals); err != nil { - return - } - var tmpStr string - if tmpStr, err = anyValueToJSONTestString(tmpAny); err != nil { - return - } - m[fieldOutputJSONEquals] = ContentJSONEqualsCondition(tmpStr) - } - - if pConf.Contains(fieldOutputJSONContains) { - var tmpAny any - if tmpAny, err = pConf.FieldAny(fieldOutputJSONContains); err != nil { - return - } - var tmpStr string - if tmpStr, err = anyValueToJSONTestString(tmpAny); err != nil { - return - } - m[fieldOutputJSONContains] = ContentJSONContainsCondition(tmpStr) - } - return -} - -type BloblangCondition struct { - m *mapping.Executor -} - -func parseBloblangCondition(expr string) (*BloblangCondition, error) { - m, err := bloblang.GlobalEnvironment().NewMapping(expr) - if err != nil { - return nil, err - } - return &BloblangCondition{m}, nil -} - -func (b *BloblangCondition) Check(fs fs.FS, dir string, p *message.Part) error { - msg := message.Batch{p} - res, err := b.m.QueryPart(0, msg) - if err != nil { - return err - } - if !res { - return errors.New("bloblang expression was false") - } - return nil -} - -type ContentEqualsCondition string - -func (c ContentEqualsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - if exp, act := string(c), string(p.AsBytes()); exp != act { - return fmt.Errorf("content mismatch\n expected: %v\n received: %v", blue(exp), red(act)) - } - return nil -} - -type ContentMatchesCondition string - -func (c ContentMatchesCondition) Check(fs fs.FS, dir string, p *message.Part) error { - re := regexp.MustCompile(string(c)) - if !re.Match(p.AsBytes()) { - return fmt.Errorf("pattern mismatch\n pattern: %v\n received: %v", blue(string(c)), red(string(p.AsBytes()))) - } - return nil -} - -type ContentJSONEqualsCondition string - -func (c ContentJSONEqualsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - jdopts := jsondiff.DefaultConsoleOptions() - diff, explanation := jsondiff.Compare(p.AsBytes(), []byte(c), &jdopts) - if diff != jsondiff.FullMatch { - return fmt.Errorf("JSON content mismatch\n%v", explanation) - } - return nil -} - -type ContentJSONContainsCondition string - -func (c ContentJSONContainsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - jdopts := jsondiff.DefaultConsoleOptions() - diff, explanation := jsondiff.Compare(p.AsBytes(), []byte(c), &jdopts) - if diff != jsondiff.FullMatch && diff != jsondiff.SupersetMatch { - return fmt.Errorf("JSON superset mismatch\n%v", explanation) - } - return nil -} - -type FileEqualsCondition string - -func (c FileEqualsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - relPath := filepath.Join(dir, string(c)) - - fileContent, err := ifs.ReadFile(fs, relPath) - if err != nil { - return fmt.Errorf("failed to read comparison file: %w", err) - } - - if exp, act := string(fileContent), string(p.AsBytes()); exp != act { - return fmt.Errorf("content mismatch\n expected: %v\n received: %v", blue(exp), red(act)) - } - return nil -} - -type FileJSONEqualsCondition string - -func (c FileJSONEqualsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - relPath := filepath.Join(dir, string(c)) - - fileContent, err := ifs.ReadFile(fs, relPath) - if err != nil { - return fmt.Errorf("failed to read comparison JSON file: %w", err) - } - - comparison := ContentJSONEqualsCondition(fileContent) - return comparison.Check(fs, dir, p) -} - -type FileJSONContainsCondition string - -func (c FileJSONContainsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - relPath := filepath.Join(dir, string(c)) - - fileContent, err := ifs.ReadFile(fs, relPath) - if err != nil { - return fmt.Errorf("failed to read comparison JSON file: %w", err) - } - - comparison := ContentJSONContainsCondition(fileContent) - return comparison.Check(fs, dir, p) -} - -type MetadataEqualsCondition map[string]any - -func (m MetadataEqualsCondition) Check(fs fs.FS, dir string, p *message.Part) error { - for k, exp := range m { - act, exists := p.MetaGetMut(k) - if !exists { - return fmt.Errorf("metadata key '%v' expected but not found", k) - } - if !value.ICompare(exp, act) { - return fmt.Errorf("metadata key '%v' mismatch\n expected: %v\n received: %v", k, blue(exp), red(act)) - } - } - return nil -} - -func anyValueToJSONTestString(v any) (string, error) { - if str, ok := v.(string); ok { - return str, nil - } - bval, err := json.Marshal(v) - return bytes.NewBuffer(bval).String(), err -} diff --git a/internal/config/test/output_test.go b/internal/config/test/output_test.go deleted file mode 100644 index 492d6b098a..0000000000 --- a/internal/config/test/output_test.go +++ /dev/null @@ -1,581 +0,0 @@ -package test - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/fatih/color" - "github.com/nsf/jsondiff" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func condsFromYAML(t testing.TB, str string, args ...any) OutputConditionsMap { - t.Helper() - - node, err := docs.UnmarshalYAML(fmt.Appendf(nil, str, args...)) - require.NoError(t, err) - - pConf, err := outputFields().ParsedConfigFromAny(node) - require.NoError(t, err) - - m, err := OutputConditionsFromParsed(pConf) - require.NoError(t, err) - - return m -} - -func TestConditionUnmarshal(t *testing.T) { - conds := condsFromYAML(t, ` -content_equals: "foo bar" -metadata_equals: - foo: bar -`) - - exp := OutputConditionsMap{ - "content_equals": ContentEqualsCondition("foo bar"), - "metadata_equals": MetadataEqualsCondition{ - "foo": "bar", - }, - } - - assert.Equal(t, exp, conds) -} - -func TestBloblangConditionHappy(t *testing.T) { - conds := condsFromYAML(t, ` -bloblang: 'content() == "foo bar"' -`) - - assert.Empty(t, conds.CheckAll(ifs.OS(), "", message.NewPart([]byte("foo bar")))) - assert.NotEmpty(t, conds.CheckAll(ifs.OS(), "", message.NewPart([]byte("bar baz")))) -} - -func TestBloblangConditionSad(t *testing.T) { - pConf, err := outputFields().ParsedConfigFromAny(map[string]any{ - "bloblang": "content() ==", - }) - require.NoError(t, err) - - _, err = OutputConditionsFromParsed(pConf) - require.EqualError(t, err, "bloblang: expected query, but reached end of input") -} - -func TestConditionUnmarshalUnknownCond(t *testing.T) { - node, err := docs.UnmarshalYAML([]byte(` -this_doesnt_exist: "foo bar" -metadata_equals: - key: "foo" - value: "bar" -`)) - require.NoError(t, err) - - lints := outputFields().LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), node) - require.Len(t, lints, 1) - assert.Equal(t, "(2,1) field this_doesnt_exist not recognised", lints[0].Error()) -} - -func TestConditionCheckAll(t *testing.T) { - color.NoColor = true - - conds := OutputConditionsMap{ - "content_equals": ContentEqualsCondition("foo bar"), - "metadata_equals": &MetadataEqualsCondition{ - "foo": "bar", - }, - } - - part := message.NewPart([]byte("foo bar")) - part.MetaSetMut("foo", "bar") - errs := conds.CheckAll(ifs.OS(), "", part) - require.Empty(t, errs) - - part = message.NewPart([]byte("nope")) - errs = conds.CheckAll(ifs.OS(), "", part) - require.Len(t, errs, 2) - assert.Contains(t, "content_equals: content mismatch\n expected: foo bar\n received: nope", errs[0].Error()) - assert.Contains(t, "metadata_equals: metadata key 'foo' expected but not found", errs[1].Error()) - - part = message.NewPart([]byte("foo bar")) - part.MetaSetMut("foo", "wrong") - errs = conds.CheckAll(ifs.OS(), "", part) - if exp, act := 1, len(errs); exp != act { - t.Fatalf("Wrong count of errors: %v != %v", act, exp) - } - if exp, act := "metadata_equals: metadata key 'foo' mismatch\n expected: bar\n received: wrong", errs[0].Error(); exp != act { - t.Errorf("Wrong error: %v != %v", act, exp) - } - - part = message.NewPart([]byte("wrong")) - part.MetaSetMut("foo", "bar") - errs = conds.CheckAll(ifs.OS(), "", part) - if exp, act := 1, len(errs); exp != act { - t.Fatalf("Wrong count of errors: %v != %v", act, exp) - } - if exp, act := "content_equals: content mismatch\n expected: foo bar\n received: wrong", errs[0].Error(); exp != act { - t.Errorf("Wrong error: %v != %v", act, exp) - } -} - -func TestContentCondition(t *testing.T) { - color.NoColor = true - - cond := ContentEqualsCondition("foo bar") - - type testCase struct { - name string - input string - expected error - } - - tests := []testCase{ - { - name: "positive 1", - input: "foo bar", - expected: nil, - }, - { - name: "negative 1", - input: "foo", - expected: errors.New("content mismatch\n expected: foo bar\n received: foo"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - actErr := cond.Check(ifs.OS(), "", message.NewPart([]byte(test.input))) - if test.expected == nil && actErr == nil { - return - } - if test.expected == nil && actErr != nil { - tt.Errorf("Wrong result, expected %v, received %v", test.expected, actErr) - return - } - if test.expected != nil && actErr == nil { - tt.Errorf("Wrong result, expected %v, received %v", test.expected, actErr) - return - } - if exp, act := test.expected.Error(), actErr.Error(); exp != act { - tt.Errorf("Wrong result, expected %v, received %v", exp, act) - } - }) - } -} - -func TestContentMatchesCondition(t *testing.T) { - color.NoColor = true - - matchPattern := "^foo [a-z]+ bar$" - cond := ContentMatchesCondition(matchPattern) - - type testCase struct { - name string - input string - expected error - } - - tests := []testCase{ - { - name: "positive 1", - input: "foo and bar", - expected: nil, - }, - { - name: "negative 1", - input: "foo", - expected: fmt.Errorf("pattern mismatch\n pattern: %s\n received: foo", matchPattern), - }, - { - name: "negative 2", - input: "foo & bar", - expected: fmt.Errorf("pattern mismatch\n pattern: %s\n received: foo & bar", matchPattern), - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - actErr := cond.Check(ifs.OS(), "", message.NewPart([]byte(test.input))) - if test.expected == nil && actErr == nil { - return - } - if test.expected == nil && actErr != nil { - tt.Errorf("Wrong result, expected %v, received %v", test.expected, actErr) - return - } - if test.expected != nil && actErr == nil { - tt.Errorf("Wrong result, expected %v, received %v", test.expected, actErr) - return - } - if exp, act := test.expected.Error(), actErr.Error(); exp != act { - tt.Errorf("Wrong result, expected %v, received %v", exp, act) - } - }) - } -} - -func TestMetadataEqualsCondition(t *testing.T) { - color.NoColor = true - - cond := MetadataEqualsCondition{ - "foo": "bar", - } - - type testCase struct { - name string - input map[string]string - expected error - } - - tests := []testCase{ - { - name: "positive 1", - input: map[string]string{ - "foo": "bar", - }, - expected: nil, - }, - { - name: "negative 1", - input: map[string]string{}, - expected: errors.New("metadata key 'foo' expected but not found"), - }, - { - name: "negative 2", - input: map[string]string{ - "foo": "not bar", - }, - expected: errors.New("metadata key 'foo' mismatch\n expected: bar\n received: not bar"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - part := message.NewPart(nil) - for k, v := range test.input { - part.MetaSetMut(k, v) - } - actErr := cond.Check(ifs.OS(), "", part) - if test.expected == nil && actErr == nil { - return - } - if test.expected == nil && actErr != nil { - tt.Errorf("Wrong result, expected %v, received %v", test.expected, actErr) - return - } - if test.expected != nil && actErr == nil { - tt.Errorf("Wrong result, expected %v, received %v", test.expected, actErr) - return - } - if exp, act := test.expected.Error(), actErr.Error(); exp != act { - tt.Errorf("Wrong result, expected %v, received %v", exp, act) - } - }) - } -} - -func TestJSONEqualsCondition(t *testing.T) { - color.NoColor = true - - cond := ContentJSONEqualsCondition(`{"foo":"bar","bim":"bam"}`) - - type testCase struct { - name string - input string - } - - tests := []testCase{ - { - name: "positive 1", - input: `{"foo":"bar","bim":"bam"}`, - }, - { - name: "positive 2", - input: `{ "bim": "bam", "foo": "bar" }`, - }, - { - name: "negative 1", - input: "foo", - }, - { - name: "negative 2", - input: `{"foo":"bar"}`, - }, - } - - jdopts := jsondiff.DefaultConsoleOptions() - for _, test := range tests { - var expected error - diff, explanation := jsondiff.Compare([]byte(test.input), []byte(cond), &jdopts) - if diff != jsondiff.FullMatch { - expected = fmt.Errorf("JSON content mismatch\n%v", explanation) - } - - t.Run(test.name, func(tt *testing.T) { - actErr := cond.Check(ifs.OS(), "", message.NewPart([]byte(test.input))) - if expected == nil && actErr == nil { - return - } - if expected == nil && actErr != nil { - tt.Errorf("Wrong result, expected %v, received %v", expected, actErr) - return - } - if expected != nil && actErr == nil { - tt.Errorf("Wrong result, expected %v, received %v", expected, actErr) - return - } - if exp, act := expected.Error(), actErr.Error(); exp != act { - tt.Errorf("Wrong result, expected %v, received %v", exp, act) - } - }) - } -} - -func TestJSONContainsCondition(t *testing.T) { - color.NoColor = true - - cond := ContentJSONContainsCondition(`{"foo":"bar","bim":"bam"}`) - - type testCase struct { - name string - input string - } - - tests := []testCase{ - { - name: "positive 1", - input: `{"foo":"bar","bim":"bam"}`, - }, - { - name: "positive 2", - input: `{ "bim": "bam", "foo": "bar", "baz": [1, 2, 3] }`, - }, - { - name: "negative 1", - input: `{"foo":"baz","bim":"bam"}`, - }, - { - name: "negative 2", - input: `{"foo":"bar"}`, - }, - } - - jdopts := jsondiff.DefaultConsoleOptions() - for _, test := range tests { - var expected error - diff, explanation := jsondiff.Compare([]byte(test.input), []byte(cond), &jdopts) - if diff != jsondiff.FullMatch && diff != jsondiff.SupersetMatch { - expected = fmt.Errorf("JSON superset mismatch\n%v", explanation) - } - - t.Run(test.name, func(tt *testing.T) { - actErr := cond.Check(ifs.OS(), "", message.NewPart([]byte(test.input))) - if expected == nil && actErr == nil { - return - } - if expected == nil && actErr != nil { - tt.Errorf("Wrong result, expected %v, received %v", expected, actErr) - return - } - if expected != nil && actErr == nil { - tt.Errorf("Wrong result, expected %v, received %v", expected, actErr) - return - } - if exp, act := expected.Error(), actErr.Error(); exp != act { - tt.Errorf("Wrong result, expected %v, received %v", exp, act) - } - }) - } -} - -func TestFileEqualsCondition(t *testing.T) { - color.NoColor = true - - tmpDir := t.TempDir() - - uppercasedPath := filepath.Join(tmpDir, "inner", "uppercased.txt") - notUppercasedPath := filepath.Join(tmpDir, "not_uppercased.txt") - - require.NoError(t, os.MkdirAll(filepath.Dir(uppercasedPath), 0o755)) - require.NoError(t, os.WriteFile(uppercasedPath, []byte(`FOO BAR BAZ`), 0o644)) - require.NoError(t, os.WriteFile(notUppercasedPath, []byte(`foo bar baz`), 0o644)) - - type testCase struct { - name string - path string - input string - errContains string - } - - tests := []testCase{ - { - name: "positive 1", - path: `./inner/uppercased.txt`, - input: `FOO BAR BAZ`, - }, - { - name: "positive 2", - path: `./not_uppercased.txt`, - input: `foo bar baz`, - }, - { - name: "negative 1", - path: `./inner/uppercased.txt`, - input: `foo bar baz`, - errContains: "content mismatch", - }, - { - name: "negative 2", - path: `./not_uppercased.txt`, - input: `FOO BAR BAZ`, - errContains: "content mismatch", - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - actErr := FileEqualsCondition(test.path).Check(ifs.OS(), tmpDir, message.NewPart([]byte(test.input))) - if test.errContains == "" { - assert.NoError(t, actErr) - } else { - assert.Contains(t, actErr.Error(), test.errContains) - } - }) - } -} - -func TestFileJSONEqualsCondition(t *testing.T) { - color.NoColor = true - - tmpDir := t.TempDir() - - // Contents of both files are unordered. - unformattedPath := filepath.Join(tmpDir, "inner", "unformatted.json") - formattedPath := filepath.Join(tmpDir, "formatted.json") - - require.NoError(t, os.MkdirAll(filepath.Dir(unformattedPath), 0o755)) - require.NoError(t, os.WriteFile(unformattedPath, []byte(`{"id":123456,"name":"Benthos"}`), 0o644)) - require.NoError(t, os.WriteFile(formattedPath, []byte( - `{ - "id": 123456, - "name": "Benthos" -}`), 0o644)) - - type testCase struct { - name string - path string - input string - errContains string - } - - tests := []testCase{ - { - name: "positive 1", - path: `./inner/unformatted.json`, - input: `{"name":"Benthos","id":123456}`, - }, - { - name: "positive 2", - path: `./formatted.json`, - input: `{"name":"Benthos","id":123456}`, - }, - { - name: "negative 1", - path: `./inner/unformatted.json`, - input: `{"name":"Benthos"}`, - errContains: "content mismatch", - }, - { - name: "negative 2", - path: `./formatted.json`, - input: `{"name":"Benthos"}`, - errContains: "content mismatch", - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - testPath := filepath.Join(tmpDir, test.path) - actErr := FileJSONEqualsCondition(testPath).Check(ifs.OS(), "", message.NewPart([]byte(test.input))) - if test.errContains == "" { - assert.NoError(t, actErr) - } else { - assert.Contains(t, actErr.Error(), test.errContains) - } - }) - } -} - -func TestFileJSONContainsCondition(t *testing.T) { - color.NoColor = true - - tmpDir := t.TempDir() - - // Contents of both files are unordered. - unformattedPath := filepath.Join(tmpDir, "inner", "unformatted.json") - formattedPath := filepath.Join(tmpDir, "formatted.json") - - require.NoError(t, os.MkdirAll(filepath.Dir(unformattedPath), 0o755)) - require.NoError(t, os.WriteFile(unformattedPath, []byte(`{"id":123456,"name":"Benthos"}`), 0o644)) - require.NoError(t, os.WriteFile(formattedPath, []byte( - `{ - "id": 123456, - "name": "Benthos" -}`), 0o644)) - - type testCase struct { - name string - path string - input string - errContains string - } - - tests := []testCase{ - { - name: "positive 1", - path: `./inner/unformatted.json`, - input: `{"name":"Benthos","id":123456}`, - }, - { - name: "positive 2", - path: `./formatted.json`, - input: `{"name":"Benthos","id":123456}`, - }, - { - name: "positive 3", - path: `./inner/unformatted.json`, - input: `{"name":"Benthos","id":123456,"file":"test"}`, - }, - { - name: "negative 1", - path: `./inner/unformatted.json`, - input: `{"name":"Benthos", "file":"test"}`, - errContains: "JSON superset mismatch", - }, - { - name: "negative 2", - path: `./formatted.json`, - input: `{"file":"test"}`, - errContains: "JSON superset mismatch", - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - testPath := filepath.Join(tmpDir, test.path) - actErr := FileJSONContainsCondition(testPath).Check(ifs.OS(), "", message.NewPart([]byte(test.input))) - if test.errContains == "" { - assert.NoError(t, actErr) - } else { - assert.Contains(t, actErr.Error(), test.errContains) - } - }) - } -} diff --git a/internal/config/watcher.go b/internal/config/watcher.go deleted file mode 100644 index 75dc3fd2ed..0000000000 --- a/internal/config/watcher.go +++ /dev/null @@ -1,202 +0,0 @@ -//go:build !wasm - -package config - -import ( - "errors" - "path/filepath" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - - "github.com/fsnotify/fsnotify" -) - -// ErrNoReread is an error type returned from update triggers that indicates an -// attempt should not be re-made unless the source file has been modified. -type ErrNoReread struct { - wrapped error -} - -func noReread(err error) error { - return &ErrNoReread{wrapped: err} -} - -// ShouldReread returns true if the error returned from an update trigger is non -// nil and also temporal, and therefore it is worth trying the update again even -// if the content has not changed. -func ShouldReread(err error) bool { - if err == nil { - return false - } - var nr *ErrNoReread - return !errors.As(err, &nr) -} - -// Unwrap the underlying error. -func (e *ErrNoReread) Unwrap() error { - return e.wrapped -} - -// Error returns a human readable error string. -func (e *ErrNoReread) Error() string { - return e.wrapped.Error() -} - -type fileChange struct { - at time.Time -} - -func (r *Reader) modifiedSinceLastRead(name string) bool { - info, err := r.fs.Stat(name) - if err != nil { - return true // Better to be safe than sorry - } - if info.ModTime().IsZero() { - return true - } - return info.ModTime().After(r.modTimeLastRead[name]) -} - -// BeginFileWatching creates a goroutine that watches all active configuration -// files for changes. If a resource is changed then it is swapped out -// automatically through the provided manager. If a main config or stream config -// changes then the closures registered with either SubscribeConfigChanges or -// SubscribeStreamChanges will be called. -// -// WARNING: Either SubscribeConfigChanges or SubscribeStreamChanges must be -// called before this, as otherwise it is unsafe to register them during -// watching. -func (r *Reader) BeginFileWatching(mgr bundle.NewManagement, strict bool) error { - if r.watcher != nil { - return errors.New("a file watcher has already been started") - } - if r.mainUpdateFn == nil && r.streamUpdateFn == nil { - return errors.New("a file watcher cannot be started without a subscription function registered") - } - - if !ifs.IsOS(r.fs) { - return errors.New("config file watching is only supported when accessing configs from the OS") - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - r.watcher = watcher - watching := map[string]struct{}{} - collapsedChanges := map[string]fileChange{} - - addNotWatching := func(paths []string) error { - for _, p := range paths { - if _, exists := watching[p]; !exists { - if err := watcher.Add(p); err != nil { - return err - } - watching[p] = struct{}{} - collapsedChanges[p] = fileChange{at: time.Now()} - } - } - return nil - } - - refreshFiles := func() error { - if !r.streamsMode && r.mainPath != "" { - if _, err := r.fs.Stat(r.mainPath); err == nil { - if err := addNotWatching([]string{r.mainPath}); err != nil { - return err - } - } - } - - streamsPaths, err := r.streamPathsExpanded() - if err != nil { - return err - } - if err := addNotWatching(streamsPaths); err != nil { - return err - } - - resourcePaths, err := r.resourcePathsExpanded() - if err != nil { - return err - } - if err := addNotWatching(resourcePaths); err != nil { - return err - } - return nil - } - - if err := refreshFiles(); err != nil { - _ = watcher.Close() - return err - } - - // Don't bother re-reading if the files haven't changed since the last read. - for k := range collapsedChanges { - if !r.modifiedSinceLastRead(k) { - delete(collapsedChanges, k) - } - } - - go func() { - filesTicker := time.NewTicker(r.filesRefreshPeriod) - defer filesTicker.Stop() - - changeTicker := time.NewTicker(r.changeFlushPeriod) - defer changeTicker.Stop() - - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - cleanPath := filepath.Clean(event.Name) - switch { - case event.Op&fsnotify.Write == fsnotify.Write: - watching[cleanPath] = struct{}{} - collapsedChanges[cleanPath] = fileChange{at: time.Now()} - - case event.Op&fsnotify.Remove == fsnotify.Remove || - event.Op&fsnotify.Rename == fsnotify.Rename: - delete(watching, cleanPath) - delete(r.modTimeLastRead, cleanPath) // Keeps the cache small - _ = watcher.Remove(cleanPath) - collapsedChanges[cleanPath] = fileChange{at: time.Now()} - } - case <-changeTicker.C: - for nameClean, change := range collapsedChanges { - if time.Since(change.at) < r.changeDelayPeriod { - continue - } - var succeeded bool - if nameClean == r.mainPath { - succeeded = !ShouldReread(r.TriggerMainUpdate(mgr, strict, r.mainPath)) - } else if _, exists := r.streamFileInfo[nameClean]; exists { - succeeded = !ShouldReread(r.TriggerStreamUpdate(mgr, strict, nameClean)) - } else { - succeeded = !ShouldReread(r.TriggerResourceUpdate(mgr, strict, nameClean)) - } - if succeeded { - delete(collapsedChanges, nameClean) - } else { - change.at = time.Now() - collapsedChanges[nameClean] = change - } - } - case <-filesTicker.C: - if err := refreshFiles(); err != nil { - mgr.Logger().Error("Failed to refresh watched paths: %v", err) - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - mgr.Logger().Error("Config watcher error: %v", err) - } - } - }() - return nil -} diff --git a/internal/config/watcher_test.go b/internal/config/watcher_test.go deleted file mode 100644 index ed28b66b33..0000000000 --- a/internal/config/watcher_test.go +++ /dev/null @@ -1,566 +0,0 @@ -package config - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -func TestReaderFileWatching(t *testing.T) { - dummyConfig := []byte(` -input: - generate: - mapping: 'root = "foo"' -output: - drop: {} -`) - - confDir := t.TempDir() - - // Create an empty config file in the config folder - confFilePath := filepath.Join(confDir, "main.yaml") - require.NoError(t, os.WriteFile(confFilePath, []byte{}, 0o644)) - - rdr := newDummyReader(confFilePath, nil) - - changeChan := make(chan struct{}) - once := sync.Once{} - var updatedConf stream.Config - require.NoError(t, rdr.SubscribeConfigChanges(func(conf *Type) error { - updatedConf = conf.Config - once.Do(func() { close(changeChan) }) - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - // Overwrite original config - require.NoError(t, os.WriteFile(confFilePath, dummyConfig, 0o644)) - - // Wait for the config watcher to reload the config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - require.FailNow(t, "Expected a config change to be triggered") - } - - assert.Equal(t, "generate", updatedConf.Input.Type) - assert.Equal(t, "drop", updatedConf.Output.Type) -} - -func TestReaderFileWatchingSymlinkReplace(t *testing.T) { - dummyConfig := []byte(` -input: - generate: - mapping: 'root = "foo"' -output: - drop: {} -`) - - rootDir := t.TempDir() - - // Create a config folder - confDir := filepath.Join(rootDir, "config") - require.NoError(t, os.Mkdir(confDir, 0o755)) - - // Create a symlink to the config folder - confDirSymlink := filepath.Join(rootDir, "symlink") - require.NoError(t, os.Symlink(confDir, confDirSymlink)) - - // Create an empty config file in the config folder through the symlink - confFilePath := filepath.Join(confDirSymlink, "main.yaml") - require.NoError(t, os.WriteFile(confFilePath, []byte{}, 0o644)) - - rdr := newDummyReader(confFilePath, nil) - - changeChan := make(chan struct{}) - var updatedConf stream.Config - require.NoError(t, rdr.SubscribeConfigChanges(func(conf *Type) error { - updatedConf = conf.Config - close(changeChan) - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - // Create a new config folder and place in it a new copy of the config file - newConfDir := filepath.Join(rootDir, "config_new") - require.NoError(t, os.Mkdir(newConfDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(newConfDir, "main.yaml"), dummyConfig, 0o644)) - - // Create a symlink to the new config folder - newConfDirSymlink := filepath.Join(rootDir, "symlink_new") - require.NoError(t, os.Symlink(newConfDir, newConfDirSymlink)) - - // Overwrite the original symlink with the new symlink - require.NoError(t, os.Rename(newConfDirSymlink, confDirSymlink)) - - // Remove the original config folder to trigger a config refresh - require.NoError(t, os.RemoveAll(confDir)) - - // Wait for the config watcher to reload the config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - require.FailNow(t, "Expected a config change to be triggered") - } - - assert.Equal(t, "generate", updatedConf.Input.Type) - assert.Equal(t, "drop", updatedConf.Output.Type) -} - -func TestWatcherErrors(t *testing.T) { - errA1 := errors.New("test a") - errB1 := errors.New("test b") - - errA2 := noReread(errA1) - errB2 := noReread(errB1) - - assert.True(t, ShouldReread(errA1)) - assert.True(t, ShouldReread(errB1)) - - assert.False(t, ShouldReread(errA2)) - assert.False(t, ShouldReread(errB2)) - assert.False(t, ShouldReread(nil)) - - assert.Equal(t, errA1.Error(), errA2.Error()) - assert.Equal(t, errB1.Error(), errB2.Error()) -} - -func TestReaderStreamDirectWatching(t *testing.T) { - confDir := t.TempDir() - - // Create an empty config file in the config folder - require.NoError(t, os.MkdirAll(filepath.Join(confDir, "inner"), 0o755)) - confAPath := filepath.Join(confDir, "inner", "a.yaml") - confBPath := filepath.Join(confDir, "b.yaml") - confCPath := filepath.Join(confDir, "c.yaml") - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a1, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b1, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c1, drop: {} }`), 0o644)) - - initConfs := map[string]stream.Config{} - rdr := newDummyReader("", nil, OptSetStreamPaths(confAPath, confBPath, confCPath)) - - lints, err := rdr.ReadStreams(initConfs) - require.NoError(t, err) - require.Empty(t, lints) - - assert.Equal(t, "a1", initConfs["a"].Output.Label) - assert.Equal(t, "b1", initConfs["b"].Output.Label) - assert.Equal(t, "c1", initConfs["c"].Output.Label) - - var confsMut sync.Mutex - updatedConfs := map[string]*stream.Config{} - changeChan := make(chan struct{}) - require.NoError(t, rdr.SubscribeStreamChanges(func(id string, conf *stream.Config) error { - confsMut.Lock() - defer confsMut.Unlock() - updatedConfs[id] = conf - changeChan <- struct{}{} - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c2, drop: {} }`), 0o644)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["a"]) - assert.Equal(t, "a2", updatedConfs["a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b2", updatedConfs["b"].Output.Label) - require.NotNil(t, updatedConfs["c"]) - assert.Equal(t, "c2", updatedConfs["c"].Output.Label) - confsMut.Unlock() - - // Update two and delete one of the files - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a3, drop: {} }`), 0o644)) - require.NoError(t, os.Remove(confBPath)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c3, drop: {} }`), 0o644)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["a"]) - assert.Equal(t, "a3", updatedConfs["a"].Output.Label) - require.Nil(t, updatedConfs["b"]) - require.NotNil(t, updatedConfs["c"]) - assert.Equal(t, "c3", updatedConfs["c"].Output.Label) - confsMut.Unlock() -} - -func TestReaderStreamWildcardWatching(t *testing.T) { - confDir := t.TempDir() - - // Create an empty config file in the config folder - require.NoError(t, os.MkdirAll(filepath.Join(confDir, "inner"), 0o755)) - confAPath := filepath.Join(confDir, "a.yaml") - confBPath := filepath.Join(confDir, "b.yaml") - confCPath := filepath.Join(confDir, "c.yaml") - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a1, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c1, drop: {} }`), 0o644)) - - initConfs := map[string]stream.Config{} - rdr := newDummyReader("", nil, OptSetStreamPaths(confDir+"/*.yaml")) - - lints, err := rdr.ReadStreams(initConfs) - require.NoError(t, err) - require.Empty(t, lints) - - assert.Equal(t, "a1", initConfs["a"].Output.Label) - assert.NotContains(t, initConfs, "b") - assert.Equal(t, "c1", initConfs["c"].Output.Label) - - var confsMut sync.Mutex - updatedConfs := map[string]*stream.Config{} - changeChan := make(chan struct{}) - require.NoError(t, rdr.SubscribeStreamChanges(func(id string, conf *stream.Config) error { - confsMut.Lock() - defer confsMut.Unlock() - updatedConfs[id] = conf - changeChan <- struct{}{} - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c2, drop: {} }`), 0o644)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["a"]) - assert.Equal(t, "a2", updatedConfs["a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b2", updatedConfs["b"].Output.Label) - require.NotNil(t, updatedConfs["c"]) - assert.Equal(t, "c2", updatedConfs["c"].Output.Label) - confsMut.Unlock() - - // Update two and delete one of the files - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a3, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b3, drop: {} }`), 0o644)) - require.NoError(t, os.Remove(confCPath)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["a"]) - assert.Equal(t, "a3", updatedConfs["a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b3", updatedConfs["b"].Output.Label) - require.Nil(t, updatedConfs["c"]) - confsMut.Unlock() -} - -func TestReaderStreamDirWatching(t *testing.T) { - confDir := t.TempDir() - - // Create an empty config file in the config folder - require.NoError(t, os.MkdirAll(filepath.Join(confDir, "inner"), 0o755)) - confAPath := filepath.Join(confDir, "inner", "a.yaml") - confBPath := filepath.Join(confDir, "b.yaml") - confCPath := filepath.Join(confDir, "c.yaml") - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a1, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c1, drop: {} }`), 0o644)) - - initConfs := map[string]stream.Config{} - rdr := newDummyReader("", nil, OptSetStreamPaths(confDir)) - - lints, err := rdr.ReadStreams(initConfs) - require.NoError(t, err) - require.Empty(t, lints) - - assert.Equal(t, "a1", initConfs["inner_a"].Output.Label) - assert.NotContains(t, initConfs, "b") - assert.Equal(t, "c1", initConfs["c"].Output.Label) - - var confsMut sync.Mutex - updatedConfs := map[string]*stream.Config{} - changeChan := make(chan struct{}) - require.NoError(t, rdr.SubscribeStreamChanges(func(id string, conf *stream.Config) error { - confsMut.Lock() - defer confsMut.Unlock() - updatedConfs[id] = conf - changeChan <- struct{}{} - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c2, drop: {} }`), 0o644)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["inner_a"]) - assert.Equal(t, "a2", updatedConfs["inner_a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b2", updatedConfs["b"].Output.Label) - require.NotNil(t, updatedConfs["c"]) - assert.Equal(t, "c2", updatedConfs["c"].Output.Label) - confsMut.Unlock() - - // Update two and delete one of the files - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a3, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b3, drop: {} }`), 0o644)) - require.NoError(t, os.Remove(confCPath)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["inner_a"]) - assert.Equal(t, "a3", updatedConfs["inner_a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b3", updatedConfs["b"].Output.Label) - require.Nil(t, updatedConfs["c"]) - confsMut.Unlock() -} - -func TestReaderWatcherRace(t *testing.T) { - t.Skip() - confDir := t.TempDir() - - // Create an empty config file in the config folder - require.NoError(t, os.MkdirAll(filepath.Join(confDir, "inner"), 0o755)) - confAPath := filepath.Join(confDir, "inner", "a.yaml") - confBPath := filepath.Join(confDir, "b.yaml") - confCPath := filepath.Join(confDir, "c.yaml") - - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a1, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confCPath, []byte(`output: { label: c1, drop: {} }`), 0o644)) - - initConfs := map[string]stream.Config{} - rdr := newDummyReader("", nil, OptSetStreamPaths(confDir)) - - lints, err := rdr.ReadStreams(initConfs) - require.NoError(t, err) - require.Empty(t, lints) - - assert.Equal(t, "a1", initConfs["inner_a"].Output.Label) - assert.NotContains(t, initConfs, "b") - assert.Equal(t, "c1", initConfs["c"].Output.Label) - - time.Sleep(time.Second) - - // Update all files - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a2, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b2, drop: {} }`), 0o644)) - - var confsMut sync.Mutex - updatedConfs := map[string]*stream.Config{} - changeChan := make(chan struct{}) - require.NoError(t, rdr.SubscribeStreamChanges(func(id string, conf *stream.Config) error { - confsMut.Lock() - defer confsMut.Unlock() - updatedConfs[id] = conf - changeChan <- struct{}{} - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - for i := 0; i < 2; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered", i) - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["inner_a"]) - assert.Equal(t, "a2", updatedConfs["inner_a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b2", updatedConfs["b"].Output.Label) - require.Nil(t, updatedConfs["c"]) - confsMut.Unlock() - - // Update two and delete one of the files - require.NoError(t, os.WriteFile(confAPath, []byte(`output: { label: a3, drop: {} }`), 0o644)) - require.NoError(t, os.WriteFile(confBPath, []byte(`output: { label: b3, drop: {} }`), 0o644)) - require.NoError(t, os.Remove(confCPath)) - - for i := 0; i < 3; i++ { - // Wait for the config watcher to reload each config - select { - case <-changeChan: - case <-time.After(time.Second * 5): - t.Fatal("Expected a config change to be triggered") - } - } - - confsMut.Lock() - require.NotNil(t, updatedConfs["inner_a"]) - assert.Equal(t, "a3", updatedConfs["inner_a"].Output.Label) - require.NotNil(t, updatedConfs["b"]) - assert.Equal(t, "b3", updatedConfs["b"].Output.Label) - require.Nil(t, updatedConfs["c"]) - confsMut.Unlock() -} - -func TestReaderResourceWildcardWatching(t *testing.T) { - confDir := t.TempDir() - - // Create an empty config file in the config folder - require.NoError(t, os.MkdirAll(filepath.Join(confDir, "inner"), 0o755)) - confAPath := filepath.Join(confDir, "a.yaml") - confBPath := filepath.Join(confDir, "b.yaml") - confCPath := filepath.Join(confDir, "c.yaml") - - procConfig := func(id, value string) []byte { - return fmt.Appendf(nil, ` -processor_resources: - - label: %v - mapping: 'root = content() + " %v"' -`, id, value) - } - - require.NoError(t, os.WriteFile(confAPath, procConfig("a", "a1"), 0o644)) - require.NoError(t, os.WriteFile(confCPath, procConfig("c", "c1"), 0o644)) - - rdr := newDummyReader("", []string{confDir + "/*.yaml"}) - - conf, _, lints, err := rdr.Read() - require.NoError(t, err) - require.Empty(t, lints) - - require.Len(t, conf.ResourceProcessors, 2) - require.Equal(t, "a", conf.ResourceProcessors[0].Label) - require.Equal(t, "c", conf.ResourceProcessors[1].Label) - - // Ignore - require.NoError(t, rdr.SubscribeConfigChanges(func(conf *Type) error { - return nil - })) - - // Watch for configuration changes - testMgr, err := manager.New(manager.ResourceConfig{}) - require.NoError(t, err) - require.NoError(t, rdr.BeginFileWatching(testMgr, true)) - - require.NoError(t, os.WriteFile(confAPath, procConfig("a", "a2"), 0o644)) - require.NoError(t, os.WriteFile(confBPath, procConfig("b", "b2"), 0o644)) - require.NoError(t, os.WriteFile(confCPath, procConfig("c", "c2"), 0o644)) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.Eventually(t, func() bool { - return testMgr.ProbeProcessor("a") && testMgr.ProbeProcessor("b") && testMgr.ProbeProcessor("c") - }, time.Second*5, time.Millisecond*100) - - runProc := func(name string) string { - var res string - require.NoError(t, testMgr.AccessProcessor(tCtx, name, func(p processor.V1) { - resBatch, err := p.ProcessBatch(tCtx, message.Batch{message.NewPart([]byte("hello world"))}) - require.NoError(t, err) - require.Len(t, resBatch, 1) - require.Len(t, resBatch[0], 1) - res = string(resBatch[0][0].AsBytes()) - })) - return res - } - - assert.Equal(t, "hello world a2", runProc("a")) - assert.Equal(t, "hello world b2", runProc("b")) - assert.Equal(t, "hello world c2", runProc("c")) - - // Update two and delete one of the files - require.NoError(t, os.WriteFile(confAPath, procConfig("a", "a3"), 0o644)) - require.NoError(t, os.WriteFile(confBPath, procConfig("b", "b3"), 0o644)) - require.NoError(t, os.Remove(confCPath)) - - require.Eventually(t, func() bool { - return testMgr.ProbeProcessor("a") && testMgr.ProbeProcessor("b") && !testMgr.ProbeProcessor("c") - }, time.Second*5, time.Millisecond*100) - - assert.Equal(t, "hello world a3", runProc("a")) - assert.Equal(t, "hello world b3", runProc("b")) -} diff --git a/internal/config/watcher_wasm.go b/internal/config/watcher_wasm.go deleted file mode 100644 index 59b7201bce..0000000000 --- a/internal/config/watcher_wasm.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build wasm - -package config - -import ( - "errors" - - "github.com/benthosdev/benthos/v4/internal/bundle" -) - -// BeginFileWatching does nothing in WASM builds as it is not supported. Sorry! -func (r *Reader) BeginFileWatching(mgr bundle.NewManagement, strict bool) error { - return errors.New("file watching is disabled in WASM builds") -} diff --git a/internal/cuegen/README.md b/internal/cuegen/README.md deleted file mode 100644 index a8792c484c..0000000000 --- a/internal/cuegen/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# CUE AST tips - -## Adding optional fields to structs - -Code: - -```go -requiredField := &ast.Field { - Label: ast.NewIdent("username"), - Value: ast.NewIdent("string"), -} -optionalField := &ast.Field { - Label: ast.NewIdent("age"), - Value: ast.NewIdent("uint"), -} - -ast.NewStruct( - requiredField, - - optionalField.Label, - token.OPTION, - optionalField.Value, -) -``` - -## Given a struct, create a disjunction - -In other words, given: - -```cue -#AllInputs: { - http_client: {url: string} - generate: {mapping: string} - file: {paths: [...string]} -} -``` - -Generate a type `#Input` that conforms to: - -```cue -#Input: {http_client: {url: string}} | {generate: {mapping: string}} | {file: {paths: [...string]}} -``` - -This can be done using the `or` built-in function in Cue and field comprehension: - -```cue -#Input: or([for name, config in #AllInputs {(name): config}]) -``` - -Expressing that using the `ast` package looks like this: - -Code: - -```go -collectionIdent := ast.NewIdent("#AllInputs") -disjunctionIdent := ast.NewIdent("#Input") - -&ast.Field{ - Label: disjunctionIdent, - Value: ast.NewCall(ast.NewIdent("or"), ast.NewList(&ast.Comprehension{ - Clauses: []ast.Clause{ - &ast.ForClause{ - Key: ast.NewIdent("name"), - Value: ast.NewIdent("config"), - Source: collectionIdent, - }, - }, - Value: ast.NewStruct(&ast.Field{ - Label: interpolateIdent(ast.NewIdent("name")), - Value: ast.NewIdent("config"), - }), - })), -}, -``` diff --git a/internal/cuegen/component.go b/internal/cuegen/component.go deleted file mode 100644 index 5988a8cbb1..0000000000 --- a/internal/cuegen/component.go +++ /dev/null @@ -1,77 +0,0 @@ -package cuegen - -import ( - "fmt" - - "cuelang.org/go/cue/ast" - "cuelang.org/go/cue/token" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -type componentOptions struct { - collectionIdent *ast.Ident - disjunctionIdent *ast.Ident - - canLabel bool - canPreProcess bool -} - -func doComponents(specs []docs.ComponentSpec, opts *componentOptions) ([]ast.Decl, error) { - fields := make([]any, 0, len(specs)) - - for _, v := range specs { - field, err := doComponentSpec(v) - if err != nil { - return nil, fmt.Errorf("failed to generate cue type for component: %s: %w", v.Name, err) - } - fields = append(fields, field) - } - - decls := []ast.Decl{ - &ast.Field{ - Label: opts.collectionIdent, - Value: ast.NewStruct(fields...), - }, - &ast.Field{ - Label: opts.disjunctionIdent, - Value: ast.NewCall(ast.NewIdent("or"), ast.NewList(&ast.Comprehension{ - Clauses: []ast.Clause{ - &ast.ForClause{ - Key: ast.NewIdent("name"), - Value: ast.NewIdent("config"), - Source: opts.collectionIdent, - }, - }, - Value: ast.NewStruct(&ast.Field{ - Label: interpolateIdent(ast.NewIdent("name")), - Value: ast.NewIdent("config"), - }), - })), - }, - } - - if opts.canLabel { - decls = append(decls, &ast.Field{ - Label: opts.disjunctionIdent, - Value: ast.NewStruct( - ast.NewIdent("label"), - token.OPTION, - ast.NewIdent("string"), - ), - }) - } - - if opts.canPreProcess { - decls = append(decls, &ast.Field{ - Label: opts.disjunctionIdent, - Value: ast.NewStruct( - ast.NewIdent("processors"), - token.OPTION, - ast.NewList(&ast.Ellipsis{Type: ast.NewIdent("#Processor")}), - ), - }) - } - - return decls, nil -} diff --git a/internal/cuegen/config.go b/internal/cuegen/config.go deleted file mode 100644 index a7312c5364..0000000000 --- a/internal/cuegen/config.go +++ /dev/null @@ -1,21 +0,0 @@ -package cuegen - -import ( - "cuelang.org/go/cue/ast" - - "github.com/benthosdev/benthos/v4/internal/config/schema" -) - -func doConfig(sch schema.Full) ([]ast.Decl, error) { - members, err := doFieldSpecs(sch.Config) - if err != nil { - return nil, err - } - - return []ast.Decl{ - &ast.Field{ - Label: identConfig, - Value: ast.NewStruct(members...), - }, - }, nil -} diff --git a/internal/cuegen/cue.go b/internal/cuegen/cue.go deleted file mode 100644 index ba0377d8f7..0000000000 --- a/internal/cuegen/cue.go +++ /dev/null @@ -1,179 +0,0 @@ -package cuegen - -import ( - "fmt" - "strings" - - "cuelang.org/go/cue/ast" - "cuelang.org/go/cue/token" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func doComponentSpec(cs docs.ComponentSpec) (*ast.Field, error) { - f, err := doFieldSpec(cs.Config) - if err != nil { - return nil, fmt.Errorf("%s: failed to generate CUE: %w", cs.Name, err) - } - - field := &ast.Field{ - Label: ast.NewIdent(cs.Name), - Value: f.Value, - } - - if cs.Summary != "" { - ast.AddComment(field, doComment(cs.Summary)) - } - - return field, nil -} - -func doComment(comment string) *ast.CommentGroup { - comments := []*ast.Comment{} - - for _, v := range strings.Split(strings.TrimSpace(comment), "\n") { - comments = append(comments, &ast.Comment{ - Slash: token.NoPos, - Text: "// " + v, - }) - } - - return &ast.CommentGroup{ - Position: 0, - List: comments, - } -} - -func doFieldSpecs(s docs.FieldSpecs) ([]any, error) { - var fields []any - for _, fieldSpec := range s { - field, err := doFieldSpec(fieldSpec) - if err != nil { - return nil, err - } - - if fieldSpec.Description != "" { - ast.AddComment(field.Label, doComment(fieldSpec.Description)) - } - - if fieldSpec.CheckRequired() { - fields = append(fields, field) - } else { - fields = append(fields, field.Label, token.OPTION, field.Value) - } - } - - return fields, nil -} - -func doFieldSpec(spec docs.FieldSpec) (*ast.Field, error) { - switch spec.Kind { - case "": - return doScalarField(spec) - case docs.KindScalar: - return doScalarField(spec) - case docs.KindArray: - f, err := doScalarField(spec) - if err != nil { - return nil, err - } - f.Value = ast.NewList(&ast.Ellipsis{Type: f.Value}) - return f, nil - case docs.Kind2DArray: - f, err := doScalarField(spec) - if err != nil { - return nil, err - } - f.Value = ast.NewList(&ast.Ellipsis{Type: ast.NewList(&ast.Ellipsis{Type: f.Value})}) - return f, nil - case docs.KindMap: - f, err := doScalarField(spec) - if err != nil { - return nil, err - } - f.Value = ast.NewStruct(&ast.Field{ - Label: ast.NewList(ast.NewIdent("string")), - Value: f.Value, - }) - return f, nil - default: - return nil, fmt.Errorf("unrecognised field kind: %s", spec.Kind) - } -} - -func doScalarField(spec docs.FieldSpec) (*ast.Field, error) { - label := ast.NewIdent(spec.Name) - optionalMark := token.Blank.Pos() - - var optional token.Pos - var val ast.Expr - - switch spec.Type { - case docs.FieldTypeString: - val = ast.NewIdent("string") - case docs.FieldTypeInt: - val = ast.NewIdent("int") - case docs.FieldTypeFloat: - val = ast.NewIdent("float") - case docs.FieldTypeBool: - val = ast.NewIdent("bool") - case docs.FieldTypeObject: - fields := make([]any, 0, len(spec.Children)) - for _, child := range spec.Children { - field, err := doFieldSpec(child) - if err != nil { - return nil, fmt.Errorf("failed to generate type for object field: %w", err) - } - - if child.Description != "" { - ast.AddComment(field.Label, doComment(child.Description)) - } - - if child.CheckRequired() { - fields = append(fields, field) - } else { - fields = append(fields, field.Label, token.OPTION, field.Value) - } - } - val = ast.NewStruct(fields...) - case docs.FieldTypeUnknown: - val = ast.NewIdent("_") - case docs.FieldTypeInput: - // The following set of cases have unresolvable structure cycles. - // We need to mark them as optional to break the cycle... - // https://cuelang.org/docs/references/spec/#structural-cycles - val, optional = identInputDisjunction, optionalMark - case docs.FieldTypeBuffer: - val, optional = identBufferDisjunction, optionalMark - case docs.FieldTypeCache: - val, optional = identCacheDisjunction, optionalMark - case docs.FieldTypeProcessor: - val, optional = identProcessorDisjunction, optionalMark - case docs.FieldTypeRateLimit: - val, optional = identRateLimitDisjunction, optionalMark - case docs.FieldTypeOutput: - val, optional = identOutputDisjunction, optionalMark - case docs.FieldTypeMetrics: - val, optional = identMetricDisjunction, optionalMark - case docs.FieldTypeTracer: - val, optional = identTracerDisjunction, optionalMark - case docs.FieldTypeScanner: - val, optional = identScannerDisjunction, optionalMark - default: - return nil, fmt.Errorf("unrecognised field type: %s", spec.Type) - } - - return &ast.Field{ - Label: label, - Value: val, - Optional: optional, - }, nil -} - -func interpolateIdent(ident *ast.Ident) ast.Label { - return &ast.Interpolation{Elts: []ast.Expr{ - ast.NewLit(token.STRING, "("), - ident, - ast.NewLit(token.STRING, ")"), - }} -} diff --git a/internal/cuegen/identifiers.go b/internal/cuegen/identifiers.go deleted file mode 100644 index f93f1eccca..0000000000 --- a/internal/cuegen/identifiers.go +++ /dev/null @@ -1,34 +0,0 @@ -package cuegen - -import "cuelang.org/go/cue/ast" - -var ( - identConfig = ast.NewIdent("#Config") - - identInputDisjunction = ast.NewIdent("#Input") - identInputCollection = ast.NewIdent("#AllInputs") - - identOutputDisjunction = ast.NewIdent("#Output") - identOutputCollection = ast.NewIdent("#AllOutputs") - - identProcessorDisjunction = ast.NewIdent("#Processor") - identProcessorCollection = ast.NewIdent("#AllProcessors") - - identRateLimitDisjunction = ast.NewIdent("#RateLimit") - identRateLimitCollection = ast.NewIdent("#AllRateLimits") - - identBufferDisjunction = ast.NewIdent("#Buffer") - identBufferCollection = ast.NewIdent("#AllBuffers") - - identCacheDisjunction = ast.NewIdent("#Cache") - identCacheCollection = ast.NewIdent("#AllCaches") - - identMetricDisjunction = ast.NewIdent("#Metric") - identMetricCollection = ast.NewIdent("#AllMetrics") - - identTracerDisjunction = ast.NewIdent("#Tracer") - identTracerCollection = ast.NewIdent("#AllTracers") - - identScannerDisjunction = ast.NewIdent("#Scanner") - identScannerCollection = ast.NewIdent("#AllScanners") -) diff --git a/internal/cuegen/schema.go b/internal/cuegen/schema.go deleted file mode 100644 index 465d58b76a..0000000000 --- a/internal/cuegen/schema.go +++ /dev/null @@ -1,144 +0,0 @@ -package cuegen - -import ( - "cuelang.org/go/cue/ast" - "cuelang.org/go/cue/format" - - // Populating default environment in order to walk it and generate Cue types. - "github.com/benthosdev/benthos/v4/internal/config/schema" -) - -// GenerateSchema generates a Cue schema which includes definitions for the -// configuration file structure and component configs. -func GenerateSchema(sch schema.Full) ([]byte, error) { - root := &ast.File{ - Decls: []ast.Decl{ - &ast.Package{ - Name: ast.NewIdent("benthos"), - }, - }, - } - - configDecls, err := doConfig(sch) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, configDecls...) - - inputDecls, err := doComponents( - sch.Inputs, - &componentOptions{ - collectionIdent: identInputCollection, - disjunctionIdent: identInputDisjunction, - canLabel: true, - canPreProcess: true, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, inputDecls...) - - outputDecls, err := doComponents( - sch.Outputs, - &componentOptions{ - collectionIdent: identOutputCollection, - disjunctionIdent: identOutputDisjunction, - canLabel: true, - canPreProcess: true, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, outputDecls...) - - processorDecls, err := doComponents( - sch.Processors, - &componentOptions{ - collectionIdent: identProcessorCollection, - disjunctionIdent: identProcessorDisjunction, - canLabel: true, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, processorDecls...) - - cacheDecls, err := doComponents( - sch.Caches, - &componentOptions{ - collectionIdent: identCacheCollection, - disjunctionIdent: identCacheDisjunction, - canLabel: true, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, cacheDecls...) - - rateLimitDecls, err := doComponents( - sch.RateLimits, - &componentOptions{ - collectionIdent: identRateLimitCollection, - disjunctionIdent: identRateLimitDisjunction, - canLabel: true, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, rateLimitDecls...) - - bufferDecls, err := doComponents( - sch.Buffers, - &componentOptions{ - collectionIdent: identBufferCollection, - disjunctionIdent: identBufferDisjunction, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, bufferDecls...) - - metricDecls, err := doComponents( - sch.Metrics, - &componentOptions{ - collectionIdent: identMetricCollection, - disjunctionIdent: identMetricDisjunction, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, metricDecls...) - - tracerDecls, err := doComponents( - sch.Tracers, - &componentOptions{ - collectionIdent: identTracerCollection, - disjunctionIdent: identTracerDisjunction, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, tracerDecls...) - - scannerDecls, err := doComponents( - sch.Scanners, - &componentOptions{ - collectionIdent: identScannerCollection, - disjunctionIdent: identScannerDisjunction, - }, - ) - if err != nil { - return nil, err - } - root.Decls = append(root.Decls, scannerDecls...) - - return format.Node(root) -} diff --git a/internal/docs/benchmark_test.go b/internal/docs/benchmark_test.go deleted file mode 100644 index 8d3f02cee5..0000000000 --- a/internal/docs/benchmark_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package docs_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func BenchmarkFields(b *testing.B) { - b.Run("with_options", func(b *testing.B) { - for i := 0; i < b.N; i++ { - metricsBundle := &bundle.MetricsSet{} - - require.NoError(b, metricsBundle.Add(func(conf metrics.Config, nm bundle.NewManagement) (metrics.Type, error) { - return nil, errors.New("not implemented") - }, docs.ComponentSpec{ - Name: "statsd", - Type: docs.TypeMetrics, - Status: docs.StatusStable, - Summary: ` -Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. -Supported tagging formats are 'none', 'datadog' and 'influxdb'.`, - Description: ` -The underlying client library has recently been updated in order to support -tagging.`, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("address", "The address to send metrics to.").HasDefault(""), - docs.FieldString("flush_period", "The time interval between metrics flushes.").HasDefault("100ms"), - docs.FieldString("tag_format", "Metrics tagging is supported in a variety of formats.").HasOptions( - "none", "datadog", "influxdb", - ).HasDefault("none"), - ), - })) - } - }) - - b.Run("without_options", func(b *testing.B) { - for i := 0; i < b.N; i++ { - metricsBundle := &bundle.MetricsSet{} - - require.NoError(b, metricsBundle.Add(func(conf metrics.Config, nm bundle.NewManagement) (metrics.Type, error) { - return nil, errors.New("not implemented") - }, docs.ComponentSpec{ - Name: "statsd", - Type: docs.TypeMetrics, - Status: docs.StatusStable, - Summary: ` -Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. -Supported tagging formats are 'none', 'datadog' and 'influxdb'.`, - Description: ` -The underlying client library has recently been updated in order to support -tagging.`, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("address", "The address to send metrics to.").HasDefault(""), - docs.FieldString("flush_period", "The time interval between metrics flushes.").HasDefault("100ms"), - docs.FieldString("tag_format", "Metrics tagging is supported in a variety of formats.").HasDefault("none"), - ), - })) - } - }) -} diff --git a/internal/docs/bloblang.go b/internal/docs/bloblang.go deleted file mode 100644 index 5e5c902674..0000000000 --- a/internal/docs/bloblang.go +++ /dev/null @@ -1,49 +0,0 @@ -package docs - -import ( - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// LintBloblangMapping is function for linting a config field expected to be a -// bloblang mapping. -func LintBloblangMapping(ctx LintContext, line, col int, v any) []Lint { - str, ok := v.(string) - if !ok { - return nil - } - if str == "" { - return nil - } - _, err := ctx.conf.BloblangEnv.Parse(str) - if err == nil { - return nil - } - if mErr, ok := err.(*bloblang.ParseError); ok { - lint := NewLintError(line+mErr.Line-1, LintBadBloblang, mErr) - lint.Column = col + mErr.Column - return []Lint{lint} - } - return []Lint{NewLintError(line, LintBadBloblang, err)} -} - -// LintBloblangField is function for linting a config field expected to be an -// interpolation string. -func LintBloblangField(ctx LintContext, line, col int, v any) []Lint { - str, ok := v.(string) - if !ok { - return nil - } - if str == "" { - return nil - } - err := ctx.conf.BloblangEnv.CheckInterpolatedString(str) - if err == nil { - return nil - } - if mErr, ok := err.(*bloblang.ParseError); ok { - lint := NewLintError(line+mErr.Line-1, LintBadBloblang, mErr) - lint.Column = col + mErr.Column - return []Lint{lint} - } - return []Lint{NewLintError(line, LintBadBloblang, err)} -} diff --git a/internal/docs/bloblang_markdown.go b/internal/docs/bloblang_markdown.go deleted file mode 100644 index b8842ee37f..0000000000 --- a/internal/docs/bloblang_markdown.go +++ /dev/null @@ -1,342 +0,0 @@ -package docs - -import ( - "bytes" - "strings" - "text/template" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -type functionCategory struct { - Name string - Specs []query.FunctionSpec -} - -type functionsContext struct { - Categories []functionCategory -} - -var bloblangParamsTemplate = `{{define "parameters" -}} -{{if gt (len .Definitions) 0}} -==== Parameters - -{{range $i, $param := .Definitions -}} -` + "*`{{$param.Name}}`*" + ` <{{if $param.IsOptional}}(optional) {{end}}{{$param.ValueType}}{{if $param.DefaultValue}}, default ` + "`{{$param.PrettyDefault}}`" + `{{end}}> {{$param.Description}} -{{end -}} -{{end -}} -{{end -}} -` - -var bloblangFunctionsTemplate = bloblangParamsTemplate + `{{define "function_example" -}} -{{if gt (len .Summary) 0 -}} -{{.Summary}} - -{{end -}} - -` + "```coffeescript" + ` -{{.Mapping}} -{{range $i, $result := .Results}} -# In: {{index $result 0}} -# Out: {{index $result 1}} -{{end -}} -` + "```" + ` -{{end -}} - -{{define "function_spec" -}} -=== ` + "`{{.Name}}`" + ` - -{{if eq .Status "beta" -}} -[NOTE] -.Beta -==== -This function is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -==== -{{end -}} -{{if eq .Status "experimental" -}} -[CAUTION] -.Experimental -==== -This function is experimental and therefore breaking changes could be made to it outside of major version releases. -==== -{{end -}} -{{.Description}}{{if gt (len .Version) 0}} - -Introduced in version {{.Version}}. -{{end}} -{{template "parameters" .Params -}} -{{if gt (len .Examples) 0}} -==== Examples - -{{range $i, $example := .Examples}} -{{template "function_example" $example -}} -{{end -}} -{{end -}} - -{{end -}} - -= Bloblang Functions -:description: A list of Bloblang functions - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the contents of: - internal/bloblang/query/functions.go - internal/docs/bloblang.go -//// - - -Functions can be placed anywhere and allow you to extract information from your environment, generate values, or access data from the underlying message being mapped: - -` + "```coffeescript" + ` -root.doc.id = uuid_v4() -root.doc.received_at = now() -root.doc.host = hostname() -` + "```" + ` - -Functions support both named and nameless style arguments: - -` + "```coffeescript" + ` -root.values_one = range(start: 0, stop: this.max, step: 2) -root.values_two = range(0, this.max, 2) -` + "```" + ` - -{{range $i, $cat := .Categories -}} -== {{$cat.Name}} - -{{range $i, $spec := $cat.Specs -}} -{{template "function_spec" $spec}} -{{end -}} -{{end -}} - -` - -func prefixExamples(s []query.ExampleSpec) { - for _, spec := range s { - for i := range spec.Results { - spec.Results[i][0] = strings.ReplaceAll( - strings.TrimSuffix(spec.Results[i][0], "\n"), - "\n", "\n# ", - ) - spec.Results[i][1] = strings.ReplaceAll( - strings.TrimSuffix(spec.Results[i][1], "\n"), - "\n", "\n# ", - ) - } - } -} - -// BloblangFunctionsMarkdown returns a markdown document for all Bloblang -// functions. -func BloblangFunctionsMarkdown() ([]byte, error) { - ctx := functionsContext{} - - specs := query.FunctionDocs() - for _, s := range specs { - prefixExamples(s.Examples) - } - - for _, cat := range []string{ - query.FunctionCategoryGeneral, - query.FunctionCategoryMessage, - query.FunctionCategoryEnvironment, - query.FunctionCategoryFakeData, - query.FunctionCategoryDeprecated, - } { - functions := functionCategory{ - Name: cat, - } - for _, spec := range specs { - if spec.Category == cat { - functions.Specs = append(functions.Specs, spec) - } - } - if len(functions.Specs) > 0 { - ctx.Categories = append(ctx.Categories, functions) - } - } - - var buf bytes.Buffer - err := template.Must(template.New("functions").Parse(bloblangFunctionsTemplate)).Execute(&buf, ctx) - - return buf.Bytes(), err -} - -//------------------------------------------------------------------------------ - -type methodCategory struct { - Name string - Specs []query.MethodSpec -} - -type methodsContext struct { - Categories []methodCategory - General []query.MethodSpec -} - -var bloblangMethodsTemplate = bloblangParamsTemplate + `{{define "method_example" -}} -{{if gt (len .Summary) 0 -}} -{{.Summary}} - -{{end -}} - -` + "```coffeescript" + ` -{{.Mapping}} -{{range $i, $result := .Results}} -# In: {{index $result 0}} -# Out: {{index $result 1}} -{{end -}} -` + "```" + ` -{{end -}} - -{{define "method_spec" -}} -=== ` + "`{{.Name}}`" + ` - -{{if eq .Status "beta" -}} -[CAUTION] -.Beta -==== -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -==== -{{end -}} -{{if eq .Status "experimental" -}} -[CAUTION] -.Experimental -==== -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -==== -{{end -}} -{{.Description}}{{if gt (len .Version) 0}} - -Introduced in version {{.Version}}. -{{end}} -{{template "parameters" .Params -}} -{{if gt (len .Examples) 0}} -==== Examples - -{{range $i, $example := .Examples}} -{{template "method_example" $example -}} -{{end -}} -{{end -}} - -{{end -}} - -= Bloblang Methods -:description: A list of Bloblang methods - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the contents of: - internal/bloblang/query/methods.go - internal/bloblang/query/methods_strings.go - internal/docs/bloblang.go -//// - - -Methods provide most of the power in Bloblang as they allow you to augment values and can be added to any expression (including other methods): - -` + "```coffeescript" + ` -root.doc.id = this.thing.id.string().catch(uuid_v4()) -root.doc.reduced_nums = this.thing.nums.map_each(num -> if num < 10 { - deleted() -} else { - num - 10 -}) -root.has_good_taste = ["pikachu","mewtwo","magmar"].contains(this.user.fav_pokemon) -` + "```" + ` - -Methods support both named and nameless style arguments: - -` + "```coffeescript" + ` -root.foo_one = this.(bar | baz).trim().replace_all(old: "dog", new: "cat") -root.foo_two = this.(bar | baz).trim().replace_all("dog", "cat") -` + "```" + ` - -{{if gt (len .General) 0 -}} -== General - -{{range $i, $spec := .General -}} -{{template "method_spec" $spec}} -{{end -}} -{{end -}} - -{{range $i, $cat := .Categories -}} -== {{$cat.Name}} - -{{range $i, $spec := $cat.Specs -}} -{{template "method_spec" $spec}} -{{end -}} -{{end -}} -` - -func methodForCat(s query.MethodSpec, cat string) (query.MethodSpec, bool) { - for _, c := range s.Categories { - if c.Category == cat { - spec := s - if c.Description != "" { - spec.Description = strings.TrimSpace(c.Description) - } - if len(c.Examples) > 0 { - spec.Examples = c.Examples - } - return spec, true - } - } - return s, false -} - -// BloblangMethodsMarkdown returns a markdown document for all Bloblang methods. -func BloblangMethodsMarkdown() ([]byte, error) { - ctx := methodsContext{} - - specs := query.MethodDocs() - for _, s := range specs { - prefixExamples(s.Examples) - for _, cat := range s.Categories { - prefixExamples(cat.Examples) - } - } - - for _, cat := range []string{ - query.MethodCategoryStrings, - query.MethodCategoryRegexp, - query.MethodCategoryNumbers, - query.MethodCategoryTime, - query.MethodCategoryCoercion, - query.MethodCategoryObjectAndArray, - query.MethodCategoryParsing, - query.MethodCategoryEncoding, - query.MethodCategoryJWT, - query.MethodCategoryGeoIP, - query.MethodCategoryDeprecated, - } { - methods := methodCategory{ - Name: cat, - } - for _, spec := range specs { - var ok bool - if spec, ok = methodForCat(spec, cat); ok { - methods.Specs = append(methods.Specs, spec) - } - } - if len(methods.Specs) > 0 { - ctx.Categories = append(ctx.Categories, methods) - } - } - - for _, spec := range specs { - if len(spec.Categories) == 0 && spec.Status != query.StatusHidden { - spec.Description = strings.TrimSpace(spec.Description) - ctx.General = append(ctx.General, spec) - } - } - - var buf bytes.Buffer - err := template.Must(template.New("methods").Parse(bloblangMethodsTemplate)).Execute(&buf, ctx) - - return buf.Bytes(), err -} diff --git a/internal/docs/bloblang_test.go b/internal/docs/bloblang_test.go deleted file mode 100644 index d32fdd7568..0000000000 --- a/internal/docs/bloblang_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package docs_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func TestLintBloblangMapping(t *testing.T) { - type Test struct { - mapping string - line int - col int - wantLints []docs.Lint - } - tests := map[string]Test{ - "mapping": { - mapping: `this.foo = "bar"`, - line: 0, - col: 0, - }, - "empty mapping": { - mapping: ``, - line: 0, - col: 0, - }, - "invalid mapping": { - mapping: `this.foo = #`, - line: 2, - col: 4, - wantLints: []docs.Lint{ - { - Line: 2, - Column: 16, - Level: docs.LintError, - Type: docs.LintBadBloblang, - What: `expected query, got: #`, - }, - }, - }, - } - - ctx := docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)) - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - gotLints := docs.LintBloblangMapping(ctx, test.line, test.col, test.mapping) - require.EqualValues(t, test.wantLints, gotLints) - }) - } -} - -func TestLintBloblangField(t *testing.T) { - type Test struct { - mapping string - line int - col int - wantLints []docs.Lint - } - tests := map[string]Test{ - "static string field": { - mapping: `foobar`, - line: 0, - col: 0, - }, - "empty field": { - mapping: ``, - line: 0, - col: 0, - }, - "interpolated field": { - mapping: `${! json() }`, - line: 0, - col: 0, - }, - "invalid interpolated field": { - mapping: `${! whoopsie{} }`, - line: 2, - col: 4, - wantLints: []docs.Lint{ - { - Line: 2, - Column: 17, - Level: docs.LintError, - Type: docs.LintBadBloblang, - What: `required: expected end of expression, got: {} }`, - }, - }, - }, - "invalid empty interpolated field": { - mapping: `${! }`, - line: 2, - col: 4, - wantLints: []docs.Lint{ - { - Line: 2, - Column: 9, - Level: docs.LintError, - Type: docs.LintBadBloblang, - What: `required: expected query, got: }`, - }, - }, - }, - } - - ctx := docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)) - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - gotLints := docs.LintBloblangField(ctx, test.line, test.col, test.mapping) - require.EqualValues(t, test.wantLints, gotLints) - }) - } -} diff --git a/internal/docs/component.go b/internal/docs/component.go deleted file mode 100644 index 318d8bbaf6..0000000000 --- a/internal/docs/component.go +++ /dev/null @@ -1,108 +0,0 @@ -package docs - -import ( - "bytes" - - "gopkg.in/yaml.v3" -) - -// Copied from ./internal/config/format.go. -func marshalYAML(v any) ([]byte, error) { - var cbytes bytes.Buffer - enc := yaml.NewEncoder(&cbytes) - enc.SetIndent(2) - if err := enc.Encode(v); err != nil { - return nil, err - } - return cbytes.Bytes(), nil -} - -// AnnotatedExample is an isolated example for a component. -type AnnotatedExample struct { - // A title for the example. - Title string `json:"title"` - - // Summary of the example. - Summary string `json:"summary"` - - // A config snippet to show. - Config string `json:"config"` -} - -// Status of a component. -type Status string - -// Component statuses. -var ( - StatusStable Status = "stable" - StatusBeta Status = "beta" - StatusExperimental Status = "experimental" - StatusDeprecated Status = "deprecated" -) - -// Type of a component. -type Type string - -// Component types. -var ( - TypeBuffer Type = "buffer" - TypeCache Type = "cache" - TypeInput Type = "input" - TypeMetrics Type = "metrics" - TypeOutput Type = "output" - TypeProcessor Type = "processor" - TypeRateLimit Type = "rate_limit" - TypeTracer Type = "tracer" - TypeScanner Type = "scanner" -) - -// Types returns a slice containing all component types. -func Types() []Type { - return []Type{ - TypeBuffer, - TypeCache, - TypeInput, - TypeMetrics, - TypeOutput, - TypeProcessor, - TypeRateLimit, - TypeTracer, - TypeScanner, - } -} - -// ComponentSpec describes a Benthos component. -type ComponentSpec struct { - // Name of the component - Name string `json:"name"` - - // Type of the component (input, output, etc) - Type Type `json:"type"` - - // The status of the component. - Status Status `json:"status"` - - // Plugin is true for all plugin components. - Plugin bool `json:"plugin"` - - // Summary of the component (in Asciidoc, must be short). - Summary string `json:"summary,omitempty"` - - // Description of the component (in Asciidoc). - Description string `json:"description,omitempty"` - - // Categories that describe the purpose of the component. - Categories []string `json:"categories"` - - // Footnotes of the component (in Asciidoc). - Footnotes string `json:"footnotes,omitempty"` - - // Examples demonstrating use cases for the component. - Examples []AnnotatedExample `json:"examples,omitempty"` - - // A summary of each field in the component configuration. - Config FieldSpec `json:"config"` - - // Version is the Benthos version this component was introduced. - Version string `json:"version,omitempty"` -} diff --git a/internal/docs/component_markdown.go b/internal/docs/component_markdown.go deleted file mode 100644 index 2a2469cad4..0000000000 --- a/internal/docs/component_markdown.go +++ /dev/null @@ -1,245 +0,0 @@ -package docs - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "text/template" - - "gopkg.in/yaml.v3" -) - -type componentContext struct { - Name string - Type string - FrontMatterSummary string - Summary string - Description string - Categories string - Examples []AnnotatedExample - Fields []FieldSpecCtx - Footnotes string - CommonConfig string - AdvancedConfig string - Status string - Version string -} - -var componentTemplate = FieldsTemplate(false) + ` -= {{.Name}} -:type: {{.Type}} -:status: {{.Status}} -{{if gt (len .FrontMatterSummary) 0 -}} -:description: "{{.FrontMatterSummary}}" -{{end -}} -{{if gt (len .Categories) 0 -}} -:categories: {{.Categories}} -{{end}} - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the corresponding source file under internal/impl/. -//// - - -component_type_dropdown::[] - - -{{if eq .Status "beta" -}} - -{{end -}} -{{if eq .Status "experimental" -}} - -{{end -}} -{{if eq .Status "deprecated" -}} -[WARNING] -.Deprecated -==== -This component is deprecated and will be removed in the next major version release. Please consider moving onto <>. -==== -{{end -}} - - -{{if gt (len .Summary) 0 -}} -{{.Summary}} -{{end -}}{{if gt (len .Version) 0}} -Introduced in version {{.Version}}. -{{end}} -{{if eq .CommonConfig .AdvancedConfig -}} -` + "```yml" + ` -# Config fields, showing default values -{{.CommonConfig -}} -` + "```" + ` -{{else}} -[tabs] -====== -Common:: -+ --- - -` + "```yml" + ` -# Common config fields, showing default values -{{.CommonConfig -}} -` + "```" + ` - --- -Advanced:: -+ --- - -` + "```yml" + ` -# All config fields, showing default values -{{.AdvancedConfig -}} -` + "```" + ` - --- -====== -{{end -}} -{{if gt (len .Description) 0}} -{{.Description}} -{{end}} -{{if and (le (len .Fields) 4) (gt (len .Fields) 0) -}} -== Fields - -{{template "field_docs" . -}} -{{end -}} - -{{if gt (len .Examples) 0 -}} -== Examples - -[tabs] -====== -{{range $i, $example := .Examples -}} -{{$example.Title}}:: -+ --- - -{{if gt (len $example.Summary) 0 -}} -{{$example.Summary}} -{{end}} -{{if gt (len $example.Config) 0 -}} -` + "```yaml" + `{{$example.Config}}` + "```" + ` -{{end}} --- -{{end -}} -====== - -{{end -}} - -{{if gt (len .Fields) 4 -}} -== Fields - -{{template "field_docs" . -}} -{{end -}} - -{{if gt (len .Footnotes) 0 -}} -{{.Footnotes}} -{{end}} -` - -func createOrderedConfig(prov Provider, t Type, rawExample any, filter FieldFilter) (*yaml.Node, error) { - var newNode yaml.Node - if err := newNode.Encode(rawExample); err != nil { - return nil, err - } - - sanitConf := NewSanitiseConfig(prov) - sanitConf.RemoveTypeField = true - sanitConf.Filter = filter - sanitConf.ForExample = true - if err := SanitiseYAML(t, &newNode, sanitConf); err != nil { - return nil, err - } - - return &newNode, nil -} - -func genExampleConfigs(prov Provider, t Type, nest bool, fullConfigExample any) (commonConfigStr, advConfigStr string, err error) { - var advConfig, commonConfig any - if advConfig, err = createOrderedConfig(prov, t, fullConfigExample, func(f FieldSpec, _ any) bool { - return !f.IsDeprecated - }); err != nil { - panic(err) - } - if commonConfig, err = createOrderedConfig(prov, t, fullConfigExample, func(f FieldSpec, _ any) bool { - return !f.IsAdvanced && !f.IsDeprecated - }); err != nil { - panic(err) - } - - if nest { - advConfig = map[string]any{string(t): advConfig} - commonConfig = map[string]any{string(t): commonConfig} - } - - advancedConfigBytes, err := marshalYAML(advConfig) - if err != nil { - panic(err) - } - commonConfigBytes, err := marshalYAML(commonConfig) - if err != nil { - panic(err) - } - - return string(commonConfigBytes), string(advancedConfigBytes), nil -} - -// AsMarkdown renders the spec of a component, along with a full configuration -// example, into a markdown document. -func (c *ComponentSpec) AsMarkdown(prov Provider, nest bool, fullConfigExample any) ([]byte, error) { - if strings.Contains(c.Summary, "\n\n") { - return nil, fmt.Errorf("%v component '%v' has a summary containing empty lines", c.Type, c.Name) - } - - ctx := componentContext{ - Name: c.Name, - Type: string(c.Type), - Summary: c.Summary, - Description: c.Description, - Examples: c.Examples, - Footnotes: c.Footnotes, - Status: string(c.Status), - Version: c.Version, - } - if ctx.Status == "" { - ctx.Status = string(StatusStable) - } - - if len(c.Categories) > 0 { - cats, _ := json.Marshal(c.Categories) - ctx.Categories = string(cats) - } - - var err error - if ctx.CommonConfig, ctx.AdvancedConfig, err = genExampleConfigs(prov, c.Type, nest, fullConfigExample); err != nil { - return nil, err - } - - if c.Description != "" && c.Description[0] == '\n' { - ctx.Description = c.Description[1:] - } - if c.Footnotes != "" && c.Footnotes[0] == '\n' { - ctx.Footnotes = c.Footnotes[1:] - } - - flattenedFields := c.Config.FlattenChildrenForDocs() - for _, v := range flattenedFields { - if v.Spec.Kind == KindMap { - v.Spec.Type = "object" - } else if v.Spec.Kind == KindArray { - v.Spec.Type = "array" - } else if v.Spec.Kind == Kind2DArray { - v.Spec.Type = "two-dimensional array" - } - v.Spec.Kind = KindScalar - ctx.Fields = append(ctx.Fields, v) - } - - var buf bytes.Buffer - err = template.Must(template.New("component").Parse(componentTemplate)).Execute(&buf, ctx) - - return buf.Bytes(), err -} diff --git a/internal/docs/config.go b/internal/docs/config.go deleted file mode 100644 index ca5fa56cb1..0000000000 --- a/internal/docs/config.go +++ /dev/null @@ -1,143 +0,0 @@ -package docs - -import ( - "fmt" - "regexp" - "sort" - "strings" - - "github.com/Jeffail/gabs/v2" -) - -const labelExpression = `^[a-z0-9_]+$` - -var ( - labelRe = regexp.MustCompile(labelExpression) - - // ErrBadLabel is returned when creating a component with a bad label. - ErrBadLabel = fmt.Errorf("should match the regular expression /%v/ and must not start with an underscore", labelExpression) -) - -// ValidateLabel attempts to validate the contents of a component label. -func ValidateLabel(label string) error { - if strings.HasPrefix(label, "_") { - return ErrBadLabel - } - if !labelRe.MatchString(label) { - return ErrBadLabel - } - return nil -} - -var labelField = FieldString( - "label", "An optional label to use as an identifier for observability data such as metrics and logging.", -).OmitWhen(func(field, parent any) (string, bool) { - gObj := gabs.Wrap(parent) - if typeStr, exists := gObj.S("type").Data().(string); exists && typeStr == "resource" { - return "label field should be omitted when pointing to a resource", true - } - if resourceStr, exists := gObj.S("resource").Data().(string); exists && resourceStr != "" { - return "label field should be omitted when pointing to a resource", true - } - return "", false -}).AtVersion("3.44.0").LinterFunc(func(ctx LintContext, line, col int, v any) []Lint { - l, _ := v.(string) - if l == "" { - return nil - } - if err := ValidateLabel(l); err != nil { - return []Lint{ - NewLintError(line, LintBadLabel, fmt.Errorf("invalid label '%v': %w", l, err)), - } - } - prevLine, exists := ctx.labelsToLine[l] - if exists { - return []Lint{ - NewLintError(line, LintDuplicateLabel, fmt.Errorf("label '%v' collides with a previously defined label at line %v", l, prevLine)), - } - } - ctx.labelsToLine[l] = line - return nil -}).HasDefault("") - -// ReservedFieldsByType returns a map of fields for a specific type. -func ReservedFieldsByType(t Type) map[string]FieldSpec { - m := map[string]FieldSpec{ - "type": FieldString("type", ""), - "plugin": FieldObject("plugin", ""), - } - if t == TypeInput || t == TypeOutput { - m["processors"] = FieldProcessor("processors", "").Array().OmitWhen(func(field, _ any) (string, bool) { - if arr, ok := field.([]any); ok && len(arr) == 0 { - return "field processors is empty and can be removed", true - } - return "", false - }) - } - if t == TypeMetrics { - m["mapping"] = MetricsMappingFieldSpec("mapping") - } - if _, isLabelType := map[Type]struct{}{ - TypeInput: {}, - TypeProcessor: {}, - TypeOutput: {}, - TypeCache: {}, - TypeRateLimit: {}, - }[t]; isLabelType { - m["label"] = labelField - } - return m -} - -func getInferenceCandidateFromList(docProvider Provider, t Type, l []string) (string, ComponentSpec, error) { - ignore := ReservedFieldsByType(t) - - var candidates []string - var inferred string - var inferredSpec ComponentSpec - for _, k := range l { - if _, exists := ignore[k]; exists { - continue - } - candidates = append(candidates, k) - if spec, exists := docProvider.GetDocs(k, t); exists { - if inferred != "" { - candidates = []string{inferred, k} - sort.Strings(candidates) - return "", ComponentSpec{}, fmt.Errorf( - "unable to infer %v type, multiple candidates '%v' and '%v'", string(t), candidates[0], candidates[1], - ) - } - inferred = k - inferredSpec = spec - } - } - - if len(candidates) == 0 { - return "", ComponentSpec{}, fmt.Errorf("an explicit %v type must be specified", string(t)) - } - - if inferred == "" { - sort.Strings(candidates) - return "", ComponentSpec{}, fmt.Errorf("unable to infer %v type from candidates: %v", string(t), candidates) - } - return inferred, inferredSpec, nil -} - -// SanitiseConfig contains fields describing the desired behaviour of the config -// sanitiser such as removing certain fields. -type SanitiseConfig struct { - RemoveTypeField bool - RemoveDeprecated bool - ScrubSecrets bool - ForExample bool - Filter FieldFilter - DocsProvider Provider -} - -// NewSanitiseConfig creates a new sanitise config. -func NewSanitiseConfig(prov Provider) SanitiseConfig { - return SanitiseConfig{ - DocsProvider: prov, - } -} diff --git a/internal/docs/config_test.go b/internal/docs/config_test.go deleted file mode 100644 index fa1dbbfe57..0000000000 --- a/internal/docs/config_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package docs_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func TestInference(t *testing.T) { - docsProv := docs.NewMappedDocsProvider() - docsProv.RegisterDocs(docs.ComponentSpec{ - Name: "stdin", - Type: docs.TypeInput, - }) - for _, t := range docs.Types() { - docsProv.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testfoo%v", string(t)), - Type: t, - }) - docsProv.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testbar%v", string(t)), - Type: t, - }) - } - - type testCase struct { - inputType docs.Type - inputConf any - - res string - err string - } - - tests := []testCase{ - { - inputType: docs.TypeOutput, - inputConf: map[string]any{ - "processors": "yep", - }, - err: "an explicit output type must be specified", - }, - { - inputType: docs.TypeOutput, - inputConf: map[string]any{ - "foo": "yep", - "bar": "yep", - "processors": "yep", - }, - err: "unable to infer output type from candidates: [bar foo]", - }, - { - inputType: docs.TypeInput, - inputConf: map[string]any{ - "foo": "yep", - "bar": "yep", - "processors": "yep", - }, - err: "unable to infer input type from candidates: [bar foo]", - }, - { - inputType: docs.TypeTracer, - inputConf: map[string]any{ - "testbartracer": "baz", - "testbarbuffer": "baz", - }, - res: "testbartracer", - }, - { - inputType: docs.TypeRateLimit, - inputConf: map[string]any{ - "testbarrate_limit": "baz", - "testbarbuffer": "baz", - }, - res: "testbarrate_limit", - }, - { - inputType: docs.TypeProcessor, - inputConf: map[string]any{ - "testbarprocessor": "baz", - "testbarbuffer": "baz", - }, - res: "testbarprocessor", - }, - { - inputType: docs.TypeOutput, - inputConf: map[string]any{ - "testbaroutput": "baz", - "testbarbuffer": "baz", - }, - res: "testbaroutput", - }, - { - inputType: docs.TypeMetrics, - inputConf: map[string]any{ - "testfoometrics": "baz", - "testbarbuffer": "baz", - }, - res: "testfoometrics", - }, - { - inputType: docs.TypeInput, - inputConf: map[string]any{ - "testfooinput": "baz", - "testbarbuffer": "baz", - }, - res: "testfooinput", - }, - { - inputType: docs.TypeCache, - inputConf: map[string]any{ - "testfoocache": "baz", - "testbarbuffer": "baz", - }, - res: "testfoocache", - }, - { - inputType: docs.TypeBuffer, - inputConf: map[string]any{ - "testfoobuffer": "baz", - "testbarbuffer": "baz", - }, - err: "unable to infer buffer type, multiple candidates 'testbarbuffer' and 'testfoobuffer'", - }, - { - inputType: docs.TypeBuffer, - inputConf: map[string]any{ - "testfoobuffer": "baz", - }, - res: "testfoobuffer", - }, - { - inputType: docs.TypeBuffer, - inputConf: map[string]any{ - "type": "testfoobuffer", - "foobar": "baz", - }, - res: "testfoobuffer", - }, - { - inputType: docs.TypeBuffer, - inputConf: map[string]any{ - "type": "notreal", - "foobar": "baz", - }, - err: "buffer type 'notreal' was not recognised", - }, - { - inputType: docs.TypeBuffer, - err: "invalid config value , expected object", - }, - } - - for i, test := range tests { - var node yaml.Node - require.NoError(t, node.Encode(test.inputConf)) - res, spec, err := docs.GetInferenceCandidateFromYAML(docsProv, test.inputType, &node) - if test.err != "" { - assert.Error(t, err, "test: %v", i) - } else { - assert.Equal(t, test.res, spec.Name, "test: %v", i) - assert.Equal(t, test.inputType, spec.Type, "test: %v", i) - assert.NoError(t, err, "test: %v", i) - assert.Equal(t, test.res, res, "test: %v", i) - } - } -} diff --git a/internal/docs/field.go b/internal/docs/field.go deleted file mode 100644 index dd64c2285f..0000000000 --- a/internal/docs/field.go +++ /dev/null @@ -1,864 +0,0 @@ -package docs - -import ( - "errors" - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// FieldType represents a field type. -type FieldType string - -// ValueType variants. -var ( - FieldTypeString FieldType = "string" - FieldTypeInt FieldType = "int" - FieldTypeFloat FieldType = "float" - FieldTypeBool FieldType = "bool" - FieldTypeObject FieldType = "object" - FieldTypeUnknown FieldType = "unknown" - - // Core component types, only components that can be a child of another - // component config are listed here. - FieldTypeInput FieldType = "input" - FieldTypeBuffer FieldType = "buffer" - FieldTypeCache FieldType = "cache" - FieldTypeProcessor FieldType = "processor" - FieldTypeRateLimit FieldType = "rate_limit" - FieldTypeOutput FieldType = "output" - FieldTypeMetrics FieldType = "metrics" - FieldTypeTracer FieldType = "tracer" - FieldTypeScanner FieldType = "scanner" -) - -// IsCoreComponent returns the core component type of a field if applicable. -func (t FieldType) IsCoreComponent() (Type, bool) { - switch t { - case FieldTypeInput: - return TypeInput, true - case FieldTypeBuffer: - return TypeBuffer, true - case FieldTypeCache: - return TypeCache, true - case FieldTypeProcessor: - return TypeProcessor, true - case FieldTypeRateLimit: - return TypeRateLimit, true - case FieldTypeOutput: - return TypeOutput, true - case FieldTypeTracer: - return TypeTracer, true - case FieldTypeMetrics: - return TypeMetrics, true - case FieldTypeScanner: - return TypeScanner, true - } - return "", false -} - -// FieldKind represents a field kind. -type FieldKind string - -// ValueType variants. -var ( - KindScalar FieldKind = "scalar" - KindArray FieldKind = "array" - Kind2DArray FieldKind = "2darray" - KindMap FieldKind = "map" -) - -//------------------------------------------------------------------------------ - -// FieldSpec describes a component config field. -type FieldSpec struct { - // Name of the field (as it appears in config). - Name string `json:"name"` - - // Type of the field. - Type FieldType `json:"type"` - - // Kind of the field. - Kind FieldKind `json:"kind"` - - // Description of the field purpose (in Asciidoc). - Description string `json:"description,omitempty"` - - // IsAdvanced is true for optional fields that will not be present in most - // configs. - IsAdvanced bool `json:"is_advanced,omitempty"` - - // IsDeprecated is true for fields that are deprecated and only exist - // for backwards compatibility reasons. - IsDeprecated bool `json:"is_deprecated,omitempty"` - - // IsOptional is a boolean flag indicating that a field is optional, even - // if there is no default. This prevents linting errors when the field - // is missing. - IsOptional bool `json:"is_optional,omitempty"` - - // IsSecret indicates whether the field represents information that is - // generally considered sensitive such as passwords or access tokens. - IsSecret bool `json:"is_secret,omitempty"` - - // Default value of the field. - Default *any `json:"default,omitempty"` - - // Interpolation indicates that the field supports interpolation - // functions. - Interpolated bool `json:"interpolated,omitempty"` - - // Bloblang indicates that a string field is a Bloblang mapping. - Bloblang bool `json:"bloblang,omitempty"` - - // Examples is a slice of optional example values for a field. - Examples []any `json:"examples,omitempty"` - - // AnnotatedOptions for this field. Each option should have a summary. - AnnotatedOptions [][2]string `json:"annotated_options,omitempty"` - - // Options for this field. - Options []string `json:"options,omitempty"` - - // Children fields of this field (it must be an object). - Children FieldSpecs `json:"children,omitempty"` - - // Version is an explicit version when this field was introduced. - Version string `json:"version,omitempty"` - - // Linter is an optional bloblang mapping that should be used in order to - // lint a field. - Linter string `json:"linter,omitempty"` - - // Scrubber is an optional bloblang mapping that should be used in order to - // scrub sensitive information from field values when echoed. - Scrubber string `json:"scrubber,omitempty"` - - omitWhenFn func(field, parent any) (why string, shouldOmit bool) - customLintFn LintFunc -} - -// IsInterpolated indicates that the field supports interpolation functions. -func (f FieldSpec) IsInterpolated() FieldSpec { - f.Interpolated = true - return f -} - -// IsBloblang indicates that the field is a Bloblang mapping. -func (f FieldSpec) IsBloblang() FieldSpec { - f.Bloblang = true - return f -} - -// HasType returns a new FieldSpec that specifies a specific type. -func (f FieldSpec) HasType(t FieldType) FieldSpec { - f.Type = t - return f -} - -// Optional marks this field as being optional, and therefore its absence in a -// config is not considered an error even when a default value is not provided. -func (f FieldSpec) Optional() FieldSpec { - f.IsOptional = true - return f -} - -const bloblREEnvVar = `\${[0-9A-Za-z_.]+(:((\${[^}]+})|[^}])*)?}` - -// Secret marks this field as being a secret, which means it represents -// information that is generally considered sensitive such as passwords or -// access tokens. -func (f FieldSpec) Secret() FieldSpec { - f.IsSecret = true - f.Scrubber = fmt.Sprintf(`root = if this != null && this != "" && !this.trim().re_match("""^%v$""") { - "!!!SECRET_SCRUBBED!!!" -} else if this == null { "" }`, bloblREEnvVar) - return f -} - -// Advanced marks this field as being advanced, and therefore not commonly used. -func (f FieldSpec) Advanced() FieldSpec { - f.IsAdvanced = true - for i, v := range f.Children { - f.Children[i] = v.Advanced() - } - return f -} - -// Deprecated marks this field as being deprecated. -func (f FieldSpec) Deprecated() FieldSpec { - f.IsDeprecated = true - for i, v := range f.Children { - f.Children[i] = v.Deprecated() - } - return f -} - -// Array determines that this field is an array of the field type. -func (f FieldSpec) Array() FieldSpec { - f.Kind = KindArray - return f -} - -// ArrayOfArrays determines that this is an array of arrays of the field type. -func (f FieldSpec) ArrayOfArrays() FieldSpec { - f.Kind = Kind2DArray - return f -} - -// Map determines that this field is a map of arbitrary keys to a field type. -func (f FieldSpec) Map() FieldSpec { - f.Kind = KindMap - return f -} - -// Scalar determines that this field is a scalar type (the default). -func (f FieldSpec) Scalar() FieldSpec { - f.Kind = KindScalar - return f -} - -// HasDefault returns a new FieldSpec that specifies a default value. -func (f FieldSpec) HasDefault(v any) FieldSpec { - f.Default = &v - return f -} - -// AtVersion specifies the version at which this fields behaviour was last -// modified. -func (f FieldSpec) AtVersion(v string) FieldSpec { - f.Version = v - return f -} - -// HasAnnotatedOptions returns a new FieldSpec that specifies a specific list of -// annotated options. Field values are linted to ensure they match one of the -// given options by a case insensitive match, use a custom lint function in -// order to change this default behaviour. -func (f FieldSpec) HasAnnotatedOptions(options ...string) FieldSpec { - if len(f.Options) > 0 { - panic("cannot combine annotated and non-annotated options for a field") - } - if len(options)%2 != 0 { - panic("annotated field options must each have a summary") - } - for i := 0; i < len(options); i += 2 { - f.AnnotatedOptions = append(f.AnnotatedOptions, [2]string{ - options[i], options[i+1], - }) - } - return f.lintOptions(false) -} - -// HasOptions returns a new FieldSpec that specifies a specific list of options. -// Field values are linted to ensure they match one of the given options by a -// case insensitive match, use a custom lint function in order to change this -// default behaviour. -func (f FieldSpec) HasOptions(options ...string) FieldSpec { - if len(f.AnnotatedOptions) > 0 { - panic("cannot combine annotated and non-annotated options for a field") - } - f.Options = options - return f.lintOptions(false) -} - -// WithChildren returns a new FieldSpec that has child fields. -func (f FieldSpec) WithChildren(children ...FieldSpec) FieldSpec { - if len(f.Type) == 0 { - f.Type = FieldTypeObject - } - if f.IsAdvanced { - for i, v := range children { - children[i] = v.Advanced() - } - } - f.Children = append(f.Children, children...) - return f -} - -// OmitWhen specifies a custom func that, when provided a generic config struct, -// returns a boolean indicating when the field can be safely omitted from a -// config. -func (f FieldSpec) OmitWhen(fn func(field, parent any) (why string, shouldOmit bool)) FieldSpec { - f.omitWhenFn = fn - return f -} - -// LinterFunc adds a linting function to a field. When linting is performed on a -// config the provided function will be called with a boxed variant of the field -// value, allowing it to perform linting on that value. -// -// It is important to note that for fields defined as a non-scalar (array, -// array of arrays, map, etc) the linting rule will be executed on the highest -// level (array) and also the individual scalar values. If your field is a high -// level type then make sure your linting rule checks the type of the value -// provided in order to limit when the linting is performed. -// -// Note that a linting rule defined this way will only be effective in the -// binary that defines it as the function cannot be serialized into a portable -// schema. -func (f FieldSpec) LinterFunc(fn LintFunc) FieldSpec { - f.Linter = "" - f.customLintFn = fn - return f -} - -func lintsFromAny(line int, v any) (lints []Lint) { - switch t := v.(type) { - case []any: - for _, e := range t { - lints = append(lints, lintsFromAny(line, e)...) - } - case map[string]any: - // Note: this is a long winded way to do IGetInt from the internal - // package, I'm doing it so that this package no longer depends on other - // internal packages (when possible). - var typeInt int64 - _ = bloblang.NewArgSpec().Int64Var(&typeInt).Extract([]any{t["type"]}) - lints = append(lints, NewLintError(line, LintType(typeInt), errors.New(t["what"].(string)))) - case string: - if t != "" { - lints = append(lints, NewLintError(line, LintCustom, errors.New(t))) - } - } - return -} - -// LinterBlobl adds a linting function to a field. When linting is performed on -// a config the provided bloblang mapping will be called with a boxed variant of -// the field value, allowing it to perform linting on that value, where an array -// of lints (strings) should be returned. -// -// It is important to note that for fields defined as a non-scalar (array, -// array of arrays, map, etc) the linting rule will be executed on the highest -// level (array) and also the individual scalar values. If your field is a high -// level type then make sure your linting rule checks the type of the value -// provided in order to limit when the linting is performed. -// -// Note that a linting rule defined this way will only be effective in the -// binary that defines it as the function cannot be serialized into a portable -// schema. -func (f FieldSpec) LinterBlobl(blobl string) FieldSpec { - if blobl == "" { - f.Linter = blobl - f.customLintFn = nil - return f - } - - env := bloblang.NewEnvironment().OnlyPure() - - m, err := env.Parse(blobl) - if err != nil { - f.customLintFn = func(ctx LintContext, line, col int, value any) (lints []Lint) { - return []Lint{NewLintError(line, LintCustom, fmt.Errorf("field lint mapping itself failed to parse: %w", err))} - } - return f - } - - f.Linter = blobl - f.customLintFn = func(ctx LintContext, line, col int, value any) (lints []Lint) { - var res any - err := m.Overlay(value, &res) - if err != nil { - if errors.Is(err, bloblang.ErrRootDeleted) { - return - } - return []Lint{NewLintError(line, LintCustom, err)} - } - lints = append(lints, lintsFromAny(line, res)...) - return - } - return f -} - -// lintOptions enforces that a field value matches one of the provided options -// and returns a linting error if that is not the case. This is currently opt-in -// because some fields express options that are only a subset due to deprecated -// functionality. -func (f FieldSpec) lintOptions(caseSensitive bool) FieldSpec { - var optionsBuilder strings.Builder - _, _ = optionsBuilder.WriteString("{\n") - addFn := func(o string) { - if !caseSensitive { - o = strings.ToLower(o) - } - _, _ = fmt.Fprintf(&optionsBuilder, " %q: true,\n", o) - } - - for _, o := range f.Options { - addFn(o) - } - for _, kv := range f.AnnotatedOptions { - addFn(kv[0]) - } - _, _ = optionsBuilder.WriteString("}\n") - - maybeLowerCase := "" - if !caseSensitive { - maybeLowerCase = ".lowercase()" - } - - f.Linter = fmt.Sprintf(` -let options = %v -root = if !$options.exists(this.string()%v) { - {"type": 2, "what": "value %%v is not a valid option for this field".format(this.string())} -} -`, optionsBuilder.String(), maybeLowerCase) - return f -} - -func (f FieldSpec) ScrubValue(v any) (any, error) { - if f.Scrubber == "" { - return v, nil - } - - env := bloblang.NewEnvironment().OnlyPure() - - m, err := env.Parse(f.Scrubber) - if err != nil { - return nil, fmt.Errorf("scrubber mapping failed to parse: %w", err) - } - - res, err := m.Query(v) - if err != nil { - if errors.Is(err, bloblang.ErrRootDeleted) { - return nil, nil - } - return nil, err - } - return res, nil -} - -func (f FieldSpec) GetLintFunc() LintFunc { - fn := f.customLintFn - if fn == nil && f.Linter != "" { - fn = f.LinterBlobl(f.Linter).customLintFn - } - if f.Interpolated { - if fn != nil { - innerFn := fn - fn = func(ctx LintContext, line, col int, value any) []Lint { - lints := innerFn(ctx, line, col, value) - moreLints := LintBloblangField(ctx, line, col, value) - return append(lints, moreLints...) - } - } else { - fn = LintBloblangField - } - } - if f.Bloblang { - if fn != nil { - innerFn := fn - fn = func(ctx LintContext, line, col int, value any) []Lint { - lints := innerFn(ctx, line, col, value) - moreLints := LintBloblangMapping(ctx, line, col, value) - return append(lints, moreLints...) - } - } else { - fn = LintBloblangMapping - } - } - return fn -} - -// FieldAnything returns a field spec for any typed field. -func FieldAnything(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeUnknown) -} - -// FieldObject returns a field spec for an object typed field. -func FieldObject(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeObject) -} - -// FieldString returns a field spec for a common string typed field. -func FieldString(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeString) -} - -// FieldInterpolatedString returns a field spec for a string typed field -// supporting dynamic interpolated functions. -func FieldInterpolatedString(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeString).IsInterpolated() -} - -// FieldBloblang returns a field spec for a string typed field containing a -// Bloblang mapping. -func FieldBloblang(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeString).IsBloblang() -} - -// FieldInt returns a field spec for a common int typed field. -func FieldInt(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeInt) -} - -// FieldFloat returns a field spec for a common float typed field. -func FieldFloat(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeFloat) -} - -// FieldBool returns a field spec for a common bool typed field. -func FieldBool(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeBool) -} - -// FieldURL returns a field spec for a string typed field containing a URL, both -// linting rules and scrubbers are added. -func FieldURL(name, description string, examples ...any) FieldSpec { - f := newField(name, description, examples...).HasType(FieldTypeString) /*.LinterBlobl(` - root = this.parse_url().(deleted()).catch(err -> err) - `)*/ - f.Scrubber = fmt.Sprintf(` -let pass = this.parse_url().user.password.or("") -root = if $pass != "" && !$pass.trim().re_match("""^%v$""") { - "!!!SECRET_SCRUBBED!!!" -} -`, bloblREEnvVar) - return f -} - -// FieldInput returns a field spec for an input typed field. -func FieldInput(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeInput) -} - -// FieldProcessor returns a field spec for a processor typed field. -func FieldProcessor(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeProcessor) -} - -// FieldOutput returns a field spec for an output typed field. -func FieldOutput(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeOutput) -} - -// FieldBuffer returns a field spec for a buffer typed field. -func FieldBuffer(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeBuffer) -} - -// FieldCache returns a field spec for a cache typed field. -func FieldCache(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeCache) -} - -// FieldRateLimit returns a field spec for a rate limit typed field. -func FieldRateLimit(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeRateLimit) -} - -// FieldMetrics returns a field spec for a metrics typed field. -func FieldMetrics(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeMetrics) -} - -// FieldTracer returns a field spec for a tracer typed field. -func FieldTracer(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeTracer) -} - -// FieldScanner returns a field spec for a scanner typed field. -func FieldScanner(name, description string, examples ...any) FieldSpec { - return newField(name, description, examples...).HasType(FieldTypeScanner) -} - -func newField(name, description string, examples ...any) FieldSpec { - return FieldSpec{ - Name: name, - Description: description, - Kind: KindScalar, - Examples: examples, - } -} - -// FieldComponent returns a field spec for a component. -func FieldComponent() FieldSpec { - return FieldSpec{ - Kind: KindScalar, - } -} - -// CheckRequired returns true if this field, due to various factors, is a field -// that must be specified within a config. The factors at play are: -// -// - Whether the field has a default value -// - Whether the field was explicitly marked as optional -// - Whether the field is an object with children, none of which are required. -func (f FieldSpec) CheckRequired() bool { - if f.IsOptional { - return false - } - if f.Default != nil { - return false - } - if len(f.Children) == 0 { - return true - } - - // If none of the children are required then this field is not required. - for _, child := range f.Children { - if child.CheckRequired() { - return true - } - } - return false -} - -//------------------------------------------------------------------------------ - -// FieldSpecs is a slice of field specs for a component. -type FieldSpecs []FieldSpec - -// Merge with another set of FieldSpecs. -func (f FieldSpecs) Merge(specs FieldSpecs) FieldSpecs { - return append(f, specs...) -} - -// Add more field specs. -func (f FieldSpecs) Add(specs ...FieldSpec) FieldSpecs { - return append(f, specs...) -} - -// FieldFilter defines a filter closure that returns a boolean for a component -// field indicating whether the field should be kept within a generated config. -type FieldFilter func(spec FieldSpec, v any) bool - -func (f FieldFilter) shouldDrop(spec FieldSpec, v any) bool { - if f == nil { - return false - } - return !f(spec, v) -} - -// ShouldDropDeprecated returns a field filter that removes all deprecated -// fields when the boolean argument is true. -func ShouldDropDeprecated(b bool) FieldFilter { - if !b { - return nil - } - return func(spec FieldSpec, _ any) bool { - return !spec.IsDeprecated - } -} - -// SetDefault attempts to override the current default value of a field, -// identified by a series of names used to walk the config spec in order to -// reach the intended field. This function currently does NOT support walking -// through arrays. -func (f FieldSpecs) SetDefault(v any, path ...string) { - if len(path) == 0 { - return - } - for i, child := range f { - if child.Name != path[0] { - continue - } - if len(path) > 1 { - child.Children.SetDefault(v, path[1:]...) - } else { - f[i] = child.HasDefault(v) - } - } -} - -//------------------------------------------------------------------------------ - -// LintConfig describes which rules apply when linting benthos configs, and also -// determines which component and bloblang environments are used. -type LintConfig struct { - // Provides documentation for component implementations. - DocsProvider Provider - - // Provides an isolated context for Bloblang parsing. - BloblangEnv *bloblang.Environment - - // Reject any deprecated components or fields as linting errors. - RejectDeprecated bool - - // Require labels for components. - RequireLabels bool -} - -// NewLintConfig creates a default linting config. -func NewLintConfig(prov Provider) LintConfig { - return LintConfig{ - DocsProvider: prov, - BloblangEnv: bloblang.GlobalEnvironment().Deactivated(), - } -} - -// LintContext is provided to linting functions, and provides context about the -// wider configuration. -type LintContext struct { - // A map of label names to the line they were defined at. - labelsToLine map[string]int - - conf LintConfig -} - -// NewLintContext creates a new linting context. -func NewLintContext(conf LintConfig) LintContext { - return LintContext{ - labelsToLine: map[string]int{}, - conf: conf, - } -} - -// LintFunc is a common linting function for field values. -type LintFunc func(ctx LintContext, line, col int, value any) []Lint - -// LintLevel describes the severity level of a linting error. -type LintLevel int - -// Lint levels. -const ( - LintError LintLevel = iota - LintWarning LintLevel = iota -) - -// LintType is a discrete linting type. -type LintType int - -const ( - // LintCustom means a custom linting rule failed. - LintCustom LintType = iota - - // LintFailedRead means a configuration could not be read. - LintFailedRead LintType = iota - - // LintMissingEnvVar means a configuration contained an environment variable - // interpolation without a default and the variable was undefined. - LintMissingEnvVar LintType = iota - - // LintInvalidOption means the field value was not one of the explicit list - // of options. - LintInvalidOption LintType = iota - - // LintBadLabel means the label contains invalid characters. - LintBadLabel LintType = iota - - // LintMissingLabel means the label is missing when required. - LintMissingLabel LintType = iota - - // LintDuplicateLabel means the label collides with another label. - LintDuplicateLabel LintType = iota - - // LintBadBloblang means the field contains invalid Bloblang. - LintBadBloblang LintType = iota - - // LintShouldOmit means the field should be omitted. - LintShouldOmit LintType = iota - - // LintComponentMissing means a component value was expected but the type is - // missing. - LintComponentMissing LintType = iota - - // LintComponentNotFound means the specified component value is not - // recognised. - LintComponentNotFound LintType = iota - - // LintUnknown means the field is unknown. - LintUnknown LintType = iota - - // LintMissing means a field was required but missing. - LintMissing LintType = iota - - // LintExpectedArray means an array value was expected but something else - // was provided. - LintExpectedArray LintType = iota - - // LintExpectedObject means an object value was expected but something else - // was provided. - LintExpectedObject LintType = iota - - // LintExpectedScalar means a scalar value was expected but something else - // was provided. - LintExpectedScalar LintType = iota - - // LintDeprecated means a field is deprecated and should not be used. - LintDeprecated LintType = iota -) - -// Lint describes a single linting issue found with a Benthos config. -type Lint struct { - Line int - Column int // Optional, set to 1 by default - Level LintLevel - Type LintType - What string -} - -// NewLintError returns an error lint. -func NewLintError(line int, t LintType, err error) Lint { - var inner Lint - if errors.As(err, &inner) { - return inner - } - return Lint{Line: line, Column: 1, Level: LintError, Type: t, What: err.Error()} -} - -// NewLintWarning returns a warning lint. -func NewLintWarning(line int, t LintType, msg string) Lint { - return Lint{Line: line, Column: 1, Level: LintWarning, Type: t, What: msg} -} - -// Error returns a formatted string explaining the lint error prefixed with its -// location within the file. -func (l Lint) Error() string { - return fmt.Sprintf("(%v,%v) %v", l.Line, l.Column, l.What) -} - -//------------------------------------------------------------------------------ - -func (f FieldSpec) needsDefault() bool { - if f.IsOptional { - return false - } - if f.IsDeprecated { - return false - } - return true -} - -func getDefault(pathName string, field FieldSpec) (any, error) { - if field.Default != nil { - if len(field.Children) > 0 && field.Kind == KindScalar { - if tmp, ok := value.IClone(*field.Default).(map[string]any); ok { - for _, v := range field.Children { - if _, exists := tmp[v.Name]; exists { - continue - } - defV, err := getDefault(pathName+"."+v.Name, v) - if err == nil { - tmp[v.Name] = defV - } else if v.needsDefault() { - return nil, err - } - } - return tmp, nil - } - } - return *field.Default, nil - } else if field.Kind == KindArray { - return []any{}, nil - } else if field.Kind == Kind2DArray { - return []any{}, nil - } else if field.Kind == KindMap { - return map[string]any{}, nil - } else if len(field.Children) > 0 { - m := map[string]any{} - for _, v := range field.Children { - defV, err := getDefault(pathName+"."+v.Name, v) - if err == nil { - m[v.Name] = defV - } else if v.needsDefault() { - return nil, err - } - } - return m, nil - } - return nil, fmt.Errorf("field '%v' is required and was not present in the config", pathName) -} diff --git a/internal/docs/field_interop.go b/internal/docs/field_interop.go deleted file mode 100644 index 1c0175e9da..0000000000 --- a/internal/docs/field_interop.go +++ /dev/null @@ -1,85 +0,0 @@ -package docs - -import ( - "gopkg.in/yaml.v3" -) - -// ComponentFieldsFromConf walks the children of a YAML node and returns a list -// of fields extracted from it. This can be used in order to infer a field spec -// for a parsed component. -// -// TODO: V5 Remove this eventually. -func ComponentFieldsFromConf(conf any) (inferred map[string]FieldSpecs) { - inferred = map[string]FieldSpecs{} - - componentNodes := map[string]yaml.Node{} - - var node yaml.Node - if err := node.Encode(conf); err != nil { - return - } - - if err := node.Decode(componentNodes); err != nil { - return - } - - for k, v := range componentNodes { - inferred[k] = FieldsFromYAML(&v) - } - return -} - -// FieldsFromConf attempts to infer field documents from a config struct. -func FieldsFromConf(conf any) FieldSpecs { - var node yaml.Node - if err := node.Encode(conf); err != nil { - return FieldSpecs{} - } - return FieldsFromYAML(&node) -} - -// ChildDefaultAndTypesFromStruct enriches a field specs children with a type -// string and default value from another field spec inferred from a config -// struct. -// -// TODO: V5 Remove this eventually. -func (f FieldSpec) ChildDefaultAndTypesFromStruct(conf any) FieldSpec { - var node yaml.Node - if err := node.Encode(conf); err != nil { - return f - } - f.Children = f.Children.DefaultAndTypeFrom(FieldsFromYAML(&node)) - return f -} - -// DefaultAndTypeFrom enriches a field spec with a type string and default value -// from another field spec. -func (f FieldSpec) DefaultAndTypeFrom(from FieldSpec) FieldSpec { - if f.Default == nil && from.Default != nil { - f.Default = from.Default - } - if f.Type == "" && from.Type != "" { - f.Type = from.Type - } - f.Children = f.Children.DefaultAndTypeFrom(from.Children) - return f -} - -// DefaultAndTypeFrom enriches a field spec with a type string and default value -// from another field spec. -func (f FieldSpecs) DefaultAndTypeFrom(from FieldSpecs) FieldSpecs { - newSpecs := make(FieldSpecs, len(f)) - fromMap := map[string]FieldSpec{} - for _, v := range from { - fromMap[v.Name] = v - } - for i, v := range f { - ref, exists := fromMap[v.Name] - if !exists { - newSpecs[i] = v - continue - } - newSpecs[i] = v.DefaultAndTypeFrom(ref) - } - return newSpecs -} diff --git a/internal/docs/field_template.go b/internal/docs/field_template.go deleted file mode 100644 index 5574fc6bc7..0000000000 --- a/internal/docs/field_template.go +++ /dev/null @@ -1,148 +0,0 @@ -package docs - -import ( - "strings" - - "github.com/Jeffail/gabs/v2" -) - -// FieldSpecCtx provides a field spec and rendered extras for documentation -// templates to use. -type FieldSpecCtx struct { - Spec FieldSpec - - // FullName describes the full dot path name of the field relative to - // the root of the documented component. - FullName string - - // ExamplesMarshalled is a list of examples marshalled into YAML format. - ExamplesMarshalled []string - - // DefaultMarshalled is a marshalled string of the default value, if there is one. - DefaultMarshalled string -} - -// FieldsTemplate returns a Go template for rendering markdown field -// documentation. The context should have a field `.Fields` of the type -// `[]FieldSpecCtx`. -func FieldsTemplate(lintableExamples bool) string { - exampleHint := "yml" - if lintableExamples { - exampleHint = "yaml" - } - // Use trailing whitespace below to render line breaks in Asciidoc - return `{{define "field_docs" -}} -{{range $i, $field := .Fields -}} -=== ` + "`{{$field.FullName}}`" + ` - -{{$field.Spec.Description}} -{{if $field.Spec.IsSecret -}} - -[WARNING] -.Secret -==== -This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. -==== - -{{end -}} -{{if $field.Spec.Interpolated -}} -This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. -{{end}} - -*Type*: {{if eq $field.Spec.Kind "array"}}list of {{end}}{{if eq $field.Spec.Kind "map"}}map of {{end}}` + "`{{$field.Spec.Type}}`" + ` - -{{if gt (len $field.DefaultMarshalled) 0}}*Default*: ` + "`{{$field.DefaultMarshalled}}`" + ` -{{end -}} -{{if gt (len $field.Spec.Version) 0}}Requires version {{$field.Spec.Version}} or newer -{{end -}} -{{if gt (len $field.Spec.AnnotatedOptions) 0}} -|=== -| Option | Summary - -{{range $j, $option := $field.Spec.AnnotatedOptions -}} -| ` + "`{{index $option 0}}`" + ` -| {{index $option 1}} -{{end}} -|=== -{{else if gt (len $field.Spec.Options) 0}} -Options: -{{range $j, $option := $field.Spec.Options -}} -{{if ne $j 0}}, {{end}}` + "`{{$option}}`" + ` -{{end}}. -{{end}} -{{if gt (len $field.Spec.Examples) 0 -}} -` + "```" + exampleHint + ` -# Examples - -{{range $j, $example := $field.ExamplesMarshalled -}} -{{if ne $j 0}} -{{end}}{{$example}}{{end -}} -` + "```" + ` - -{{end -}} -{{end -}} -{{end -}}` -} - -// FlattenChildrenForDocs converts the children of a field into a flat list, -// where the names contain hints as to their position in a structured hierarchy. -// This makes it easier to list the fields in documentation. -func (f FieldSpec) FlattenChildrenForDocs() []FieldSpecCtx { - flattenedFields := []FieldSpecCtx{} - var walkFields func(path string, f FieldSpecs) - walkFields = func(path string, f FieldSpecs) { - for _, v := range f { - if v.IsDeprecated { - continue - } - newV := FieldSpecCtx{ - Spec: v, - } - newV.FullName = newV.Spec.Name - if path != "" { - newV.FullName = path + newV.Spec.Name - } - if len(v.Examples) > 0 { - newV.ExamplesMarshalled = make([]string, len(v.Examples)) - for i, e := range v.Examples { - exampleBytes, err := marshalYAML(map[string]any{ - v.Name: e, - }) - if err == nil { - newV.ExamplesMarshalled[i] = string(exampleBytes) - } - } - } - if v.Default != nil { - newV.DefaultMarshalled = gabs.Wrap(*v.Default).String() - } - newV.Spec.Description = strings.TrimSpace(v.Description) - if newV.Spec.Description == "" { - newV.Spec.Description = "Sorry! This field is missing documentation." - } - - flattenedFields = append(flattenedFields, newV) - if len(v.Children) > 0 { - newPath := path + v.Name - switch newV.Spec.Kind { - case KindArray: - newPath += "[]" - case Kind2DArray: - newPath += "[][]" - case KindMap: - newPath += "." - } - walkFields(newPath+".", v.Children) - } - } - } - rootPath := "" - switch f.Kind { - case KindArray: - rootPath = "[]." - case KindMap: - rootPath = "." - } - walkFields(rootPath, f.Children) - return flattenedFields -} diff --git a/internal/docs/field_test.go b/internal/docs/field_test.go deleted file mode 100644 index 3f87fcd354..0000000000 --- a/internal/docs/field_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package docs_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func TestBloblLinter(t *testing.T) { - f := docs.FieldString("foo", "").LinterBlobl(`root = if this.length() > 0 { - if this.contains("meow") { - [ "no cats allowed" ] - } else if this.contains("woof") { - [ "no dogs allowed", "no noise allowed" ] - } -} else { - "expected non-empty string, got empty %T".format(this) -} -`) - - tests := []struct { - name string - input any - expected []docs.Lint - }{ - { - name: "No lints", - input: `hello world`, - expected: nil, - }, - { - name: "Single type lint", - input: "", - expected: []docs.Lint{ - docs.NewLintError(0, docs.LintCustom, errors.New("expected non-empty string, got empty string")), - }, - }, - { - name: "One lint", - input: `hello meow world`, - expected: []docs.Lint{ - docs.NewLintError(0, docs.LintCustom, errors.New("no cats allowed")), - }, - }, - { - name: "Two lints", - input: `hello woof world`, - expected: []docs.Lint{ - docs.NewLintError(0, docs.LintCustom, errors.New("no dogs allowed")), - docs.NewLintError(0, docs.LintCustom, errors.New("no noise allowed")), - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var node yaml.Node - require.NoError(t, node.Encode(test.input)) - - lints := f.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), &node) - assert.Equal(t, test.expected, lints) - }) - } -} - -func TestFieldLinting(t *testing.T) { - tests := []struct { - name string - f docs.FieldSpec - input any - output []docs.Lint - }{ - { - name: "normal string no linter", - f: docs.FieldString("foo", "").LinterBlobl(`root = []`), - input: "hello world", - }, - { - name: "url valid", - f: docs.FieldURL("foo", ""), - input: "tcp://admin@example.com", - }, - { - name: "url invalid", - f: docs.FieldURL("foo", ""), - input: "not a %#$ valid URL", - // output: []Lint{ - // {Column: 1, What: "field `this`: parse \"not a %\": invalid URL escape \"%\""}, - // }, - // TODO: Disabled until our rule takes interpolation functions into account when necessary. - }, - { - name: "enum valid option", - f: docs.FieldString("foo", "").HasOptions("foo", "bar", "baz:x"), - input: "foo", - }, - { - name: "enum invalid option", - f: docs.FieldString("foo", "").HasOptions("foo", "bar", "baz:x"), - input: "buz", - output: []docs.Lint{ - {Column: 1, Type: 2, What: "value buz is not a valid option for this field"}, - }, - }, - { - name: "enum valid case insensitive option", - f: docs.FieldString("foo", "").HasOptions("foo", "bar", "baz"), - input: "BAR", - }, - { - name: "enum invalid pattern option", - f: docs.FieldString("foo", "").HasOptions("foo", "bar", "baz:x"), - input: "baz", - output: []docs.Lint{ - {Column: 1, Type: 2, What: "value baz is not a valid option for this field"}, - }, - }, - } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var lints []docs.Lint - linter := test.f.GetLintFunc() - if linter != nil { - lints = linter(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), 0, 0, test.input) - } - assert.Equal(t, test.output, lints) - }) - } -} - -func TestSecretScrubbing(t *testing.T) { - tests := []struct { - name string - f docs.FieldSpec - input any - output any - }{ - { - name: "not a secret", - f: docs.FieldString("foo", ""), - input: "hello world", - output: "hello world", - }, - { - name: "raw secret", - f: docs.FieldString("foo", "").Secret(), - input: "hello world", - output: "!!!SECRET_SCRUBBED!!!", - }, - { - name: "raw secret with empty value", - f: docs.FieldString("foo", "").Secret(), - input: "", - output: "", - }, - { - name: "env var secret", - f: docs.FieldString("foo", "").Secret(), - input: "${FOO}", - output: "${FOO}", - }, - { - name: "env var secret whitespaced", - f: docs.FieldString("foo", "").Secret(), - input: " ${FOO} ", - output: " ${FOO} ", - }, - { - name: "url no user", - f: docs.FieldURL("foo", ""), - input: "tcp://example.com", - output: "tcp://example.com", - }, - { - name: "url user no secret", - f: docs.FieldURL("foo", ""), - input: "tcp://admin@example.com", - output: "tcp://admin@example.com", - }, - { - name: "url user with password secret", - f: docs.FieldURL("foo", ""), - input: "tcp://admin:foo@example.com", - output: "!!!SECRET_SCRUBBED!!!", - }, - { - name: "url user with password env var", - f: docs.FieldURL("foo", ""), - input: "tcp://admin:${FOO}@example.com", - output: "tcp://admin:${FOO}@example.com", - }, - { - name: "url user with empty password", - f: docs.FieldURL("foo", ""), - input: "tcp://admin:@example.com", - output: "tcp://admin:@example.com", - }, - } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - out, err := test.f.ScrubValue(test.input) - require.NoError(t, err) - assert.Equal(t, test.output, out) - }) - } -} diff --git a/internal/docs/format_any.go b/internal/docs/format_any.go deleted file mode 100644 index 809511f927..0000000000 --- a/internal/docs/format_any.go +++ /dev/null @@ -1,136 +0,0 @@ -package docs - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// GetInferenceCandidateFromMap checks a map config structure for a component -// and returns either the inferred type name or an error if one cannot be -// inferred. -func GetInferenceCandidateFromMap(docProv Provider, t Type, m map[string]any) (string, ComponentSpec, error) { - if tStr, ok := m["type"].(string); ok { - spec, exists := docProv.GetDocs(tStr, t) - if !exists { - return "", ComponentSpec{}, fmt.Errorf("%v type '%v' was not recognised", string(t), tStr) - } - return tStr, spec, nil - } - - var keys []string - for k := range m { - keys = append(keys, k) - } - return getInferenceCandidateFromList(docProv, t, keys) -} - -// AnyToValue converts any value into a generic value containing fleshed out -// defaults by referencing the expected type. -func (f FieldSpec) AnyToValue(v any, conf ToValueConfig) (any, error) { - switch f.Kind { - case Kind2DArray: - a, ok := v.([]any) - if !conf.Passive && !ok { - return nil, fmt.Errorf("expected array value, got %T", v) - } - subSpec := f.Array() - - var s []any - for i := 0; i < len(a); i++ { - v, err := subSpec.AnyToValue(a[i], conf) - if err != nil { - return nil, err - } - s = append(s, v) - } - return s, nil - case KindArray: - a, ok := v.([]any) - if !conf.Passive && !ok { - return nil, fmt.Errorf("expected array value, got %T", v) - } - subSpec := f.Scalar() - - var s []any - for i := 0; i < len(a); i++ { - v, err := subSpec.AnyToValue(a[i], conf) - if err != nil { - return nil, err - } - s = append(s, v) - } - return s, nil - case KindMap: - m, ok := v.(map[string]any) - if !conf.Passive && !ok { - return nil, fmt.Errorf("expected map value, got %T", v) - } - subSpec := f.Scalar() - - for k, v := range m { - var err error - if m[k], err = subSpec.AnyToValue(v, conf); err != nil { - return nil, err - } - } - return m, nil - } - switch f.Type { - case FieldTypeString: - return value.IGetString(v) - case FieldTypeInt: - i64, err := value.IGetInt(v) - return int(i64), err - case FieldTypeFloat: - return value.IGetNumber(v) - case FieldTypeBool: - return value.IGetBool(v) - case FieldTypeUnknown: - return v, nil - case FieldTypeObject: - return f.Children.AnyToMap(v, conf) - } - return v, nil -} - -// AnyToMap converts a raw map value node into a generic map structure -// referencing expected fields, adding default values to the map when the node -// does not contain them. -func (f FieldSpecs) AnyToMap(v any, conf ToValueConfig) (map[string]any, error) { - m, ok := v.(map[string]any) - if !ok { - return nil, fmt.Errorf("expected map value, got %T", v) - } - - pendingFieldsMap := map[string]FieldSpec{} - for _, field := range f { - pendingFieldsMap[field.Name] = field - } - - for fieldName, fieldValue := range m { - f, exists := pendingFieldsMap[fieldName] - if !exists { - continue - } - - delete(pendingFieldsMap, f.Name) - var err error - if m[fieldName], err = f.AnyToValue(fieldValue, conf); err != nil { - return nil, fmt.Errorf("field '%v': %w", fieldName, err) - } - } - - for k, v := range pendingFieldsMap { - defValue, err := getDefault(k, v) - if err != nil { - if v.needsDefault() && !conf.Passive { - return nil, err - } - continue - } - m[k] = value.IClone(defValue) - } - - return m, nil -} diff --git a/internal/docs/format_yaml.go b/internal/docs/format_yaml.go deleted file mode 100644 index 8bc4eb642f..0000000000 --- a/internal/docs/format_yaml.go +++ /dev/null @@ -1,1121 +0,0 @@ -package docs - -import ( - "bytes" - "errors" - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// UnmarshalYAML attempts to parse a byte slice as a YAML document and returns -// the root of the underlying document contents. -func UnmarshalYAML(rawBytes []byte) (*yaml.Node, error) { - var rawNode yaml.Node - if err := yaml.Unmarshal(rawBytes, &rawNode); err != nil { - return nil, err - } - return unwrapDocumentNode(&rawNode), nil -} - -// MarshalYAML marshals a structure into YAML with consistent formatting across -// all Benthos components. -func MarshalYAML(v yaml.Node) ([]byte, error) { - var cbytes bytes.Buffer - enc := yaml.NewEncoder(&cbytes) - enc.SetIndent(2) - if err := enc.Encode(v); err != nil { - return nil, err - } - return cbytes.Bytes(), nil -} - -// FieldsFromYAML walks the children of a YAML node and returns a list of fields -// extracted from it. This can be used in order to infer a field spec for a -// parsed component. -func FieldsFromYAML(node *yaml.Node) FieldSpecs { - node = unwrapDocumentNode(node) - - var fields FieldSpecs - for i := 0; i < len(node.Content)-1; i += 2 { - fields = append(fields, FieldFromYAML(node.Content[i].Value, node.Content[i+1])) - } - return fields -} - -// FieldFromYAML infers a field spec from a YAML node. This mechanism has many -// limitations and should only be used for pre-hydrating field specs for old -// components with struct based config. -func FieldFromYAML(name string, node *yaml.Node) FieldSpec { - node = unwrapDocumentNode(node) - - field := newField(name, "") - - switch node.Kind { - case yaml.MappingNode: - field = field.WithChildren(FieldsFromYAML(node)...) - field.Type = FieldTypeObject - if len(field.Children) == 0 { - var defaultI any = map[string]any{} - field.Default = &defaultI - } - case yaml.SequenceNode: - field.Kind = KindArray - field.Type = FieldTypeUnknown - if len(node.Content) > 0 { - tmpField := FieldFromYAML("", node.Content[0]) - field.Type = tmpField.Type - field.Children = tmpField.Children - switch field.Type { - case FieldTypeString: - var defaultArray []string - _ = node.Decode(&defaultArray) - - var defaultI any = defaultArray - field.Default = &defaultI - case FieldTypeInt: - var defaultArray []int64 - _ = node.Decode(&defaultArray) - - var defaultI any = defaultArray - field.Default = &defaultI - } - } else { - var defaultI any = []any{} - field.Default = &defaultI - } - case yaml.ScalarNode: - switch node.Tag { - case "!!bool": - field.Type = FieldTypeBool - - var defaultBool bool - _ = node.Decode(&defaultBool) - - var defaultI any = defaultBool - field.Default = &defaultI - case "!!int": - field.Type = FieldTypeInt - - var defaultInt int64 - _ = node.Decode(&defaultInt) - - var defaultI any = defaultInt - field.Default = &defaultI - case "!!float": - field.Type = FieldTypeFloat - - var defaultFloat float64 - _ = node.Decode(&defaultFloat) - - var defaultI any = defaultFloat - field.Default = &defaultI - default: - field.Type = FieldTypeString - - var defaultStr string - _ = node.Decode(&defaultStr) - - var defaultI any = defaultStr - field.Default = &defaultI - } - } - - return field -} - -// GetInferenceCandidateFromYAML checks a yaml node config structure for a -// component and returns either the inferred type name or an error if one cannot -// be inferred. -func GetInferenceCandidateFromYAML(docProv Provider, t Type, node *yaml.Node) (string, ComponentSpec, error) { - node = unwrapDocumentNode(node) - - if node.Kind != yaml.MappingNode { - return "", ComponentSpec{}, fmt.Errorf("invalid type %v, expected object", node.ShortTag()) - } - - var keys []string - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "type" { - tStr := node.Content[i+1].Value - spec, exists := docProv.GetDocs(tStr, t) - if !exists { - return "", ComponentSpec{}, fmt.Errorf("%v type '%v' was not recognised", string(t), tStr) - } - return tStr, spec, nil - } - keys = append(keys, node.Content[i].Value) - } - - return getInferenceCandidateFromList(docProv, t, keys) -} - -// GetPluginConfigYAML extracts a plugin configuration node from a component -// config. This exists because there are two styles of plugin config. -func GetPluginConfigYAML(name string, node *yaml.Node) (yaml.Node, error) { - node = unwrapDocumentNode(node) - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == name { - if yamlIsNil(node.Content[i+1]) { - break - } - return *node.Content[i+1], nil - } - } - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "plugin" { - return *node.Content[i+1], nil - } - } - var n yaml.Node - _ = n.Encode(nil) - return n, nil -} - -//------------------------------------------------------------------------------ - -func (f FieldSpec) shouldOmitYAML(parentFields FieldSpecs, fieldNode, parentNode *yaml.Node) (why string, shouldOmit bool) { - conf := ToValueConfig{ - Passive: true, - FallbackToAny: true, - } - - if f.omitWhenFn == nil { - return - } - field, err := f.YAMLToValue(fieldNode, conf) - if err != nil { - // If we weren't able to infer a value type then it's assumed - // that we'll capture this type error elsewhere. - return - } - parent, err := parentFields.YAMLToMap(parentNode, conf) - if err != nil { - // If we weren't able to infer a value type then it's assumed - // that we'll capture this type error elsewhere. - return - } - return f.omitWhenFn(field, parent) -} - -func yamlIsNil(node *yaml.Node) bool { - if node.Kind == 0 { - return true - } - if node.Kind == yaml.ScalarNode && node.Tag == "!!null" && node.Value == "null" { - return true - } - return false -} - -// SanitiseYAML takes a yaml.Node and a config spec and sorts the fields of the -// node according to the spec. Also optionally removes the `type` field from -// this and all nested components. -func SanitiseYAML(cType Type, node *yaml.Node, conf SanitiseConfig) error { - node = unwrapDocumentNode(node) - - newNodes := []*yaml.Node{} - - var name string - var keys []string - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "label" { - if _, omit := labelField.shouldOmitYAML(nil, node.Content[i+1], node); !omit { - newNodes = append(newNodes, node.Content[i], node.Content[i+1]) - } - break - } - } - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "type" { - if name = node.Content[i+1].Value; name == "" { - continue - } - if !conf.RemoveTypeField { - newNodes = append(newNodes, node.Content[i], node.Content[i+1]) - } - break - } - - keys = append(keys, node.Content[i].Value) - } - if name == "" { - if len(node.Content) == 0 { - return nil - } - var err error - if name, _, err = getInferenceCandidateFromList(conf.DocsProvider, cType, keys); err != nil { - return err - } - } - - cSpec, exists := conf.DocsProvider.GetDocs(name, cType) - if !exists { - return fmt.Errorf("failed to obtain docs for %v type %v", cType, name) - } - - nameFound := false - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "plugin" && cSpec.Plugin { - if !yamlIsNil(node.Content[i+1]) { - // Plugin conf is here but the value is null because it's - // non-existent. This can happen in cases where a config is - // parsed out with `{type: foo}` form. - node.Content[i].Value = name - } - } - - if node.Content[i].Value != name { - continue - } - - nameFound = true - if err := cSpec.Config.SanitiseYAML(node.Content[i+1], conf); err != nil { - return err - } - newNodes = append(newNodes, node.Content[i], node.Content[i+1]) - break - } - - // If the type field was omitted but we didn't see a config under the name - // then we need to add an empty object. - if !nameFound && conf.RemoveTypeField { - var keyNode yaml.Node - if err := keyNode.Encode(name); err != nil { - return err - } - bodyNode, err := cSpec.Config.ToYAML(conf.ForExample) - if err != nil { - return err - } - if err := cSpec.Config.SanitiseYAML(bodyNode, conf); err != nil { - return err - } - newNodes = append(newNodes, &keyNode, bodyNode) - } - - reservedFields := ReservedFieldsByType(cType) - for i := 0; i < len(node.Content)-1; i += 2 { - nodeKey := node.Content[i].Value - if _, exists := map[string]struct{}{ - name: {}, - "type": {}, - "label": {}, - "plugin": {}, - }[nodeKey]; exists { - continue - } - if spec, exists := reservedFields[nodeKey]; exists { - if _, omit := spec.shouldOmitYAML(nil, node.Content[i+1], node); omit { - continue - } - if err := spec.SanitiseYAML(node.Content[i+1], conf); err != nil { - return err - } - newNodes = append(newNodes, node.Content[i], node.Content[i+1]) - } - } - - node.Content = newNodes - return nil -} - -// SanitiseYAML attempts to reduce a parsed config (as a *yaml.Node) down into a -// minimal representation without changing the behaviour of the config. The -// fields of the result will also be sorted according to the field spec. -func (f FieldSpec) SanitiseYAML(node *yaml.Node, conf SanitiseConfig) error { - node = unwrapDocumentNode(node) - - if coreType, isCore := f.Type.IsCoreComponent(); isCore { - switch f.Kind { - case Kind2DArray: - for i := 0; i < len(node.Content); i++ { - for j := 0; j < len(node.Content[i].Content); j++ { - if err := SanitiseYAML(coreType, node.Content[i].Content[j], conf); err != nil { - return err - } - } - } - case KindArray: - for i := 0; i < len(node.Content); i++ { - if err := SanitiseYAML(coreType, node.Content[i], conf); err != nil { - return err - } - } - case KindMap: - for i := 0; i < len(node.Content)-1; i += 2 { - if err := SanitiseYAML(coreType, node.Content[i+1], conf); err != nil { - return err - } - } - default: - if err := SanitiseYAML(coreType, node, conf); err != nil { - return err - } - } - } else if len(f.Children) > 0 { - switch f.Kind { - case Kind2DArray: - for i := 0; i < len(node.Content); i++ { - for j := 0; j < len(node.Content[i].Content); j++ { - if err := f.Children.SanitiseYAML(node.Content[i].Content[j], conf); err != nil { - return err - } - } - } - case KindArray: - for i := 0; i < len(node.Content); i++ { - if err := f.Children.SanitiseYAML(node.Content[i], conf); err != nil { - return err - } - } - case KindMap: - for i := 0; i < len(node.Content)-1; i += 2 { - if err := f.Children.SanitiseYAML(node.Content[i+1], conf); err != nil { - return err - } - } - default: - if err := f.Children.SanitiseYAML(node, conf); err != nil { - return err - } - } - } else if f.Scrubber != "" { - scrubNode := func(n *yaml.Node) error { - var scrubValue any - err := n.Decode(&scrubValue) - if err != nil { - return err - } - if scrubValue, err = f.ScrubValue(scrubValue); err != nil { - return err - } - comment := n.LineComment - if err := n.Encode(scrubValue); err != nil { - return err - } - n.LineComment = comment - return nil - } - switch f.Kind { - case Kind2DArray: - for i := 0; i < len(node.Content); i++ { - for j := 0; j < len(node.Content[i].Content); j++ { - if err := scrubNode(node.Content[i].Content[j]); err != nil { - return err - } - } - } - case KindArray: - for i := 0; i < len(node.Content); i++ { - if err := scrubNode(node.Content[i]); err != nil { - return err - } - } - case KindMap: - for i := 0; i < len(node.Content)-1; i += 2 { - if err := scrubNode(node.Content[i+1]); err != nil { - return err - } - } - default: - if err := scrubNode(node); err != nil { - return err - } - } - } - return nil -} - -// SanitiseYAML attempts to reduce a parsed config (as a *yaml.Node) down into a -// minimal representation without changing the behaviour of the config. The -// fields of the result will also be sorted according to the field spec. -func (f FieldSpecs) SanitiseYAML(node *yaml.Node, conf SanitiseConfig) error { - node = unwrapDocumentNode(node) - - nodeKeys := map[string]*yaml.Node{} - for i := 0; i < len(node.Content)-1; i += 2 { - nodeKeys[node.Content[i].Value] = node.Content[i+1] - } - - // Following the order of our field specs, extract each field. - newNodes := []*yaml.Node{} - for _, field := range f { - if field.IsDeprecated && conf.RemoveDeprecated { - continue - } - value, exists := nodeKeys[field.Name] - if !exists { - continue - } - if conf.Filter.shouldDrop(field, value) { - continue - } - if _, omit := field.shouldOmitYAML(f, value, node); omit { - continue - } - if err := field.SanitiseYAML(value, conf); err != nil { - return err - } - var keyNode yaml.Node - if err := keyNode.Encode(field.Name); err != nil { - return err - } - newNodes = append(newNodes, &keyNode, value) - } - node.Content = newNodes - return nil -} - -//------------------------------------------------------------------------------ - -func lintYAMLFromOmit(parentSpec FieldSpecs, lintTargetSpec FieldSpec, parent, node *yaml.Node) []Lint { - why, shouldOmit := lintTargetSpec.shouldOmitYAML(parentSpec, node, parent) - if shouldOmit { - return []Lint{NewLintError(node.Line, LintShouldOmit, errors.New(why))} - } - return nil -} - -func customLintFromYAML(ctx LintContext, spec FieldSpec, node *yaml.Node) []Lint { - lintFn := spec.GetLintFunc() - if lintFn == nil { - return nil - } - fieldValue, err := spec.YAMLToValue(node, ToValueConfig{ - Passive: true, - FallbackToAny: true, - }) - if err != nil { - // If we weren't able to infer a value type then it's assumed - // that we'll capture this type error elsewhere. - return []Lint{} - } - line := node.Line - if node.Style == yaml.LiteralStyle { - line++ - } - - lints := lintFn(ctx, line, node.Column, fieldValue) - return lints -} - -// LintYAML takes a yaml.Node and a config spec and returns a list of linting -// errors found in the config. -func LintYAML(ctx LintContext, cType Type, node *yaml.Node) []Lint { - node = unwrapDocumentNode(node) - - var lints []Lint - - var name string - var keys []string - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "type" { - name = node.Content[i+1].Value - break - } - - keys = append(keys, node.Content[i].Value) - } - if name == "" { - if len(node.Content) == 0 { - return nil - } - var err error - if name, _, err = getInferenceCandidateFromList(ctx.conf.DocsProvider, cType, keys); err != nil { - lints = append(lints, NewLintWarning(node.Line, LintComponentMissing, "unable to infer component type")) - return lints - } - } - - cSpec, exists := ctx.conf.DocsProvider.GetDocs(name, cType) - if !exists { - lints = append(lints, NewLintWarning(node.Line, LintComponentNotFound, fmt.Sprintf("failed to obtain docs for %v type %v", cType, name))) - return lints - } - - if ctx.conf.RejectDeprecated && cSpec.Status == StatusDeprecated { - lints = append(lints, NewLintError(node.Line, LintDeprecated, fmt.Errorf("component %v is deprecated", cSpec.Name))) - } - - nameFound := false - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == name { - nameFound = true - lints = append(lints, cSpec.Config.LintYAML(ctx, node.Content[i+1])...) - break - } - } - - reservedFields := ReservedFieldsByType(cType) - _, canLabel := reservedFields["label"] - hasLabel := false - for i := 0; i < len(node.Content)-1; i += 2 { - key := node.Content[i].Value - if key == name || key == "type" { - continue - } - if key == "plugin" { - if nameFound || !cSpec.Plugin { - lints = append(lints, NewLintError(node.Content[i].Line, LintShouldOmit, errors.New("plugin object is ineffective"))) - } else { - lints = append(lints, cSpec.Config.LintYAML(ctx, node.Content[i+1])...) - } - } - spec, exists := reservedFields[key] - hasLabel = hasLabel || (key == "label") - if exists { - lints = append(lints, lintYAMLFromOmit(cSpec.Config.Children, spec, node, node.Content[i+1])...) - lints = append(lints, spec.LintYAML(ctx, node.Content[i+1])...) - } else { - lints = append(lints, NewLintError( - node.Content[i].Line, - LintUnknown, - fmt.Errorf("field %v is invalid when the component type is %v (%v)", node.Content[i].Value, name, cType), - )) - } - } - - if ctx.conf.RequireLabels && canLabel && !hasLabel && name != "resource" { - lints = append(lints, NewLintError(node.Line, LintMissingLabel, fmt.Errorf("label is required for %s", cSpec.Name))) - } - - return lints -} - -// LintYAML returns a list of linting errors found by checking a field -// definition against a yaml node. -func (f FieldSpec) LintYAML(ctx LintContext, node *yaml.Node) []Lint { - node = unwrapDocumentNode(node) - - var lints []Lint - - if ctx.conf.RejectDeprecated && f.IsDeprecated { - lints = append(lints, NewLintError(node.Line, LintDeprecated, fmt.Errorf("field %v is deprecated", f.Name))) - } - - // Execute custom linters, if the kind is non-scalar this means we execute - // the linter from the perspective of both the scalar and higher level types - // and it's up to the linting implementation to distinguish between them. - lints = append(lints, customLintFromYAML(ctx, f, node)...) - - // Check basic kind matches, and execute custom linters - switch f.Kind { - case Kind2DArray: - if node.Kind != yaml.SequenceNode { - lints = append(lints, NewLintError(node.Line, LintExpectedArray, errors.New("expected array value"))) - return lints - } - for i := 0; i < len(node.Content); i++ { - lints = append(lints, f.Array().LintYAML(ctx, node.Content[i])...) - } - return lints - case KindArray: - if node.Kind != yaml.SequenceNode { - lints = append(lints, NewLintError(node.Line, LintExpectedArray, errors.New("expected array value"))) - return lints - } - for i := 0; i < len(node.Content); i++ { - lints = append(lints, f.Scalar().LintYAML(ctx, node.Content[i])...) - } - return lints - case KindMap: - if node.Kind != yaml.MappingNode { - lints = append(lints, NewLintError(node.Line, LintExpectedObject, fmt.Errorf("expected object value, got %v", node.ShortTag()))) - return lints - } - for i := 0; i < len(node.Content)-1; i += 2 { - lints = append(lints, f.Scalar().LintYAML(ctx, node.Content[i+1])...) - } - return lints - } - - // If we're a core type then execute component specific linting - if coreType, isCore := f.Type.IsCoreComponent(); isCore { - return append(lints, LintYAML(ctx, coreType, node)...) - } - - // If the field has children then lint the child fields - if len(f.Children) > 0 { - return append(lints, f.Children.LintYAML(ctx, node)...) - } - - // Otherwise we're a leaf node, so do basic type checking - switch f.Type { - // TODO: Do proper checking for bool and number types. - case FieldTypeBool, FieldTypeString, FieldTypeInt, FieldTypeFloat: - if node.Kind == yaml.MappingNode || node.Kind == yaml.SequenceNode { - lints = append(lints, NewLintError(node.Line, LintExpectedScalar, fmt.Errorf("expected %v value", f.Type))) - } - case FieldTypeObject: - if node.Kind != yaml.MappingNode && node.Kind != yaml.AliasNode { - lints = append(lints, NewLintError(node.Line, LintExpectedObject, fmt.Errorf("expected object value, got %v", node.ShortTag()))) - } - } - return lints -} - -// LintYAML walks a yaml node and returns a list of linting errors found. -func (f FieldSpecs) LintYAML(ctx LintContext, node *yaml.Node) []Lint { - node = unwrapDocumentNode(node) - - var lints []Lint - if node.Kind != yaml.MappingNode { - if node.Kind == yaml.AliasNode { - // TODO: Actually lint through aliases - return nil - } - lints = append(lints, NewLintError(node.Line, LintExpectedObject, fmt.Errorf("expected object value, got %v", node.ShortTag()))) - return lints - } - - specNamesMissing, specNamesAll := map[string]FieldSpec{}, map[string]FieldSpec{} - for _, field := range f { - specNamesMissing[field.Name] = field - specNamesAll[field.Name] = field - } - - var walkNodeContent func(*yaml.Node) - walkNodeContent = func(walkNode *yaml.Node) { - for i := 0; i < len(walkNode.Content)-1; i += 2 { - if walkNode.Content[i].Tag == "!!merge" && walkNode.Content[i+1].Alias != nil { - walkNodeContent(walkNode.Content[i+1].Alias) - continue - } - spec, exists := specNamesAll[walkNode.Content[i].Value] - if !exists { - if walkNode.Content[i+1].Kind != yaml.AliasNode { - lints = append(lints, NewLintError(walkNode.Content[i].Line, LintUnknown, fmt.Errorf("field %v not recognised", walkNode.Content[i].Value))) - } - continue - } - lints = append(lints, lintYAMLFromOmit(f, spec, walkNode, walkNode.Content[i+1])...) - lints = append(lints, spec.LintYAML(ctx, walkNode.Content[i+1])...) - delete(specNamesMissing, walkNode.Content[i].Value) - } - } - walkNodeContent(node) - - for name, remaining := range specNamesMissing { - _, isCore := remaining.Type.IsCoreComponent() - if remaining.needsDefault() && - remaining.Default == nil && - !isCore && - remaining.Kind == KindScalar && - len(remaining.Children) == 0 { - lints = append(lints, NewLintError(node.Line, LintMissing, fmt.Errorf("field %v is required", name))) - } - } - return lints -} - -//------------------------------------------------------------------------------ - -// ToYAML creates a YAML node from a field spec. If a default value has been -// specified then it is used. Otherwise, a zero value is generated. If recurse -// is enabled and the field has children then all children will also have values -// generated. -func (f FieldSpec) ToYAML(recurse bool) (*yaml.Node, error) { - var node yaml.Node - if f.Default != nil { - if err := node.Encode(*f.Default); err != nil { - return nil, err - } - return &node, nil - } - - _, isCore := f.Type.IsCoreComponent() - if f.Kind == KindArray || f.Kind == Kind2DArray { - s := []any{} - if err := node.Encode(s); err != nil { - return nil, err - } - } else if f.Kind == KindMap || len(f.Children) > 0 { - if len(f.Children) > 0 && recurse { - return f.Children.ToYAML() - } - s := map[string]any{} - if err := node.Encode(s); err != nil { - return nil, err - } - } else if isCore { - if err := node.Encode(nil); err != nil { - return nil, err - } - } else { - if len(f.Examples) > 0 { - if err := node.Encode(f.Examples[0]); err != nil { - return nil, err - } - } else { - switch f.Type { - case FieldTypeString: - if err := node.Encode(""); err != nil { - return nil, err - } - case FieldTypeInt: - if err := node.Encode(0); err != nil { - return nil, err - } - case FieldTypeFloat: - if err := node.Encode(0.0); err != nil { - return nil, err - } - case FieldTypeBool: - if err := node.Encode(false); err != nil { - return nil, err - } - default: - if err := node.Encode(nil); err != nil { - return nil, err - } - } - } - } - if f.IsOptional { - node.LineComment = "No default (optional)" - } else { - node.LineComment = "No default (required)" - } - return &node, nil -} - -// ToYAML creates a YAML node from a list of field specs. If a default value has -// been specified for a given field then it is used. Otherwise, a zero value is -// generated. -func (f FieldSpecs) ToYAML() (*yaml.Node, error) { - var node yaml.Node - node.Kind = yaml.MappingNode - - for _, spec := range f { - var keyNode yaml.Node - if err := keyNode.Encode(spec.Name); err != nil { - return nil, err - } - valueNode, err := spec.ToYAML(true) - if err != nil { - return nil, err - } - node.Content = append(node.Content, &keyNode, valueNode) - } - - return &node, nil -} - -// ToValueConfig describes custom options for how documentation fields should be -// used to convert a parsed node to a value type. -type ToValueConfig struct { - // Whether an problem in the config node detected during conversion - // should return a "best attempt" structure rather than an error. - Passive bool - - // When a field spec is for a non-scalar type (a component) fall back to - // decoding it into an interface, otherwise the raw yaml.Node is - // returned in its place. - FallbackToAny bool -} - -// YAMLToValue converts a yaml node into a generic value by referencing the -// expected type. -func (f FieldSpec) YAMLToValue(node *yaml.Node, conf ToValueConfig) (any, error) { - node = unwrapDocumentNode(node) - - switch f.Kind { - case Kind2DArray: - if !conf.Passive && node.Kind != yaml.SequenceNode { - return nil, fmt.Errorf("line %v: expected array value, got %v", node.Line, node.ShortTag()) - } - subSpec := f.Array() - - var s []any - for i := 0; i < len(node.Content); i++ { - v, err := subSpec.YAMLToValue(node.Content[i], conf) - if err != nil { - return nil, err - } - s = append(s, v) - } - return s, nil - case KindArray: - if !conf.Passive && node.Kind != yaml.SequenceNode { - return nil, fmt.Errorf("line %v: expected array value, got %v", node.Line, node.ShortTag()) - } - subSpec := f.Scalar() - - var s []any - for i := 0; i < len(node.Content); i++ { - v, err := subSpec.YAMLToValue(node.Content[i], conf) - if err != nil { - return nil, err - } - s = append(s, v) - } - return s, nil - case KindMap: - if !conf.Passive && node.Kind != yaml.MappingNode { - return nil, fmt.Errorf("line %v: expected map value, got %v", node.Line, node.ShortTag()) - } - subSpec := f.Scalar() - - m := map[string]any{} - for i := 0; i < len(node.Content)-1; i += 2 { - var err error - if m[node.Content[i].Value], err = subSpec.YAMLToValue(node.Content[i+1], conf); err != nil { - return nil, err - } - } - return m, nil - } - switch f.Type { - case FieldTypeString: - var s string - if err := node.Decode(&s); err != nil { - return nil, err - } - return s, nil - case FieldTypeInt: - var i int - if err := node.Decode(&i); err != nil { - return nil, err - } - return i, nil - case FieldTypeFloat: - var f float64 - if err := node.Decode(&f); err != nil { - return nil, err - } - return f, nil - case FieldTypeBool: - var b bool - if err := node.Decode(&b); err != nil { - return nil, err - } - return b, nil - case FieldTypeUnknown: - var i any - if err := node.Decode(&i); err != nil { - return nil, err - } - return i, nil - case FieldTypeObject: - return f.Children.YAMLToMap(node, conf) - } - - if conf.FallbackToAny { - // We don't know what the field actually is (likely a component - // type), so if we can either decode into a generic interface - // or return the raw node itself. - var v any - if err := node.Decode(&v); err != nil { - return nil, err - } - return v, nil - } - return node, nil -} - -// YAMLToMap converts a yaml node into a generic map structure by referencing -// expected fields, adding default values to the map when the node does not -// contain them. -func (f FieldSpecs) YAMLToMap(node *yaml.Node, conf ToValueConfig) (map[string]any, error) { - node = unwrapDocumentNode(node) - - pendingFieldsMap := map[string]FieldSpec{} - for _, field := range f { - pendingFieldsMap[field.Name] = field - } - - resultMap := map[string]any{} - - for i := 0; i < len(node.Content)-1; i += 2 { - fieldName := node.Content[i].Value - - if f, exists := pendingFieldsMap[fieldName]; exists { - delete(pendingFieldsMap, f.Name) - var err error - if resultMap[fieldName], err = f.YAMLToValue(node.Content[i+1], conf); err != nil { - return nil, fmt.Errorf("field '%v': %w", fieldName, err) - } - } else { - var v any - if err := node.Content[i+1].Decode(&v); err != nil { - return nil, err - } - resultMap[fieldName] = v - } - } - - for k, v := range pendingFieldsMap { - defValue, err := getDefault(k, v) - if err != nil { - if v.needsDefault() && !conf.Passive { - return nil, err - } - continue - } - resultMap[k] = value.IClone(defValue) - } - - return resultMap, nil -} - -//------------------------------------------------------------------------------ - -func walkComponentsYAML(cType Type, node *yaml.Node, prov Provider, fn ComponentWalkYAMLFunc) error { - node = unwrapDocumentNode(node) - - name, spec, err := GetInferenceCandidateFromYAML(prov, cType, node) - if err != nil { - return err - } - - var label string - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == "label" { - label = node.Content[i+1].Value - break - } - } - - if err := fn(WalkedYAMLComponent{ - ComponentType: cType, - Name: name, - Label: label, - Conf: node, - }); err != nil { - return err - } - - reservedFields := ReservedFieldsByType(cType) - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == name { - if err := spec.Config.WalkYAML(node.Content[i+1], prov, fn); err != nil { - return err - } - continue - } - if node.Content[i].Value == "type" || node.Content[i].Value == "label" { - continue - } - if spec, exists := reservedFields[node.Content[i].Value]; exists { - if err := spec.WalkYAML(node.Content[i+1], prov, fn); err != nil { - return err - } - } - } - return nil -} - -// WalkYAML walks each node of a YAML tree and for any component types within -// the config a provided func is called. -func (f FieldSpec) WalkYAML(node *yaml.Node, prov Provider, fn ComponentWalkYAMLFunc) error { - node = unwrapDocumentNode(node) - - if coreType, isCore := f.Type.IsCoreComponent(); isCore { - switch f.Kind { - case Kind2DArray: - for i := 0; i < len(node.Content); i++ { - for j := 0; j < len(node.Content[i].Content); j++ { - if err := walkComponentsYAML(coreType, node.Content[i].Content[j], prov, fn); err != nil { - return err - } - } - } - case KindArray: - for i := 0; i < len(node.Content); i++ { - if err := walkComponentsYAML(coreType, node.Content[i], prov, fn); err != nil { - return err - } - } - case KindMap: - for i := 0; i < len(node.Content)-1; i += 2 { - if err := walkComponentsYAML(coreType, node.Content[i+1], prov, fn); err != nil { - return err - } - } - default: - if err := walkComponentsYAML(coreType, node, prov, fn); err != nil { - return err - } - } - } else if len(f.Children) > 0 { - switch f.Kind { - case Kind2DArray: - for i := 0; i < len(node.Content); i++ { - for j := 0; j < len(node.Content[i].Content); j++ { - if err := f.Children.WalkYAML(node.Content[i].Content[j], prov, fn); err != nil { - return err - } - } - } - case KindArray: - for i := 0; i < len(node.Content); i++ { - if err := f.Children.WalkYAML(node.Content[i], prov, fn); err != nil { - return err - } - } - case KindMap: - for i := 0; i < len(node.Content)-1; i += 2 { - if err := f.Children.WalkYAML(node.Content[i+1], prov, fn); err != nil { - return err - } - } - default: - if err := f.Children.WalkYAML(node, prov, fn); err != nil { - return err - } - } - } - return nil -} - -// ComponentWalkYAMLFunc is called for each component type within a YAML config, -// where the node representing that component is provided along with the type -// and implementation name. -type ComponentWalkYAMLFunc func(c WalkedYAMLComponent) error - -// WalkedYAMLComponent is a struct containing information about a component -// yielded via the WalkYAML method. -type WalkedYAMLComponent struct { - ComponentType Type - Name string - Label string - Conf *yaml.Node -} - -// WalkYAML walks each node of a YAML tree and for any component types within -// the config a provided func is called. -func (f FieldSpecs) WalkYAML(node *yaml.Node, prov Provider, fn ComponentWalkYAMLFunc) error { - node = unwrapDocumentNode(node) - - nodeKeys := map[string]*yaml.Node{} - for i := 0; i < len(node.Content)-1; i += 2 { - nodeKeys[node.Content[i].Value] = node.Content[i+1] - } - - // Following the order of our field specs, walk each field. - for _, field := range f { - value, exists := nodeKeys[field.Name] - if !exists { - continue - } - if err := field.WalkYAML(value, prov, fn); err != nil { - return err - } - } - return nil -} - -//------------------------------------------------------------------------------ - -func unwrapDocumentNode(node *yaml.Node) *yaml.Node { - if node != nil && node.Kind == yaml.DocumentNode && len(node.Content) > 0 { - node = node.Content[0] - } - if node != nil && node.Alias != nil { - node = node.Alias - } - return node -} diff --git a/internal/docs/format_yaml_path.go b/internal/docs/format_yaml_path.go deleted file mode 100644 index 8127c046a3..0000000000 --- a/internal/docs/format_yaml_path.go +++ /dev/null @@ -1,376 +0,0 @@ -package docs - -import ( - "errors" - "fmt" - "strconv" - - "gopkg.in/yaml.v3" -) - -func removeFieldFromMapping(name string, node *yaml.Node) error { - var newContent []*yaml.Node - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value != name { - newContent = append(newContent, node.Content[i], node.Content[i+1]) - } - } - node.Content = newContent - return nil -} - -func getFieldFromMapping(name string, createMissing bool, node *yaml.Node) (*yaml.Node, error) { - node.Kind = yaml.MappingNode - node.Style = yaml.LiteralStyle - var foundNode *yaml.Node - for i := 0; i < len(node.Content)-1; i += 2 { - if node.Content[i].Value == name { - foundNode = node.Content[i+1] - break - } - } - if foundNode == nil { - if !createMissing { - return nil, fmt.Errorf("%v: key not found in mapping", name) - } - var keyNode yaml.Node - if err := keyNode.Encode(name); err != nil { - return nil, fmt.Errorf("%v: failed to encode key: %w", name, err) - } - node.Content = append(node.Content, &keyNode) - - foundNode = &yaml.Node{} - node.Content = append(node.Content, foundNode) - } - return foundNode, nil -} - -func removeIndexFromMapping(name string, node *yaml.Node) error { - removeIndex, err := strconv.Atoi(name) - if err != nil { - return err - } - if len(node.Content) <= removeIndex { - return fmt.Errorf("invalid delete index: %v, length: %v", removeIndex, len(node.Content)) - } - newContent := make([]*yaml.Node, 0, len(node.Content)-1) - for i := 0; i < len(node.Content); i++ { - if i != removeIndex { - newContent = append(newContent, node.Content[i]) - } - } - node.Content = newContent - return nil -} - -func getIndexFromSequence(name string, allowAppend bool, node *yaml.Node) (*yaml.Node, error) { - node.Kind = yaml.SequenceNode - node.Style = yaml.LiteralStyle - var foundNode *yaml.Node - if name != "-" { - index, err := strconv.Atoi(name) - if err != nil { - return nil, fmt.Errorf("%v: failed to parse path segment as array index: %w", name, err) - } - if len(node.Content) <= index { - return nil, fmt.Errorf("%v: target index greater than array length", name) - } - foundNode = node.Content[index] - } else { - if !allowAppend { - return nil, fmt.Errorf("%v: append directive not allowed", name) - } - foundNode = &yaml.Node{} - node.Content = append(node.Content, foundNode) - } - return foundNode, nil -} - -// SetYAMLPath sets the value of a node within a YAML document identified by a -// path to a value. -func (f FieldSpecs) SetYAMLPath(docsProvider Provider, root, value *yaml.Node, path ...string) error { - root = unwrapDocumentNode(root) - value = unwrapDocumentNode(value) - - var foundSpec FieldSpec - for _, spec := range f { - if spec.Name == path[0] { - foundSpec = spec - break - } - } - if foundSpec.Name == "" { - return fmt.Errorf("%v: field not recognised", path[0]) - } - - // Check for delete - if len(path) == 1 && value == nil { - return removeFieldFromMapping(path[0], root) - } - - foundNode, err := getFieldFromMapping(path[0], true, root) - if err != nil { - return err - } - - if err := foundSpec.SetYAMLPath(docsProvider, foundNode, value, path[1:]...); err != nil { - return fmt.Errorf("%v.%w", path[0], err) - } - return nil -} - -func setYAMLPathCore(docsProvider Provider, coreType Type, root, value *yaml.Node, path ...string) error { - foundNode, err := getFieldFromMapping(path[0], true, root) - if err != nil { - return err - } - if f, exists := ReservedFieldsByType(coreType)[path[0]]; exists { - if err = f.SetYAMLPath(docsProvider, foundNode, value, path[1:]...); err != nil { - return fmt.Errorf("%v.%w", path[0], err) - } - return nil - } - cSpec, exists := docsProvider.GetDocs(path[0], coreType) - if !exists { - return fmt.Errorf("%v: field not recognised", path[0]) - } - if err = cSpec.Config.SetYAMLPath(docsProvider, foundNode, value, path[1:]...); err != nil { - return fmt.Errorf("%v.%w", path[0], err) - } - return nil -} - -// SetYAMLPath sets the value of a node within a YAML document identified by a -// path to a value. -func (f FieldSpec) SetYAMLPath(docsProvider Provider, root, value *yaml.Node, path ...string) error { - root = unwrapDocumentNode(root) - value = unwrapDocumentNode(value) - - switch f.Kind { - case Kind2DArray: - if len(path) == 0 { - if value.Kind == yaml.SequenceNode { - *root = *value - } else { - root.Kind = yaml.SequenceNode - root.Style = yaml.LiteralStyle - root.Content = []*yaml.Node{{ - Kind: yaml.SequenceNode, - Style: yaml.LiteralStyle, - Content: []*yaml.Node{value}, - }} - } - return nil - } - if len(path) == 1 && value == nil { - return removeIndexFromMapping(path[0], root) - } - target, err := getIndexFromSequence(path[0], true, root) - if err != nil { - return err - } - if err = f.Array().SetYAMLPath(docsProvider, target, value, path[1:]...); err != nil { - return fmt.Errorf("%v.%w", path[0], err) - } - return nil - case KindArray: - if len(path) == 0 { - if value.Kind == yaml.SequenceNode { - *root = *value - } else { - root.Kind = yaml.SequenceNode - root.Style = yaml.LiteralStyle - root.Content = []*yaml.Node{value} - } - return nil - } - if len(path) == 1 && value == nil { - return removeIndexFromMapping(path[0], root) - } - target, err := getIndexFromSequence(path[0], true, root) - if err != nil { - return err - } - if err = f.Scalar().SetYAMLPath(docsProvider, target, value, path[1:]...); err != nil { - return fmt.Errorf("%v.%w", path[0], err) - } - return nil - case KindMap: - if len(path) == 0 { - return errors.New("cannot set map directly") - } - if len(path) == 1 && value == nil { - return removeFieldFromMapping(path[0], root) - } - target, err := getFieldFromMapping(path[0], true, root) - if err != nil { - return err - } - if err = f.Scalar().SetYAMLPath(docsProvider, target, value, path[1:]...); err != nil { - return fmt.Errorf("%v.%w", path[0], err) - } - return nil - } - if len(path) == 0 { - *root = *value - return nil - } - if len(path) == 1 && value == nil { - return removeFieldFromMapping(path[0], root) - } - if coreType, isCore := f.Type.IsCoreComponent(); isCore { - if len(path) == 0 { - return fmt.Errorf("(%v): cannot set core type directly", coreType) - } - return setYAMLPathCore(docsProvider, coreType, root, value, path...) - } - if len(f.Children) > 0 { - return f.Children.SetYAMLPath(docsProvider, root, value, path...) - } - return fmt.Errorf("%v: field not recognised", path[0]) -} - -//------------------------------------------------------------------------------ - -// GetDocsForPath attempts to find the documentation for a given node of a -// config identified by a path. -func (f FieldSpecs) GetDocsForPath(docsProvider Provider, path ...string) (FieldSpec, error) { - target := path[0] - for _, spec := range f { - if spec.Name == target { - return spec.GetDocsForPath(docsProvider, path[1:]...) - } - } - return FieldSpec{}, fmt.Errorf("%v: field not recognised", path[0]) -} - -func getDocsForPathCore(docsProvider Provider, coreType Type, path ...string) (FieldSpec, error) { - if f, exists := ReservedFieldsByType(coreType)[path[0]]; exists { - return f.GetDocsForPath(docsProvider, path[1:]...) - } - cSpec, exists := docsProvider.GetDocs(path[0], coreType) - if !exists { - return FieldSpec{}, fmt.Errorf("%v: field not recognised", path[0]) - } - return cSpec.Config.GetDocsForPath(docsProvider, path[1:]...) -} - -// GetDocsForPath attempts to find the documentation for a given node of a -// config identified by a path. -func (f FieldSpec) GetDocsForPath(docsProvider Provider, path ...string) (FieldSpec, error) { - if len(path) == 0 { - return f, nil - } - switch f.Kind { - case Kind2DArray: - return f.Array().GetDocsForPath(docsProvider, path[1:]...) - case KindArray, KindMap: - return f.Scalar().GetDocsForPath(docsProvider, path[1:]...) - } - if coreType, isCore := f.Type.IsCoreComponent(); isCore { - return getDocsForPathCore(docsProvider, coreType, path...) - } - if len(f.Children) > 0 { - return f.Children.GetDocsForPath(docsProvider, path...) - } - return FieldSpec{}, fmt.Errorf("%v: field not recognised", path[0]) -} - -// GetYAMLPath attempts to obtain a specific value within a YAML tree by -// following a sequence of path identifiers. -func GetYAMLPath(root *yaml.Node, path ...string) (*yaml.Node, error) { - root = unwrapDocumentNode(root) - - if len(path) == 0 { - return root, nil - } - - if root.Kind == yaml.SequenceNode { - newRoot, err := getIndexFromSequence(path[0], false, root) - if err != nil { - return nil, err - } - if newRoot, err = GetYAMLPath(newRoot, path[1:]...); err != nil { - return nil, fmt.Errorf("%v.%w", path[0], err) - } - return newRoot, nil - } - - newRoot, err := getFieldFromMapping(path[0], false, root) - if err != nil { - return nil, err - } - if newRoot, err = GetYAMLPath(newRoot, path[1:]...); err != nil { - return nil, fmt.Errorf("%v.%w", path[0], err) - } - return newRoot, nil -} - -//------------------------------------------------------------------------------ - -// YAMLLabelsToPaths walks a YAML tree using a field spec as a reference point. -// When a component of the YAML tree has a label field it is added to the -// provided labelsToPaths map with the path to the component. -func (f FieldSpecs) YAMLLabelsToPaths(docsProvider Provider, node *yaml.Node, labelsToPaths map[string][]string, path []string) { - node = unwrapDocumentNode(node) - - fieldMap := map[string]FieldSpec{} - for _, spec := range f { - fieldMap[spec.Name] = spec - } - - for i := 0; i < len(node.Content)-1; i += 2 { - key := node.Content[i].Value - if spec, exists := fieldMap[key]; exists { - spec.YAMLLabelsToPaths(docsProvider, node.Content[i+1], labelsToPaths, append(path, key)) - } - } -} - -// YAMLLabelsToPaths walks a YAML tree using a field spec as a reference point. -// When a component of the YAML tree has a label field it is added to the -// provided labelsToPaths map with the path to the component. -func (f FieldSpec) YAMLLabelsToPaths(docsProvider Provider, node *yaml.Node, labelsToPaths map[string][]string, path []string) { - node = unwrapDocumentNode(node) - - switch f.Kind { - case Kind2DArray: - nextSpec := f.Array() - for i, child := range node.Content { - nextSpec.YAMLLabelsToPaths(docsProvider, child, labelsToPaths, append(path, strconv.Itoa(i))) - } - case KindArray: - nextSpec := f.Scalar() - for i, child := range node.Content { - nextSpec.YAMLLabelsToPaths(docsProvider, child, labelsToPaths, append(path, strconv.Itoa(i))) - } - case KindMap: - nextSpec := f.Scalar() - for i, child := range node.Content { - nextSpec.YAMLLabelsToPaths(docsProvider, child, labelsToPaths, append(path, strconv.Itoa(i))) - } - for i := 0; i < len(node.Content)-1; i += 2 { - key := node.Content[i].Value - nextSpec.YAMLLabelsToPaths(docsProvider, node.Content[i+1], labelsToPaths, append(path, key)) - } - default: - if coreType, isCore := f.Type.IsCoreComponent(); isCore { - coreFields := FieldSpecs{} - for _, f := range ReservedFieldsByType(coreType) { - coreFields = append(coreFields, f) - } - if inferred, cSpec, err := GetInferenceCandidateFromYAML(docsProvider, coreType, node); err == nil { - conf := cSpec.Config - conf.Name = inferred - coreFields = append(coreFields, conf) - } - coreFields.YAMLLabelsToPaths(docsProvider, node, labelsToPaths, path) - } else if len(f.Children) > 0 { - f.Children.YAMLLabelsToPaths(docsProvider, node, labelsToPaths, path) - } else if f.Name == labelField.Name && f.Description == labelField.Description { - pathCopy := make([]string, len(path)-1) - copy(pathCopy, path[:len(path)-1]) - labelsToPaths[node.Value] = pathCopy // Add path to the parent node - } - } -} diff --git a/internal/docs/format_yaml_path_test.go b/internal/docs/format_yaml_path_test.go deleted file mode 100644 index cd7fd9043c..0000000000 --- a/internal/docs/format_yaml_path_test.go +++ /dev/null @@ -1,837 +0,0 @@ -package docs_test - -import ( - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -var configSpec = docs.FieldSpecs{ - docs.FieldInput("input", ""), - docs.FieldBuffer("buffer", ""), - docs.FieldObject("pipeline", "").WithChildren( - docs.FieldProcessor("processors", "").Array(), - ), - docs.FieldOutput("output", ""), - docs.FieldCache("cache_resources", "").Array(), -} - -func TestSetYAMLPath(t *testing.T) { - mockProv := docs.NewMappedDocsProvider() - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "kafka", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("addresses", "").Array(), - docs.FieldString("topics", "").Array(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "generate", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("mapping", ""), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "dynamic", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldInput("inputs", "").Map(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "nats", - Type: docs.TypeOutput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("urls", "").Array(), - docs.FieldString("subject", ""), - docs.FieldInt("max_in_flight", ""), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "compress", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("algorithm", ""), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "workflow", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("order", "").ArrayOfArrays(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "switch", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().Array().WithChildren( - docs.FieldString("check", ""), - docs.FieldProcessor("processors", "").Array(), - ), - }) - - tests := []struct { - name string - input string - path string - value string - output string - errContains string - }{ - { - name: "set input", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] - -output: - nats: - urls: [ nats://127.0.0.1:4222 ] - subject: benthos_messages - max_in_flight: 1 -`, - path: "/input", - value: ` -generate: - mapping: 'root = {"foo":"bar"}'`, - output: ` -input: - generate: - mapping: 'root = {"foo":"bar"}' -output: - nats: - urls: [ nats://127.0.0.1:4222 ] - subject: benthos_messages - max_in_flight: 1 -`, - }, - { - name: "set input addresses total", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/kafka/addresses", - value: `"foobar"`, - output: ` -input: - kafka: - addresses: [ "foobar" ] - topics: [ "baz" ] -`, - }, - { - name: "set mapping value", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/dynamic/inputs/foo/type", - value: `"foobar"`, - output: ` -input: - dynamic: - inputs: - foo: - type: "foobar" - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - }, - { - name: "set value to object", - input: `input: "hello world"`, - path: "/input/kafka/addresses", - value: `"foobar"`, - output: ` -input: - kafka: - addresses: ["foobar"] -`, - }, - { - name: "set array index", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/kafka/addresses/0", - value: `"baz"`, - output: ` -input: - kafka: - addresses: [ "baz", "bar" ] - topics: [ "baz" ] -`, - }, - { - name: "set array index child", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] - processors: - - compress: - algorithm: gzip -`, - path: "/input/processors/0/compress/algorithm", - value: `"baz"`, - output: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] - processors: - - compress: - algorithm: baz -`, - }, - { - name: "set array append", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/kafka/addresses/-", - value: `"baz"`, - output: ` -input: - kafka: - addresses: [ "foo", "bar", "baz" ] - topics: [ "baz" ] -`, - }, - { - name: "set array NaN", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] -`, - path: "/input/kafka/addresses/nope", - value: `"baz"`, - errContains: "input.kafka.addresses.nope: failed to parse path segment as array index", - }, - { - name: "set array big index", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] -`, - path: "/input/kafka/addresses/2", - value: `"baz"`, - errContains: "input.kafka.addresses.2: target index greater than", - }, - { - name: "set nested array big index", - input: ` -input: - kafka: - addresses: [ [ "foo", "bar" ] ] -`, - path: "/input/kafka/addresses/0/2", - value: `"baz"`, - errContains: "input.kafka.addresses.0.2: field not recognised", - }, - { - name: "set 2D array value abs", - input: ` -pipeline: - processors: - - workflow: - order: [] -`, - path: "/pipeline/processors/0/workflow/order", - value: `"baz"`, - output: ` -pipeline: - processors: - - workflow: - order: [["baz"]] -`, - }, - { - name: "set 2D array value outer index", - input: ` -pipeline: - processors: - - workflow: - order: [] -`, - path: "/pipeline/processors/0/workflow/order/-", - value: `"baz"`, - output: ` -pipeline: - processors: - - workflow: - order: [["baz"]] -`, - }, - { - name: "set 2D array value inner index", - input: ` -pipeline: - processors: - - workflow: - order: [] -`, - path: "/pipeline/processors/0/workflow/order/-/-", - value: `"baz"`, - output: ` -pipeline: - processors: - - workflow: - order: [["baz"]] -`, - }, - { - name: "delete field", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/kafka/addresses", - output: ` -input: - kafka: - topics: [ "baz" ] -`, - }, - { - name: "delete element", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/kafka/addresses/1", - output: ` -input: - kafka: - addresses: [ "foo" ] - topics: [ "baz" ] -`, - }, - { - name: "delete input", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] -`, - path: "/input/kafka", - output: ` -input: {} -`, - }, - { - name: "set switch case check", - input: ` -pipeline: - processors: - - switch: - - check: 'root = "foobar"' - processors: - - compress: - algorithm: meow1 - - check: 'root = "foobar2"' - processors: - - compress: - algorithm: meow2 - - compress: - algorithm: meow3 -`, - path: "/pipeline/processors/0/switch/0/check", - value: "foobar3", - output: ` -pipeline: - processors: - - switch: - - check: 'foobar3' - processors: - - compress: - algorithm: meow1 - - check: 'root = "foobar2"' - processors: - - compress: - algorithm: meow2 - - compress: - algorithm: meow3 -`, - }, - { - name: "set switch case processors", - input: ` -pipeline: - processors: - - switch: - - check: 'root = "foobar"' - processors: - - compress: - algorithm: meow1 -`, - path: "/pipeline/processors/0/switch/0/processors", - value: ` -- compress: - algorithm: meow2 -- compress: - algorithm: meow3 -`, - output: ` -pipeline: - processors: - - switch: - - check: 'root = "foobar"' - processors: - - compress: - algorithm: meow2 - - compress: - algorithm: meow3 -`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - input, value := &yaml.Node{}, &yaml.Node{} - - require.NoError(t, yaml.Unmarshal([]byte(test.input), input)) - if test.value != "" { - require.NoError(t, yaml.Unmarshal([]byte(test.value), value)) - } else { - value = nil - } - - path, err := gabs.JSONPointerToSlice(test.path) - require.NoError(t, err) - - err = configSpec.SetYAMLPath(mockProv, input, value, path...) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - - var iinput, ioutput any - require.NoError(t, input.Decode(&iinput)) - require.NoError(t, yaml.Unmarshal([]byte(test.output), &ioutput)) - assert.Equal(t, ioutput, iinput) - } - }) - } -} - -func TestGetPathDocs(t *testing.T) { - mockProv := docs.NewMappedDocsProvider() - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "kafka", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("addresses", "").Array(), - docs.FieldString("topics", "").Array(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "dynamic", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldInput("inputs", "").Map(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "workflow", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("order", "").ArrayOfArrays(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "try", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().Array().HasType(docs.FieldTypeProcessor), - }) - - tests := []struct { - name string - path string - resName string - resType string - resKind string - resChildren int - errContains string - }{ - { - name: "root input", - path: "/input", - resName: "input", - resType: "input", - resKind: "scalar", - }, - { - name: "kafka input", - path: "/input/kafka", - resName: "", - resType: "object", - resKind: "scalar", - resChildren: 2, - }, - { - name: "kafka input addresses", - path: "/input/kafka/addresses", - resName: "addresses", - resType: "string", - resKind: "array", - }, - { - name: "dynamic input inputs", - path: "/input/dynamic/inputs", - resName: "inputs", - resType: "input", - resKind: "map", - }, - { - name: "dynamic input named child", - path: "/input/dynamic/inputs/foo", - resName: "inputs", - resType: "input", - resKind: "scalar", - }, - { - name: "workflow 2D array outer", - path: "/pipeline/processors/0/workflow/order", - resName: "order", - resType: "string", - resKind: "2darray", - }, - { - name: "workflow 2D array inner", - path: "/pipeline/processors/0/workflow/order/1", - resName: "order", - resType: "string", - resKind: "array", - }, - { - name: "workflow 2D array element", - path: "/pipeline/processors/0/workflow/order/1/3", - resName: "order", - resType: "string", - resKind: "scalar", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - path, err := gabs.JSONPointerToSlice(test.path) - require.NoError(t, err) - - docs, err := configSpec.GetDocsForPath(mockProv, path...) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - - assert.Equal(t, test.resName, docs.Name) - assert.Equal(t, test.resType, string(docs.Type)) - assert.Equal(t, test.resKind, string(docs.Kind)) - assert.Len(t, docs.Children, test.resChildren) - } - }) - } -} - -func TestGetYAMLPath(t *testing.T) { - tests := []struct { - name string - input string - path string - output string - errContains string - }{ - { - name: "all of input", - input: ` -input: - kafka: - addresses: [ "foo" ] -`, - path: "/input", - output: ` -kafka: - addresses: [ "foo" ] -`, - }, - { - name: "first address of input", - input: ` -input: - kafka: - addresses: [ "foo" ] -`, - path: "/input/kafka/addresses/0", - output: `"foo"`, - }, - { - name: "unknown field", - input: ` -input: - kafka: - addresses: [ "foo" ] -`, - path: "/input/meow", - errContains: "input.meow: key not found in mapping", - }, - { - name: "bad index", - input: ` -input: - kafka: - addresses: [ "foo" ] -`, - path: "/input/kafka/addresses/10", - errContains: "input.kafka.addresses.10: target index greater", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var input yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(test.input), &input)) - - path, err := gabs.JSONPointerToSlice(test.path) - require.NoError(t, err) - - output, err := docs.GetYAMLPath(&input, path...) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - - var expected, actual any - require.NoError(t, output.Decode(&actual)) - require.NoError(t, yaml.Unmarshal([]byte(test.output), &expected)) - assert.Equal(t, expected, actual) - } - }) - } -} - -func TestYAMLLabelsToPath(t *testing.T) { - mockProv := docs.NewMappedDocsProvider() - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "kafka", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("addresses", "").Array(), - docs.FieldString("topics", "").Array(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "dynamic", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldInput("inputs", "").Map(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "nats", - Type: docs.TypeOutput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("urls", "").Array(), - docs.FieldString("subject", ""), - docs.FieldInt("max_in_flight", ""), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "compress", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("algorithm", ""), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "for_each", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldProcessor("things", "").Array(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "mega_for_each", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldProcessor("things", "").ArrayOfArrays(), - ), - }) - mockProv.RegisterDocs(docs.ComponentSpec{ - Name: "workflow", - Type: docs.TypeProcessor, - Config: docs.FieldComponent().WithChildren( - docs.FieldProcessor("things", "").Map(), - ), - }) - - tests := []struct { - name string - input string - output map[string][]string - }{ - { - name: "no labels", - input: ` -input: - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] - -output: - nats: - urls: [ nats://127.0.0.1:4222 ] - subject: benthos_messages - max_in_flight: 1 -`, - output: map[string][]string{}, - }, - { - name: "basic components all with labels", - input: ` -input: - label: fooinput - kafka: - addresses: [ "foo", "bar" ] - topics: [ "baz" ] - -pipeline: - processors: - - label: fooproc1 - compress: - algorithm: nahm8 - -output: - label: foooutput - nats: - urls: [ nats://127.0.0.1:4222 ] - subject: benthos_messages - max_in_flight: 1 -`, - output: map[string][]string{ - "fooinput": {"input"}, - "fooproc1": {"pipeline", "processors", "0"}, - "foooutput": {"output"}, - }, - }, - { - name: "Array of procs", - input: ` -pipeline: - processors: - - label: fooproc1 - for_each: - things: - - label: fooproc2 - compress: - algorithm: nahm8 - - label: fooproc3 - compress: - algorithm: nahm8 -`, - output: map[string][]string{ - "fooproc1": {"pipeline", "processors", "0"}, - "fooproc2": {"pipeline", "processors", "0", "for_each", "things", "0"}, - "fooproc3": {"pipeline", "processors", "0", "for_each", "things", "1"}, - }, - }, - { - name: "array of array of procs", - input: ` -pipeline: - processors: - - label: fooproc1 - mega_for_each: - things: - - - - label: fooproc2 - compress: - algorithm: nahm8 - - label: fooproc3 - compress: - algorithm: nahm8 - - - - label: fooproc4 - compress: - algorithm: nahm8 -`, - output: map[string][]string{ - "fooproc1": {"pipeline", "processors", "0"}, - "fooproc2": {"pipeline", "processors", "0", "mega_for_each", "things", "0", "0"}, - "fooproc3": {"pipeline", "processors", "0", "mega_for_each", "things", "0", "1"}, - "fooproc4": {"pipeline", "processors", "0", "mega_for_each", "things", "1", "0"}, - }, - }, - { - name: "map of procs", - input: ` -pipeline: - processors: - - label: fooproc1 - workflow: - things: - first: - label: fooproc2 - compress: - algorithm: nahm8 - second: - label: fooproc3 - compress: - algorithm: nahm8 - third: - label: fooproc4 - compress: - algorithm: nahm8 -`, - output: map[string][]string{ - "fooproc1": {"pipeline", "processors", "0"}, - "fooproc2": {"pipeline", "processors", "0", "workflow", "things", "first"}, - "fooproc3": {"pipeline", "processors", "0", "workflow", "things", "second"}, - "fooproc4": {"pipeline", "processors", "0", "workflow", "things", "third"}, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var input yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(test.input), &input)) - - paths := map[string][]string{} - - configSpec.YAMLLabelsToPaths(mockProv, &input, paths, nil) - assert.Equal(t, test.output, paths) - }) - } -} diff --git a/internal/docs/format_yaml_test.go b/internal/docs/format_yaml_test.go deleted file mode 100644 index 5a61140981..0000000000 --- a/internal/docs/format_yaml_test.go +++ /dev/null @@ -1,1408 +0,0 @@ -package docs_test - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func TestSecretScrubbingYAML(t *testing.T) { - fields := docs.FieldSpecs{ - docs.FieldString("foo", "").Secret(), - docs.FieldString("bar", ""), - docs.FieldString("bazes", "").Secret().Array(), - docs.FieldString("buzes", "").Secret().ArrayOfArrays(), - docs.FieldString("bevers", "").Secret().Map(), - } - - tests := []struct { - name string - input string - output string - }{ - { - name: "all env vars", - input: `foo: "${foo_value}" -bar: "${bar_value}" -bazes: [ "${baz_value_one}", "${baz_value_two}" ] -buzes: [ [ "${buz_value_one}" ], [ "${buz_value_two}", "${buz_value_three}" ] ] -bevers: - first: "${bev_value_one}" - second: "${bev_value_one}" -`, - output: `foo: ${foo_value} -bar: "${bar_value}" -bazes: ['${baz_value_one}', '${baz_value_two}'] -buzes: [['${buz_value_one}'], ['${buz_value_two}', '${buz_value_three}']] -bevers: - first: ${bev_value_one} - second: ${bev_value_one} -`, - }, - { - name: "all real secrets", - input: `foo: dont print me!" -bar: you can print me -bazes: [ 'dont print me either', 'nor me' ] -buzes: [ [ 'and definitely' ], [ 'not any', 'of these' ] ] -bevers: - first: dont even think - second: about showing these ones -`, - output: `foo: '!!!SECRET_SCRUBBED!!!' -bar: you can print me -bazes: ['!!!SECRET_SCRUBBED!!!', '!!!SECRET_SCRUBBED!!!'] -buzes: [['!!!SECRET_SCRUBBED!!!'], ['!!!SECRET_SCRUBBED!!!', '!!!SECRET_SCRUBBED!!!']] -bevers: - first: '!!!SECRET_SCRUBBED!!!' - second: '!!!SECRET_SCRUBBED!!!' -`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var node yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(test.input), &node)) - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.DocsProvider = docs.NewMappedDocsProvider() - - require.NoError(t, fields.SanitiseYAML(&node, sanitConf)) - - resBytes, err := yaml.Marshal(node.Content[0]) - require.NoError(t, err) - assert.Equal(t, test.output, string(resBytes)) - }) - } -} - -func TestFieldsFromNode(t *testing.T) { - tests := []struct { - name string - yaml string - fields docs.FieldSpecs - }{ - { - name: "flat object", - yaml: `a: foo -b: bar -c: 21`, - fields: docs.FieldSpecs{ - docs.FieldString("a", "").HasDefault("foo"), - docs.FieldString("b", "").HasDefault("bar"), - docs.FieldInt("c", "").HasDefault(int64(21)), - }, - }, - { - name: "nested object", - yaml: `a: foo -b: - d: bar - e: 22 -c: true`, - fields: docs.FieldSpecs{ - docs.FieldString("a", "").HasDefault("foo"), - docs.FieldObject("b", "").WithChildren( - docs.FieldString("d", "").HasDefault("bar"), - docs.FieldInt("e", "").HasDefault(int64(22)), - ), - docs.FieldBool("c", "").HasDefault(true), - }, - }, - { - name: "array of strings", - yaml: `a: -- foo`, - fields: docs.FieldSpecs{ - docs.FieldString("a", "").Array().HasDefault([]string{"foo"}), - }, - }, - { - name: "array of ints", - yaml: `a: -- 5 -- 8`, - fields: docs.FieldSpecs{ - docs.FieldInt("a", "").Array().HasDefault([]int64{5, 8}), - }, - }, - { - name: "nested array of strings", - yaml: `a: - b: - - foo - - bar`, - fields: docs.FieldSpecs{ - docs.FieldObject("a", "").WithChildren( - docs.FieldString("b", "").Array().HasDefault([]string{"foo", "bar"}), - ), - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - confBytes := []byte(test.yaml) - - var node yaml.Node - require.NoError(t, yaml.Unmarshal(confBytes, &node)) - - assert.Equal(t, test.fields, docs.FieldsFromYAML(&node)) - }) - } -} - -func TestFieldsNodeToMap(t *testing.T) { - spec := docs.FieldSpecs{ - docs.FieldString("a", ""), - docs.FieldInt("b", "").HasDefault(11), - docs.FieldObject("c", "").WithChildren( - docs.FieldBool("d", "").HasDefault(true), - docs.FieldString("e", "").HasDefault("evalue"), - docs.FieldObject("f", "").WithChildren( - docs.FieldInt("g", "").HasDefault(12), - docs.FieldString("h", ""), - docs.FieldFloat("i", "").HasDefault(13.0), - ), - ), - } - - var node yaml.Node - err := yaml.Unmarshal([]byte(` -a: setavalue -c: - f: - g: 22 - h: sethvalue - i: 23.1 -`), &node) - require.NoError(t, err) - - generic, err := spec.YAMLToMap(&node, docs.ToValueConfig{}) - require.NoError(t, err) - - assert.Equal(t, map[string]any{ - "a": "setavalue", - "b": 11, - "c": map[string]any{ - "d": true, - "e": "evalue", - "f": map[string]any{ - "g": 22, - "h": "sethvalue", - "i": 23.1, - }, - }, - }, generic) -} - -func TestFieldsNodeToMapTypeCoercion(t *testing.T) { - tests := []struct { - name string - spec docs.FieldSpecs - yaml string - result any - }{ - { - name: "string fields", - spec: docs.FieldSpecs{ - docs.FieldString("a", ""), - docs.FieldString("b", ""), - docs.FieldString("c", ""), - docs.FieldString("d", ""), - docs.FieldString("e", "").Array(), - docs.FieldString("f", "").Map(), - }, - yaml: ` -a: no -b: false -c: 10 -d: 30.4 -e: - - no - - false - - 10 -f: - "1": no - "2": false - "3": 10 -`, - result: map[string]any{ - "a": "no", - "b": "false", - "c": "10", - "d": "30.4", - "e": []any{ - "no", "false", "10", - }, - "f": map[string]any{ - "1": "no", "2": "false", "3": "10", - }, - }, - }, - { - name: "bool fields", - spec: docs.FieldSpecs{ - docs.FieldBool("a", ""), - docs.FieldBool("b", ""), - docs.FieldBool("c", ""), - docs.FieldBool("d", "").Array(), - docs.FieldBool("e", "").Map(), - }, - yaml: ` -a: no -b: false -c: true -d: - - no - - false - - true -e: - "1": no - "2": false - "3": true -`, - result: map[string]any{ - "a": false, - "b": false, - "c": true, - "d": []any{ - false, false, true, - }, - "e": map[string]any{ - "1": false, "2": false, "3": true, - }, - }, - }, - { - name: "int fields", - spec: docs.FieldSpecs{ - docs.FieldInt("a", ""), - docs.FieldInt("b", ""), - docs.FieldInt("c", ""), - docs.FieldInt("d", "").Array(), - docs.FieldInt("e", "").Map(), - }, - yaml: ` -a: 11 -b: -12 -c: 13.4 -d: - - 11 - - -12 - - 13.4 -e: - "1": 11 - "2": -12 - "3": 13.4 -`, - result: map[string]any{ - "a": 11, - "b": -12, - "c": 13, - "d": []any{ - 11, -12, 13, - }, - "e": map[string]any{ - "1": 11, "2": -12, "3": 13, - }, - }, - }, - { - name: "float fields", - spec: docs.FieldSpecs{ - docs.FieldFloat("a", ""), - docs.FieldFloat("b", ""), - docs.FieldFloat("c", ""), - docs.FieldFloat("d", "").Array(), - docs.FieldFloat("e", "").Map(), - }, - yaml: ` -a: 11 -b: -12 -c: 13.4 -d: - - 11 - - -12 - - 13.4 -e: - "1": 11 - "2": -12 - "3": 13.4 -`, - result: map[string]any{ - "a": 11.0, - "b": -12.0, - "c": 13.4, - "d": []any{ - 11.0, -12.0, 13.4, - }, - "e": map[string]any{ - "1": 11.0, "2": -12.0, "3": 13.4, - }, - }, - }, - { - name: "recurse array of objects", - spec: docs.FieldSpecs{ - docs.FieldObject("foo", "").WithChildren( - docs.FieldObject("eels", "").Array().WithChildren( - docs.FieldString("bar", "").HasDefault("default"), - ), - ), - }, - yaml: ` -foo: - eels: - - bar: bar1 - - bar: bar2 -`, - result: map[string]any{ - "foo": map[string]any{ - "eels": []any{ - map[string]any{ - "bar": "bar1", - }, - map[string]any{ - "bar": "bar2", - }, - }, - }, - }, - }, - { - name: "recurse map of objects", - spec: docs.FieldSpecs{ - docs.FieldObject("foo", "").WithChildren( - docs.FieldObject("eels", "").Map().WithChildren( - docs.FieldString("bar", "").HasDefault("default"), - ), - ), - }, - yaml: ` -foo: - eels: - first: - bar: bar1 - second: - bar: bar2 -`, - result: map[string]any{ - "foo": map[string]any{ - "eels": map[string]any{ - "first": map[string]any{ - "bar": "bar1", - }, - "second": map[string]any{ - "bar": "bar2", - }, - }, - }, - }, - }, - { - name: "component field", - spec: docs.FieldSpecs{ - docs.FieldString("a", "").HasDefault("adefault"), - docs.FieldProcessor("b", ""), - docs.FieldBool("c", ""), - }, - yaml: ` -b: - bloblang: 'root = "hello world"' -c: true -`, - result: map[string]any{ - "a": "adefault", - "b": &yaml.Node{ - Kind: yaml.MappingNode, - Tag: "!!map", - Line: 3, - Column: 3, - Content: []*yaml.Node{ - { - Kind: yaml.ScalarNode, - Tag: "!!str", - Value: "bloblang", - Line: 3, - Column: 3, - }, - { - Kind: yaml.ScalarNode, - Style: yaml.SingleQuotedStyle, - Tag: "!!str", - Value: `root = "hello world"`, - Line: 3, - Column: 13, - }, - }, - }, - "c": true, - }, - }, - { - name: "component field in array", - spec: docs.FieldSpecs{ - docs.FieldString("a", "").HasDefault("adefault"), - docs.FieldProcessor("b", "").Array(), - docs.FieldBool("c", ""), - }, - yaml: ` -b: - - bloblang: 'root = "hello world"' -c: true -`, - result: map[string]any{ - "a": "adefault", - "b": []any{ - &yaml.Node{ - Kind: yaml.MappingNode, - Tag: "!!map", - Line: 3, - Column: 5, - Content: []*yaml.Node{ - { - Kind: yaml.ScalarNode, - Tag: "!!str", - Value: "bloblang", - Line: 3, - Column: 5, - }, - { - Kind: yaml.ScalarNode, - Style: yaml.SingleQuotedStyle, - Tag: "!!str", - Value: `root = "hello world"`, - Line: 3, - Column: 15, - }, - }, - }, - }, - "c": true, - }, - }, - { - name: "component field in map", - spec: docs.FieldSpecs{ - docs.FieldString("a", "").HasDefault("adefault"), - docs.FieldProcessor("b", "").Map(), - docs.FieldBool("c", ""), - }, - yaml: ` -b: - foo: - bloblang: 'root = "hello world"' -c: true -`, - result: map[string]any{ - "a": "adefault", - "b": map[string]any{ - "foo": &yaml.Node{ - Kind: yaml.MappingNode, - Tag: "!!map", - Line: 4, - Column: 5, - Content: []*yaml.Node{ - { - Kind: yaml.ScalarNode, - Tag: "!!str", - Value: "bloblang", - Line: 4, - Column: 5, - }, - { - Kind: yaml.ScalarNode, - Style: yaml.SingleQuotedStyle, - Tag: "!!str", - Value: `root = "hello world"`, - Line: 4, - Column: 15, - }, - }, - }, - }, - "c": true, - }, - }, - { - name: "array of array of string", - spec: docs.FieldSpecs{ - docs.FieldString("foo", "").ArrayOfArrays(), - }, - yaml: ` -foo: - - - - bar1 - - bar2 - - - - bar3 -`, - result: map[string]any{ - "foo": []any{ - []any{"bar1", "bar2"}, - []any{"bar3"}, - }, - }, - }, - { - name: "array of array of int, float and bool", - spec: docs.FieldSpecs{ - docs.FieldInt("foo", "").ArrayOfArrays(), - docs.FieldFloat("bar", "").ArrayOfArrays(), - docs.FieldBool("baz", "").ArrayOfArrays(), - }, - yaml: ` -foo: [[3,4],[5]] -bar: [[3.3,4.4],[5.5]] -baz: [[true,false],[true]] -`, - result: map[string]any{ - "foo": []any{ - []any{3, 4}, []any{5}, - }, - "bar": []any{ - []any{3.3, 4.4}, []any{5.5}, - }, - "baz": []any{ - []any{true, false}, []any{true}, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var node yaml.Node - err := yaml.Unmarshal([]byte(test.yaml), &node) - require.NoError(t, err) - - generic, err := test.spec.YAMLToMap(&node, docs.ToValueConfig{}) - require.NoError(t, err) - - assert.Equal(t, test.result, generic) - }) - } -} - -func TestFieldToNode(t *testing.T) { - tests := []struct { - name string - spec docs.FieldSpec - recurse bool - expected string - }{ - { - name: "no recurse single node null", - spec: docs.FieldObject("foo", ""), - expected: `null # No default (required) -`, - }, - { - name: "no recurse with children", - spec: docs.FieldObject("foo", "").WithChildren( - docs.FieldString("bar", ""), - docs.FieldString("baz", ""), - ), - expected: `{} # No default (required) -`, - }, - { - name: "no recurse map", - spec: docs.FieldString("foo", "").Map(), - expected: `{} # No default (required) -`, - }, - { - name: "recurse with children", - spec: docs.FieldObject("foo", "").WithChildren( - docs.FieldString("bar", ""), - docs.FieldString("baz", "").HasDefault("baz default"), - docs.FieldInt("buz", ""), - docs.FieldFloat("bev", ""), - docs.FieldBool("bun", ""), - docs.FieldString("bud", "").Array(), - ), - recurse: true, - expected: `bar: "" # No default (required) -baz: baz default -buz: 0 # No default (required) -bev: 0 # No default (required) -bun: false # No default (required) -bud: [] # No default (required) -`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - n, err := test.spec.ToYAML(test.recurse) - require.NoError(t, err) - - b, err := yaml.Marshal(n) - require.NoError(t, err) - - assert.Equal(t, test.expected, string(b)) - }) - } -} - -func TestYAMLComponentLinting(t *testing.T) { - prov := docs.NewMappedDocsProvider() - - for _, t := range docs.Types() { - prov.RegisterDocs(docs.ComponentSpec{ - Name: "resource", - Type: t, - Config: docs.FieldString("", ""), - }) - prov.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testlintfoo%v", string(t)), - Type: t, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("foo1", "").LinterFunc(func(ctx docs.LintContext, line, col int, v any) []docs.Lint { - if v == "lint me please" { - return []docs.Lint{ - docs.NewLintError(line, docs.LintCustom, errors.New("this is a custom lint")), - } - } - return nil - }).Optional(), - docs.FieldString("foo2", "").Advanced().OmitWhen(func(field, parent any) (string, bool) { - if field == "drop me" { - return "because foo", true - } - return "", false - }).Optional(), - docs.FieldProcessor("foo3", "").Optional(), - docs.FieldProcessor("foo4", "").Array().Advanced().Optional(), - docs.FieldProcessor("foo5", "").Map().Optional(), - docs.FieldString("foo6", "").Optional().Deprecated(), - docs.FieldObject("foo7", "").Array().WithChildren( - docs.FieldString("foochild1", "").Optional(), - ).Optional().Advanced(), - docs.FieldObject("foo8", "").Map().WithChildren( - docs.FieldInt("foochild1", "").Optional(), - ).Optional().Advanced(), - ), - }) - prov.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testlintbar%v", string(t)), - Type: t, - Status: docs.StatusDeprecated, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("bar1", "").Optional(), - ), - }) - } - - type testCase struct { - name string - inputType docs.Type - inputConf string - rejectDeprecated bool - requireLabels bool - - res []docs.Lint - } - - tests := []testCase{ - { - name: "ignores comments", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - # comment here - foo1: hello world # And what's this?`, - }, - { - name: "no problem with deprecated component", - inputType: docs.TypeInput, - inputConf: ` -testlintbarinput: - bar1: hello world`, - }, - { - name: "no problem with deprecated fields", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo1: hello world - foo6: hello world`, - }, - { - name: "reject deprecated component", - inputType: docs.TypeInput, - inputConf: ` -testlintbarinput: - bar1: hello world`, - rejectDeprecated: true, - res: []docs.Lint{ - docs.NewLintError(2, docs.LintDeprecated, errors.New("component testlintbarinput is deprecated")), - }, - }, - { - name: "reject deprecated fields", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo1: hello world - foo6: hello world`, - rejectDeprecated: true, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintDeprecated, errors.New("field foo6 is deprecated")), - }, - }, - { - name: "require label", - inputType: docs.TypeInput, - inputConf: ` -testlintbarinput: - bar1: hello world`, - requireLabels: true, - res: []docs.Lint{ - docs.NewLintError(2, docs.LintMissingLabel, errors.New("label is required for testlintbarinput")), - }, - }, - { - name: "do not require label for resource", - inputType: docs.TypeInput, - inputConf: ` -resource: something`, - requireLabels: true, - }, - { - name: "allows anchors", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: &test-anchor - foo1: hello world -processors: - - testlintfooprocessor: *test-anchor`, - }, - { - name: "lints through anchors", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: &test-anchor - foo1: hello world - nope: bad field -processors: - - testlintfooprocessor: *test-anchor`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintUnknown, errors.New("field nope not recognised")), - docs.NewLintError(4, docs.LintUnknown, errors.New("field nope not recognised")), - }, - }, - { - name: "unknown fields", - inputType: docs.TypeInput, - inputConf: ` -type: testlintfooinput -testlintfooinput: - not_recognised: yuh - foo1: hello world - also_not_recognised: nah -definitely_not_recognised: huh`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintUnknown, errors.New("field not_recognised not recognised")), - docs.NewLintError(6, docs.LintUnknown, errors.New("field also_not_recognised not recognised")), - docs.NewLintError(7, docs.LintUnknown, errors.New("field definitely_not_recognised is invalid when the component type is testlintfooinput (input)")), - }, - }, - { - name: "reserved field unknown fields", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - not_recognised: yuh - foo1: hello world -processors: - - testlintfooprocessor: - also_not_recognised: nah`, - res: []docs.Lint{ - docs.NewLintError(3, docs.LintUnknown, errors.New("field not_recognised not recognised")), - docs.NewLintError(7, docs.LintUnknown, errors.New("field also_not_recognised not recognised")), - }, - }, - { - name: "collision of labels", - inputType: docs.TypeInput, - inputConf: ` -label: foo -testlintfooinput: - foo1: hello world -processors: - - label: bar - testlintfooprocessor: {} - - label: foo - testlintfooprocessor: {}`, - res: []docs.Lint{ - docs.NewLintError(8, docs.LintDuplicateLabel, errors.New("label 'foo' collides with a previously defined label at line 2")), - }, - }, - { - name: "empty processors", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo1: hello world -processors: []`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintShouldOmit, errors.New("field processors is empty and can be removed")), - }, - }, - { - name: "custom omit func", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo1: hello world - foo2: drop me`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintShouldOmit, errors.New("because foo")), - }, - }, - { - name: "nested array not an array", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo4: - key1: - testlintfooprocessor: - foo1: somevalue - not_recognised: nah`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintExpectedArray, errors.New("expected array value")), - }, - }, - { - name: "nested fields", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo3: - testlintfooprocessor: - foo1: somevalue - not_recognised: nah`, - res: []docs.Lint{ - docs.NewLintError(6, docs.LintUnknown, errors.New("field not_recognised not recognised")), - }, - }, - { - name: "array for string", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo3: - testlintfooprocessor: - foo1: [ somevalue ] -`, - res: []docs.Lint{ - docs.NewLintError(5, docs.LintExpectedScalar, errors.New("expected string value")), - }, - }, - { - name: "nested map fields", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo5: - key1: - testlintfooprocessor: - foo1: somevalue - not_recognised: nah`, - res: []docs.Lint{ - docs.NewLintError(7, docs.LintUnknown, errors.New("field not_recognised not recognised")), - }, - }, - { - name: "nested map not a map", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo5: - - testlintfooprocessor: - foo1: somevalue - not_recognised: nah`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintExpectedObject, errors.New("expected object value, got !!seq")), - }, - }, - { - name: "array field", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo7: - - foochild1: yep`, - }, - { - name: "array field bad", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo7: - - wat: no`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintUnknown, errors.New("field wat not recognised")), - }, - }, - { - name: "array field not array", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo7: - key1: - wat: no`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintExpectedArray, errors.New("expected array value")), - }, - }, - { - name: "map field", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo8: - key1: - foochild1: 10`, - }, - { - name: "map field bad", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo8: - key1: - wat: nope`, - res: []docs.Lint{ - docs.NewLintError(5, docs.LintUnknown, errors.New("field wat not recognised")), - }, - }, - { - name: "map field not map", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo8: - - wat: nope`, - res: []docs.Lint{ - docs.NewLintError(4, docs.LintExpectedObject, errors.New("expected object value, got !!seq")), - }, - }, - { - name: "custom lint", - inputType: docs.TypeInput, - inputConf: ` -testlintfooinput: - foo1: lint me please`, - res: []docs.Lint{ - docs.NewLintError(3, docs.LintCustom, errors.New("this is a custom lint")), - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - lConf := docs.NewLintConfig(bundle.GlobalEnvironment) - lConf.RejectDeprecated = test.rejectDeprecated - lConf.RequireLabels = test.requireLabels - lConf.DocsProvider = prov - - var node yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(test.inputConf), &node)) - lints := docs.LintYAML(docs.NewLintContext(lConf), test.inputType, &node) - assert.Equal(t, test.res, lints) - }) - } -} - -func TestYAMLLintYAMLMerge(t *testing.T) { - prov := docs.NewMappedDocsProvider() - prov.RegisterDocs(docs.ComponentSpec{ - Name: "meowthing", - Type: docs.TypeInput, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("foo", ""), - docs.FieldString("bar", "").Optional(), - ), - }) - - lConf := docs.NewLintConfig(bundle.GlobalEnvironment) - lConf.DocsProvider = prov - - lintConf := func(t *testing.T, name, conf string, expected []docs.Lint) { - t.Run(name, func(t *testing.T) { - var node yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(conf), &node)) - lints := docs.FieldInput("root", "").Map().LintYAML(docs.NewLintContext(lConf), &node) - assert.Equal(t, expected, lints) - }) - } - - lintConf(t, "no lint errors", ` -first: - meowthing: &a - foo: one -second: - meowthing: - <<: *a - bar: two -`, nil) - - lintConf(t, "unknown field from", ` -first: - meowthing: &a - foo: one - baz: three -second: - meowthing: - <<: *a - bar: two -`, []docs.Lint{ - {Line: 5, Column: 1, Level: docs.LintError, Type: docs.LintUnknown, What: "field baz not recognised"}, - {Line: 5, Column: 1, Level: docs.LintError, Type: docs.LintUnknown, What: "field baz not recognised"}, - }) - - lintConf(t, "unknown field into", ` -first: - meowthing: &a - foo: one - bar: two -second: - meowthing: - <<: *a - baz: three -`, []docs.Lint{ - {Line: 9, Column: 1, Level: docs.LintError, Type: docs.LintUnknown, What: "field baz not recognised"}, - }) -} - -func TestYAMLLinting(t *testing.T) { - type testCase struct { - name string - inputSpec docs.FieldSpec - inputConf string - - res []docs.Lint - } - - tests := []testCase{ - { - name: "expected string got array", - inputSpec: docs.FieldString("foo", ""), - inputConf: `["foo","bar"]`, - res: []docs.Lint{ - docs.NewLintError(1, docs.LintExpectedScalar, errors.New("expected string value")), - }, - }, - { - name: "expected array got string", - inputSpec: docs.FieldString("foo", "").Array(), - inputConf: `"foo"`, - res: []docs.Lint{ - docs.NewLintError(1, docs.LintExpectedArray, errors.New("expected array value")), - }, - }, - { - name: "expected object got string", - inputSpec: docs.FieldObject("foo", "").WithChildren( - docs.FieldString("bar", ""), - ), - inputConf: `"foo"`, - res: []docs.Lint{ - docs.NewLintError(1, docs.LintExpectedObject, errors.New("expected object value, got !!str")), - }, - }, - { - name: "expected string got object", - inputSpec: docs.FieldObject("foo", "").WithChildren( - docs.FieldString("bar", ""), - ), - inputConf: `bar: {}`, - res: []docs.Lint{ - docs.NewLintError(1, docs.LintExpectedScalar, errors.New("expected string value")), - }, - }, - { - name: "expected string got object nested", - inputSpec: docs.FieldObject("foo", "").WithChildren( - docs.FieldObject("bar", "").WithChildren( - docs.FieldString("baz", ""), - ), - ), - inputConf: `bar: - baz: {}`, - res: []docs.Lint{ - docs.NewLintError(2, docs.LintExpectedScalar, errors.New("expected string value")), - }, - }, - { - name: "missing non-optional field", - inputSpec: docs.FieldObject("foo", "").WithChildren( - docs.FieldString("bar", "").HasDefault("barv"), - docs.FieldString("baz", ""), - docs.FieldString("buz", "").Optional(), - docs.FieldString("bev", ""), - ), - inputConf: `bev: hello world`, - res: []docs.Lint{ - docs.NewLintError(1, docs.LintMissing, errors.New("field baz is required")), - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var node yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(test.inputConf), &node)) - - lints := test.inputSpec.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), &node) - assert.Equal(t, test.res, lints) - }) - } -} - -func TestYAMLSanitation(t *testing.T) { - prov := docs.NewMappedDocsProvider() - - for _, t := range docs.Types() { - prov.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testyamlsanitfoo%v", string(t)), - Type: t, - Config: docs.FieldComponent().WithChildren( - docs.FieldString("foo1", ""), - docs.FieldString("foo2", "").Advanced(), - docs.FieldProcessor("foo3", ""), - docs.FieldProcessor("foo4", "").Array().Advanced(), - docs.FieldProcessor("foo5", "").Map(), - docs.FieldString("foo6", "").Deprecated(), - ), - }) - prov.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testyamlsanitbar%v", string(t)), - Type: t, - Config: docs.FieldComponent().Array().WithChildren( - docs.FieldString("bar1", ""), - docs.FieldString("bar2", "").Advanced(), - docs.FieldProcessor("bar3", ""), - ), - }) - prov.RegisterDocs(docs.ComponentSpec{ - Name: fmt.Sprintf("testyamlsanitbaz%v", string(t)), - Type: t, - Config: docs.FieldComponent().Map().WithChildren( - docs.FieldString("baz1", ""), - docs.FieldString("baz2", "").Advanced(), - docs.FieldProcessor("baz3", ""), - ), - }) - } - - type testCase struct { - name string - inputType docs.Type - inputConf string - inputFilter func(f docs.FieldSpec, v any) bool - - res string - err string - } - - tests := []testCase{ - { - name: "input with processors", - inputType: docs.TypeInput, - inputConf: `testyamlsanitfooinput: - foo1: simple field - foo2: advanced field - foo6: deprecated field -someotherinput: - ignore: me please -processors: - - testyamlsanitbarprocessor: - bar1: bar value - bar5: undocumented field - someotherprocessor: - ignore: me please -`, - res: `testyamlsanitfooinput: - foo1: simple field - foo2: advanced field - foo6: deprecated field -processors: - - testyamlsanitbarprocessor: - bar1: bar value - bar5: undocumented field -`, - }, - { - name: "output array with nested map processor", - inputType: docs.TypeOutput, - inputConf: `testyamlsanitbaroutput: - - bar1: simple field - bar3: - testyamlsanitbazprocessor: - customkey1: - baz1: simple field - someotherprocessor: - ignore: me please - - bar2: advanced field -`, - res: `testyamlsanitbaroutput: - - bar1: simple field - bar3: - testyamlsanitbazprocessor: - customkey1: - baz1: simple field - - bar2: advanced field -`, - }, - { - name: "output with empty processors", - inputType: docs.TypeOutput, - inputConf: `testyamlsanitbaroutput: - - bar1: simple field -processors: [] -`, - res: `testyamlsanitbaroutput: - - bar1: simple field -`, - }, - { - name: "metrics map with nested map processor", - inputType: docs.TypeMetrics, - inputConf: `testyamlsanitbazmetrics: - customkey1: - baz1: simple field - baz3: - testyamlsanitbazprocessor: - customkey1: - baz1: simple field - someotherprocessor: - ignore: me please - customkey2: - baz2: advanced field -`, - res: `testyamlsanitbazmetrics: - customkey1: - baz1: simple field - baz3: - testyamlsanitbazprocessor: - customkey1: - baz1: simple field - customkey2: - baz2: advanced field -`, - }, - { - name: "ratelimit with array field processor", - inputType: docs.TypeRateLimit, - inputConf: `testyamlsanitfoorate_limit: - foo1: simple field - foo4: - - testyamlsanitbazprocessor: - customkey1: - baz1: simple field - someotherprocessor: - ignore: me please -`, - res: `testyamlsanitfoorate_limit: - foo1: simple field - foo4: - - testyamlsanitbazprocessor: - customkey1: - baz1: simple field -`, - }, - { - name: "ratelimit with map field processor", - inputType: docs.TypeRateLimit, - inputConf: `testyamlsanitfoorate_limit: - foo1: simple field - foo5: - customkey1: - testyamlsanitbazprocessor: - customkey1: - baz1: simple field - someotherprocessor: - ignore: me please -`, - res: `testyamlsanitfoorate_limit: - foo1: simple field - foo5: - customkey1: - testyamlsanitbazprocessor: - customkey1: - baz1: simple field -`, - }, - { - name: "input with processors no deprecated", - inputType: docs.TypeInput, - inputFilter: docs.ShouldDropDeprecated(true), - inputConf: `testyamlsanitfooinput: - foo1: simple field - foo2: advanced field - foo6: deprecated field -someotherinput: - ignore: me please -processors: - - testyamlsanitfooprocessor: - foo1: simple field - foo2: advanced field - foo6: deprecated field - someotherprocessor: - ignore: me please -`, - res: `testyamlsanitfooinput: - foo1: simple field - foo2: advanced field -processors: - - testyamlsanitfooprocessor: - foo1: simple field - foo2: advanced field -`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var node yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(test.inputConf), &node)) - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.DocsProvider = prov - sanitConf.RemoveTypeField = true - sanitConf.Filter = test.inputFilter - sanitConf.RemoveDeprecated = false - - err := docs.SanitiseYAML(test.inputType, &node, sanitConf) - if test.err != "" { - assert.EqualError(t, err, test.err) - } else { - assert.NoError(t, err) - - resBytes, err := yaml.Marshal(node.Content[0]) - require.NoError(t, err) - assert.Equal(t, test.res, string(resBytes)) - } - }) - } -} diff --git a/internal/docs/interop/interop.go b/internal/docs/interop/interop.go deleted file mode 100644 index 71306f3127..0000000000 --- a/internal/docs/interop/interop.go +++ /dev/null @@ -1,19 +0,0 @@ -package interop - -import ( - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/public/service" -) - -// Unwrap a public *service.ConfigField type into an internal docs.FieldSpec. -// This is useful in situations where a config spec needs to be shared by new -// components built by the service package at the same time as older components -// using the internal APIs directly. -// -// In these cases we want the canonical spec to be made with the service package -// but still extract a docs.FieldSpec from it. -func Unwrap(f *service.ConfigField) docs.FieldSpec { - return f.XUnwrapper().(interface { - Unwrap() docs.FieldSpec - }).Unwrap() -} diff --git a/internal/docs/json_schema.go b/internal/docs/json_schema.go deleted file mode 100644 index a1448402c0..0000000000 --- a/internal/docs/json_schema.go +++ /dev/null @@ -1,92 +0,0 @@ -package docs - -func jSchemaIsRequired(f *FieldSpec) bool { - if f.IsOptional || f.Default != nil { - return false - } - if len(f.Children) == 0 { - return true - } - for _, f := range f.Children { - if jSchemaIsRequired(&f) { - return true - } - } - return false -} - -// JSONSchema serializes a field spec into a JSON schema structure. -func (f FieldSpec) JSONSchema() any { - spec := map[string]any{} - switch f.Kind { - case Kind2DArray: - innerField := f - innerField.Kind = KindArray - spec["type"] = "array" - spec["items"] = innerField.JSONSchema() - case KindArray: - innerField := f - innerField.Kind = KindScalar - spec["type"] = "array" - spec["items"] = innerField.JSONSchema() - case KindMap: - innerField := f - innerField.Kind = KindScalar - spec["type"] = "object" - spec["patternProperties"] = map[string]any{ - ".": innerField.JSONSchema(), - } - default: - switch f.Type { - case FieldTypeBool: - spec["type"] = "boolean" - case FieldTypeString: - spec["type"] = "string" - case FieldTypeInt: - spec["type"] = "number" - case FieldTypeFloat: - spec["type"] = "number" - case FieldTypeObject: - spec["type"] = "object" - spec["properties"] = f.Children.JSONSchema() - var required []string - for _, child := range f.Children { - if jSchemaIsRequired(&child) { - required = append(required, child.Name) - } - } - if len(required) > 0 { - spec["required"] = required - } - spec["additionalProperties"] = false - case FieldTypeInput: - spec["$ref"] = "#/definitions/input" - case FieldTypeBuffer: - spec["$ref"] = "#/definitions/buffer" - case FieldTypeCache: - spec["$ref"] = "#/definitions/cache" - case FieldTypeProcessor: - spec["$ref"] = "#/definitions/processor" - case FieldTypeRateLimit: - spec["$ref"] = "#/definitions/rate_limit" - case FieldTypeOutput: - spec["$ref"] = "#/definitions/output" - case FieldTypeMetrics: - spec["$ref"] = "#/definitions/metrics" - case FieldTypeTracer: - spec["$ref"] = "#/definitions/tracer" - case FieldTypeScanner: - spec["$ref"] = "#/definitions/scanner" - } - } - return spec -} - -// JSONSchema serializes a field spec into a JSON schema structure. -func (f FieldSpecs) JSONSchema() map[string]any { - spec := map[string]any{} - for _, field := range f { - spec[field.Name] = field.JSONSchema() - } - return spec -} diff --git a/internal/docs/metrics_mapping.go b/internal/docs/metrics_mapping.go deleted file mode 100644 index 14dd7a15b7..0000000000 --- a/internal/docs/metrics_mapping.go +++ /dev/null @@ -1,16 +0,0 @@ -package docs - -// MetricsMappingFieldSpec is a field spec that describes a Bloblang mapping for -// renaming metrics. -func MetricsMappingFieldSpec(name string) FieldSpec { - examples := []any{ - `this.replace("input", "source").replace("output", "sink")`, - `root = if ![ - "input_received", - "input_latency", - "output_sent" -].contains(this) { deleted() }`, - } - summary := "An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that allows you to rename or prevent certain metrics paths from being exported. For more information check out the xref:components:metrics/about.adoc#metric-mapping[metrics documentation]. When metric paths are created, renamed and dropped a trace log is written, enabling TRACE level logging is therefore a good way to diagnose path mappings." - return FieldBloblang(name, summary, examples...).HasDefault("") -} diff --git a/internal/docs/package.go b/internal/docs/package.go deleted file mode 100644 index 499bd37649..0000000000 --- a/internal/docs/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package docs provides useful functions for creating documentation from -// Benthos components -package docs diff --git a/internal/docs/parsed.go b/internal/docs/parsed.go deleted file mode 100644 index fbef7cbb85..0000000000 --- a/internal/docs/parsed.go +++ /dev/null @@ -1,483 +0,0 @@ -package docs - -import ( - "fmt" - "strings" - "time" - - "github.com/Jeffail/gabs/v2" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -func (f FieldSpec) ParsedConfigFromAny(v any) (pConf *ParsedConfig, err error) { - pConf = &ParsedConfig{} - switch t := v.(type) { - case *yaml.Node: - pConf.line = &t.Line - pConf.generic, err = f.YAMLToValue(t, ToValueConfig{}) - default: - pConf.generic, err = f.AnyToValue(v, ToValueConfig{}) - } - return -} - -func (f FieldSpecs) ParsedConfigFromAny(v any) (pConf *ParsedConfig, err error) { - pConf = &ParsedConfig{} - switch t := v.(type) { - case *yaml.Node: - pConf.line = &t.Line - pConf.generic, err = f.YAMLToMap(t, ToValueConfig{}) - default: - pConf.generic, err = f.AnyToMap(v, ToValueConfig{}) - } - return -} - -// ParsedConfig represents a plugin configuration that has been validated and -// parsed from a ConfigSpec, and allows plugin constructors to access -// configuration fields. -type ParsedConfig struct { - hiddenPath []string - generic any - line *int -} - -func (p *ParsedConfig) Raw() any { - return p.generic -} - -func (p *ParsedConfig) Line() (int, bool) { - if p.line == nil { - return 0, false - } - return *p.line, true -} - -// Namespace returns a version of the parsed config at a given field namespace. -// This is useful for extracting multiple fields under the same grouping. -func (p *ParsedConfig) Namespace(path ...string) *ParsedConfig { - tmpConfig := *p - tmpConfig.hiddenPath = append([]string{}, p.hiddenPath...) - tmpConfig.hiddenPath = append(tmpConfig.hiddenPath, path...) - return &tmpConfig -} - -// Field accesses a Field from the parsed config by its name and returns the -// value if the Field is found and a boolean indicating whether it was found. -// Nested fields can be accessed by specifying the series of Field names. -func (p *ParsedConfig) Field(path ...string) (any, bool) { - gObj := gabs.Wrap(p.generic).S(p.hiddenPath...) - if exists := gObj.Exists(path...); !exists { - return nil, false - } - return gObj.S(path...).Data(), true -} - -func (p *ParsedConfig) FullDotPath(path ...string) string { - var fullPath []string - fullPath = append(fullPath, p.hiddenPath...) - fullPath = append(fullPath, path...) - return strings.Join(fullPath, ".") -} - -// Contains checks whether the parsed config contains a given field identified -// by its name. -func (p *ParsedConfig) Contains(path ...string) bool { - gObj := gabs.Wrap(p.generic).S(p.hiddenPath...) - return gObj.Exists(path...) -} - -// FieldAny accesses a field from the parsed config by its name that can assume -// any value type. If the field is not found an error is returned. -func (p *ParsedConfig) FieldAny(path ...string) (any, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - return v, nil -} - -// FieldAnyList accesses a field that is a list of any value types from the -// parsed config by its name and returns the value as an array of *ParsedConfig -// types, where each one represents an object or value in the list. Returns an -// error if the field is not found, or is not a list of values. -func (p *ParsedConfig) FieldAnyList(path ...string) ([]*ParsedConfig, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a list, got %T", p.FullDotPath(path...), v) - } - sList := make([]*ParsedConfig, len(iList)) - for i, ev := range iList { - sList[i] = &ParsedConfig{ - generic: ev, - } - } - return sList, nil -} - -// FieldAnyMap accesses a field that is an object of arbitrary keys and any -// values from the parsed config by its name and returns a map of *ParsedConfig -// types, where each one represents an object or value in the map. Returns an -// error if the field is not found, or is not an object. -func (p *ParsedConfig) FieldAnyMap(path ...string) (map[string]*ParsedConfig, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iMap, ok := v.(map[string]any) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a string map, got %T", p.FullDotPath(path...), v) - } - sMap := make(map[string]*ParsedConfig, len(iMap)) - for k, v := range iMap { - sMap[k] = &ParsedConfig{ - generic: v, - } - } - return sMap, nil -} - -// FieldString accesses a string field from the parsed config by its name. If -// the field is not found or is not a string an error is returned. -func (p *ParsedConfig) FieldString(path ...string) (string, error) { - v, exists := p.Field(path...) - if !exists { - return "", fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - str, ok := v.(string) - if !ok { - return "", fmt.Errorf("expected field '%v' to be a string, got %T", p.FullDotPath(path...), v) - } - return str, nil -} - -// FieldDuration accesses a duration string field from the parsed config by its -// name. If the field is not found or is not a valid duration string an error is -// returned. -func (p *ParsedConfig) FieldDuration(path ...string) (time.Duration, error) { - v, exists := p.Field(path...) - if !exists { - return 0, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - str, ok := v.(string) - if !ok { - return 0, fmt.Errorf("expected field '%v' to be a string, got %T", p.FullDotPath(path...), v) - } - d, err := time.ParseDuration(str) - if err != nil { - return 0, fmt.Errorf("failed to parse '%v' as a duration string: %w", p.FullDotPath(path...), err) - } - return d, nil -} - -// FieldStringList accesses a field that is a list of strings from the parsed -// config by its name and returns the value. Returns an error if the field is -// not found, or is not a list of strings. -func (p *ParsedConfig) FieldStringList(path ...string) ([]string, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - if sList, ok := v.([]string); ok { - return sList, nil - } - return nil, fmt.Errorf("expected field '%v' to be a string list, got %T", p.FullDotPath(path...), v) - } - sList := make([]string, len(iList)) - for i, ev := range iList { - if sList[i], ok = ev.(string); !ok { - return nil, fmt.Errorf("expected field '%v' to be a string list, found an element of type %T", p.FullDotPath(path...), ev) - } - } - return sList, nil -} - -// FieldStringListOfLists accesses a field that is a list of lists of strings -// from the parsed config by its name and returns the value. Returns an error if -// the field is not found, or is not a list of lists of strings. -func (p *ParsedConfig) FieldStringListOfLists(path ...string) ([][]string, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - if sList, ok := v.([][]string); ok { - return sList, nil - } - return nil, fmt.Errorf("expected field '%v' to be a list of string lists, got %T", p.FullDotPath(path...), v) - } - sList := make([][]string, len(iList)) - for i, ev := range iList { - switch t := ev.(type) { - case []string: - sList[i] = t - case []any: - tmpList := make([]string, len(t)) - for j, evv := range t { - if tmpList[j], ok = evv.(string); !ok { - return nil, fmt.Errorf("expected field '%v' to be a string list, found an element of type %T", p.FullDotPath(path...), evv) - } - } - sList[i] = tmpList - } - } - return sList, nil -} - -// FieldStringMap accesses a field that is an object of arbitrary keys and -// string values from the parsed config by its name and returns the value. -// Returns an error if the field is not found, or is not an object of strings. -func (p *ParsedConfig) FieldStringMap(path ...string) (map[string]string, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iMap, ok := v.(map[string]any) - if !ok { - if sMap, ok := v.(map[string]string); ok { - return sMap, nil - } - return nil, fmt.Errorf("expected field '%v' to be a string map, got %T", p.FullDotPath(path...), v) - } - sMap := make(map[string]string, len(iMap)) - for k, ev := range iMap { - if sMap[k], ok = ev.(string); !ok { - return nil, fmt.Errorf("expected field '%v' to be a string map, found an element of type %T", p.FullDotPath(path...), ev) - } - } - return sMap, nil -} - -// FieldInt accesses an int field from the parsed config by its name and returns -// the value. Returns an error if the field is not found or is not an int. -func (p *ParsedConfig) FieldInt(path ...string) (int, error) { - v, exists := p.Field(path...) - if !exists { - return 0, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - i, err := value.IGetInt(v) - if err != nil { - return 0, fmt.Errorf("expected field '%v' to be an int, got %T", p.FullDotPath(path...), v) - } - return int(i), nil -} - -// FieldIntList accesses a field that is a list of integers from the parsed -// config by its name and returns the value. Returns an error if the field is -// not found, or is not a list of integers. -func (p *ParsedConfig) FieldIntList(path ...string) ([]int, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - if sList, ok := v.([]int); ok { - return sList, nil - } - return nil, fmt.Errorf("expected field '%v' to be an integer list, got %T", p.FullDotPath(path...), v) - } - sList := make([]int, len(iList)) - for i, ev := range iList { - iv, err := value.IToInt(ev) - if err != nil { - return nil, fmt.Errorf("expected field '%v' to be an integer list, found an element of type %T", p.FullDotPath(path...), ev) - } - sList[i] = int(iv) - } - return sList, nil -} - -// FieldIntMap accesses a field that is an object of arbitrary keys and -// integer values from the parsed config by its name and returns the value. -// Returns an error if the field is not found, or is not an object of integers. -func (p *ParsedConfig) FieldIntMap(path ...string) (map[string]int, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iMap, ok := v.(map[string]any) - if !ok { - if sMap, ok := v.(map[string]int); ok { - return sMap, nil - } - return nil, fmt.Errorf("expected field '%v' to be an integer map, got %T", p.FullDotPath(path...), v) - } - sMap := make(map[string]int, len(iMap)) - for k, ev := range iMap { - iv, err := value.IToInt(ev) - if err != nil { - return nil, fmt.Errorf("expected field '%v' to be an integer map, found an element of type %T", p.FullDotPath(path...), ev) - } - sMap[k] = int(iv) - } - return sMap, nil -} - -// FieldFloat accesses a float field from the parsed config by its name and -// returns the value. Returns an error if the field is not found or is not a -// float. -func (p *ParsedConfig) FieldFloat(path ...string) (float64, error) { - v, exists := p.Field(path...) - if !exists { - return 0, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - f, err := value.IGetNumber(v) - if err != nil { - return 0, fmt.Errorf("expected field '%v' to be a float, got %T", p.FullDotPath(path...), v) - } - return f, nil -} - -// FieldFloatList accesses a field that is a list of floats from the parsed -// config by its name and returns the value. Returns an error if the field is -// not found, or is not a list of floats. -func (p *ParsedConfig) FieldFloatList(path ...string) ([]float64, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - if sList, ok := v.([]float64); ok { - return sList, nil - } - return nil, fmt.Errorf("expected field '%v' to be an float list, got %T", p.FullDotPath(path...), v) - } - sList := make([]float64, len(iList)) - for i, ev := range iList { - var err error - if sList[i], err = value.IGetNumber(ev); err != nil { - return nil, fmt.Errorf("expected field '%v' to be an float list, found an element of type %T", p.FullDotPath(path...), ev) - } - } - return sList, nil -} - -// FieldFloatMap accesses a field that is an object of arbitrary keys and -// float values from the parsed config by its name and returns the value. -// Returns an error if the field is not found, or is not an object of floats. -func (p *ParsedConfig) FieldFloatMap(path ...string) (map[string]float64, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iMap, ok := v.(map[string]any) - if !ok { - if sMap, ok := v.(map[string]float64); ok { - return sMap, nil - } - return nil, fmt.Errorf("expected field '%v' to be an float map, got %T", p.FullDotPath(path...), v) - } - sMap := make(map[string]float64, len(iMap)) - for k, ev := range iMap { - var err error - if sMap[k], err = value.IGetNumber(ev); err != nil { - return nil, fmt.Errorf("expected field '%v' to be an float map, found an element of type %T", p.FullDotPath(path...), ev) - } - } - return sMap, nil -} - -// FieldBool accesses a bool field from the parsed config by its name and -// returns the value. Returns an error if the field is not found or is not a -// bool. -func (p *ParsedConfig) FieldBool(path ...string) (bool, error) { - v, e := p.Field(path...) - if !e { - return false, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - b, ok := v.(bool) - if !ok { - return false, fmt.Errorf("expected field '%v' to be a bool, got %T", p.FullDotPath(path...), v) - } - return b, nil -} - -// FieldObjectList accesses a field that is a list of objects from the parsed -// config by its name and returns the value as an array of *ParsedConfig types, -// where each one represents an object in the list. Returns an error if the -// field is not found, or is not a list of objects. -func (p *ParsedConfig) FieldObjectList(path ...string) ([]*ParsedConfig, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a list, got %T", p.FullDotPath(path...), v) - } - sList := make([]*ParsedConfig, len(iList)) - for i, ev := range iList { - sList[i] = &ParsedConfig{ - generic: ev, - } - } - return sList, nil -} - -// FieldObjectListOfLists accesses a field that is a list of lists of objects -// from the parsed config by its name and returns the value as an array of -// arrays of *ParsedConfig types, where each one represents an object in the -// list. Returns an error if the field is not found, or is not a list of lists -// of objects. -func (p *ParsedConfig) FieldObjectListOfLists(path ...string) ([][]*ParsedConfig, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iList, ok := v.([]any) - if !ok { - if sList, ok := v.([][]*ParsedConfig); ok { - return sList, nil - } - return nil, fmt.Errorf("expected field '%v' to be a list of object lists, got %T", p.FullDotPath(path...), v) - } - sList := make([][]*ParsedConfig, len(iList)) - for i, ev := range iList { - switch t := ev.(type) { - case []*ParsedConfig: - sList[i] = t - case []any: - tmpList := make([]*ParsedConfig, len(t)) - for j, evv := range t { - tmpList[j] = &ParsedConfig{ - generic: evv, - } - } - sList[i] = tmpList - } - } - return sList, nil -} - -// FieldObjectMap accesses a field that is a map of objects from the parsed -// config by its name and returns the value as a map of *ParsedConfig types, -// where each one represents an object in the map. Returns an error if the -// field is not found, or is not a map of objects. -func (p *ParsedConfig) FieldObjectMap(path ...string) (map[string]*ParsedConfig, error) { - v, exists := p.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.FullDotPath(path...)) - } - iMap, ok := v.(map[string]any) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a map, got %T", p.FullDotPath(path...), v) - } - sMap := make(map[string]*ParsedConfig, len(iMap)) - for i, ev := range iMap { - sMap[i] = &ParsedConfig{ - generic: ev, - } - } - return sMap, nil -} diff --git a/internal/docs/registry.go b/internal/docs/registry.go deleted file mode 100644 index 498fb5caa3..0000000000 --- a/internal/docs/registry.go +++ /dev/null @@ -1,147 +0,0 @@ -package docs - -import ( - "sync" -) - -// Provider stores the component spec definitions of various component -// implementations. -type Provider interface { - GetDocs(name string, ctype Type) (ComponentSpec, bool) -} - -//------------------------------------------------------------------------------ - -// MappedDocsProvider stores component documentation in maps, protected by a -// mutex, allowing safe concurrent use. -type MappedDocsProvider struct { - bufferMap map[string]ComponentSpec - cacheMap map[string]ComponentSpec - inputMap map[string]ComponentSpec - metricsMap map[string]ComponentSpec - outputMap map[string]ComponentSpec - processorMap map[string]ComponentSpec - rateLimitMap map[string]ComponentSpec - tracerMap map[string]ComponentSpec - scannerMap map[string]ComponentSpec - componentLock sync.Mutex -} - -// NewMappedDocsProvider creates a new (empty) provider of component docs. -func NewMappedDocsProvider() *MappedDocsProvider { - return &MappedDocsProvider{ - bufferMap: map[string]ComponentSpec{}, - cacheMap: map[string]ComponentSpec{}, - inputMap: map[string]ComponentSpec{}, - metricsMap: map[string]ComponentSpec{}, - outputMap: map[string]ComponentSpec{}, - processorMap: map[string]ComponentSpec{}, - rateLimitMap: map[string]ComponentSpec{}, - tracerMap: map[string]ComponentSpec{}, - scannerMap: map[string]ComponentSpec{}, - } -} - -// Clone returns a copied version of the provider that can be modified -// independently. -func (m *MappedDocsProvider) Clone() *MappedDocsProvider { - newM := &MappedDocsProvider{ - bufferMap: map[string]ComponentSpec{}, - cacheMap: map[string]ComponentSpec{}, - inputMap: map[string]ComponentSpec{}, - metricsMap: map[string]ComponentSpec{}, - outputMap: map[string]ComponentSpec{}, - processorMap: map[string]ComponentSpec{}, - rateLimitMap: map[string]ComponentSpec{}, - tracerMap: map[string]ComponentSpec{}, - scannerMap: map[string]ComponentSpec{}, - } - - for k, v := range m.bufferMap { - newM.bufferMap[k] = v - } - for k, v := range m.cacheMap { - newM.cacheMap[k] = v - } - for k, v := range m.inputMap { - newM.inputMap[k] = v - } - for k, v := range m.metricsMap { - newM.metricsMap[k] = v - } - for k, v := range m.outputMap { - newM.outputMap[k] = v - } - for k, v := range m.processorMap { - newM.processorMap[k] = v - } - for k, v := range m.rateLimitMap { - newM.rateLimitMap[k] = v - } - for k, v := range m.tracerMap { - newM.tracerMap[k] = v - } - for k, v := range m.scannerMap { - newM.scannerMap[k] = v - } - return newM -} - -// RegisterDocs adds the documentation of a component implementation. -func (m *MappedDocsProvider) RegisterDocs(spec ComponentSpec) { - m.componentLock.Lock() - defer m.componentLock.Unlock() - - switch spec.Type { - case TypeBuffer: - m.bufferMap[spec.Name] = spec - case TypeCache: - m.cacheMap[spec.Name] = spec - case TypeInput: - m.inputMap[spec.Name] = spec - case TypeMetrics: - m.metricsMap[spec.Name] = spec - case TypeOutput: - m.outputMap[spec.Name] = spec - case TypeProcessor: - m.processorMap[spec.Name] = spec - case TypeRateLimit: - m.rateLimitMap[spec.Name] = spec - case TypeTracer: - m.tracerMap[spec.Name] = spec - case TypeScanner: - m.scannerMap[spec.Name] = spec - } -} - -// GetDocs attempts to obtain component implementation docs. -func (m *MappedDocsProvider) GetDocs(name string, ctype Type) (ComponentSpec, bool) { - m.componentLock.Lock() - defer m.componentLock.Unlock() - - var spec ComponentSpec - var ok bool - - switch ctype { - case TypeBuffer: - spec, ok = m.bufferMap[name] - case TypeCache: - spec, ok = m.cacheMap[name] - case TypeInput: - spec, ok = m.inputMap[name] - case TypeMetrics: - spec, ok = m.metricsMap[name] - case TypeOutput: - spec, ok = m.outputMap[name] - case TypeProcessor: - spec, ok = m.processorMap[name] - case TypeRateLimit: - spec, ok = m.rateLimitMap[name] - case TypeTracer: - spec, ok = m.tracerMap[name] - case TypeScanner: - spec, ok = m.scannerMap[name] - } - - return spec, ok -} diff --git a/internal/filepath/glob.go b/internal/filepath/glob.go deleted file mode 100644 index 3b0a68ee68..0000000000 --- a/internal/filepath/glob.go +++ /dev/null @@ -1,144 +0,0 @@ -package filepath - -import ( - "errors" - "io/fs" - "runtime" - "strings" -) - -// GlobsAndSuperPaths attempts to expand a list of paths, which may include glob -// patterns and super paths (the ... thing) to a list of explicit file paths. -// Extensions must be provided, and limit the file types that are captured with -// a super path. -func GlobsAndSuperPaths(f fs.FS, paths []string, extensions ...string) ([]string, error) { - if len(extensions) == 0 { - return nil, errors.New("must specify at least one extension for super paths") - } - - var superPaths, skippedPaths []string - for _, p := range paths { - if strings.HasSuffix(p, "...") { - if p == "./..." || p == "..." { - p = "." - } else { - p = strings.TrimSuffix(p, "/...") - } - if err := fs.WalkDir(f, p, func(path string, info fs.DirEntry, werr error) error { - if werr != nil { - return werr - } - if info.IsDir() { - return nil - } - for _, ext := range extensions { - if strings.HasSuffix(path, ext) { - superPaths = append(superPaths, path) - return nil - } - } - return nil - }); err != nil { - return nil, err - } - } else { - skippedPaths = append(skippedPaths, p) - } - } - - resultPaths := append([]string{}, superPaths...) - if len(skippedPaths) > 0 { - globPaths, err := Globs(f, skippedPaths) - if err != nil { - return nil, err - } - resultPaths = append(resultPaths, globPaths...) - } - return resultPaths, nil -} - -// hasMeta reports whether path contains any of the magic characters -// recognised by Match. -// -// Taken from path/filepath/match.go. -func hasMeta(path string) bool { - magicChars := `*?[` - if runtime.GOOS != "windows" { - magicChars = `*?[\` - } - return strings.ContainsAny(path, magicChars) -} - -// Globs attempts to expand a list of paths, which may include glob patterns, to -// a list of explicit file paths. The paths are de-duplicated but are not -// sorted. -func Globs(f fs.FS, paths []string) ([]string, error) { - var expandedPaths []string - seenPaths := map[string]struct{}{} - - for _, path := range paths { - var globbed []string - var err error - if segments := strings.Split(path, "**"); len(segments) == 1 { - globbed, err = fs.Glob(f, path) - } else { - globbed, err = superGlobs(f, segments) - } - if err != nil { - return nil, err - } - for _, gPath := range globbed { - if _, seen := seenPaths[gPath]; !seen { - expandedPaths = append(expandedPaths, gPath) - seenPaths[gPath] = struct{}{} - } - } - if len(globbed) == 0 && !hasMeta(path) { - if _, seen := seenPaths[path]; !seen { - expandedPaths = append(expandedPaths, path) - seenPaths[path] = struct{}{} - } - } - } - - return expandedPaths, nil -} - -// Inspired by https://github.com/yargevad/filepathx/blob/master/filepathx.go -func superGlobs(f fs.FS, segments []string) ([]string, error) { - matches := map[string]struct{}{"": {}} - - for i, segment := range segments { - newMatches := map[string]struct{}{} - lastSegment := (len(segments) - 1) == i - - for match := range matches { - paths, err := fs.Glob(f, match+segment) - if err != nil { - return nil, err - } - for _, path := range paths { - if err := fs.WalkDir(f, path, func(newPath string, info fs.DirEntry, err error) error { - if err != nil { - return err - } - if lastSegment && info.IsDir() { - return nil - } - newMatches[newPath] = struct{}{} - return nil - }); err != nil { - return nil, err - } - } - } - - matches = newMatches - } - - matchSlice := make([]string, 0, len(matches)) - for path := range matches { - matchSlice = append(matchSlice, path) - } - return matchSlice, nil -} diff --git a/internal/filepath/glob_test.go b/internal/filepath/glob_test.go deleted file mode 100644 index b54d230e5b..0000000000 --- a/internal/filepath/glob_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package filepath - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -func TestGlobPatterns(t *testing.T) { - dirStructure := []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/b.txt`, - `src/cats/toys`, - `src/cats/meows/c.js`, - `src/cats/meows/c.js.tmp`, - } - - tmpDir := t.TempDir() - - for _, path := range dirStructure { - tmpPath := filepath.Join(tmpDir, path) - if filepath.Ext(tmpPath) == "" { - require.NoError(t, os.MkdirAll(tmpPath, 0o755)) - } else { - require.NoError(t, os.MkdirAll(filepath.Dir(tmpPath), 0o755)) - require.NoError(t, os.WriteFile(tmpPath, []byte("keep me"), 0o755)) - } - } - - tests := []struct { - pattern string - matches []string - }{ - { - pattern: `/src/cats/*.js`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - }, - }, - { - pattern: `/src/cats/a.js`, - matches: []string{ - `src/cats/a.js`, - }, - }, - { - pattern: `/src/cats/z.js`, - matches: []string{ - `src/cats/z.js`, - }, - }, - { - pattern: `/src/**/a.js`, - matches: []string{ - `src/cats/a.js`, - }, - }, - { - pattern: `/src/**/*.js`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/meows/c.js`, - }, - }, - { - pattern: `/src/**/*`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/b.txt`, - `src/cats/meows/c.js`, - `src/cats/meows/c.js.tmp`, - }, - }, - } - - for _, test := range tests { - t.Run(test.pattern, func(t *testing.T) { - matches, err := Globs(ifs.OS(), []string{tmpDir + test.pattern}) - require.NoError(t, err) - - for i, match := range matches { - matches[i], err = filepath.Rel(tmpDir, match) - require.NoError(t, err) - } - assert.ElementsMatch(t, test.matches, matches) - }) - } -} - -func TestGlobsAndSuperPaths(t *testing.T) { - dirStructure := []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/b.txt`, - `src/cats/toys`, - `src/cats/meows/c.js`, - `src/cats/meows/c.js.tmp`, - } - - tmpDir := t.TempDir() - - for _, path := range dirStructure { - tmpPath := filepath.Join(tmpDir, path) - if filepath.Ext(tmpPath) == "" { - require.NoError(t, os.MkdirAll(tmpPath, 0o755)) - } else { - require.NoError(t, os.MkdirAll(filepath.Dir(tmpPath), 0o755)) - require.NoError(t, os.WriteFile(tmpPath, []byte("keep me"), 0o755)) - } - } - - tests := []struct { - pattern string - matches []string - }{ - { - pattern: `/src/cats/*.js`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - }, - }, - { - pattern: `/src/cats/a.js`, - matches: []string{ - `src/cats/a.js`, - }, - }, - { - pattern: `/src/cats/z.js`, - matches: []string{ - `src/cats/z.js`, - }, - }, - { - pattern: `/src/**/a.js`, - matches: []string{ - `src/cats/a.js`, - }, - }, - { - pattern: `/src/**/*.js`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/meows/c.js`, - }, - }, - { - pattern: `/src/**/*`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/b.txt`, - `src/cats/meows/c.js`, - `src/cats/meows/c.js.tmp`, - }, - }, - { - pattern: `/src/...`, - matches: []string{ - `src/cats/a.js`, - `src/cats/b.js`, - `src/cats/meows/c.js`, - }, - }, - } - - for _, test := range tests { - t.Run(test.pattern, func(t *testing.T) { - matches, err := GlobsAndSuperPaths(ifs.OS(), []string{tmpDir + test.pattern}, "js") - require.NoError(t, err) - - for i, match := range matches { - matches[i], err = filepath.Rel(tmpDir, match) - require.NoError(t, err) - } - assert.ElementsMatch(t, test.matches, matches) - }) - } -} diff --git a/internal/filepath/ifs/http.go b/internal/filepath/ifs/http.go deleted file mode 100644 index ff77ed20a0..0000000000 --- a/internal/filepath/ifs/http.go +++ /dev/null @@ -1,83 +0,0 @@ -package ifs - -import ( - "errors" - "io" - "io/fs" - "net/http" -) - -var _ http.FileSystem = ToHTTP(OS()) - -type asHTTP struct { - f fs.FS -} - -// ToHTTP converts an fs.FS into an http.FileSystem in a way that doesn't -// modify the root path. -func ToHTTP(f fs.FS) *asHTTP { - return &asHTTP{f: f} -} - -func (h *asHTTP) Open(name string) (http.File, error) { - f, err := h.f.Open(name) - if err != nil { - return nil, err - } - return asHTTPFile{file: f}, nil -} - -func (f asHTTPFile) ReadDir(count int) ([]fs.DirEntry, error) { - d, ok := f.file.(fs.ReadDirFile) - if !ok { - return nil, errMissingReadDir - } - return d.ReadDir(count) -} - -type asHTTPFile struct { - file fs.File -} - -func (f asHTTPFile) Close() error { return f.file.Close() } -func (f asHTTPFile) Read(b []byte) (int, error) { return f.file.Read(b) } -func (f asHTTPFile) Stat() (fs.FileInfo, error) { return f.file.Stat() } - -var ( - errMissingSeek = errors.New("io.File missing Seek method") - errMissingReadDir = errors.New("io.File directory missing ReadDir method") -) - -func (f asHTTPFile) Seek(offset int64, whence int) (int64, error) { - s, ok := f.file.(io.Seeker) - if !ok { - return 0, errMissingSeek - } - return s.Seek(offset, whence) -} - -func (f asHTTPFile) Readdir(count int) ([]fs.FileInfo, error) { - d, ok := f.file.(fs.ReadDirFile) - if !ok { - return nil, errMissingReadDir - } - var list []fs.FileInfo - for { - dirs, err := d.ReadDir(count - len(list)) - for _, dir := range dirs { - info, err := dir.Info() - if err != nil { - // Pretend it doesn't exist, like (*os.File).Readdir does. - continue - } - list = append(list, info) - } - if err != nil { - return list, err - } - if count < 0 || len(list) >= count { - break - } - } - return list, nil -} diff --git a/internal/filepath/ifs/os.go b/internal/filepath/ifs/os.go deleted file mode 100644 index 6339a10a6f..0000000000 --- a/internal/filepath/ifs/os.go +++ /dev/null @@ -1,95 +0,0 @@ -package ifs - -import ( - "errors" - "io" - "io/fs" - "os" -) - -var _ fs.FS = OS() - -// FS is a superset of fs.FS that includes goodies that benthos components -// specifically need. -type FS interface { - Open(name string) (fs.File, error) - OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) - Stat(name string) (fs.FileInfo, error) - Remove(name string) error - MkdirAll(path string, perm fs.FileMode) error -} - -// ReadFile opens a file with the RDONLY flag and returns all bytes from it. -func ReadFile(f fs.FS, name string) ([]byte, error) { - var i fs.File - var err error - if ef, ok := f.(FS); ok { - i, err = ef.OpenFile(name, os.O_RDONLY, 0) - } else { - i, err = f.Open(name) - } - if err != nil { - return nil, err - } - return io.ReadAll(i) -} - -// WriteFile opens a file with O_WRONLY|O_CREATE|O_TRUNC flags and writes the -// data to it. -func WriteFile(f fs.FS, name string, data []byte, perm fs.FileMode) error { - h, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) - if err != nil { - return err - } - _, err = h.Write(data) - if err1 := h.Close(); err1 != nil && err == nil { - err = err1 - } - return err -} - -// FileWrite attempts to write to an fs.File provided it supports io.Writer. -func FileWrite(file fs.File, data []byte) (int, error) { - writer, isw := file.(io.Writer) - if !isw { - return 0, errors.New("failed to open a writable file") - } - return writer.Write(data) -} - -// OS implements fs.FS as if calls were being made directly via the os package, -// with which relative paths are resolved from the directory the process is -// executed from. -func OS() FS { - return osPTI -} - -// IsOS returns true if the provided FS implementation is a wrapper around OS -// access obtained via OS(). -func IsOS(f FS) bool { - return f == osPTI -} - -var osPTI = &osPT{} - -type osPT struct{} - -func (o *osPT) Open(name string) (fs.File, error) { - return os.Open(name) -} - -func (o *osPT) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - return os.OpenFile(name, flag, perm) -} - -func (o *osPT) Stat(name string) (fs.FileInfo, error) { - return os.Stat(name) -} - -func (o *osPT) Remove(name string) error { - return os.Remove(name) -} - -func (o *osPT) MkdirAll(path string, perm fs.FileMode) error { - return os.MkdirAll(path, perm) -} diff --git a/internal/filepath/ifs/os_test.go b/internal/filepath/ifs/os_test.go deleted file mode 100644 index 49df0b14bb..0000000000 --- a/internal/filepath/ifs/os_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package ifs - -import ( - "errors" - "io/fs" - "testing" - "testing/fstest" - - "github.com/stretchr/testify/require" -) - -type testFS struct { - fstest.MapFS -} - -func (t testFS) MkdirAll(path string, perm fs.FileMode) error { - return errors.New("not implemented") -} - -func (t testFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - return nil, errors.New("not implemented") -} - -func (t testFS) Remove(name string) error { - return errors.New("not implemented") -} - -func TestOSAccess(t *testing.T) { - var fs FS = testFS{} - - require.False(t, IsOS(fs)) - - fs = OS() - - require.True(t, IsOS(fs)) -} diff --git a/internal/httpclient/auth_oauth2.go b/internal/httpclient/auth_oauth2.go deleted file mode 100644 index ca3a3754d1..0000000000 --- a/internal/httpclient/auth_oauth2.go +++ /dev/null @@ -1,149 +0,0 @@ -package httpclient - -import ( - "context" - "net/http" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const aFieldOAuth2 = "oauth2" - -// AuthFieldSpecsExpanded includes OAuth2 and JWT fields that might not be -// appropriate for all components. -func AuthFieldSpecsExpanded() []*service.ConfigField { - pubAuthFields := service.NewHTTPRequestAuthSignerFields() - splicedFields := []*service.ConfigField{ - pubAuthFields[0], - oAuth2FieldSpec(), - } - return append(splicedFields, pubAuthFields[1:]...) -} - -type oauth2Config struct { - Enabled bool - ClientKey string - ClientSecret string - TokenURL string - Scopes []string - EndpointParams map[string][]string -} - -// Client returns an http.Client with OAuth2 configured. -func (oauth oauth2Config) Client(ctx context.Context, base *http.Client) *http.Client { - if !oauth.Enabled { - return base - } - - conf := &clientcredentials.Config{ - ClientID: oauth.ClientKey, - ClientSecret: oauth.ClientSecret, - TokenURL: oauth.TokenURL, - Scopes: oauth.Scopes, - EndpointParams: oauth.EndpointParams, - } - - return conf.Client(context.WithValue(ctx, oauth2.HTTPClient, base)) -} - -//------------------------------------------------------------------------------ - -const ( - ao2FieldEnabled = "enabled" - ao2FieldClientKey = "client_key" - ao2FieldClientSecret = "client_secret" - ao2FieldTokenURL = "token_url" - ao2FieldScopes = "scopes" - ao2FieldEndpointParams = "endpoint_params" -) - -func oAuth2FieldSpec() *service.ConfigField { - return service.NewObjectField(aFieldOAuth2, - service.NewBoolField(ao2FieldEnabled). - Description("Whether to use OAuth version 2 in requests."). - Default(false), - - service.NewStringField(ao2FieldClientKey). - Description("A value used to identify the client to the token provider."). - Default(""), - - service.NewStringField(ao2FieldClientSecret). - Description("A secret used to establish ownership of the client key."). - Default("").Secret(), - - service.NewURLField(ao2FieldTokenURL). - Description("The URL of the token provider."). - Default(""), - - service.NewStringListField(ao2FieldScopes). - Description("A list of optional requested permissions."). - Default([]any{}). - Advanced(). - Version("3.45.0"), - - service.NewAnyMapField(ao2FieldEndpointParams). - Description("A list of optional endpoint parameters, values should be arrays of strings."). - Advanced(). - Example(map[string]any{ - "foo": []string{"meow", "quack"}, - "bar": []string{"woof"}, - }). - Default(map[string]any{}). - Version("4.21.0"). - Optional(). - LintRule(` -root = if this.type() == "object" { - this.values().map_each(ele -> if ele.type() != "array" { - "field must be an object containing arrays of strings, got %s (%v)".format(ele.format_json(no_indent: true), ele.type()) - } else { - ele.map_each(str -> if str.type() != "string" { - "field values must be strings, got %s (%v)".format(str.format_json(no_indent: true), str.type()) - } else { deleted() }) - }). - flatten() -} -`), - ). - Description("Allows you to specify open authentication via OAuth version 2 using the client credentials token flow."). - Optional().Advanced() -} - -func oauth2ClientCtorFromParsed(conf *service.ParsedConfig) (res func(context.Context, *http.Client) *http.Client, err error) { - if !conf.Contains(aFieldOAuth2) { - return - } - conf = conf.Namespace(aFieldOAuth2) - - var oldConf oauth2Config - if oldConf.Enabled, err = conf.FieldBool(ao2FieldEnabled); err != nil { - return - } - if oldConf.ClientKey, err = conf.FieldString(ao2FieldClientKey); err != nil { - return - } - if oldConf.ClientSecret, err = conf.FieldString(ao2FieldClientSecret); err != nil { - return - } - if oldConf.TokenURL, err = conf.FieldString(ao2FieldTokenURL); err != nil { - return - } - if oldConf.Scopes, err = conf.FieldStringList(ao2FieldScopes); err != nil { - return - } - var endpointParams map[string]*service.ParsedConfig - if endpointParams, err = conf.FieldAnyMap(ao2FieldEndpointParams); err != nil { - return - } - oldConf.EndpointParams = map[string][]string{} - for k, v := range endpointParams { - if oldConf.EndpointParams[k], err = v.FieldStringList(); err != nil { - return - } - } - - res = oldConf.Client - return -} diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go deleted file mode 100644 index 0273345bfb..0000000000 --- a/internal/httpclient/client.go +++ /dev/null @@ -1,446 +0,0 @@ -package httpclient - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "mime" - "mime/multipart" - "net/http" - "net/url" - "strings" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/old/util/throttle" - "github.com/benthosdev/benthos/v4/internal/tracing/v2" - "github.com/benthosdev/benthos/v4/public/service" -) - -// Client is a component able to send and receive Benthos messages over HTTP. -type Client struct { - reqCreator *RequestCreator - - // Client creator - client *http.Client - clientCtx context.Context - clientCancel func() - - // Request execution and retry logic - rateLimit string - numRetries int - retryThrottle *throttle.Type - backoffOn map[int]struct{} - dropOn map[int]struct{} - successOn map[int]struct{} - - // Response extraction - metaExtractFilter *service.MetadataFilter - - // Observability - log *service.Logger - mgr *service.Resources - - mLatency *service.MetricTimer - mCodes map[int]*service.MetricCounter - codesMut sync.RWMutex -} - -// NewClientFromOldConfig creates a new request creator from an old struct style -// config. Eventually I'd like to phase these out for the more dynamic service -// style parses, but it'll take a while so we have this for now. -func NewClientFromOldConfig(conf OldConfig, mgr *service.Resources, opts ...RequestOpt) (*Client, error) { - reqCreator, err := RequestCreatorFromOldConfig(conf, mgr, opts...) - if err != nil { - return nil, err - } - - h := Client{ - reqCreator: reqCreator, - client: &http.Client{}, - metaExtractFilter: conf.ExtractMetadata, - - backoffOn: map[int]struct{}{}, - dropOn: map[int]struct{}{}, - successOn: map[int]struct{}{}, - - mgr: mgr, - log: mgr.Logger(), - } - h.clientCtx, h.clientCancel = context.WithCancel(context.Background()) - - if conf.Timeout > 0 { - h.client.Timeout = conf.Timeout - } - - if conf.TLSEnabled && conf.TLSConf != nil { - if c, ok := http.DefaultTransport.(*http.Transport); ok { - cloned := c.Clone() - cloned.TLSClientConfig = conf.TLSConf - h.client.Transport = cloned - } else { - h.client.Transport = &http.Transport{ - TLSClientConfig: conf.TLSConf, - } - } - } - - if conf.ProxyURL != "" { - proxyURL, err := url.Parse(conf.ProxyURL) - if err != nil { - return nil, fmt.Errorf("failed to parse proxy_url string: %v", err) - } - if h.client.Transport != nil { - if tr, ok := h.client.Transport.(*http.Transport); ok { - tr.Proxy = http.ProxyURL(proxyURL) - } else { - return nil, fmt.Errorf("unable to apply proxy_url to transport, unexpected type %T", h.client.Transport) - } - } else { - h.client.Transport = &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - } - } - } - - h.client.Transport, err = newRequestLog(h.client.Transport, h.log, conf.DumpRequestLogLevel) - if err != nil { - return nil, fmt.Errorf("failed to config logger for request dump: %v", err) - } - - h.client = conf.clientCtor(h.clientCtx, h.client) - - for _, c := range conf.BackoffOn { - h.backoffOn[c] = struct{}{} - } - for _, c := range conf.DropOn { - h.dropOn[c] = struct{}{} - } - for _, c := range conf.SuccessfulOn { - h.successOn[c] = struct{}{} - } - - h.mLatency = h.mgr.Metrics().NewTimer("http_request_latency_ns") - h.mCodes = map[int]*service.MetricCounter{} - - if h.rateLimit = conf.RateLimit; h.rateLimit != "" { - if !h.mgr.HasRateLimit(h.rateLimit) { - return nil, fmt.Errorf("rate limit resource '%v' was not found", h.rateLimit) - } - } - - h.numRetries = conf.NumRetries - h.retryThrottle = throttle.New( - throttle.OptMaxUnthrottledRetries(0), - throttle.OptThrottlePeriod(conf.Retry), - throttle.OptMaxExponentPeriod(conf.MaxBackoff), - ) - - return &h, nil -} - -//------------------------------------------------------------------------------ - -func (h *Client) incrCode(code int) { - h.codesMut.RLock() - ctr, exists := h.mCodes[code] - h.codesMut.RUnlock() - - if exists { - ctr.Incr(1) - return - } - - tier := code / 100 - if tier < 0 || tier > 5 { - return - } - ctr = h.mgr.Metrics().NewCounter(fmt.Sprintf("http_request_code_%vxx", tier)) - ctr.Incr(1) - - h.codesMut.Lock() - h.mCodes[code] = ctr - h.codesMut.Unlock() -} - -func (h *Client) waitForAccess(ctx context.Context) bool { - if h.rateLimit == "" { - return true - } - for { - var period time.Duration - var err error - if rerr := h.mgr.AccessRateLimit(ctx, h.rateLimit, func(rl service.RateLimit) { - period, err = rl.Access(ctx) - }); rerr != nil { - err = rerr - } - if err != nil { - h.log.Errorf("Rate limit error: %v\n", err) - period = time.Second - } - - if period > 0 { - select { - case <-time.After(period): - case <-ctx.Done(): - return false - } - } else { - return true - } - } -} - -// ResponseToBatch attempts to parse an HTTP response into a 2D slice of bytes. -func (h *Client) ResponseToBatch(res *http.Response) (service.MessageBatch, error) { - var resMsg service.MessageBatch - - annotatePart := func(p *service.Message) { - p.MetaSetMut("http_status_code", res.StatusCode) - if !h.metaExtractFilter.IsEmpty() { - for k, values := range res.Header { - normalisedHeader := strings.ToLower(k) - if len(values) > 0 && h.metaExtractFilter.Match(normalisedHeader) { - p.MetaSetMut(normalisedHeader, values[0]) - } - } - } - } - - if res.Body == nil { - nextPart := service.NewMessage(nil) - annotatePart(nextPart) - resMsg = append(resMsg, nextPart) - return resMsg, nil - } - defer res.Body.Close() - - var mediaType string - var params map[string]string - var err error - if contentType := res.Header.Get("Content-Type"); contentType != "" { - if mediaType, params, err = mime.ParseMediaType(contentType); err != nil { - h.log.Warnf("Failed to parse media type from Content-Type header: %v\n", err) - } - } - - var buffer bytes.Buffer - if !strings.HasPrefix(mediaType, "multipart/") { - var bytesRead int64 - if bytesRead, err = buffer.ReadFrom(res.Body); err != nil { - h.log.Errorf("Failed to read response: %v\n", err) - return resMsg, err - } - - nextPart := service.NewMessage(nil) - if bytesRead > 0 { - nextPart.SetBytes(buffer.Bytes()[:bytesRead]) - } - - annotatePart(nextPart) - resMsg = append(resMsg, nextPart) - return resMsg, nil - } - - mr := multipart.NewReader(res.Body, params["boundary"]) - var bufferIndex int64 - for { - var p *multipart.Part - if p, err = mr.NextPart(); err != nil { - if err == io.EOF { - break - } - return resMsg, err - } - - var bytesRead int64 - if bytesRead, err = buffer.ReadFrom(p); err != nil { - h.log.Errorf("Failed to read response: %v\n", err) - return resMsg, err - } - - nextPart := service.NewMessage(buffer.Bytes()[bufferIndex : bufferIndex+bytesRead]) - bufferIndex += bytesRead - - annotatePart(nextPart) - resMsg = append(resMsg, nextPart) - } - - return resMsg, nil -} - -type retryStrategy int - -const ( - noRetry retryStrategy = iota - retryLinear - retryBackoff -) - -// checkStatus compares a returned status code against configured logic -// determining whether the send succeeded, and if not what the retry strategy -// should be. -func (h *Client) checkStatus(code int) (succeeded bool, retStrat retryStrategy) { - if _, exists := h.dropOn[code]; exists { - return false, noRetry - } - if _, exists := h.backoffOn[code]; exists { - return false, retryBackoff - } - if _, exists := h.successOn[code]; exists { - return true, noRetry - } - if code < 200 || code > 299 { - return false, retryLinear - } - return true, noRetry -} - -var errTimedOut = errors.New("timed out waiting for next request") - -// SendToResponse attempts to create an HTTP request from a provided message, -// performs it, and then returns the *http.Response, allowing the raw response -// to be consumed. -func (h *Client) SendToResponse(ctx context.Context, sendMsg service.MessageBatch) (res *http.Response, err error) { - var spans []*tracing.Span - if sendMsg != nil { - sendMsg, spans = tracing.WithChildSpans(h.mgr.OtelTracer(), "http_request", sendMsg) - defer func() { - for _, s := range spans { - s.Finish() - } - }() - } - logErr := func(e error) { - for _, s := range spans { - s.LogKV( - "event", "error", - "type", e.Error(), - ) - } - } - - var req *http.Request - if req, err = h.reqCreator.Create(sendMsg); err != nil { - logErr(err) - return nil, err - } - // Make sure we log the actual request URL - defer func() { - if err != nil { - err = fmt.Errorf("%s: %w", req.URL, err) - } - }() - - if !h.waitForAccess(ctx) { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, errTimedOut - } - - rateLimited := false - numRetries := h.numRetries - - startedAt := time.Now() - if res, err = h.client.Do(req.WithContext(ctx)); err == nil { - h.incrCode(res.StatusCode) - if resolved, retryStrat := h.checkStatus(res.StatusCode); !resolved { - rateLimited = retryStrat == retryBackoff - if retryStrat == noRetry { - numRetries = 0 - } - err = unexpectedErr(res) - if res.Body != nil { - res.Body.Close() - } - } - } - h.mLatency.Timing(time.Since(startedAt).Nanoseconds()) - - i, j := 0, numRetries - for i < j && err != nil { - logErr(err) - if req, err = h.reqCreator.Create(sendMsg); err != nil { - continue - } - if rateLimited { - if !h.retryThrottle.ExponentialRetryWithContext(ctx) { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, errTimedOut - } - } else { - if !h.retryThrottle.RetryWithContext(ctx) { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, errTimedOut - } - } - if !h.waitForAccess(ctx) { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, errTimedOut - } - rateLimited = false - - startedAt = time.Now() - if res, err = h.client.Do(req.WithContext(ctx)); err == nil { - h.incrCode(res.StatusCode) - if resolved, retryStrat := h.checkStatus(res.StatusCode); !resolved { - rateLimited = retryStrat == retryBackoff - if retryStrat == noRetry { - j = 0 - } - err = unexpectedErr(res) - if res.Body != nil { - res.Body.Close() - } - } - } - h.mLatency.Timing(time.Since(startedAt).Nanoseconds()) - i++ - } - if err != nil { - logErr(err) - return nil, err - } - - h.retryThrottle.Reset() - return res, nil -} - -func unexpectedErr(res *http.Response) error { - body, err := io.ReadAll(res.Body) - if err != nil { - return err - } - return ErrUnexpectedHTTPRes{Code: res.StatusCode, S: res.Status, Body: body} -} - -// Send creates an HTTP request from the client config, a provided message to be -// sent as the body of the request, and a reference message used to establish -// interpolated fields for the request (which can be the same as the message -// used for the body). -// -// If the request is successful then the response is parsed into a message, -// including headers added as metadata (when configured to do so). -func (h *Client) Send(ctx context.Context, sendMsg service.MessageBatch) (service.MessageBatch, error) { - res, err := h.SendToResponse(ctx, sendMsg) - if err != nil { - return nil, err - } - return h.ResponseToBatch(res) -} - -// Close the client. -func (h *Client) Close(ctx context.Context) error { - h.clientCancel() - return nil -} diff --git a/internal/httpclient/client_test.go b/internal/httpclient/client_test.go deleted file mode 100644 index 80495438f5..0000000000 --- a/internal/httpclient/client_test.go +++ /dev/null @@ -1,710 +0,0 @@ -package httpclient - -import ( - "bytes" - "context" - "fmt" - "io" - "mime" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/textproto" - "strconv" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func clientConfig(t testing.TB, confStr string, args ...any) OldConfig { - t.Helper() - - spec := service.NewConfigSpec().Field(ConfigField("GET", false)) - parsed, err := spec.ParseYAML(fmt.Sprintf(confStr, args...), nil) - require.NoError(t, err) - - conf, err := ConfigFromParsed(parsed) - require.NoError(t, err) - return conf -} - -func TestHTTPClientRetries(t *testing.T) { - var reqCount uint32 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddUint32(&reqCount, 1) - http.Error(w, "test error", http.StatusForbidden) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -retry_period: 1ms -retries: 3 -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - defer h.Close(context.Background()) - - _, err = h.Send(context.Background(), service.MessageBatch{service.NewMessage([]byte("test"))}) - assert.Error(t, err) - assert.Equal(t, uint32(4), atomic.LoadUint32(&reqCount)) -} - -func TestHTTPClientBadRequest(t *testing.T) { - conf := clientConfig(t, ` -url: htp://notvalid:1111 -verb: notvalid -retry_period: 1ms -retries: 3 -`) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - _, err = h.Send(context.Background(), service.MessageBatch{service.NewMessage([]byte("test"))}) - assert.Error(t, err) -} - -func TestHTTPClientSendBasic(t *testing.T) { - nTestLoops := 1000 - - resultChan := make(chan string, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var s string - defer func() { - resultChan <- s - }() - - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - - s = string(b) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - testMsg := service.MessageBatch{ - service.NewMessage([]byte(testStr)), - } - - _, err = h.Send(context.Background(), testMsg) - require.NoError(t, err) - - select { - case resMsg := <-resultChan: - assert.Equal(t, testStr, resMsg) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } -} - -func TestHTTPClientBadContentType(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - - _, err = w.Write(bytes.ToUpper(b)) - require.NoError(t, err) - })) - t.Cleanup(ts.Close) - - conf := clientConfig(t, ` -url: %v -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - testMsg := service.MessageBatch{service.NewMessage([]byte("hello world"))} - - res, err := h.Send(context.Background(), testMsg) - require.NoError(t, err) - - require.Len(t, res, 1) - - mBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(mBytes)) -} - -func TestHTTPClientDropOn(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`{"foo":"bar"}`)) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -drop_on: [ 400 ] -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - testMsg := service.MessageBatch{service.NewMessage([]byte(`{"bar":"baz"}`))} - - _, err = h.Send(context.Background(), testMsg) - require.Error(t, err) -} - -func TestHTTPClientSuccessfulOn(t *testing.T) { - var reqs int32 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`{"foo":"bar"}`)) - atomic.AddInt32(&reqs, 1) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -successful_on: [ 400 ] -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - testMsg := service.MessageBatch{service.NewMessage([]byte(`{"bar":"baz"}`))} - resMsg, err := h.Send(context.Background(), testMsg) - require.NoError(t, err) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"foo":"bar"}`, string(mBytes)) - assert.Equal(t, int32(1), atomic.LoadInt32(&reqs)) -} - -func TestHTTPClientSendInterpolate(t *testing.T) { - nTestLoops := 1000 - - resultChan := make(chan string, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/firstvar", r.URL.Path) - assert.Equal(t, "hdr-secondvar", r.Header.Get("dynamic")) - assert.Equal(t, "foo", r.Header.Get("static")) - assert.Equal(t, "simpleHost.com", r.Host) - - var s string - defer func() { - resultChan <- s - }() - - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - s = string(b) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -headers: - "static": "foo" - "dynamic": 'hdr-${!json("foo.baz")}' - "Host": "simpleHost.com" -`, ts.URL+`/${! json("foo.bar") }`) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf(`{"test":%v,"foo":{"bar":"firstvar","baz":"secondvar"}}`, i) - testMsg := service.MessageBatch{service.NewMessage([]byte(testStr))} - - _, err = h.Send(context.Background(), testMsg) - require.NoError(t, err) - - select { - case resMsg := <-resultChan: - assert.Equal(t, testStr, resMsg) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } -} - -func TestHTTPClientSendMultipart(t *testing.T) { - nTestLoops := 1000 - - resultChan := make(chan []string, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var batch []string - defer func() { - resultChan <- batch - }() - - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - require.NoError(t, err) - - if strings.HasPrefix(mediaType, "multipart/") { - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - require.NoError(t, err) - - msgBytes, err := io.ReadAll(p) - require.NoError(t, err) - - batch = append(batch, string(msgBytes)) - } - } else { - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - - batch = append(batch, string(b)) - } - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - testMsg := service.MessageBatch{ - service.NewMessage([]byte(testStr + "PART-A")), - service.NewMessage([]byte(testStr + "PART-B")), - } - - _, err = h.Send(context.Background(), testMsg) - require.NoError(t, err) - - select { - case resMsg := <-resultChan: - assert.Len(t, resMsg, 2) - assert.Equal(t, testStr+"PART-A", resMsg[0]) - assert.Equal(t, testStr+"PART-B", resMsg[1]) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } -} - -func TestHTTPClientReceive(t *testing.T) { - nTestLoops := 1000 - - j := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - testStr := fmt.Sprintf("test%v", j) - j++ - w.Header().Set("foo-bar", "baz-0") - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte(testStr + "PART-A")) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", j) - resMsg, err := h.Send(context.Background(), nil) - require.NoError(t, err) - - assert.Len(t, resMsg, 1) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, testStr+"PART-A", string(mBytes)) - - v, _ := resMsg[0].MetaGet("foo-bar") - assert.Equal(t, "", v) - v, _ = resMsg[0].MetaGet("http_status_code") - assert.Equal(t, "201", v) - } -} - -func TestHTTPClientSendMetaFilter(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, ` -foo_a: %v -bar_a: %v -foo_b: %v -bar_b: %v -`, - r.Header.Get("foo_a"), - r.Header.Get("bar_a"), - r.Header.Get("foo_b"), - r.Header.Get("bar_b"), - ) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -metadata: - include_prefixes: [ "foo_" ] -`, ts.URL+"/testpost") - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - sendMsg := service.MessageBatch{service.NewMessage([]byte("hello world"))} - part := sendMsg[0] - part.MetaSetMut("foo_a", "foo a value") - part.MetaSetMut("foo_b", "foo b value") - part.MetaSetMut("bar_a", "bar a value") - part.MetaSetMut("bar_b", "bar b value") - - resMsg, err := h.Send(context.Background(), sendMsg) - require.NoError(t, err) - - assert.Len(t, resMsg, 1) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, ` -foo_a: foo a value -bar_a: -foo_b: foo b value -bar_b: -`, string(mBytes)) -} - -func TestHTTPClientReceiveHeadersWithMetadataFiltering(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("foobar", "baz") - w.Header().Set("extra", "val") - w.WriteHeader(http.StatusCreated) - })) - defer ts.Close() - - for _, tt := range []struct { - name string - noExtraMetadata bool - includePrefixes []string - includePatterns []string - }{ - { - name: "no extra metadata", - noExtraMetadata: true, - }, - { - name: "include_prefixes only", - includePrefixes: []string{"foo"}, - }, - { - name: "include_patterns only", - includePatterns: []string{".*bar"}, - }, - } { - if tt.includePrefixes == nil { - tt.includePrefixes = []string{} - } - if tt.includePatterns == nil { - tt.includePatterns = []string{} - } - conf := clientConfig(t, ` -url: %v -extract_headers: - include_prefixes: %v - include_patterns: %v -`, ts.URL, gabs.Wrap(tt.includePrefixes).String(), gabs.Wrap(tt.includePatterns).String()) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - if err != nil { - t.Fatalf("%s: %s", tt.name, err) - } - - resMsg, err := h.Send(context.Background(), nil) - if err != nil { - t.Fatalf("%s: %s", tt.name, err) - } - - metadataCount := 0 - _ = resMsg[0].MetaWalk(func(_, _ string) error { metadataCount++; return nil }) - - if tt.noExtraMetadata { - if metadataCount > 1 { - t.Errorf("%s: wrong number of metadata items: %d", tt.name, metadataCount) - } - v, _ := resMsg[0].MetaGet("foobar") - if exp, act := "", v; exp != act { - t.Errorf("%s: wrong metadata value: %v != %v", tt.name, act, exp) - } - } else { - v, _ := resMsg[0].MetaGet("foobar") - if exp, act := "baz", v; exp != act { - t.Errorf("%s: wrong metadata value: %v != %v", tt.name, act, exp) - } - } - } -} - -func TestHTTPClientReceiveMultipart(t *testing.T) { - nTestLoops := 1000 - - j := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - testStr := fmt.Sprintf("test%v", j) - j++ - msg := service.MessageBatch{ - service.NewMessage([]byte(testStr + "PART-A")), - service.NewMessage([]byte(testStr + "PART-B")), - } - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - for i := 0; i < len(msg); i++ { - part, err := writer.CreatePart(textproto.MIMEHeader{ - "Content-Type": []string{"application/octet-stream"}, - "foo-bar": []string{"baz-" + strconv.Itoa(i), "ignored"}, - }) - require.NoError(t, err) - - mBytes, err := msg[i].AsBytes() - require.NoError(t, err) - - _, err = io.Copy(part, bytes.NewReader(mBytes)) - require.NoError(t, err) - } - writer.Close() - - w.Header().Add("Content-Type", writer.FormDataContentType()) - w.WriteHeader(http.StatusCreated) - _, _ = w.Write(body.Bytes()) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -`, ts.URL) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", j) - resMsg, err := h.Send(context.Background(), nil) - require.NoError(t, err) - - assert.Len(t, resMsg, 2) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, testStr+"PART-A", string(mBytes)) - - mBytes, err = resMsg[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, testStr+"PART-B", string(mBytes)) - - v, _ := resMsg[0].MetaGet("foo-bar") - assert.Equal(t, "", v) - - v, _ = resMsg[0].MetaGet("http_status_code") - assert.Equal(t, "201", v) - - v, _ = resMsg[1].MetaGet("foo-bar") - assert.Equal(t, "", v) - - v, _ = resMsg[1].MetaGet("http_status_code") - assert.Equal(t, "201", v) - } -} - -func TestHTTPClientBadTLS(t *testing.T) { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - _, _ = w.Write(bytes.ToUpper(b)) - })) - defer ts.Close() - - conf := clientConfig(t, ` -url: %v -retries: 0 -`, ts.URL) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - _, err = h.Send(context.Background(), service.MessageBatch{ - service.NewMessage([]byte("hello world")), - }) - require.Error(t, err) -} - -func TestHTTPClientProxyConf(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("this shouldnt be hit directly") - })) - defer ts.Close() - - tsProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Host, strings.TrimPrefix(ts.URL, "http://")) - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - _, _ = w.Write(bytes.ToUpper(b)) - })) - defer tsProxy.Close() - - conf := clientConfig(t, ` -url: %v -proxy_url: %v -`, ts.URL+"/testpost", tsProxy.URL) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - resBatch, err := h.Send(context.Background(), service.MessageBatch{ - service.NewMessage([]byte("hello world")), - }) - require.NoError(t, err) - require.Len(t, resBatch, 1) - - mBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(mBytes)) -} - -func TestHTTPClientProxyAndTLSConf(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("this shouldnt be hit directly") - })) - defer ts.Close() - - tsProxy := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Host, strings.TrimPrefix(ts.URL, "http://")) - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - _, _ = w.Write(bytes.ToUpper(b)) - })) - defer tsProxy.Close() - - conf := clientConfig(t, ` -url: %v -tls: - enabled: true - skip_cert_verify: true -proxy_url: %v -`, ts.URL+"/testpost", tsProxy.URL) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - resBatch, err := h.Send(context.Background(), service.MessageBatch{ - service.NewMessage([]byte("hello world")), - }) - require.NoError(t, err) - require.Len(t, resBatch, 1) - - mBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(mBytes)) -} - -func TestHTTPClientOAuth2Conf(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Bearer footoken", r.Header.Get("Authorization")) - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - _, _ = w.Write(bytes.ToUpper(b)) - })) - defer ts.Close() - - tsOAuth2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Basic Zm9va2V5OmZvb3NlY3JldA==", r.Header.Get("Authorization")) - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - assert.Equal(t, "grant_type=client_credentials", string(b)) - _, _ = w.Write([]byte(`access_token=footoken&token_type=Bearer`)) - })) - defer tsOAuth2.Close() - - conf := clientConfig(t, ` -url: %v -oauth2: - enabled: true - token_url: %v - client_key: fookey - client_secret: foosecret -`, ts.URL+"/testpost", tsOAuth2.URL) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - resBatch, err := h.Send(context.Background(), service.MessageBatch{ - service.NewMessage([]byte("hello world")), - }) - require.NoError(t, err) - require.Len(t, resBatch, 1) - - mBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(mBytes)) -} - -func TestHTTPClientOAuth2AndTLSConf(t *testing.T) { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Bearer footoken", r.Header.Get("Authorization")) - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - _, _ = w.Write(bytes.ToUpper(b)) - })) - defer ts.Close() - - tsOAuth2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Basic Zm9va2V5OmZvb3NlY3JldA==", r.Header.Get("Authorization")) - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - assert.Equal(t, "grant_type=client_credentials", string(b)) - _, _ = w.Write([]byte(`access_token=footoken&token_type=Bearer`)) - })) - defer tsOAuth2.Close() - - conf := clientConfig(t, ` -url: %v -oauth2: - enabled: true - token_url: %v - client_key: fookey - client_secret: foosecret -tls: - enabled: true - skip_cert_verify: true -`, ts.URL+"/testpost", tsOAuth2.URL) - - h, err := NewClientFromOldConfig(conf, service.MockResources()) - require.NoError(t, err) - - resBatch, err := h.Send(context.Background(), service.MessageBatch{ - service.NewMessage([]byte("hello world")), - }) - require.NoError(t, err) - require.Len(t, resBatch, 1) - - mBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(mBytes)) -} diff --git a/internal/httpclient/config.go b/internal/httpclient/config.go deleted file mode 100644 index 422a21b99f..0000000000 --- a/internal/httpclient/config.go +++ /dev/null @@ -1,187 +0,0 @@ -package httpclient - -import ( - "context" - "crypto/tls" - "io/fs" - "net/http" - "time" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - hcFieldURL = "url" - hcFieldVerb = "verb" - hcFieldHeaders = "headers" - hcFieldMetadata = "metadata" - hcFieldExtractHeaders = "extract_headers" - hcFieldRateLimit = "rate_limit" - hcFieldTimeout = "timeout" - hcFieldRetryPeriod = "retry_period" - hcFieldMaxRetryBackoff = "max_retry_backoff" - hcFieldRetries = "retries" - hcFieldBackoffOn = "backoff_on" - hcFieldDropOn = "drop_on" - hcFieldSuccessfulOn = "successful_on" - hcFieldDumpRequestLogLevel = "dump_request_log_level" - hcFieldTLS = "tls" - hcFieldProxyURL = "proxy_url" -) - -// ConfigField returns a public API config field spec for an HTTP component, -// with optional extra fields added to the end. -func ConfigField(defaultVerb string, forOutput bool, extraChildren ...*service.ConfigField) *service.ConfigField { - innerFields := []*service.ConfigField{ - service.NewInterpolatedStringField(hcFieldURL). - Description("The URL to connect to."), - service.NewStringField(hcFieldVerb). - Description("A verb to connect with"). - Examples("POST", "GET", "DELETE"). - Default(defaultVerb), - service.NewInterpolatedStringMapField(hcFieldHeaders). - Description("A map of headers to add to the request."). - Example(map[string]any{ - "Content-Type": "application/octet-stream", - "traceparent": `${! tracing_span().traceparent }`, - }). - Default(map[string]any{}), - service.NewMetadataFilterField(hcFieldMetadata). - Description("Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers."). - Advanced(). - Optional(), - service.NewStringEnumField(hcFieldDumpRequestLogLevel, "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", ""). - Description("EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged."). - Advanced(). - Default(""). - Version("4.12.0"), - } - innerFields = append(innerFields, AuthFieldSpecsExpanded()...) - - extractHeadersDesc := "Specify which response headers should be added to resulting messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect." - if forOutput { - extractHeadersDesc = "Specify which response headers should be added to resulting synchronous response messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. This field is not applicable unless `propagate_response` is set to `true`." - } - innerFields = append(innerFields, - service.NewTLSToggledField(hcFieldTLS), - service.NewMetadataFilterField(hcFieldExtractHeaders). - Description(extractHeadersDesc). - Advanced(), - service.NewStringField(hcFieldRateLimit). - Description("An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by."). - Optional(), - service.NewDurationField(hcFieldTimeout). - Description("A static timeout to apply to requests."). - Default("5s"), - service.NewDurationField(hcFieldRetryPeriod). - Description("The base period to wait between failed requests."). - Advanced(). - Default("1s"), - service.NewDurationField(hcFieldMaxRetryBackoff). - Description("The maximum period to wait between failed requests."). - Advanced(). - Default("300s"), - service.NewIntField(hcFieldRetries). - Description("The maximum number of retry attempts to make."). - Advanced(). - Default(3), - service.NewIntListField(hcFieldBackoffOn). - Description("A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually."). - Advanced(). - Default([]any{429}), - service.NewIntListField(hcFieldDropOn). - Description("A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped."). - Advanced(). - Default([]any{}), - service.NewIntListField(hcFieldSuccessfulOn). - Description("A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field."). - Advanced(). - Default([]any{}), - service.NewStringField(hcFieldProxyURL). - Description("An optional HTTP proxy URL."). - Advanced(). - Optional(), - ) - - innerFields = append(innerFields, extraChildren...) - return service.NewObjectField("", innerFields...) -} - -//------------------------------------------------------------------------------ - -// ConfigFromParsed attempts to parse an http client config struct from a parsed -// plugin config. -func ConfigFromParsed(pConf *service.ParsedConfig) (conf OldConfig, err error) { - if conf.URL, err = pConf.FieldInterpolatedString(hcFieldURL); err != nil { - return - } - if conf.Verb, err = pConf.FieldString(hcFieldVerb); err != nil { - return - } - if conf.Headers, err = pConf.FieldInterpolatedStringMap(hcFieldHeaders); err != nil { - return - } - if conf.Metadata, err = pConf.FieldMetadataFilter(hcFieldMetadata); err != nil { - return - } - if conf.ExtractMetadata, err = pConf.FieldMetadataFilter(hcFieldExtractHeaders); err != nil { - return - } - conf.RateLimit, _ = pConf.FieldString(hcFieldRateLimit) - if conf.Timeout, err = pConf.FieldDuration(hcFieldTimeout); err != nil { - return - } - if conf.Retry, err = pConf.FieldDuration(hcFieldRetryPeriod); err != nil { - return - } - if conf.MaxBackoff, err = pConf.FieldDuration(hcFieldMaxRetryBackoff); err != nil { - return - } - if conf.NumRetries, err = pConf.FieldInt(hcFieldRetries); err != nil { - return - } - if conf.BackoffOn, err = pConf.FieldIntList(hcFieldBackoffOn); err != nil { - return - } - if conf.DropOn, err = pConf.FieldIntList(hcFieldDropOn); err != nil { - return - } - if conf.SuccessfulOn, err = pConf.FieldIntList(hcFieldSuccessfulOn); err != nil { - return - } - conf.DumpRequestLogLevel, _ = pConf.FieldString(hcFieldDumpRequestLogLevel) - if conf.TLSConf, conf.TLSEnabled, err = pConf.FieldTLSToggled(hcFieldTLS); err != nil { - return - } - conf.ProxyURL, _ = pConf.FieldString(hcFieldProxyURL) - if conf.authSigner, err = pConf.HTTPRequestAuthSignerFromParsed(); err != nil { - return - } - if conf.clientCtor, err = oauth2ClientCtorFromParsed(pConf); err != nil { - return - } - return -} - -// OldConfig is a configuration struct for an HTTP client. -type OldConfig struct { - URL *service.InterpolatedString - Verb string - Headers map[string]*service.InterpolatedString - Metadata *service.MetadataFilter - ExtractMetadata *service.MetadataFilter - RateLimit string - Timeout time.Duration - Retry time.Duration - MaxBackoff time.Duration - NumRetries int - BackoffOn []int - DropOn []int - SuccessfulOn []int - DumpRequestLogLevel string - TLSEnabled bool - TLSConf *tls.Config - ProxyURL string - authSigner func(f fs.FS, req *http.Request) error - clientCtor func(context.Context, *http.Client) *http.Client -} diff --git a/internal/httpclient/config_test.go b/internal/httpclient/config_test.go deleted file mode 100644 index 81333a31b9..0000000000 --- a/internal/httpclient/config_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package httpclient - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestNewStyleConfigs(t *testing.T) { - tests := []struct { - name string - verbOverride string - forOutput bool - inputYAML string - validator func(t *testing.T, c *OldConfig) - }{ - { - name: "basic fields", - inputYAML: ` -url: example.com/foo1 -verb: PUT -headers: - foo1: bar1 - foo2: bar2 -`, - validator: func(t *testing.T, o *OldConfig) { - sURL, _ := o.URL.Static() - assert.Equal(t, "example.com/foo1", sURL) - assert.Equal(t, "PUT", o.Verb) - - sHeaders := map[string]string{} - for k, v := range o.Headers { - sHeaders[k], _ = v.Static() - } - assert.Equal(t, map[string]string{ - "foo1": "bar1", - "foo2": "bar2", - }, sHeaders) - }, - }, - { - name: "verb default", - inputYAML: ` -url: example.com/foo2 -rate_limit: nah -`, - verbOverride: "GET", - validator: func(t *testing.T, o *OldConfig) { - sURL, _ := o.URL.Static() - assert.Equal(t, "example.com/foo2", sURL) - assert.Equal(t, "GET", o.Verb) - assert.Equal(t, "nah", o.RateLimit) - }, - }, - { - name: "code overrides", - inputYAML: ` -url: example.com/foo3 -successful_on: [ 1, 2, 3 ] -backoff_on: [ 4, 5, 6 ] -drop_on: [ 7, 8, 9 ] -`, - validator: func(t *testing.T, o *OldConfig) { - sURL, _ := o.URL.Static() - assert.Equal(t, "example.com/foo3", sURL) - assert.Equal(t, []int{1, 2, 3}, o.SuccessfulOn) - assert.Equal(t, []int{4, 5, 6}, o.BackoffOn) - assert.Equal(t, []int{7, 8, 9}, o.DropOn) - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - verb := "POST" - if test.verbOverride != "" { - verb = test.verbOverride - } - - spec := service.NewConfigSpec().Field(ConfigField(verb, test.forOutput)) - parsed, err := spec.ParseYAML(test.inputYAML, nil) - require.NoError(t, err) - - conf, err := ConfigFromParsed(parsed) - require.NoError(t, err) - - test.validator(t, &conf) - }) - } -} diff --git a/internal/httpclient/errors.go b/internal/httpclient/errors.go deleted file mode 100644 index 43873f825f..0000000000 --- a/internal/httpclient/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package httpclient - -import ( - "fmt" - "strings" -) - -// ErrUnexpectedHTTPRes is an error returned when an HTTP request returned an -// unexpected response. -type ErrUnexpectedHTTPRes struct { - Code int - S string - Body []byte -} - -// Error returns the Error string. -func (e ErrUnexpectedHTTPRes) Error() string { - body := strings.ReplaceAll(string(e.Body), "\n", "") - return fmt.Sprintf("HTTP request returned unexpected response code (%v): %v, Error: %v", e.Code, e.S, body) -} diff --git a/internal/httpclient/errors_test.go b/internal/httpclient/errors_test.go deleted file mode 100644 index d3e71e0033..0000000000 --- a/internal/httpclient/errors_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package httpclient - -import "testing" - -func TestHTTPError(t *testing.T) { - err := ErrUnexpectedHTTPRes{ - Code: 0, - S: "test str", - Body: []byte("test body str"), - } - - exp, act := `HTTP request returned unexpected response code (0): test str, Error: test body str`, err.Error() - if exp != act { - t.Errorf("Wrong Error() from ErrUnexpectedHTTPRes: %v != %v", exp, act) - } -} diff --git a/internal/httpclient/logger.go b/internal/httpclient/logger.go deleted file mode 100644 index b89c6fca79..0000000000 --- a/internal/httpclient/logger.go +++ /dev/null @@ -1,163 +0,0 @@ -package httpclient - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/benthosdev/benthos/v4/public/service" - - "go.uber.org/multierr" -) - -type roundTripper struct { - base http.RoundTripper - logger *service.Logger - level string -} - -var _ http.RoundTripper = (*roundTripper)(nil) - -func newRequestLog(base http.RoundTripper, logger *service.Logger, lvl string) (http.RoundTripper, error) { - if base == nil { - base = http.DefaultTransport - } - - if lvl == "" { - return base, nil - } - - return &roundTripper{ - base: base, - logger: logger, - level: strings.ToUpper(strings.TrimSpace(lvl)), - }, nil -} - -func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - t0 := time.Now() - - var ( - respOriginal *http.Response // final response - errCum error // final error - ) - - var ( - reqBodyCaptured interface{} - reqBodyBuf = &bytes.Buffer{} - reqBodyErr error - ) - - if req != nil && req.Body != nil { - _, reqBodyErr = io.Copy(reqBodyBuf, req.Body) - if reqBodyErr != nil { - errCum = multierr.Append(errCum, fmt.Errorf("error copy request body: %w", reqBodyErr)) - reqBodyBuf = &bytes.Buffer{} - } - - if _err := req.Body.Close(); _err != nil { - errCum = multierr.Append(errCum, fmt.Errorf("error closing request body: %w", _err)) - } - - req.Body = io.NopCloser(reqBodyBuf) - } - - // use json.Unmarshal instead of json.NewDecoder to make sure we can re-read the buffer - if _err := json.Unmarshal(reqBodyBuf.Bytes(), &reqBodyCaptured); _err != nil && reqBodyBuf.Len() > 0 { - reqBodyCaptured = reqBodyBuf.String() - } - - var roundTripErr error - respOriginal, roundTripErr = r.base.RoundTrip(req) - if roundTripErr != nil { - errCum = multierr.Append(errCum, fmt.Errorf("error doing actual request: %w", roundTripErr)) - } - - var ( - respBodyCaptured interface{} - respBodyBuf = &bytes.Buffer{} - respErrBody error - ) - - if respOriginal != nil && respOriginal.Body != nil { - _, respErrBody = io.Copy(respBodyBuf, respOriginal.Body) - if respErrBody != nil { - errCum = multierr.Append(errCum, fmt.Errorf("error copy response body: %w", respErrBody)) - respBodyBuf = &bytes.Buffer{} - } - - if _err := respOriginal.Body.Close(); _err != nil { - errCum = multierr.Append(errCum, fmt.Errorf("error closing response body: %w", _err)) - } - - respOriginal.Body = io.NopCloser(respBodyBuf) - } - - // use json.Unmarshal instead of json.NewDecoder to make sure we can re-read the buffer - if _err := json.Unmarshal(respBodyBuf.Bytes(), &respBodyCaptured); _err != nil && respBodyBuf.Len() > 0 { - respBodyCaptured = respBodyBuf.String() - } - - // log outgoing request as simple map - accessLog := map[string]any{ - "elapsed_ns": time.Since(t0).Nanoseconds(), - } - - // append to map only when the http.Request is not nil - if req != nil { - accessLog["request"] = map[string]any{ - "url": req.URL.Redacted(), - "method": req.Method, - "header": toSimpleMap(req.Header), - "body": reqBodyCaptured, - } - } - - // append to map only when the http.Response is not nil - if respOriginal != nil { - accessLog["response"] = map[string]any{ - "status_code": respOriginal.StatusCode, - "content_length": respOriginal.ContentLength, - "header": toSimpleMap(respOriginal.Header), - "body": respBodyCaptured, - } - } - - // append error if any - if errCum != nil { - accessLog["error"] = errCum.Error() - } - - logger := r.logger.With("access_log", accessLog) - msg := "http request log" - - switch r.level { - case "TRACE": - logger.Trace(msg) - case "DEBUG": - logger.Debug(msg) - case "INFO": - logger.Info(msg) - case "WARN": - logger.Warn(msg) - case "ERROR": - logger.Error(msg) - case "FATAL": - logger.Error(msg) - } - - return respOriginal, roundTripErr -} - -var toSimpleMap = func(h http.Header) map[string]string { - out := map[string]string{} - for k, v := range h { - out[k] = strings.Join(v, " ") - } - - return out -} diff --git a/internal/httpclient/logger_test.go b/internal/httpclient/logger_test.go deleted file mode 100644 index 7567757ffa..0000000000 --- a/internal/httpclient/logger_test.go +++ /dev/null @@ -1,343 +0,0 @@ -package httpclient - -import ( - "bytes" - "fmt" - "io" - "net/http" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewRequestLog(t *testing.T) { - t.Run("nil base", func(t *testing.T) { - httpRoundTrip, err := newRequestLog(nil, nil, "") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - }) - - t.Run("enable with log", func(t *testing.T) { - httpRoundTrip, err := newRequestLog(http.DefaultTransport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - }) -} - -func TestToSimpleMap(t *testing.T) { - t.Run("empty header", func(t *testing.T) { - m := toSimpleMap(http.Header{}) - require.EqualValues(t, map[string]string{}, m) - }) - - t.Run("values header", func(t *testing.T) { - m := toSimpleMap(http.Header{ - "Content Type": []string{"application/json", "charset=utf-8"}, - }) - require.EqualValues(t, map[string]string{ - "Content Type": "application/json charset=utf-8", - }, m) - }) -} - -type mockHTTPRoundTrip struct { - Error error - CallRoundTrip func(request *http.Request) (*http.Response, error) -} - -var _ http.RoundTripper = (*mockHTTPRoundTrip)(nil) - -func newMockHTTPRoundTripper() *mockHTTPRoundTrip { - return &mockHTTPRoundTrip{} -} - -func newMockHTTPRoundTripperWithErr(err error) *mockHTTPRoundTrip { - return &mockHTTPRoundTrip{ - Error: err, - } -} - -func (m *mockHTTPRoundTrip) RoundTrip(request *http.Request) (*http.Response, error) { - if m.Error != nil { - return nil, m.Error - } - - if request == nil { - return nil, fmt.Errorf("nil *http.Request") - } - - // by default, non-nil error returned with empty http.Response - if m.CallRoundTrip == nil { - return &http.Response{}, nil - } - - return m.CallRoundTrip(request) -} - -func TestRoundTripper_RoundTrip(t *testing.T) { - t.Run("nil request", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - resp, err := httpRoundTrip.RoundTrip(nil) - require.Nil(t, resp) - require.Error(t, err) - }) - - t.Run("nil request body", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{} - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("non-empty request body", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("non-empty request body but error read", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(&Buf{err: fmt.Errorf("mock error buffer")}), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("non-empty request body but error close", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: &Closer{ - buf: bytes.NewBufferString(`{"foo":"bar"}`), - err: fmt.Errorf("mock error buffer"), - }, - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("non-empty request body no valid json", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(``)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("failed http.RoundTripper", func(t *testing.T) { - expectedErr := fmt.Errorf("failed to fetch data") - transport := newMockHTTPRoundTripperWithErr(expectedErr) - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, respErr := httpRoundTrip.RoundTrip(req) - require.Nil(t, resp) - require.ErrorIs(t, respErr, expectedErr) - }) - - t.Run("not-nil response with empty resp body", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - transport.CallRoundTrip = func(request *http.Request) (*http.Response, error) { - return &http.Response{}, nil - } - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("not-nil response non-empty resp body", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - transport.CallRoundTrip = func(request *http.Request) (*http.Response, error) { - return &http.Response{ - Body: io.NopCloser(bytes.NewBufferString(`{"FOO":"BAR"}`)), - }, nil - } - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("not-nil response body fail on close", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - transport.CallRoundTrip = func(request *http.Request) (*http.Response, error) { - return &http.Response{ - Body: &Closer{ - buf: bytes.NewBufferString(`{"FOO":"BAR"}`), - err: fmt.Errorf("mock error close resp body"), - }, - }, nil - } - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("not-nil response non-empty resp body with non-valid json", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - transport.CallRoundTrip = func(request *http.Request) (*http.Response, error) { - return &http.Response{ - Body: io.NopCloser(bytes.NewBufferString(`BAR`)), - }, nil - } - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("not-nil response non-empty resp body but failed to read", func(t *testing.T) { - transport := newMockHTTPRoundTripper() - - transport.CallRoundTrip = func(request *http.Request) (*http.Response, error) { - return &http.Response{ - Body: io.NopCloser(&Buf{err: fmt.Errorf("failed to read response body buffer")}), - }, nil - } - - httpRoundTrip, err := newRequestLog(transport, nil, "INFO") - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - req := &http.Request{ - Body: io.NopCloser(bytes.NewBufferString(`{"foo":"bar"}`)), - } - - resp, err := httpRoundTrip.RoundTrip(req) - require.NotNil(t, resp) - require.NoError(t, err) - }) - - t.Run("test all log level", func(t *testing.T) { - // cannot test FATAL because it really os.Exit(1) which make unit test error - levels := []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"} - - for _, level := range levels { - t.Run(level, func(t *testing.T) { - transport := newMockHTTPRoundTripper() - httpRoundTrip, err := newRequestLog(transport, nil, level) - require.NotNil(t, httpRoundTrip) - require.NoError(t, err) - - resp, err := httpRoundTrip.RoundTrip(nil) - require.Nil(t, resp) - require.Error(t, err) - }) - } - }) -} - -type Buf struct { - err error -} - -func (b *Buf) Read(p []byte) (n int, err error) { - if b.err != nil { - return 0, b.err - } - - return len(p), nil -} - -type Closer struct { - buf io.Reader - err error -} - -var _ io.ReadCloser = (*Closer)(nil) - -func (c *Closer) Read(p []byte) (n int, err error) { - return c.buf.Read(p) -} - -func (c *Closer) Close() error { - return c.err -} diff --git a/internal/httpclient/request.go b/internal/httpclient/request.go deleted file mode 100644 index 9d254b3c64..0000000000 --- a/internal/httpclient/request.go +++ /dev/null @@ -1,259 +0,0 @@ -package httpclient - -import ( - "bytes" - "fmt" - "io" - "io/fs" - "mime/multipart" - "net/http" - "net/textproto" - "strings" - - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/service" -) - -// MultipartExpressions represents three dynamic expressions that define a -// multipart message part in an HTTP request. Specifying one or more of these -// can be used as a way of creating HTTP requests that overrides the default -// behaviour. -type MultipartExpressions struct { - ContentDisposition *service.InterpolatedString - ContentType *service.InterpolatedString - Body *service.InterpolatedString -} - -// RequestCreator creates *http.Request types from messages based on various -// configurable parameters. -type RequestCreator struct { - // Explicit body overrides, in order of precedence - explicitBody *service.InterpolatedString - explicitMultiparts []MultipartExpressions - - fs fs.FS - reqSigner func(f fs.FS, req *http.Request) error - - url *service.InterpolatedString - host *service.InterpolatedString - verb string - headers map[string]*service.InterpolatedString - metaInsertFilter *service.MetadataFilter -} - -// RequestOpt represents a customisation of a request creator. -type RequestOpt func(r *RequestCreator) - -// RequestCreatorFromOldConfig creates a new request creator from an old struct -// style config. Eventually I'd like to phase these out for the more dynamic -// service style parses, but it'll take a while so we have this for now. -func RequestCreatorFromOldConfig(conf OldConfig, mgr *service.Resources, opts ...RequestOpt) (*RequestCreator, error) { - r := &RequestCreator{ - fs: mgr.FS(), - url: conf.URL, - reqSigner: conf.authSigner, - verb: conf.Verb, - headers: conf.Headers, - metaInsertFilter: conf.Metadata, - } - for _, opt := range opts { - opt(r) - } - for k, v := range r.headers { - if strings.EqualFold(k, "host") { - r.host = v - delete(r.headers, k) - break - } - } - return r, nil -} - -// WithExplicitBody modifies the request creator to instead only use input -// reference messages for headers and metadata, and use the expression for -// creating a body. -func WithExplicitBody(e *service.InterpolatedString) RequestOpt { - if e == nil { - e, _ = service.NewInterpolatedString("") - } - return func(r *RequestCreator) { - r.explicitBody = e - } -} - -// WithExplicitMultipart modifies the request creator to instead only use input -// reference messages for headers and metadata, and use a list of multipart -// expressions for creating a body. -func WithExplicitMultipart(m []MultipartExpressions) RequestOpt { - return func(r *RequestCreator) { - r.explicitMultiparts = m - } -} - -func (r *RequestCreator) bodyFromExplicit(refBatch service.MessageBatch) (body io.Reader, overrideContentType string, err error) { - if _, exists := r.headers["Content-Type"]; !exists { - overrideContentType = "application/octet-stream" - } - var bBytes []byte - if bBytes, err = refBatch.TryInterpolatedBytes(0, r.explicitBody); err != nil { - return - } - body = bytes.NewBuffer(bBytes) - return -} - -func (r *RequestCreator) bodyFromExplicitMultipart(refBatch service.MessageBatch) (body io.Reader, overrideContentType string, err error) { - buf := &bytes.Buffer{} - writer := multipart.NewWriter(buf) - for _, v := range r.explicitMultiparts { - mh := make(textproto.MIMEHeader) - var cTypeStr, cDispStr string - if cTypeStr, err = refBatch.TryInterpolatedString(0, v.ContentType); err != nil { - err = fmt.Errorf("content-type interpolation error: %w", err) - return - } - if cDispStr, err = refBatch.TryInterpolatedString(0, v.ContentDisposition); err != nil { - err = fmt.Errorf("content-disposition interpolation error: %w", err) - return - } - mh.Set("Content-Type", cTypeStr) - mh.Set("Content-Disposition", cDispStr) - - var part io.Writer - if part, err = writer.CreatePart(mh); err != nil { - return - } - var partBytes []byte - if partBytes, err = refBatch.TryInterpolatedBytes(0, v.Body); err != nil { - err = fmt.Errorf("part body interpolation error: %w", err) - return - } - if _, err = io.Copy(part, bytes.NewReader(partBytes)); err != nil { - return - } - } - writer.Close() - body = buf - overrideContentType = writer.FormDataContentType() - return -} - -func (r *RequestCreator) body(refBatch service.MessageBatch) (body io.Reader, overrideContentType string, err error) { - if r.explicitBody != nil { - body, overrideContentType, err = r.bodyFromExplicit(refBatch) - return - } - - if len(r.explicitMultiparts) > 0 { - body, overrideContentType, err = r.bodyFromExplicitMultipart(refBatch) - return - } - - if len(refBatch) == 0 { - return - } - - if len(refBatch) == 1 { - if _, exists := r.headers["Content-Type"]; !exists { - overrideContentType = "application/octet-stream" - } - var bodyBytes []byte - if bodyBytes, err = refBatch[0].AsBytes(); err != nil { - return - } - body = bytes.NewBuffer(bodyBytes) - return - } - - // More than one message in the batch, create a multipart message by - // default. - buf := &bytes.Buffer{} - writer := multipart.NewWriter(buf) - - for i, p := range refBatch { - contentType := "application/octet-stream" - if v, exists := r.headers["Content-Type"]; exists { - if contentType, err = refBatch.TryInterpolatedString(i, v); err != nil { - err = fmt.Errorf("content-type interpolation error: %w", err) - return - } - } - - headers := textproto.MIMEHeader{ - "Content-Type": []string{contentType}, - } - _ = r.metaInsertFilter.WalkMut(p, func(k string, v any) error { - headers[k] = append(headers[k], value.IToString(v)) - return nil - }) - - var part io.Writer - if part, err = writer.CreatePart(headers); err != nil { - return - } - - var pBytes []byte - if pBytes, err = p.AsBytes(); err != nil { - return - } - if _, err = io.Copy(part, bytes.NewReader(pBytes)); err != nil { - return - } - } - - writer.Close() - overrideContentType = writer.FormDataContentType() - - body = buf - return -} - -// Create an *http.Request using a reference message batch to extract the body -// and headers of the request. It's possible that the creator has been given -// explicit overrides for the body, in which case the reference batch is only -// used for general request headers/metadata enrichment. -func (r *RequestCreator) Create(refBatch service.MessageBatch) (req *http.Request, err error) { - var overrideContentType string - var body io.Reader - if body, overrideContentType, err = r.body(refBatch); err != nil { - return - } - - var urlStr string - if urlStr, err = refBatch.TryInterpolatedString(0, r.url); err != nil { - err = fmt.Errorf("url interpolation error: %w", err) - return - } - if req, err = http.NewRequest(r.verb, urlStr, body); err != nil { - return - } - - for k, v := range r.headers { - var hStr string - if hStr, err = refBatch.TryInterpolatedString(0, v); err != nil { - err = fmt.Errorf("header '%v' interpolation error: %w", k, err) - return - } - req.Header.Add(k, hStr) - } - if len(refBatch) > 0 { - _ = r.metaInsertFilter.WalkMut(refBatch[0], func(k string, v any) error { - req.Header.Add(k, value.IToString(v)) - return nil - }) - } - - if r.host != nil { - if req.Host, err = refBatch.TryInterpolatedString(0, r.host); err != nil { - err = fmt.Errorf("host interpolation error: %w", err) - return - } - } - if overrideContentType != "" { - req.Header.Del("Content-Type") - req.Header.Add("Content-Type", overrideContentType) - } - - err = r.reqSigner(r.fs, req) - return -} diff --git a/internal/httpclient/request_test.go b/internal/httpclient/request_test.go deleted file mode 100644 index 59078a0121..0000000000 --- a/internal/httpclient/request_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package httpclient - -import ( - "testing" - - "github.com/benthosdev/benthos/v4/public/service" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSingleMessageHeaders(t *testing.T) { - spec := service.NewConfigSpec().Field(ConfigField("GET", false)) - parsed, err := spec.ParseYAML(` -url: example.com/foo -headers: - "Content-Type": "foo" -metadata: - include_prefixes: [ "more_" ] -`, nil) - require.NoError(t, err) - - oldConf, err := ConfigFromParsed(parsed) - require.NoError(t, err) - - reqCreator, err := RequestCreatorFromOldConfig(oldConf, service.MockResources()) - require.NoError(t, err) - - part := service.NewMessage([]byte("hello world")) - part.MetaSetMut("more_bar", "barvalue") - part.MetaSetMut("ignore_baz", "bazvalue") - - b := service.MessageBatch{part} - - req, err := reqCreator.Create(b) - require.NoError(t, err) - - assert.Equal(t, []string{"foo"}, req.Header.Values("Content-Type")) - assert.Equal(t, []string{"barvalue"}, req.Header.Values("more_bar")) - assert.Equal(t, []string(nil), req.Header.Values("ignore_baz")) -} diff --git a/internal/httpserver/basic_auth.go b/internal/httpserver/basic_auth.go deleted file mode 100644 index b241e60041..0000000000 --- a/internal/httpserver/basic_auth.go +++ /dev/null @@ -1,187 +0,0 @@ -package httpserver - -import ( - "crypto/md5" - "crypto/sha256" - "crypto/subtle" - "encoding/base64" - "errors" - "fmt" - "net/http" - - "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/scrypt" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -const ( - fieldBasicAuth = "basic_auth" - fieldBasicAuthEnabled = "enabled" - fieldBasicAuthRealm = "realm" - fieldBasicAuthUsername = "username" - fieldBasicAuthPasswordHash = "password_hash" - fieldBasicAuthAlgorithm = "algorithm" - fieldBasicAuthSalt = "salt" -) - -const ( - scryptN = 32768 - scryptR = 8 - scryptP = 1 - scryptKeyLen = 32 -) - -// BasicAuthConfig contains struct based fields for basic authentication. -type BasicAuthConfig struct { - Enabled bool `json:"enabled" yaml:"enabled"` - Username string `json:"username" yaml:"username"` - PasswordHash string `json:"password_hash" yaml:"password_hash"` - Realm string `json:"realm" yaml:"realm"` - Algorithm string `json:"algorithm" yaml:"algorithm"` - Salt string `json:"salt" yaml:"salt"` -} - -// NewBasicAuthConfig returns a BasicAuthConfig with default values. -func NewBasicAuthConfig() BasicAuthConfig { - return BasicAuthConfig{ - Enabled: false, - Username: "", - PasswordHash: "", - Realm: "restricted", - Algorithm: "sha256", - Salt: "", - } -} - -// Validate confirms that the BasicAuth is properly configured. -func (b BasicAuthConfig) Validate() error { - if !b.Enabled { - return nil - } - - if b.Username == "" || b.PasswordHash == "" { - return errors.New("both username and password_hash are required") - } - - if !(b.Algorithm == "md5" || b.Algorithm == "sha256" || b.Algorithm == "bcrypt" || b.Algorithm == "scrypt") { - return errors.New("algorithm should be one of md5, sha256, bcrypt, or scrypt") - } - - if b.Algorithm == "scrypt" && b.Salt == "" { - return errors.New("salt is required for scrypt") - } - - if b.Algorithm == "scrypt" { - if _, err := base64.StdEncoding.DecodeString(b.Salt); err != nil { - return fmt.Errorf("invalid salt : %w", err) - } - } - - return nil -} - -// WrapHandler wraps the provided HTTP handler with middleware that enforces -// BasicAuth if it's enabled. -func (b BasicAuthConfig) WrapHandler(next http.HandlerFunc) http.HandlerFunc { - if !b.Enabled { - return next - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok { - user = "" - pass = "" - } - - if ok, err := b.matches(user, pass); !ok || err != nil { - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q, charset="UTF-8"`, b.Realm)) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - next.ServeHTTP(w, r) - }) -} - -// BasicAuthFieldSpec returns the spec for an HTTP BasicAuth component. -func BasicAuthFieldSpec() docs.FieldSpec { - return docs.FieldObject(fieldBasicAuth, "Allows you to enforce and customise basic authentication for requests to the HTTP server.").WithChildren( - docs.FieldBool(fieldBasicAuthEnabled, "Enable basic authentication").HasDefault(false), - docs.FieldString(fieldBasicAuthRealm, "Custom realm name").HasDefault("restricted"), - docs.FieldString(fieldBasicAuthUsername, "Username required to authenticate.").HasDefault(""), - docs.FieldString(fieldBasicAuthPasswordHash, "Hashed password required to authenticate. (base64 encoded)").HasDefault(""), - docs.FieldString(fieldBasicAuthAlgorithm, "Encryption algorithm used to generate `password_hash`.", "md5", "sha256", "bcrypt", "scrypt").HasDefault("sha256"), - docs.FieldString(fieldBasicAuthSalt, "Salt for scrypt algorithm. (base64 encoded)").HasDefault(""), - ).Advanced() -} - -func BasicAuthConfigFromParsed(pConf *docs.ParsedConfig) (conf BasicAuthConfig, err error) { - pConf = pConf.Namespace(fieldBasicAuth) - if conf.Enabled, err = pConf.FieldBool(fieldBasicAuthEnabled); err != nil { - return - } - if conf.Username, err = pConf.FieldString(fieldBasicAuthUsername); err != nil { - return - } - if conf.PasswordHash, err = pConf.FieldString(fieldBasicAuthPasswordHash); err != nil { - return - } - if conf.Realm, err = pConf.FieldString(fieldBasicAuthRealm); err != nil { - return - } - if conf.Algorithm, err = pConf.FieldString(fieldBasicAuthAlgorithm); err != nil { - return - } - if conf.Salt, err = pConf.FieldString(fieldBasicAuthSalt); err != nil { - return - } - return -} - -func (b BasicAuthConfig) matches(user, pass string) (bool, error) { - expectedPassHash, err := base64.StdEncoding.DecodeString(b.PasswordHash) - if err != nil { - return false, err - } - - userMatch := (subtle.ConstantTimeCompare([]byte(user), []byte(b.Username)) == 1) - passMatch := b.compareHashAndPassword(expectedPassHash, []byte(pass)) - - return (userMatch && passMatch), nil -} - -func (b BasicAuthConfig) compareHashAndPassword(hashedPassword, password []byte) bool { - switch b.Algorithm { - case "md5": - v := md5.Sum(password) - return (subtle.ConstantTimeCompare(hashedPassword, v[:]) == 1) - case "sha256": - v := sha256.Sum256(password) - return (subtle.ConstantTimeCompare(hashedPassword, v[:]) == 1) - case "bcrypt": - if err := bcrypt.CompareHashAndPassword(hashedPassword, password); err != nil { - return false - } - return true - case "scrypt": - salt, err := base64.StdEncoding.DecodeString(b.Salt) - if err != nil { - return false - } - - v, err := scrypt.Key(password, salt, scryptN, scryptR, scryptP, scryptKeyLen) - if err != nil { - return false - } - return (subtle.ConstantTimeCompare(hashedPassword, v) == 1) - default: - return false - } -} diff --git a/internal/httpserver/cors.go b/internal/httpserver/cors.go deleted file mode 100644 index b074f9f125..0000000000 --- a/internal/httpserver/cors.go +++ /dev/null @@ -1,64 +0,0 @@ -package httpserver - -import ( - "errors" - "net/http" - - "github.com/gorilla/handlers" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -const ( - fieldCORS = "cors" - fieldCORSEnabled = "enabled" - fieldCORSAllowedOrigins = "allowed_origins" -) - -// CORSConfig contains struct configuration for allowing CORS headers. -type CORSConfig struct { - Enabled bool `json:"enabled" yaml:"enabled"` - AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"` -} - -// NewServerCORSConfig returns a new server CORS config with default fields. -func NewServerCORSConfig() CORSConfig { - return CORSConfig{ - Enabled: false, - AllowedOrigins: []string{}, - } -} - -// WrapHandler wraps a provided HTTP handler with middleware that enables CORS -// requests (when configured). -func (conf CORSConfig) WrapHandler(handler http.Handler) (http.Handler, error) { - if !conf.Enabled { - return handler, nil - } - if len(conf.AllowedOrigins) == 0 { - return nil, errors.New("must specify at least one allowed origin") - } - return handlers.CORS( - handlers.AllowedOrigins(conf.AllowedOrigins), - handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}), - )(handler), nil -} - -// ServerCORSFieldSpec returns a field spec for an http server CORS component. -func ServerCORSFieldSpec() docs.FieldSpec { - return docs.FieldObject(fieldCORS, "Adds Cross-Origin Resource Sharing headers.").WithChildren( - docs.FieldBool(fieldCORSEnabled, "Whether to allow CORS requests.").HasDefault(false), - docs.FieldString(fieldCORSAllowedOrigins, "An explicit list of origins that are allowed for CORS requests.").Array().HasDefault([]any{}), - ).AtVersion("3.63.0").Advanced() -} - -func CORSConfigFromParsed(pConf *docs.ParsedConfig) (conf CORSConfig, err error) { - pConf = pConf.Namespace(fieldCORS) - if conf.Enabled, err = pConf.FieldBool(fieldCORSEnabled); err != nil { - return - } - if conf.AllowedOrigins, err = pConf.FieldStringList(fieldCORSAllowedOrigins); err != nil { - return - } - return -} diff --git a/internal/httpserver/cors_test.go b/internal/httpserver/cors_test.go deleted file mode 100644 index 234b706c2c..0000000000 --- a/internal/httpserver/cors_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package httpserver - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAPIEnableCORS(t *testing.T) { - conf := NewServerCORSConfig() - conf.Enabled = true - conf.AllowedOrigins = []string{"*"} - - tmpHandler := http.NewServeMux() - tmpHandler.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("1.2.3")) - }) - - handler, err := conf.WrapHandler(tmpHandler) - require.NoError(t, err) - - request, _ := http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "meow") - request.Header.Add("Access-Control-Request-Method", "POST") - - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "*", response.Header().Get("Access-Control-Allow-Origin")) -} - -func TestAPIEnableCORSOrigins(t *testing.T) { - conf := NewServerCORSConfig() - conf.Enabled = true - conf.AllowedOrigins = []string{"foo", "bar"} - - tmpHandler := http.NewServeMux() - tmpHandler.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("1.2.3")) - }) - - handler, err := conf.WrapHandler(tmpHandler) - require.NoError(t, err) - - request, _ := http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "foo") - request.Header.Add("Access-Control-Request-Method", "POST") - - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "foo", response.Header().Get("Access-Control-Allow-Origin")) - - request, _ = http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "bar") - request.Header.Add("Access-Control-Request-Method", "POST") - - response = httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "bar", response.Header().Get("Access-Control-Allow-Origin")) - - request, _ = http.NewRequest("OPTIONS", "/version", http.NoBody) - request.Header.Add("Origin", "baz") - request.Header.Add("Access-Control-Request-Method", "POST") - - response = httptest.NewRecorder() - handler.ServeHTTP(response, request) - - assert.Equal(t, http.StatusOK, response.Code) - assert.Equal(t, "", response.Header().Get("Access-Control-Allow-Origin")) -} - -func TestAPIEnableCORSNoHeaders(t *testing.T) { - conf := NewServerCORSConfig() - conf.Enabled = true - - _, err := conf.WrapHandler(http.NewServeMux()) - require.Error(t, err) - assert.Contains(t, err.Error(), "must specify at least one allowed origin") -} diff --git a/internal/impl/aws/cache_dynamodb.go b/internal/impl/aws/cache_dynamodb.go index 83be791f6a..3444929168 100644 --- a/internal/impl/aws/cache_dynamodb.go +++ b/internal/impl/aws/cache_dynamodb.go @@ -14,8 +14,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) func dynCacheConfig() *service.ConfigSpec { diff --git a/internal/impl/aws/cache_s3.go b/internal/impl/aws/cache_s3.go index f64601bbd3..812e2dccae 100644 --- a/internal/impl/aws/cache_s3.go +++ b/internal/impl/aws/cache_s3.go @@ -12,8 +12,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) func s3CacheConfig() *service.ConfigSpec { diff --git a/internal/impl/aws/input_kinesis.go b/internal/impl/aws/input_kinesis.go index 8e7613b3eb..56e2774fdb 100644 --- a/internal/impl/aws/input_kinesis.go +++ b/internal/impl/aws/input_kinesis.go @@ -15,8 +15,9 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/gofrs/uuid" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/aws/input_s3.go b/internal/impl/aws/input_s3.go index 4b919789ee..ffab56742c 100644 --- a/internal/impl/aws/input_s3.go +++ b/internal/impl/aws/input_s3.go @@ -17,9 +17,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/codec" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/aws/input_sqs.go b/internal/impl/aws/input_sqs.go index 910c2a7c5c..0fdfd6130a 100644 --- a/internal/impl/aws/input_sqs.go +++ b/internal/impl/aws/input_sqs.go @@ -13,8 +13,9 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/aws/integration_sqs_test.go b/internal/impl/aws/integration_sqs_test.go index 901a972b70..9a5c89d1fd 100644 --- a/internal/impl/aws/integration_sqs_test.go +++ b/internal/impl/aws/integration_sqs_test.go @@ -8,7 +8,7 @@ import ( "github.com/benthosdev/benthos/v4/public/service/integration" - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" + _ "github.com/redpanda-data/connect/v4/public/components/pure" ) func sqsIntegrationSuite(t *testing.T, lsPort string) { diff --git a/internal/impl/aws/integration_test.go b/internal/impl/aws/integration_test.go index 528bffb1e3..1a1968dce7 100644 --- a/internal/impl/aws/integration_test.go +++ b/internal/impl/aws/integration_test.go @@ -14,7 +14,7 @@ import ( "github.com/benthosdev/benthos/v4/public/service/integration" - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" + _ "github.com/redpanda-data/connect/v4/public/components/pure" ) func getLocalStack(t testing.TB) (port string) { diff --git a/internal/impl/aws/metrics_cloudwatch.go b/internal/impl/aws/metrics_cloudwatch.go index 0e9b518293..e005929bbc 100644 --- a/internal/impl/aws/metrics_cloudwatch.go +++ b/internal/impl/aws/metrics_cloudwatch.go @@ -11,8 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/aws/output_dynamodb.go b/internal/impl/aws/output_dynamodb.go index d2525643f9..88d00e5374 100644 --- a/internal/impl/aws/output_dynamodb.go +++ b/internal/impl/aws/output_dynamodb.go @@ -15,9 +15,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/aws/output_kinesis.go b/internal/impl/aws/output_kinesis.go index f2e0c6edf1..4baf97c5fe 100644 --- a/internal/impl/aws/output_kinesis.go +++ b/internal/impl/aws/output_kinesis.go @@ -11,9 +11,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kinesis/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/aws/output_kinesis_firehose.go b/internal/impl/aws/output_kinesis_firehose.go index 0c4fe15404..973dec7d07 100644 --- a/internal/impl/aws/output_kinesis_firehose.go +++ b/internal/impl/aws/output_kinesis_firehose.go @@ -10,9 +10,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/firehose/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/aws/output_s3.go b/internal/impl/aws/output_s3.go index 64f85bafd6..ec2f67029c 100644 --- a/internal/impl/aws/output_s3.go +++ b/internal/impl/aws/output_s3.go @@ -14,9 +14,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/bloblang" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/aws/output_sns.go b/internal/impl/aws/output_sns.go index 85a3e93fc3..2a5c91e481 100644 --- a/internal/impl/aws/output_sns.go +++ b/internal/impl/aws/output_sns.go @@ -12,9 +12,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/bloblang" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/aws/output_sqs.go b/internal/impl/aws/output_sqs.go index 46d72d7d3d..ec6263130a 100644 --- a/internal/impl/aws/output_sqs.go +++ b/internal/impl/aws/output_sqs.go @@ -16,10 +16,11 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/bloblang" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/aws/processor_dynamodb_partiql.go b/internal/impl/aws/processor_dynamodb_partiql.go index be17e597e1..dffe0c75a0 100644 --- a/internal/impl/aws/processor_dynamodb_partiql.go +++ b/internal/impl/aws/processor_dynamodb_partiql.go @@ -8,9 +8,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/bloblang" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) func init() { diff --git a/internal/impl/aws/processor_lambda.go b/internal/impl/aws/processor_lambda.go index d1e75b5560..5350bb44b7 100644 --- a/internal/impl/aws/processor_lambda.go +++ b/internal/impl/aws/processor_lambda.go @@ -10,8 +10,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lambda" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) func init() { diff --git a/internal/impl/azure/input_cosmosdb.go b/internal/impl/azure/input_cosmosdb.go index ae9a1309fb..a645764d11 100644 --- a/internal/impl/azure/input_cosmosdb.go +++ b/internal/impl/azure/input_cosmosdb.go @@ -11,8 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/mitchellh/mapstructure" - "github.com/benthosdev/benthos/v4/internal/impl/azure/cosmosdb" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/azure/cosmosdb" ) const ( diff --git a/internal/impl/azure/output_cosmosdb.go b/internal/impl/azure/output_cosmosdb.go index f5dfee2a6e..57b1cba78c 100644 --- a/internal/impl/azure/output_cosmosdb.go +++ b/internal/impl/azure/output_cosmosdb.go @@ -7,8 +7,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" - "github.com/benthosdev/benthos/v4/internal/impl/azure/cosmosdb" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/azure/cosmosdb" ) const ( diff --git a/internal/impl/azure/processor_cosmosdb.go b/internal/impl/azure/processor_cosmosdb.go index fab9c2fff5..2f83dadbc0 100644 --- a/internal/impl/azure/processor_cosmosdb.go +++ b/internal/impl/azure/processor_cosmosdb.go @@ -6,8 +6,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" - "github.com/benthosdev/benthos/v4/internal/impl/azure/cosmosdb" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/azure/cosmosdb" ) const ( diff --git a/internal/impl/confluent/serde_protobuf.go b/internal/impl/confluent/serde_protobuf.go index 04c7f00414..505fd25398 100644 --- a/internal/impl/confluent/serde_protobuf.go +++ b/internal/impl/confluent/serde_protobuf.go @@ -13,8 +13,9 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" - "github.com/benthosdev/benthos/v4/internal/impl/protobuf" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/protobuf" ) func (s *schemaRegistryDecoder) getProtobufDecoder(ctx context.Context, info SchemaInfo) (schemaDecoder, error) { diff --git a/internal/impl/couchbase/cache.go b/internal/impl/couchbase/cache.go index f6e0b79885..316821a195 100644 --- a/internal/impl/couchbase/cache.go +++ b/internal/impl/couchbase/cache.go @@ -7,8 +7,9 @@ import ( "github.com/couchbase/gocb/v2" - "github.com/benthosdev/benthos/v4/internal/impl/couchbase/client" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/couchbase/client" ) // CacheConfig export couchbase Cache specification. diff --git a/internal/impl/couchbase/client.go b/internal/impl/couchbase/client.go index e69e12cb95..5d5869e68d 100644 --- a/internal/impl/couchbase/client.go +++ b/internal/impl/couchbase/client.go @@ -7,8 +7,9 @@ import ( "github.com/couchbase/gocb/v2" - "github.com/benthosdev/benthos/v4/internal/impl/couchbase/client" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/couchbase/client" ) // ErrInvalidTranscoder specified transcoder is not supported. diff --git a/internal/impl/couchbase/processor.go b/internal/impl/couchbase/processor.go index 02d61e3d73..9cd29eb13b 100644 --- a/internal/impl/couchbase/processor.go +++ b/internal/impl/couchbase/processor.go @@ -7,9 +7,10 @@ import ( "github.com/couchbase/gocb/v2" - "github.com/benthosdev/benthos/v4/internal/impl/couchbase/client" "github.com/benthosdev/benthos/v4/public/bloblang" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/couchbase/client" ) var ( diff --git a/internal/impl/couchbase/processor_test.go b/internal/impl/couchbase/processor_test.go index 350788ddbd..59508d8d91 100644 --- a/internal/impl/couchbase/processor_test.go +++ b/internal/impl/couchbase/processor_test.go @@ -10,9 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/couchbase" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" + + "github.com/redpanda-data/connect/v4/internal/impl/couchbase" ) func TestProcessorConfigLinting(t *testing.T) { diff --git a/internal/impl/elasticsearch/aws/aws.go b/internal/impl/elasticsearch/aws/aws.go index 315e2a49aa..94143edc48 100644 --- a/internal/impl/elasticsearch/aws/aws.go +++ b/internal/impl/elasticsearch/aws/aws.go @@ -14,10 +14,11 @@ import ( "github.com/olivere/elastic/v7" - baws "github.com/benthosdev/benthos/v4/internal/impl/aws" - "github.com/benthosdev/benthos/v4/internal/impl/elasticsearch" "github.com/benthosdev/benthos/v4/public/service" + baws "github.com/redpanda-data/connect/v4/internal/impl/aws" + "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" + "github.com/aws/aws-sdk-go-v2/aws" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" ) diff --git a/internal/impl/elasticsearch/aws/integration_test.go b/internal/impl/elasticsearch/aws/integration_test.go index 93ba5fecb6..60d67afb24 100644 --- a/internal/impl/elasticsearch/aws/integration_test.go +++ b/internal/impl/elasticsearch/aws/integration_test.go @@ -11,11 +11,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/elasticsearch" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" - _ "github.com/benthosdev/benthos/v4/internal/impl/elasticsearch/aws" + "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" + + _ "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch/aws" ) var elasticIndex = `{ diff --git a/internal/impl/elasticsearch/output.go b/internal/impl/elasticsearch/output.go index 1cdc434499..90f9f79e2b 100644 --- a/internal/impl/elasticsearch/output.go +++ b/internal/impl/elasticsearch/output.go @@ -12,9 +12,10 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/olivere/elastic/v7" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/elasticsearch/writer_integration_test.go b/internal/impl/elasticsearch/writer_integration_test.go index d4acc94140..da2d298889 100644 --- a/internal/impl/elasticsearch/writer_integration_test.go +++ b/internal/impl/elasticsearch/writer_integration_test.go @@ -14,9 +14,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/elasticsearch" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" + + "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" ) func outputFromConf(t testing.TB, confStr string, args ...any) *elasticsearch.Output { diff --git a/internal/impl/io/bloblang.go b/internal/impl/io/bloblang.go deleted file mode 100644 index 468a9148bb..0000000000 --- a/internal/impl/io/bloblang.go +++ /dev/null @@ -1,186 +0,0 @@ -package io - -import ( - "os" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func init() { - if err := bloblang.RegisterFunctionV2("hostname", - bloblang.NewPluginSpec(). - Impure(). - Category(query.FunctionCategoryEnvironment). - Description(`Returns a string matching the hostname of the machine running Benthos.`). - Example("", `root.thing.host = hostname()`), - func(_ *bloblang.ParsedParams) (bloblang.Function, error) { - return func() (any, error) { - hn, err := os.Hostname() - if err != nil { - return nil, err - } - return hn, err - }, nil - }, - ); err != nil { - panic(err) - } - - if err := bloblang.RegisterFunctionV2("env", - bloblang.NewPluginSpec(). - Impure(). - StaticWithFunc(func(args *bloblang.ParsedParams) bool { - noCache, _ := args.GetBool("no_cache") - return !noCache - }). - Category(query.FunctionCategoryEnvironment). - Description("Returns the value of an environment variable, or `null` if the environment variable does not exist."). - Param(bloblang.NewStringParam("name"). - Description("The name of an environment variable.")). - Param(bloblang.NewBoolParam("no_cache"). - Description("Force the variable lookup to occur for each mapping invocation."). - Default(false)). - Example("", `root.thing.key = env("key").or("default value")`). - Example("", `root.thing.key = env(this.thing.key_name)`). - Example( - "When the name parameter is static this function will only resolve once and yield the same result for each invocation as an optimization, this means that updates to env vars during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the variable lookup to be performed for each execution of the mapping.", - `root.thing.key = env(name: "key", no_cache: true)`, - ), - func(args *bloblang.ParsedParams) (bloblang.Function, error) { - name, err := args.GetString("name") - if err != nil { - return nil, err - } - - noCache, err := args.GetBool("no_cache") - if err != nil { - return nil, err - } - - var cachedValue any - if !noCache { - if valueStr, exists := os.LookupEnv(name); exists { - cachedValue = valueStr - } - } - - return func() (any, error) { - if noCache { - if valueStr, exists := os.LookupEnv(name); exists { - return valueStr, nil - } - return nil, nil - } - return cachedValue, nil - }, nil - }, - ); err != nil { - panic(err) - } - - if err := bloblang.RegisterFunctionV2("file", - bloblang.NewPluginSpec(). - Impure(). - StaticWithFunc(func(args *bloblang.ParsedParams) bool { - noCache, _ := args.GetBool("no_cache") - return !noCache - }). - Category(query.FunctionCategoryEnvironment). - Description("Reads a file and returns its contents. Relative paths are resolved from the directory of the process executing the mapping. In order to read files relative to the mapping file use the newer <>"). - Param(bloblang.NewStringParam("path"). - Description("The path of the target file.")). - Param(bloblang.NewBoolParam("no_cache"). - Description("Force the file to be read for each mapping invocation."). - Default(false)). - Example("", `root.doc = file(env("BENTHOS_TEST_BLOBLANG_FILE")).parse_json()`, [2]string{ - `{}`, - `{"doc":{"foo":"bar"}}`, - }). - Example( - "When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimization, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping.", - `root.doc = file(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json()`, - [2]string{`{}`, `{"doc":{"foo":"bar"}}`}, - ), - func(args *bloblang.ParsedParams) (bloblang.Function, error) { - path, err := args.GetString("path") - if err != nil { - return nil, err - } - - noCache, err := args.GetBool("no_cache") - if err != nil { - return nil, err - } - - var cachedPathBytes []byte - if !noCache { - // TODO: Obtain FS from bloblang environment. - if cachedPathBytes, err = os.ReadFile(path); err != nil { - return nil, err - } - } - - return func() (any, error) { - if noCache { - return os.ReadFile(path) - } - return cachedPathBytes, nil - }, nil - }, - ); err != nil { - panic(err) - } - - if err := bloblang.RegisterFunctionV2("file_rel", - bloblang.NewPluginSpec(). - Impure(). - StaticWithFunc(func(args *bloblang.ParsedParams) bool { - noCache, _ := args.GetBool("no_cache") - return !noCache - }). - Category(query.FunctionCategoryEnvironment). - Description("Reads a file and returns its contents. Relative paths are resolved from the directory of the mapping."). - Param(bloblang.NewStringParam("path"). - Description("The path of the target file.")). - Param(bloblang.NewBoolParam("no_cache"). - Description("Force the file to be read for each mapping invocation."). - Default(false)). - Example("", `root.doc = file_rel(env("BENTHOS_TEST_BLOBLANG_FILE")).parse_json()`, [2]string{ - `{}`, - `{"doc":{"foo":"bar"}}`, - }). - Example( - "When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimization, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping.", - `root.doc = file_rel(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json()`, - [2]string{`{}`, `{"doc":{"foo":"bar"}}`}, - ), - func(args *bloblang.ParsedParams) (bloblang.Function, error) { - path, err := args.GetString("path") - if err != nil { - return nil, err - } - - noCache, err := args.GetBool("no_cache") - if err != nil { - return nil, err - } - - var cachedPathBytes []byte - if !noCache { - if cachedPathBytes, err = args.ImportFile(path); err != nil { - return nil, err - } - } - - return func() (any, error) { - if noCache { - return args.ImportFile(path) - } - return cachedPathBytes, nil - }, nil - }, - ); err != nil { - panic(err) - } -} diff --git a/internal/impl/io/bloblang_test.go b/internal/impl/io/bloblang_test.go deleted file mode 100644 index c31651f002..0000000000 --- a/internal/impl/io/bloblang_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package io_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestEnvFunctionCaching(t *testing.T) { - key := "BENTHOS_TEST_BLOBLANG_FUNCTION" - require.NoError(t, os.Setenv(key, "foobar")) - t.Cleanup(func() { - os.Unsetenv(key) - }) - - eCached, err := query.InitFunctionHelper("env", key) - require.NoError(t, err) - - eNotCached, err := query.InitFunctionHelper("env", key, true) - require.NoError(t, err) - - res, err := eCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "foobar", res) - - res, err = eNotCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "foobar", res) - - require.NoError(t, os.Setenv(key, "barbaz")) - - res, err = eCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "foobar", res) - - res, err = eNotCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "barbaz", res) -} - -func TestHostname(t *testing.T) { - hostname, _ := os.Hostname() - - e, err := query.InitFunctionHelper("hostname") - require.NoError(t, err) - - res, err := e.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, hostname, res) -} - -func TestFileFunctionCaching(t *testing.T) { - tmpDir := t.TempDir() - fooFile := filepath.Join(tmpDir, "foo.txt") - - require.NoError(t, os.WriteFile(fooFile, []byte("hello world 123"), 0o644)) - - eCached, err := query.InitFunctionHelper("file", fooFile) - require.NoError(t, err) - - eNotCached, err := query.InitFunctionHelper("file", fooFile, true) - require.NoError(t, err) - - res, err := eCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 123", value.IToString(res)) - - res, err = eNotCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 123", value.IToString(res)) - - require.NoError(t, os.WriteFile(fooFile, []byte("hello world 456"), 0x644)) - - res, err = eCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 123", value.IToString(res)) - - res, err = eNotCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 456", value.IToString(res)) -} - -func TestFileRelFunctionCaching(t *testing.T) { - tmpDir := t.TempDir() - fooFile := filepath.Join(tmpDir, "foo.txt") - - require.NoError(t, os.WriteFile(fooFile, []byte("hello world 123"), 0o644)) - - eCached, err := query.InitFunctionHelper("file_rel", fooFile) - require.NoError(t, err) - - eNotCached, err := query.InitFunctionHelper("file_rel", fooFile, true) - require.NoError(t, err) - - res, err := eCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 123", value.IToString(res)) - - res, err = eNotCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 123", value.IToString(res)) - - require.NoError(t, os.WriteFile(fooFile, []byte("hello world 456"), 0x644)) - - res, err = eCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 123", value.IToString(res)) - - res, err = eNotCached.Exec(query.FunctionContext{}) - require.NoError(t, err) - assert.Equal(t, "hello world 456", value.IToString(res)) -} diff --git a/internal/impl/io/cache_file.go b/internal/impl/io/cache_file.go deleted file mode 100644 index 498c6cae3b..0000000000 --- a/internal/impl/io/cache_file.go +++ /dev/null @@ -1,93 +0,0 @@ -package io - -import ( - "context" - "errors" - "io/fs" - "os" - "path/filepath" - "time" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/public/service" -) - -func fileCacheConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Summary(`Stores each item in a directory as a file, where an item ID is the path relative to the configured directory.`). - Description(`This type currently offers no form of item expiry or garbage collection, and is intended to be used for development and debugging purposes only.`). - Field(service.NewStringField("directory"). - Description("The directory within which to store items.")) - - return spec -} - -func init() { - err := service.RegisterCache( - "file", fileCacheConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - f, err := newFileCacheFromConfig(conf, mgr) - if err != nil { - return nil, err - } - return f, nil - }) - if err != nil { - panic(err) - } -} - -func newFileCacheFromConfig(conf *service.ParsedConfig, mgr *service.Resources) (*fileCache, error) { - directory, err := conf.FieldString("directory") - if err != nil { - return nil, err - } - return newFileCache(directory, mgr), nil -} - -//------------------------------------------------------------------------------ - -func newFileCache(dir string, mgr *service.Resources) *fileCache { - return &fileCache{mgr: mgr, dir: dir} -} - -type fileCache struct { - mgr *service.Resources - dir string -} - -func (f *fileCache) Get(_ context.Context, key string) ([]byte, error) { - b, err := ifs.ReadFile(f.mgr.FS(), filepath.Join(f.dir, key)) - if errors.Is(err, fs.ErrNotExist) { - return nil, service.ErrKeyNotFound - } - return b, err -} - -func (f *fileCache) Set(_ context.Context, key string, value []byte, _ *time.Duration) error { - return ifs.WriteFile(f.mgr.FS(), filepath.Join(f.dir, key), value, 0o644) -} - -func (f *fileCache) Add(_ context.Context, key string, value []byte, _ *time.Duration) error { - file, err := f.mgr.FS().OpenFile(filepath.Join(f.dir, key), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644) - if err != nil { - if errors.Is(err, fs.ErrExist) { - return service.ErrKeyAlreadyExists - } - return err - } - if _, err = ifs.FileWrite(file, value); err != nil { - file.Close() - return err - } - return file.Close() -} - -func (f *fileCache) Delete(_ context.Context, key string) error { - return f.mgr.FS().Remove(filepath.Join(f.dir, key)) -} - -func (f *fileCache) Close(context.Context) error { - return nil -} diff --git a/internal/impl/io/cache_file_test.go b/internal/impl/io/cache_file_test.go deleted file mode 100644 index 93685a8e6a..0000000000 --- a/internal/impl/io/cache_file_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package io - -import ( - "context" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestFileCache(t *testing.T) { - dir, err := os.MkdirTemp("", "benthos_file_cache_test") - require.NoError(t, err) - - defer os.RemoveAll(dir) - - tCtx := context.Background() - c := newFileCache(dir, service.MockResources()) - - _, err = c.Get(tCtx, "foo") - assert.Equal(t, service.ErrKeyNotFound, err) - - require.NoError(t, c.Set(tCtx, "foo", []byte("1"), nil)) - - act, err := c.Get(tCtx, "foo") - require.NoError(t, err) - assert.Equal(t, "1", string(act)) - - require.NoError(t, c.Add(tCtx, "bar", []byte("2"), nil)) - - act, err = c.Get(tCtx, "bar") - require.NoError(t, err) - assert.Equal(t, "2", string(act)) - - assert.Equal(t, service.ErrKeyAlreadyExists, c.Add(tCtx, "foo", []byte("2"), nil)) - - require.NoError(t, c.Set(tCtx, "foo", []byte("3"), nil)) - - act, err = c.Get(tCtx, "foo") - require.NoError(t, err) - assert.Equal(t, "3", string(act)) - - require.NoError(t, c.Delete(tCtx, "foo")) - - _, err = c.Get(tCtx, "foo") - assert.Equal(t, service.ErrKeyNotFound, err) -} diff --git a/internal/impl/io/input_csv.go b/internal/impl/io/input_csv.go deleted file mode 100644 index 27dbbe8fc1..0000000000 --- a/internal/impl/io/input_csv.go +++ /dev/null @@ -1,480 +0,0 @@ -package io - -import ( - "context" - "encoding/csv" - "errors" - "fmt" - "io" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/public/service" -) - -var ( - // CSV Input Fields - csviFieldPaths = "paths" - csviFieldParseHeaderRow = "parse_header_row" - csviFieldDelim = "delimiter" - csviFieldLazyQuotes = "lazy_quotes" - csviFieldBatchCount = "batch_count" - csviFieldDeleteOnFinish = "delete_on_finish" -) - -func csviFieldSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Local"). - Summary("Reads one or more CSV files as structured records following the format described in RFC 4180."). - Description(` -This input offers more control over CSV parsing than the `+"xref:components:inputs/file.adoc[`file` input]"+`. - -When parsing with a header row each line of the file will be consumed as a structured object, where the key names are determined from the header now. For example, the following CSV file: - -`+"```csv"+` -foo,bar,baz -first foo,first bar,first baz -second foo,second bar,second baz -`+"```"+` - -Would produce the following messages: - -`+"```json"+` -{"foo":"first foo","bar":"first bar","baz":"first baz"} -{"foo":"second foo","bar":"second bar","baz":"second baz"} -`+"```"+` - -If, however, the field `+"`parse_header_row` is set to `false`"+` then arrays are produced instead, like follows: - -`+"```json"+` -["first foo","first bar","first baz"] -["second foo","second bar","second baz"] -`+"```"+` - -== Metadata - -This input adds the following metadata fields to each message: - -`+"```text"+` -- header -- path -- mod_time_unix -- mod_time (RFC3339) -`+"```"+` - -You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. - -Note: The `+"`header`"+` field is only set when `+"`parse_header_row`"+` is `+"`true`"+`. - -=== Output CSV column order - -When xref:guides:bloblang/advanced.adoc#creating-csv[creating CSV] from Benthos messages, the columns must be sorted lexicographically to make the output deterministic. Alternatively, when using the `+"`csv`"+` input, one can leverage the `+"`header`"+` metadata field to retrieve the column order: - -`+"```yaml"+` -input: - csv: - paths: - - ./foo.csv - - ./bar.csv - parse_header_row: true - - processors: - - mapping: | - map escape_csv { - root = if this.re_match("[\"\n,]+") { - "\"" + this.replace_all("\"", "\"\"") + "\"" - } else { - this - } - } - - let header = if count(@path) == 1 { - @header.map_each(c -> c.apply("escape_csv")).join(",") + "\n" - } else { "" } - - root = $header + @header.map_each(c -> this.get(c).string().apply("escape_csv")).join(",") - -output: - file: - path: ./output/${! @path.filepath_split().index(-1) } -`+"```"+` -`). - Footnotes(`This input is particularly useful when consuming CSV from files too large to parse entirely within memory. However, in cases where CSV is consumed from other input types it's also possible to parse them using the `+"xref:guides:bloblang/methods.adoc#parse_csv[Bloblang `parse_csv` method]"+`.`). - Fields( - service.NewStringListField(csviFieldPaths). - Description("A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star)."). - Example([]string{ - "/tmp/foo.csv", - "/tmp/bar/*.csv", - "/tmp/data/**/*.csv", - }), - service.NewBoolField(csviFieldParseHeaderRow). - Description("Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, each message will consist of an array of values from the corresponding CSV row."). - Default(true), - service.NewStringField(csviFieldDelim). - Description(`The delimiter to use for splitting values in each record. It must be a single character.`). - Default(","), - service.NewBoolField(csviFieldLazyQuotes). - Description("If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field."). - Version("4.1.0"). - Default(false), - service.NewBoolField(csviFieldDeleteOnFinish). - Description("Whether to delete input files from the disk once they are fully consumed."). - Advanced(). - Default(false), - service.NewIntField(csviFieldBatchCount). - Description(`Optionally process records in batches. This can help to speed up the consumption of exceptionally large CSV files. When the end of the file is reached the remaining records are processed as a (potentially smaller) batch.`). - Advanced(). - Default(1), - service.NewAutoRetryNacksToggleField(), - ) -} - -type csvScannerInfo struct { - handle io.Reader - deleteFn func() error - currentPath string - modTimeUTC time.Time -} - -func init() { - err := service.RegisterBatchInput("csv", csviFieldSpec(), - func(conf *service.ParsedConfig, nm *service.Resources) (service.BatchInput, error) { - delim, err := conf.FieldString(csviFieldDelim) - if err != nil { - return nil, err - } - - delimRunes := []rune(delim) - if len(delimRunes) != 1 { - return nil, errors.New("delimiter value must be exactly one character") - } - - comma := delimRunes[0] - - csvPaths, err := conf.FieldStringList(csviFieldPaths) - if err != nil { - return nil, err - } - - pathsRemaining, err := filepath.Globs(nm.FS(), csvPaths) - if err != nil { - return nil, fmt.Errorf("failed to resolve path glob: %w", err) - } - if len(pathsRemaining) == 0 { - return nil, errors.New("requires at least one input file path") - } - - batchCount, err := conf.FieldInt(csviFieldBatchCount) - if err != nil { - return nil, err - } - if batchCount < 1 { - return nil, errors.New("batch_count must be at least 1") - } - - parseHeaderRow, err := conf.FieldBool(csviFieldParseHeaderRow) - if err != nil { - return nil, err - } - - lazyQuotes, err := conf.FieldBool(csviFieldLazyQuotes) - if err != nil { - return nil, err - } - - deleteOnFinish, err := conf.FieldBool(csviFieldDeleteOnFinish) - if err != nil { - return nil, err - } - - rdr, err := newCSVReader( - func(context.Context) (csvScannerInfo, error) { - if len(pathsRemaining) == 0 { - return csvScannerInfo{}, io.EOF - } - - path := pathsRemaining[0] - handle, err := nm.FS().Open(path) - if err != nil { - return csvScannerInfo{}, err - } - - var modTimeUTC time.Time - if fInfo, err := handle.Stat(); err == nil { - modTimeUTC = fInfo.ModTime().UTC() - } else { - nm.Logger().Errorf("Failed to read metadata from file '%v'", path) - } - - pathsRemaining = pathsRemaining[1:] - - return csvScannerInfo{ - handle: handle, - deleteFn: func() error { - return nm.FS().Remove(path) - }, - currentPath: path, - modTimeUTC: modTimeUTC, - }, nil - }, - func(context.Context) {}, - optCSVSetComma(comma), - optCSVSetExpectHeader(parseHeaderRow), - optCSVSetGroupCount(batchCount), - optCSVSetLazyQuotes(lazyQuotes), - optCSVSetDeleteOnFinish(deleteOnFinish), - ) - if err != nil { - return nil, err - } - - return service.AutoRetryNacksBatchedToggled(conf, rdr) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type csvReader struct { - handleCtor func(ctx context.Context) (csvScannerInfo, error) - onClose func(ctx context.Context) - - mut sync.Mutex - handle io.Reader - scanner *csv.Reader - scannerInfo csvScannerInfo - header []any - - expectHeader bool - comma rune - strict bool - groupCount int - lazyQuotes bool - delete bool -} - -// newCSVReader creates a new reader input type able to create a feed of line -// delimited CSV records from an io.Reader. -// -// Callers must provide a constructor function for the target io.Reader, which -// is called on start up and again each time a reader is exhausted. If the -// constructor is called but there is no more content to create a Reader for -// then the error `io.EOF` should be returned and the CSV will close. -// -// Callers must also provide an onClose function, which will be called if the -// CSV has been instructed to shut down. This function should unblock any -// blocked Read calls. -func newCSVReader( - handleCtor func(ctx context.Context) (csvScannerInfo, error), - onClose func(ctx context.Context), - options ...func(r *csvReader), -) (*csvReader, error) { - r := csvReader{ - handleCtor: handleCtor, - onClose: onClose, - comma: ',', - expectHeader: true, - strict: false, - groupCount: 1, - lazyQuotes: false, - delete: false, - } - - for _, opt := range options { - opt(&r) - } - - return &r, nil -} - -//------------------------------------------------------------------------------ - -// OptCSVSetComma is a option func that sets the comma character (default ',') -// to be used to divide record fields. -func optCSVSetComma(comma rune) func(r *csvReader) { - return func(r *csvReader) { - r.comma = comma - } -} - -// OptCSVSetGroupCount is a option func that sets the group count used to batch -// process records. -func optCSVSetGroupCount(groupCount int) func(r *csvReader) { - return func(r *csvReader) { - r.groupCount = groupCount - } -} - -// OptCSVSetExpectHeader is an option func that determines whether the first -// record from the CSV input outlines the names of columns. -func optCSVSetExpectHeader(expect bool) func(r *csvReader) { - return func(r *csvReader) { - r.expectHeader = expect - } -} - -// OptCSVSetStrict is an option func that determines whether records with -// misaligned numbers of fields should be rejected. -func optCSVSetStrict(strict bool) func(r *csvReader) { - return func(r *csvReader) { - r.strict = strict - } -} - -// optCSVSetLazyQuotes is an option func that determines whether a quote may -// appear in an unquoted field and a non-doubled quote may appear in a quoted field. -func optCSVSetLazyQuotes(lazyQuotes bool) func(r *csvReader) { - return func(r *csvReader) { - r.lazyQuotes = lazyQuotes - } -} - -// optCSVSetDeleteOnFinish is an option func that determines whether to delete -// consumed files from the disk once they are fully consumed. -func optCSVSetDeleteOnFinish(del bool) func(r *csvReader) { - return func(r *csvReader) { - r.delete = del - } -} - -//------------------------------------------------------------------------------ - -func (r *csvReader) closeHandle() (err error) { - if r.handle != nil { - if closer, ok := r.handle.(io.ReadCloser); ok { - err = closer.Close() - } - r.handle = nil - } - return -} - -func (r *csvReader) Connect(ctx context.Context) error { - r.mut.Lock() - defer r.mut.Unlock() - if r.scanner != nil { - return nil - } - - scannerInfo, err := r.handleCtor(ctx) - if err != nil { - if errors.Is(err, io.EOF) { - return service.ErrEndOfInput - } - return err - } - - scanner := csv.NewReader(scannerInfo.handle) - scanner.LazyQuotes = r.lazyQuotes - scanner.Comma = r.comma - scanner.ReuseRecord = true - - r.scanner = scanner - r.scannerInfo = scannerInfo - - return nil -} - -func (r *csvReader) readNext(reader *csv.Reader) ([]string, error) { - record, err := reader.Read() - if err != nil && (r.strict || len(record) == 0) { - if errors.Is(err, io.EOF) { - var deleteFn func() error - r.mut.Lock() - r.scanner = nil - r.header = nil - deleteFn = r.scannerInfo.deleteFn - r.mut.Unlock() - - if r.delete { - if err := deleteFn(); err != nil { - return nil, err - } - } - return nil, service.ErrNotConnected - } - return nil, err - } - return record, nil -} - -func (r *csvReader) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - r.mut.Lock() - scanner := r.scanner - scannerInfo := r.scannerInfo - header := r.header - r.mut.Unlock() - - if scanner == nil { - return nil, nil, service.ErrNotConnected - } - - msg := service.MessageBatch{} - for i := 0; i < r.groupCount; i++ { - record, err := r.readNext(scanner) - if err != nil { - if i == 0 { - return nil, nil, err - } - break - } - - if r.expectHeader && header == nil { - header = make([]any, 0, len(record)) - for _, rec := range record { - header = append(header, rec) - } - - r.mut.Lock() - r.header = header - r.mut.Unlock() - - if record, err = r.readNext(scanner); err != nil { - return nil, nil, err - } - } - - part := service.NewMessage(nil) - - var structured any - if len(header) == 0 || len(header) < len(record) { - slice := make([]any, 0, len(record)) - for _, r := range record { - slice = append(slice, r) - } - structured = slice - } else { - obj := make(map[string]any, len(record)) - for i, r := range record { - // The `header` slice contains only strings, but we define it as `[]any` so it resolves to a bloblang - // array when we extract it from the metadata. - obj[header[i].(string)] = r - } - structured = obj - - part.MetaSetMut("header", header) - } - - part.MetaSetMut("path", scannerInfo.currentPath) - part.MetaSetMut("mod_time_unix", scannerInfo.modTimeUTC.Unix()) - part.MetaSetMut("mod_time", scannerInfo.modTimeUTC.Format(time.RFC3339)) - - part.SetStructuredMut(structured) - msg = append(msg, part) - } - - return msg, func(context.Context, error) error { return nil }, nil -} - -func (r *csvReader) Close(ctx context.Context) error { - r.mut.Lock() - defer r.mut.Unlock() - - r.onClose(ctx) - return r.closeHandle() -} diff --git a/internal/impl/io/input_csv_integration_test.go b/internal/impl/io/input_csv_integration_test.go deleted file mode 100644 index 61e6c0f302..0000000000 --- a/internal/impl/io/input_csv_integration_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "io/fs" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - - _ "github.com/benthosdev/benthos/v4/internal/impl/io" -) - -func csvInput(t testing.TB, confPattern string, args ...any) input.Streamed { - iConf, err := testutil.InputFromYAML(fmt.Sprintf(confPattern, args...)) - require.NoError(t, err) - - i, err := mock.NewManager().NewInput(iConf) - require.NoError(t, err) - - return i -} - -func TestCSVInputGPaths(t *testing.T) { - dir := t.TempDir() - - dummyFileA := filepath.Join(dir, "a.csv") - dummyFileB := filepath.Join(dir, "b.csv") - require.NoError(t, os.WriteFile(dummyFileA, []byte(`header1,header2,header3 -foo1,bar1,baz1 -foo2,bar2,baz2 -foo3,bar3,baz3 -`), 0o777)) - require.NoError(t, os.WriteFile(dummyFileB, []byte(`header4,header5,header6 -foo4,bar4,baz4 -foo5,bar5,baz5 -foo6,bar6,baz6 -`), 0o777)) - - f := csvInput(t, ` -csv: - paths: [ "%v", "%v" ] - delete_on_finish: false -`, dummyFileA, dummyFileB) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second) - require.NoError(t, f.WaitForClose(ctx)) - done() - }) - - for _, exp := range []string{ - `{"header1":"foo1","header2":"bar1","header3":"baz1"}`, - `{"header1":"foo2","header2":"bar2","header3":"baz2"}`, - `{"header1":"foo3","header2":"bar3","header3":"baz3"}`, - `{"header4":"foo4","header5":"bar4","header6":"baz4"}`, - `{"header4":"foo5","header5":"bar5","header6":"baz5"}`, - `{"header4":"foo6","header5":"bar6","header6":"baz6"}`, - } { - m := readMsg(t, f.TransactionChan()) - assert.Equal(t, exp, string(m.Get(0).AsBytes())) - } - - _, err := os.Stat(dummyFileA) - require.NoError(t, err) - - _, err = os.Stat(dummyFileB) - require.NoError(t, err) -} - -func TestCSVInputDeleteOnFinish(t *testing.T) { - dummyCSVFile := filepath.Join(t.TempDir(), "dummy.csv") - require.NoError(t, os.WriteFile(dummyCSVFile, []byte(`header1,header2,header3 -foo1,bar1,baz1 -`), 0o777)) - - f := csvInput(t, ` -csv: - paths: [ "%v" ] - delete_on_finish: true -`, dummyCSVFile) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second) - require.NoError(t, f.WaitForClose(ctx)) - done() - }) - - for _, exp := range []string{ - `{"header1":"foo1","header2":"bar1","header3":"baz1"}`, - } { - m := readMsg(t, f.TransactionChan()) - assert.Equal(t, exp, string(m.Get(0).AsBytes())) - } - - // Make sure the input shut down after reading the file - select { - case _, ok := <-f.TransactionChan(): - require.False(t, ok) - case <-time.After(time.Second * 2): - require.FailNow(t, "failed to read after input is closed") - } - - _, err := os.Stat(dummyCSVFile) - require.ErrorIs(t, err, fs.ErrNotExist) -} - -func TestCSVInputGlobPaths(t *testing.T) { - dir := t.TempDir() - - require.NoError(t, os.WriteFile(filepath.Join(dir, "a.csv"), []byte(`header1,header2,header3 -foo1,bar1,baz1 -foo2,bar2,baz2 -foo3,bar3,baz3 -`), 0o777)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "b.csv"), []byte(`header4,header5,header6 -foo4,bar4,baz4 -foo5,bar5,baz5 -foo6,bar6,baz6 -`), 0o777)) - - f := csvInput(t, ` -csv: - paths: [ "%v/*.csv" ] -`, dir) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second) - require.NoError(t, f.WaitForClose(ctx)) - done() - }) - - for _, exp := range []string{ - `{"header1":"foo1","header2":"bar1","header3":"baz1"}`, - `{"header1":"foo2","header2":"bar2","header3":"baz2"}`, - `{"header1":"foo3","header2":"bar3","header3":"baz3"}`, - `{"header4":"foo4","header5":"bar4","header6":"baz4"}`, - `{"header4":"foo5","header5":"bar5","header6":"baz5"}`, - `{"header4":"foo6","header5":"bar6","header6":"baz6"}`, - } { - m := readMsg(t, f.TransactionChan()) - assert.Equal(t, exp, string(m.Get(0).AsBytes())) - } -} diff --git a/internal/impl/io/input_csv_test.go b/internal/impl/io/input_csv_test.go deleted file mode 100644 index db67b43f6b..0000000000 --- a/internal/impl/io/input_csv_test.go +++ /dev/null @@ -1,498 +0,0 @@ -package io - -import ( - "bytes" - "context" - "errors" - "io" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestCSVReaderHappy(t *testing.T) { - var handle bytes.Buffer - - for _, msg := range []string{ - "header1,header2,header3", - "foo1,foo2,foo3", - "bar1,bar2,bar3", - "baz1,baz2,baz3", - } { - handle.WriteString(msg) - handle.WriteString("\n") - } - - dummyFile := "foo/bar.csv" - dummyTimeUTC := time.Now().UTC() - ctored := false - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - if ctored { - return csvScannerInfo{}, io.EOF - } - ctored = true - return csvScannerInfo{ - handle: &handle, - currentPath: dummyFile, - modTimeUTC: dummyTimeUTC, - }, nil - }, - func(ctx context.Context) {}, - ) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background())) - - for _, exp := range []string{ - `{"header1":"foo1","header2":"foo2","header3":"foo3"}`, - `{"header1":"bar1","header2":"bar2","header3":"bar3"}`, - `{"header1":"baz1","header2":"baz2","header3":"baz3"}`, - } { - var resMsg service.MessageBatch - resMsg, _, err = f.ReadBatch(context.Background()) - require.NoError(t, err) - - msgBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(msgBytes)) - - m, _ := resMsg[0].MetaGet("path") - assert.Equal(t, dummyFile, m) - m, _ = resMsg[0].MetaGet("mod_time") - assert.Equal(t, dummyTimeUTC.Format(time.RFC3339), m) - m, _ = resMsg[0].MetaGet("mod_time_unix") - assert.Equal(t, strconv.Itoa(int(dummyTimeUTC.Unix())), m) - } - - _, _, err = f.ReadBatch(context.Background()) - assert.Equal(t, service.ErrNotConnected, err) - - err = f.Connect(context.Background()) - assert.Equal(t, service.ErrEndOfInput, err) -} - -func TestCSVReaderGroupCount(t *testing.T) { - var handle bytes.Buffer - - for _, msg := range []string{ - "foo,bar,baz", - "foo1,bar1,baz1", - "foo2,bar2,baz2", - "foo3,bar3,baz3", - "foo4,bar4,baz4", - "foo5,bar5,baz5", - "foo6,bar6,baz6", - "foo7,bar7,baz7", - } { - handle.WriteString(msg) - handle.WriteString("\n") - } - - ctored := false - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - if ctored { - return csvScannerInfo{}, io.EOF - } - ctored = true - return csvScannerInfo{handle: &handle}, nil - }, - func(ctx context.Context) {}, - optCSVSetGroupCount(3), - ) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background())) - - for _, exp := range [][]string{ - { - `{"bar":"bar1","baz":"baz1","foo":"foo1"}`, - `{"bar":"bar2","baz":"baz2","foo":"foo2"}`, - `{"bar":"bar3","baz":"baz3","foo":"foo3"}`, - }, - { - `{"bar":"bar4","baz":"baz4","foo":"foo4"}`, - `{"bar":"bar5","baz":"baz5","foo":"foo5"}`, - `{"bar":"bar6","baz":"baz6","foo":"foo6"}`, - }, - { - `{"bar":"bar7","baz":"baz7","foo":"foo7"}`, - }, - } { - var resMsg service.MessageBatch - resMsg, _, err = f.ReadBatch(context.Background()) - require.NoError(t, err) - - require.Equal(t, len(exp), len(resMsg)) - for i := 0; i < len(exp); i++ { - mBytes, err := resMsg[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp[i], string(mBytes)) - } - } - - _, _, err = f.ReadBatch(context.Background()) - assert.Equal(t, service.ErrNotConnected, err) - - err = f.Connect(context.Background()) - assert.Equal(t, service.ErrEndOfInput, err) -} - -func TestCSVReadersTwoFiles(t *testing.T) { - var handleOne, handleTwo bytes.Buffer - - for _, msg := range []string{ - "header1,header2,header3", - "foo1,foo2,foo3", - "bar1,bar2,bar3", - "baz1,baz2,baz3", - } { - handleOne.WriteString(msg) - handleOne.WriteString("\n") - } - - for _, msg := range []string{ - "header4,header5,header6", - "foo1,foo2,foo3", - "bar1,bar2,bar3", - "baz1,baz2,baz3", - } { - handleTwo.WriteString(msg) - handleTwo.WriteString("\n") - } - - consumedFirst, consumedSecond := false, false - - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - if !consumedFirst { - consumedFirst = true - return csvScannerInfo{handle: &handleOne}, nil - } else if !consumedSecond { - consumedSecond = true - return csvScannerInfo{handle: &handleTwo}, nil - } - return csvScannerInfo{}, io.EOF - }, - func(ctx context.Context) {}, - ) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background())) - - for i, exp := range []string{ - `{"header1":"foo1","header2":"foo2","header3":"foo3"}`, - `{"header1":"bar1","header2":"bar2","header3":"bar3"}`, - `{"header1":"baz1","header2":"baz2","header3":"baz3"}`, - `{"header4":"foo1","header5":"foo2","header6":"foo3"}`, - `{"header4":"bar1","header5":"bar2","header6":"bar3"}`, - `{"header4":"baz1","header5":"baz2","header6":"baz3"}`, - } { - var resMsg service.MessageBatch - var ackFn service.AckFunc - resMsg, ackFn, err = f.ReadBatch(context.Background()) - if err == service.ErrNotConnected { - require.NoError(t, f.Connect(context.Background())) - resMsg, ackFn, err = f.ReadBatch(context.Background()) - } - require.NoError(t, err, i) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(mBytes), i) - _ = ackFn(context.Background(), nil) - } - - _, _, err = f.ReadBatch(context.Background()) - assert.Equal(t, service.ErrNotConnected, err) - - err = f.Connect(context.Background()) - assert.Equal(t, service.ErrEndOfInput, err) -} - -func TestCSVReaderCustomComma(t *testing.T) { - var handle bytes.Buffer - - for _, msg := range []string{ - "header1|header2|header3", - "foo1|foo2|foo3", - "bar1|bar2|bar3", - "baz1|baz2|baz3", - } { - handle.WriteString(msg) - handle.WriteString("\n") - } - - ctored := false - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - if ctored { - return csvScannerInfo{}, io.EOF - } - ctored = true - return csvScannerInfo{handle: &handle}, nil - }, - func(ctx context.Context) {}, - optCSVSetComma('|'), - ) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background())) - - for _, exp := range []string{ - `{"header1":"foo1","header2":"foo2","header3":"foo3"}`, - `{"header1":"bar1","header2":"bar2","header3":"bar3"}`, - `{"header1":"baz1","header2":"baz2","header3":"baz3"}`, - } { - var resMsg service.MessageBatch - resMsg, _, err = f.ReadBatch(context.Background()) - require.NoError(t, err) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, exp, string(mBytes)) - } - - _, _, err = f.ReadBatch(context.Background()) - assert.Equal(t, service.ErrNotConnected, err) - - err = f.Connect(context.Background()) - assert.Equal(t, service.ErrEndOfInput, err) -} - -func TestCSVReaderRelaxed(t *testing.T) { - var handle bytes.Buffer - - for _, msg := range []string{ - "header1,header2,header3", - "foo1,foo2,foo3", - "bar1,bar2,bar3,bar4", - "baz1,baz2,baz3", - "buz1,buz2", - } { - handle.WriteString(msg) - handle.WriteString("\n") - } - - ctored := false - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - if ctored { - return csvScannerInfo{}, io.EOF - } - ctored = true - return csvScannerInfo{handle: &handle}, nil - }, - func(ctx context.Context) {}, - optCSVSetStrict(false), - ) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background())) - - for _, exp := range []string{ - `{"header1":"foo1","header2":"foo2","header3":"foo3"}`, - `["bar1","bar2","bar3","bar4"]`, - `{"header1":"baz1","header2":"baz2","header3":"baz3"}`, - `{"header1":"buz1","header2":"buz2"}`, - } { - var resMsg service.MessageBatch - resMsg, _, err = f.ReadBatch(context.Background()) - require.NoError(t, err) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, exp, string(mBytes)) - } - - _, _, err = f.ReadBatch(context.Background()) - assert.Equal(t, service.ErrNotConnected, err) - - err = f.Connect(context.Background()) - assert.Equal(t, service.ErrEndOfInput, err) -} - -func TestCSVReaderStrict(t *testing.T) { - var handle bytes.Buffer - - for _, msg := range []string{ - "header1,header2,header3", - "foo1,foo2,foo3", - "bar1,bar2,bar3,bar4", - "baz1,baz2,baz3", - "buz1,buz2", - } { - handle.WriteString(msg) - handle.WriteString("\n") - } - - ctored := false - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - if ctored { - return csvScannerInfo{}, io.EOF - } - ctored = true - return csvScannerInfo{handle: &handle}, nil - }, - func(ctx context.Context) {}, - optCSVSetStrict(true), - ) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background())) - - for _, exp := range []any{ - `{"header1":"foo1","header2":"foo2","header3":"foo3"}`, - errors.New("record on line 3: wrong number of fields"), - `{"header1":"baz1","header2":"baz2","header3":"baz3"}`, - errors.New("record on line 5: wrong number of fields"), - } { - var resMsg service.MessageBatch - resMsg, _, err = f.ReadBatch(context.Background()) - - switch expT := exp.(type) { - case string: - require.NoError(t, err) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, expT, string(mBytes)) - - case error: - assert.EqualError(t, err, expT.Error()) - } - } - - _, _, err = f.ReadBatch(context.Background()) - assert.Equal(t, service.ErrNotConnected, err) - - err = f.Connect(context.Background()) - assert.Equal(t, service.ErrEndOfInput, err) -} - -func TestCSVReaderLazyQuotes(t *testing.T) { - tests := []struct { - name string - lazyQuotes bool - input string - expected string - errContains string - }{ - { - name: "quotes in unquoted field w/ LazyQuotes = true", - input: `f"oo"1,f"oo"2,f"oo"3`, - expected: `["f\"oo\"1","f\"oo\"2","f\"oo\"3"]`, - lazyQuotes: true, - }, - { - name: "quotes in unquoted field w/ LazyQuotes = false", - input: `f"oo"1,f"oo"2,f"oo"3`, - errContains: `bare " in non-quoted-field`, - lazyQuotes: false, - }, - { - name: "non-doubled quote in quoted field w/ LazyQuotes = true", - input: `"f"oo1","f"oo2","f"oo3"`, - expected: `["f\"oo1","f\"oo2","f\"oo3"]`, - lazyQuotes: true, - }, - { - name: "non-doubled quote in quoted field w/ LazyQuotes = false", - input: `f"oo1,"f'oo'2","f'oo'3"`, - errContains: `bare " in non-quoted-field`, - lazyQuotes: false, - }, - { - name: "quotes in unquoted field AND non-doubled quote in quoted field w/ LazyQuotes = true", - input: `f"oo"1,"f"oo2",f"oo"3`, - expected: `[\"f"oo"1\","f"oo2",\"f"oo"3\"]`, - lazyQuotes: true, - }, - { - name: "quotes in unquoted field AND non-doubled quote in quoted field w/ LazyQuotes = false", - input: `f"oo"1,"f"oo2",f"oo"3`, - errContains: `bare " in non-quoted-field`, - lazyQuotes: false, - }, - } - for _, test := range tests { - var handle bytes.Buffer - - handle.WriteString(test.input) - - f, err := newCSVReader( - func(ctx context.Context) (csvScannerInfo, error) { - return csvScannerInfo{handle: &handle}, nil - }, - func(ctx context.Context) {}, - optCSVSetExpectHeader(false), - optCSVSetLazyQuotes(test.lazyQuotes), - ) - require.NoError(t, err, test.name) - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - require.NoError(t, f.Close(ctx)) - done() - }) - - require.NoError(t, f.Connect(context.Background()), test.name) - - resMsg, _, err := f.ReadBatch(context.Background()) - if test.errContains != "" { - require.Contains(t, err.Error(), test.errContains, test.name) - return - } - require.NoError(t, err, test.name) - - mBytes, err := resMsg[0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, test.expected, string(mBytes), test.name) - } -} diff --git a/internal/impl/io/input_dynamic.go b/internal/impl/io/input_dynamic.go deleted file mode 100644 index 51c197d490..0000000000 --- a/internal/impl/io/input_dynamic.go +++ /dev/null @@ -1,198 +0,0 @@ -package io - -import ( - "context" - "path" - "sync" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - diFieldInputs = "inputs" - diFieldPrefix = "prefix" -) - -func dynInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`A special broker type where the inputs are identified by unique labels and can be created, changed and removed during runtime via a REST HTTP interface.`). - Footnotes(` -== Endpoints - -=== GET `+"`/inputs`"+` - -Returns a JSON object detailing all dynamic inputs, providing information such as their current uptime and configuration. - -=== GET `+"`/inputs/\\{id}`"+` - -Returns the configuration of an input. - -=== POST `+"`/inputs/\\{id}`"+` - -Creates or updates an input with a configuration provided in the request body (in YAML or JSON format). - -=== DELETE `+"`/inputs/\\{id}`"+` - -Stops and removes an input. - -=== GET `+"`/inputs/\\{id}/uptime`"+` - -Returns the uptime of an input as a duration string (of the form "72h3m0.5s"), or "stopped" in the case where the input has gracefully terminated.`). - Fields( - service.NewInputMapField(diFieldInputs). - Description("A map of inputs to statically create."). - Default(map[string]any{}), - service.NewStringField(diFieldPrefix). - Description("A path prefix for HTTP endpoints that are registered."). - Default(""), - ) -} - -func init() { - err := service.RegisterBatchInput("dynamic", dynInputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - i, err := newDynamicInputFromParsed(conf, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalInput(i), nil - }) - if err != nil { - panic(err) - } -} - -func dynInputAnyToYAMLConf(v any) []byte { - var node yaml.Node - if err := node.Encode(v); err != nil { - return nil - } - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - if err := docs.FieldInput("input", "").SanitiseYAML(&node, sanitConf); err != nil { - return nil - } - - confBytes, _ := yaml.Marshal(node) - return confBytes -} - -func newDynamicInputFromParsed(conf *service.ParsedConfig, res *service.Resources) (input.Streamed, error) { - inputsMap, err := conf.FieldInputMap(diFieldInputs) - if err != nil { - return nil, err - } - - inputsAnyMap, err := conf.FieldAnyMap(diFieldInputs) - if err != nil { - return nil, err - } - - prefix, err := conf.FieldString(diFieldPrefix) - if err != nil { - return nil, err - } - - inputs := map[string]input.Streamed{} - for k, v := range inputsMap { - inputs[k] = interop.UnwrapOwnedInput(v) - } - - var inputConfigsMut sync.Mutex - inputYAMLConfs := map[string][]byte{} - for k, v := range inputsAnyMap { - a, _ := v.FieldAny() - inputYAMLConfs[k] = dynInputAnyToYAMLConf(a) - } - - dynAPI := api.NewDynamic() - mgr := interop.UnwrapManagement(res) - fanIn, err := newDynamicFanInInput( - inputs, mgr.Logger(), - func(ctx context.Context, l string) { - inputConfigsMut.Lock() - defer inputConfigsMut.Unlock() - - confBytes, exists := inputYAMLConfs[l] - if !exists { - return - } - - dynAPI.Started(l, confBytes) - delete(inputYAMLConfs, l) - }, - func(ctx context.Context, l string) { - dynAPI.Stopped(l) - }, - ) - if err != nil { - return nil, err - } - - dynAPI.OnUpdate(func(ctx context.Context, id string, c []byte) error { - confNode, err := docs.UnmarshalYAML(c) - if err != nil { - return err - } - - newConf, err := input.FromAny(bundle.GlobalEnvironment, confNode) - if err != nil { - return err - } - - iMgr := mgr.IntoPath("dynamic", "inputs", id) - newInput, err := iMgr.NewInput(newConf) - if err != nil { - return err - } - - inputConfigsMut.Lock() - inputYAMLConfs[id] = dynInputAnyToYAMLConf(newConf) - inputConfigsMut.Unlock() - if err = fanIn.SetInput(ctx, id, newInput); err != nil { - mgr.Logger().Error("Failed to set input '%v': %v", id, err) - inputConfigsMut.Lock() - delete(inputYAMLConfs, id) - inputConfigsMut.Unlock() - } - return err - }) - - dynAPI.OnDelete(func(ctx context.Context, id string) error { - err := fanIn.SetInput(ctx, id, nil) - if err != nil { - mgr.Logger().Error("Failed to close input '%v': %v", id, err) - } - return err - }) - - mgr.RegisterEndpoint( - path.Join(prefix, "/inputs/{id}/uptime"), - `Returns the uptime of a specific input as a duration string, or "stopped" for inputs that are no longer running and have gracefully terminated.`, - dynAPI.HandleUptime, - ) - mgr.RegisterEndpoint( - path.Join(prefix, "/inputs/{id}"), - "Perform CRUD operations on the configuration of dynamic inputs. For"+ - " more information read the `dynamic` input type documentation.", - dynAPI.HandleCRUD, - ) - mgr.RegisterEndpoint( - path.Join(prefix, "/inputs"), - "Get a map of running input identifiers with their current uptimes.", - dynAPI.HandleList, - ) - - return fanIn, nil -} diff --git a/internal/impl/io/input_dynamic_fan_in.go b/internal/impl/io/input_dynamic_fan_in.go deleted file mode 100644 index ebb4aaf96d..0000000000 --- a/internal/impl/io/input_dynamic_fan_in.go +++ /dev/null @@ -1,219 +0,0 @@ -package io - -import ( - "context" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// wrappedInput is a struct that wraps a input.Streamed with an identifying name. -type wrappedInput struct { - ctx context.Context - Name string - Input input.Streamed - ResChan chan<- error -} - -type dynamicFanInInput struct { - log log.Modular - - transactionChan chan message.Transaction - - onAdd func(ctx context.Context, label string) - onRemove func(ctx context.Context, label string) - - newInputChan chan wrappedInput - inputs map[string]input.Streamed - inputClosedChans map[string]chan struct{} - - shutSig *shutdown.Signaller -} - -func newDynamicFanInInput( - inputs map[string]input.Streamed, - logger log.Modular, - onAdd func(ctx context.Context, l string), - onRemove func(ctx context.Context, l string), -) (*dynamicFanInInput, error) { - d := &dynamicFanInInput{ - log: logger, - - transactionChan: make(chan message.Transaction), - - onAdd: func(ctx context.Context, l string) {}, - onRemove: func(ctx context.Context, l string) {}, - - newInputChan: make(chan wrappedInput), - inputs: make(map[string]input.Streamed), - inputClosedChans: make(map[string]chan struct{}), - - shutSig: shutdown.NewSignaller(), - } - if onAdd != nil { - d.onAdd = onAdd - } - if onRemove != nil { - d.onRemove = onRemove - } - for key, input := range inputs { - if err := d.addInput(key, input); err != nil { - d.log.Error("Failed to start new dynamic input '%v': %v\n", key, err) - } - } - go d.managerLoop() - return d, nil -} - -// SetInput attempts to add a new input to the dynamic input broker. If an input -// already exists with the same identifier it will be closed and removed. If -// either action takes longer than the timeout period an error will be returned. -// -// A nil input is safe and will simply remove the previous input under the -// indentifier, if there was one. -func (d *dynamicFanInInput) SetInput(ctx context.Context, ident string, input input.Streamed) error { - if d.shutSig.IsSoftStopSignalled() { - return component.ErrTypeClosed - } - resChan := make(chan error) - select { - case d.newInputChan <- wrappedInput{ - ctx: ctx, - Name: ident, - Input: input, - ResChan: resChan, - }: - case <-d.shutSig.SoftStopChan(): - return component.ErrTypeClosed - } - return <-resChan -} - -func (d *dynamicFanInInput) TransactionChan() <-chan message.Transaction { - return d.transactionChan -} - -func (d *dynamicFanInInput) Connected() bool { - // Always return true as this is fuzzy right now. - return true -} - -func (d *dynamicFanInInput) addInput(ident string, in input.Streamed) error { - closedChan := make(chan struct{}) - // Launch goroutine that async writes input into single channel - go func(in input.Streamed, cChan chan struct{}) { - defer func() { - d.onRemove(context.Background(), ident) - close(cChan) - }() - d.onAdd(context.Background(), ident) - for { - in, open := <-in.TransactionChan() - if !open { - // Race condition: This will be called when shutting down. - return - } - d.transactionChan <- in - } - }(in, closedChan) - - // Add new input to our map - d.inputs[ident] = in - d.inputClosedChans[ident] = closedChan - - return nil -} - -func (d *dynamicFanInInput) removeInput(ctx context.Context, ident string) error { - input, exists := d.inputs[ident] - if !exists { - // Nothing to do - return nil - } - - input.TriggerStopConsuming() - select { - case <-d.inputClosedChans[ident]: - case <-ctx.Done(): - // Do NOT remove inputs from our map unless we are sure they are - // closed. - return ctx.Err() - } - - delete(d.inputs, ident) - delete(d.inputClosedChans, ident) - - return nil -} - -// managerLoop is an internal loop that monitors new and dead input types. -func (d *dynamicFanInInput) managerLoop() { - defer func() { - for _, i := range d.inputs { - i.TriggerStopConsuming() - } - - closeNowCtx, done := d.shutSig.HardStopCtx(context.Background()) - for key := range d.inputs { - _ = d.removeInput(closeNowCtx, key) - } - - for _, i := range d.inputs { - i.TriggerCloseNow() - } - - done() - close(d.transactionChan) - d.shutSig.TriggerHasStopped() - }() - - for { - select { - case wrappedInput, open := <-d.newInputChan: - if !open { - return - } - var err error - if _, exists := d.inputs[wrappedInput.Name]; exists { - if err = d.removeInput(wrappedInput.ctx, wrappedInput.Name); err != nil { - d.log.Error("Failed to stop old copy of dynamic input '%v': %v\n", wrappedInput.Name, err) - } - } - if err == nil && wrappedInput.Input != nil { - // If the input is nil then we only wanted to remove the input. - if err = d.addInput(wrappedInput.Name, wrappedInput.Input); err != nil { - d.log.Error("Failed to start new dynamic input '%v': %v\n", wrappedInput.Name, err) - } - } - select { - case wrappedInput.ResChan <- err: - case <-d.shutSig.SoftStopChan(): - close(wrappedInput.ResChan) - return - } - case <-d.shutSig.SoftStopChan(): - return - } - } -} - -func (d *dynamicFanInInput) TriggerStopConsuming() { - d.shutSig.TriggerSoftStop() -} - -func (d *dynamicFanInInput) TriggerCloseNow() { - d.shutSig.TriggerHardStop() -} - -func (d *dynamicFanInInput) WaitForClose(ctx context.Context) error { - select { - case <-d.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/io/input_dynamic_fan_in_test.go b/internal/impl/io/input_dynamic_fan_in_test.go deleted file mode 100644 index da60d0bd9e..0000000000 --- a/internal/impl/io/input_dynamic_fan_in_test.go +++ /dev/null @@ -1,320 +0,0 @@ -package io - -import ( - "bytes" - "context" - "errors" - "fmt" - "reflect" - "sort" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ input.Streamed = &dynamicFanInInput{} - -func TestStaticBasicDynamicFanIn(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nInputs, nMsgs := 10, 1000 - - Inputs := map[string]input.Streamed{} - mockInputs := []*mock.Input{} - resChan := make(chan error) - - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - Inputs[fmt.Sprintf("testinput%v", i)] = mockInputs[i] - } - - fanIn, err := newDynamicFanInInput(Inputs, log.Noop(), nil, nil) - if err != nil { - t.Error(err) - return - } - - for i := 0; i < nMsgs; i++ { - for j := 0; j < nInputs; j++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case mockInputs[j].TChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send: %v, %v", i, j) - return - } - go func() { - var ts message.Transaction - select { - case ts = <-fanIn.TransactionChan(): - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate: %v, %v", i, j) - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - select { - case <-resChan: - case <-time.After(time.Second): - t.Errorf("Timed out waiting for response to input: %v, %v", i, j) - return - } - } - } - - fanIn.TriggerStopConsuming() - require.NoError(t, fanIn.WaitForClose(tCtx)) -} - -func TestBasicDynamicFanIn(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nMsgs := 1000 - - inputOne := &mock.Input{ - TChan: make(chan message.Transaction), - } - inputTwo := &mock.Input{ - TChan: make(chan message.Transaction), - } - - fanIn, err := newDynamicFanInInput(nil, log.Noop(), nil, nil) - if err != nil { - t.Error(err) - return - } - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - if err = fanIn.SetInput(ctx, "foo", inputOne); err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - sendAllTestMessages := func(input *mock.Input, label string) { - rChan := make(chan error) - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("%v-%v", label, i))} - input.TChan <- message.NewTransaction(message.QuickBatch(content), rChan) - select { - case <-rChan: - case <-time.After(time.Second): - t.Errorf("Timed out waiting for response to input: %v", i) - return - } - } - wg.Done() - } - - wg.Add(2) - go sendAllTestMessages(inputOne, "inputOne") - go sendAllTestMessages(inputTwo, "inputTwo") - - for i := 0; i < nMsgs; i++ { - var ts message.Transaction - expContent := fmt.Sprintf("inputOne-%v", i) - select { - case ts = <-fanIn.TransactionChan(): - if string(ts.Payload.Get(0).AsBytes()) != expContent { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), expContent) - } - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate: %v", i) - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - if err = fanIn.SetInput(ctx, "foo", inputTwo); err != nil { - t.Fatal(err) - } - - for i := 0; i < nMsgs; i++ { - var ts message.Transaction - expContent := fmt.Sprintf("inputTwo-%v", i) - select { - case ts = <-fanIn.TransactionChan(): - if string(ts.Payload.Get(0).AsBytes()) != expContent { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), expContent) - } - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate: %v", i) - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - wg.Wait() - - fanIn.TriggerStopConsuming() - require.NoError(t, fanIn.WaitForClose(tCtx)) -} - -func TestStaticDynamicFanInShutdown(t *testing.T) { - nInputs := 10 - - Inputs := map[string]input.Streamed{} - mockInputs := []*mock.Input{} - - expInputAddedList := []string{} - expInputRemovedList := []string{} - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - label := fmt.Sprintf("testinput%v", i) - Inputs[label] = mockInputs[i] - expInputAddedList = append(expInputAddedList, label) - expInputRemovedList = append(expInputRemovedList, label) - } - - var mapMut sync.Mutex - inputAddedList := []string{} - inputRemovedList := []string{} - - fanIn, err := newDynamicFanInInput( - Inputs, log.Noop(), - func(ctx context.Context, label string) { - mapMut.Lock() - inputAddedList = append(inputAddedList, label) - mapMut.Unlock() - }, - func(ctx context.Context, label string) { - mapMut.Lock() - inputRemovedList = append(inputRemovedList, label) - mapMut.Unlock() - }, - ) - if err != nil { - t.Error(err) - return - } - - for _, mockIn := range mockInputs { - select { - case _, open := <-mockIn.TransactionChan(): - if !open { - t.Error("fan in closed early") - } else { - t.Error("fan in sent unexpected message") - } - default: - } - } - - fanIn.TriggerStopConsuming() - - // All inputs should be closed. - for _, mockIn := range mockInputs { - select { - case _, open := <-mockIn.TransactionChan(): - if open { - t.Error("fan in sent unexpected message") - } - case <-time.After(time.Second): - t.Error("fan in failed to close an input") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second) - require.NoError(t, fanIn.WaitForClose(ctx)) - done() - - mapMut.Lock() - - sort.Strings(expInputAddedList) - sort.Strings(inputAddedList) - sort.Strings(expInputRemovedList) - sort.Strings(inputRemovedList) - - if exp, act := expInputAddedList, inputAddedList; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong list of added inputs: %v != %v", act, exp) - } - if exp, act := expInputRemovedList, inputRemovedList; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong list of removed inputs: %v != %v", act, exp) - } - - mapMut.Unlock() -} - -func TestStaticDynamicFanInAsync(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nInputs, nMsgs := 10, 1000 - - Inputs := map[string]input.Streamed{} - mockInputs := []*mock.Input{} - - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - Inputs[fmt.Sprintf("testinput%v", i)] = mockInputs[i] - } - - fanIn, err := newDynamicFanInInput(Inputs, log.Noop(), nil, nil) - if err != nil { - t.Error(err) - return - } - defer fanIn.TriggerStopConsuming() - - wg := sync.WaitGroup{} - wg.Add(nInputs) - - for j := 0; j < nInputs; j++ { - go func(index int) { - rChan := make(chan error) - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v %v", i, index))} - select { - case mockInputs[index].TChan <- message.NewTransaction(message.QuickBatch(content), rChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send: %v, %v", i, index) - return - } - select { - case res := <-rChan: - if expected, actual := string(content[0]), res.Error(); expected != actual { - t.Errorf("Wrong response: %v != %v", expected, actual) - } - case <-time.After(time.Second): - t.Errorf("Timed out waiting for response to input: %v, %v", i, index) - return - } - } - wg.Done() - }(j) - } - - for i := 0; i < nMsgs*nInputs; i++ { - var ts message.Transaction - select { - case ts = <-fanIn.TransactionChan(): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate: %v", i) - return - } - require.NoError(t, ts.Ack(tCtx, errors.New(string(ts.Payload.Get(0).AsBytes())))) - } - - wg.Wait() -} - -//------------------------------------------------------------------------------ diff --git a/internal/impl/io/input_dynamic_test.go b/internal/impl/io/input_dynamic_test.go deleted file mode 100644 index b54d10de3d..0000000000 --- a/internal/impl/io/input_dynamic_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package io_test - -import ( - "bytes" - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - bmock "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestDynamicInputAPI(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - gMux := mux.NewRouter() - - mgr := bmock.NewManager() - mgr.OnRegisterEndpoint = func(path string, h http.HandlerFunc) { - gMux.HandleFunc(path, h) - } - - conf := input.NewConfig() - conf.Type = "dynamic" - - i, err := mgr.NewInput(conf) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodGet, "/inputs", http.NoBody) - res := httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - assert.Equal(t, `{}`, res.Body.String()) - - fooConf := ` -generate: - interval: 100ms - mapping: 'root.source = "foo"' -` - req = httptest.NewRequest("POST", "/inputs/foo", bytes.NewBufferString(fooConf)) - res = httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - - select { - case ts, open := <-i.TransactionChan(): - require.True(t, open) - assert.Equal(t, `{"source":"foo"}`, string(ts.Payload.Get(0).AsBytes())) - require.NoError(t, ts.Ack(ctx, nil)) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - - req = httptest.NewRequest(http.MethodGet, "/inputs/foo", http.NoBody) - res = httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - assert.Equal(t, `label: "" -generate: - mapping: 'root.source = "foo"' - interval: 100ms -`, res.Body.String()) - - req = httptest.NewRequest(http.MethodGet, "/inputs/foo/uptime", http.NoBody) - res = httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - durStr := res.Body.String() - uptime, err := time.ParseDuration(durStr) - require.NoError(t, err) - assert.Greater(t, uptime, time.Nanosecond) - - i.TriggerStopConsuming() - require.NoError(t, i.WaitForClose(ctx)) -} - -func TestDynamicInputAPIStopped(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - gMux := mux.NewRouter() - - mgr := bmock.NewManager() - mgr.OnRegisterEndpoint = func(path string, h http.HandlerFunc) { - gMux.HandleFunc(path, h) - } - - conf := input.NewConfig() - conf.Type = "dynamic" - - i, err := mgr.NewInput(conf) - require.NoError(t, err) - - fooConf := ` -generate: - interval: 1ns - count: 1 - mapping: 'root.source = "foo"' -` - req := httptest.NewRequest("POST", "/inputs/foo", bytes.NewBufferString(fooConf)) - res := httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - - select { - case ts, open := <-i.TransactionChan(): - require.True(t, open) - assert.Equal(t, `{"source":"foo"}`, string(ts.Payload.Get(0).AsBytes())) - require.NoError(t, ts.Ack(ctx, nil)) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - - assert.Eventually(t, func() bool { - req = httptest.NewRequest(http.MethodGet, "/inputs/foo/uptime", http.NoBody) - res = httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - return res.Code == 200 && res.Body.String() == "stopped" - }, time.Second*5, time.Millisecond*10) - - i.TriggerStopConsuming() - require.NoError(t, i.WaitForClose(ctx)) -} - -func TestBrokerConfigs(t *testing.T) { - for _, test := range []struct { - name string - config string - output map[string]struct{} - }{ - { - name: "simple inputs", - config: ` -dynamic: - inputs: - foo: - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' - bar: - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 2"' -`, - output: map[string]struct{}{ - "hello world 1": {}, - "hello world 2": {}, - }, - }, - { - name: "input processors", - config: ` -dynamic: - inputs: - foo: - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' - processors: - - bloblang: 'root = content().uppercase()' -processors: - - bloblang: 'root = "meow " + content().string()' -`, - output: map[string]struct{}{ - "meow HELLO WORLD 1": {}, - }, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - builder := service.NewEnvironment().NewStreamBuilder() - require.NoError(t, builder.AddInputYAML(test.config)) - require.NoError(t, builder.SetLoggerYAML(`level: none`)) - - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - outputMsgs := map[string]struct{}{} - require.NoError(t, builder.AddConsumerFunc(func(ctx context.Context, msg *service.Message) error { - mBytes, _ := msg.AsBytes() - outputMsgs[string(mBytes)] = struct{}{} - if len(outputMsgs) == len(test.output) { - done() - } - return nil - })) - - strm, err := builder.Build() - require.NoError(t, err) - - require.EqualError(t, strm.Run(tCtx), "context canceled") - assert.Equal(t, test.output, outputMsgs) - }) - } -} diff --git a/internal/impl/io/input_file.go b/internal/impl/io/input_file.go deleted file mode 100644 index 99c202bace..0000000000 --- a/internal/impl/io/input_file.go +++ /dev/null @@ -1,237 +0,0 @@ -package io - -import ( - "context" - "errors" - "io" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" -) - -const ( - fileInputFieldPaths = "paths" - fileInputFieldDeleteOnFinish = "delete_on_finish" -) - -func fileInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Local"). - Summary(`Consumes data from files on disk, emitting messages according to a chosen codec.`). - Description(` -== Metadata - -This input adds the following metadata fields to each message: - -`+"```text"+` -- path -- mod_time_unix -- mod_time (RFC3339) -`+"```"+` - -You can access these metadata fields using -xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). - Example( - "Read a Bunch of CSVs", - "If we wished to consume a directory of CSV files as structured documents we can use a glob pattern and the `csv` scanner:", - ` -input: - file: - paths: [ ./data/*.csv ] - scanner: - csv: {} -`, - ). - Fields( - service.NewStringListField(fileInputFieldPaths). - Description("A list of paths to consume sequentially. Glob patterns are supported, including super globs (double star)."), - ). - Fields(codec.DeprecatedCodecFields("lines")...). - Fields( - service.NewBoolField(fileInputFieldDeleteOnFinish). - Description("Whether to delete input files from the disk once they are fully consumed."). - Advanced(). - Default(false), - service.NewAutoRetryNacksToggleField(), - ) -} - -func init() { - err := service.RegisterBatchInput("file", fileInputSpec(), - func(pConf *service.ParsedConfig, res *service.Resources) (service.BatchInput, error) { - r, err := fileConsumerFromParsed(pConf, res) - if err != nil { - return nil, err - } - return service.AutoRetryNacksBatchedToggled(pConf, r) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type scannerInfo struct { - scanner codec.DeprecatedFallbackStream - currentPath string - modTimeUTC time.Time -} - -type fileConsumer struct { - log *service.Logger - nm *service.Resources - - paths []string - scannerCtor codec.DeprecatedFallbackCodec - - scannerMut sync.Mutex - scannerInfo *scannerInfo - - delete bool -} - -func fileConsumerFromParsed(conf *service.ParsedConfig, nm *service.Resources) (*fileConsumer, error) { - paths, err := conf.FieldStringList(fileInputFieldPaths) - if err != nil { - return nil, err - } - - deleteOnFinish, err := conf.FieldBool(fileInputFieldDeleteOnFinish) - if err != nil { - return nil, err - } - - expandedPaths, err := filepath.Globs(nm.FS(), paths) - if err != nil { - return nil, err - } - - ctor, err := codec.DeprecatedCodecFromParsed(conf) - if err != nil { - return nil, err - } - - return &fileConsumer{ - nm: nm, - log: nm.Logger(), - scannerCtor: ctor, - paths: expandedPaths, - delete: deleteOnFinish, - }, nil -} - -func (f *fileConsumer) Connect(ctx context.Context) error { - return nil -} - -func (f *fileConsumer) getReader(ctx context.Context) (scannerInfo, error) { - f.scannerMut.Lock() - defer f.scannerMut.Unlock() - - if f.scannerInfo != nil { - return *f.scannerInfo, nil - } - - if len(f.paths) == 0 { - return scannerInfo{}, component.ErrTypeClosed - } - - nextPath := f.paths[0] - - file, err := f.nm.FS().Open(nextPath) - if err != nil { - return scannerInfo{}, err - } - - details := service.NewScannerSourceDetails() - details.SetName(nextPath) - - scanner, err := f.scannerCtor.Create(file, func(ctx context.Context, err error) error { - if err == nil && f.delete { - return f.nm.FS().Remove(nextPath) - } - return nil - }, details) - if err != nil { - file.Close() - return scannerInfo{}, err - } - - var modTimeUTC time.Time - if fInfo, err := file.Stat(); err == nil { - modTimeUTC = fInfo.ModTime().UTC() - } else { - f.log.Errorf("Failed to read metadata from file '%v'", nextPath) - } - - f.scannerInfo = &scannerInfo{ - scanner: scanner, - currentPath: nextPath, - modTimeUTC: modTimeUTC, - } - - f.paths = f.paths[1:] - - f.log.Debugf("Consuming from file '%v'\n", nextPath) - return *f.scannerInfo, nil -} - -func (f *fileConsumer) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - for { - scannerInfo, err := f.getReader(ctx) - if err != nil { - return nil, nil, err - } - - parts, codecAckFn, err := scannerInfo.scanner.NextBatch(ctx) - if err != nil { - if errors.Is(err, context.Canceled) || - errors.Is(err, context.DeadlineExceeded) { - err = component.ErrTimeout - } - if err != component.ErrTimeout { - f.scannerMut.Lock() - scannerInfo.scanner.Close(ctx) - f.scannerInfo = nil - f.scannerMut.Unlock() - } - if errors.Is(err, io.EOF) { - continue - } - return nil, nil, err - } - - for _, part := range parts { - part.MetaSetMut("path", scannerInfo.currentPath) - part.MetaSetMut("mod_time_unix", scannerInfo.modTimeUTC.Unix()) - part.MetaSetMut("mod_time", scannerInfo.modTimeUTC.Format(time.RFC3339)) - } - - if len(parts) == 0 { - _ = codecAckFn(ctx, nil) - return nil, nil, component.ErrTimeout - } - - return parts, func(rctx context.Context, res error) error { - return codecAckFn(rctx, res) - }, nil - } -} - -func (f *fileConsumer) Close(ctx context.Context) (err error) { - f.scannerMut.Lock() - defer f.scannerMut.Unlock() - - if f.scannerInfo != nil { - err = f.scannerInfo.scanner.Close(ctx) - f.scannerInfo = nil - f.paths = nil - } - return -} diff --git a/internal/impl/io/input_file_test.go b/internal/impl/io/input_file_test.go deleted file mode 100644 index 9a679dd55d..0000000000 --- a/internal/impl/io/input_file_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "os" - "reflect" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestFileDirectory(t *testing.T) { - tmpDir := t.TempDir() - - tmpInnerDir, err := os.MkdirTemp(tmpDir, "benthos_inner") - require.NoError(t, err) - - tmpFile, err := os.CreateTemp(tmpDir, "f1*.txt") - require.NoError(t, err) - - _, err = tmpFile.WriteString("foo") - require.NoError(t, err) - - err = tmpFile.Close() - require.NoError(t, err) - - err = os.Chtimes(tmpFile.Name(), mockTime(), mockTime()) - require.NoError(t, err) - - tmpFileTwo, err := os.CreateTemp(tmpInnerDir, "f2*.txt") - require.NoError(t, err) - - _, err = tmpFileTwo.WriteString("bar") - require.NoError(t, err) - - err = tmpFileTwo.Close() - require.NoError(t, err) - - err = os.Chtimes(tmpFileTwo.Name(), mockTime(), mockTime()) - require.NoError(t, err) - - expFiles := map[string]*os.File{ - "foo": tmpFile, - "bar": tmpFileTwo, - } - exp := map[string]struct{}{ - "foo": {}, - "bar": {}, - } - act := map[string]struct{}{} - - conf, err := testutil.InputFromYAML(fmt.Sprintf(` -file: - paths: - - "%v/*.txt" - - "%v/**/*.txt" - scanner: - to_the_end: {} -`, tmpDir, tmpDir)) - require.NoError(t, err) - - i, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for range exp { - var tran message.Transaction - var open bool - select { - case tran, open = <-i.TransactionChan(): - assert.True(t, open) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - res := tran.Payload.Get(0) - resStr := string(res.AsBytes()) - if _, exists := act[resStr]; exists { - t.Errorf("Received duplicate message: %v", resStr) - } - assertValidMetaData(t, res, expFiles[resStr]) - act[resStr] = struct{}{} - - require.NoError(t, tran.Ack(context.Background(), nil)) - } - - var open bool - select { - case _, open = <-i.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - assert.False(t, open) - - if !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func TestFileDirectoryDeprecated(t *testing.T) { - tmpDir := t.TempDir() - - tmpInnerDir, err := os.MkdirTemp(tmpDir, "benthos_inner") - require.NoError(t, err) - - tmpFile, err := os.CreateTemp(tmpDir, "f1*.txt") - require.NoError(t, err) - - _, err = tmpFile.WriteString("foo") - require.NoError(t, err) - - err = tmpFile.Close() - require.NoError(t, err) - - err = os.Chtimes(tmpFile.Name(), mockTime(), mockTime()) - require.NoError(t, err) - - tmpFileTwo, err := os.CreateTemp(tmpInnerDir, "f2*.txt") - require.NoError(t, err) - - _, err = tmpFileTwo.WriteString("bar") - require.NoError(t, err) - - err = tmpFileTwo.Close() - require.NoError(t, err) - - err = os.Chtimes(tmpFileTwo.Name(), mockTime(), mockTime()) - require.NoError(t, err) - - expFiles := map[string]*os.File{ - "foo": tmpFile, - "bar": tmpFileTwo, - } - exp := map[string]struct{}{ - "foo": {}, - "bar": {}, - } - act := map[string]struct{}{} - - conf, err := testutil.InputFromYAML(fmt.Sprintf(` -file: - paths: - - "%v/*.txt" - - "%v/**/*.txt" - codec: all-bytes -`, tmpDir, tmpDir)) - require.NoError(t, err) - - i, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for range exp { - var tran message.Transaction - var open bool - select { - case tran, open = <-i.TransactionChan(): - assert.True(t, open) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - res := tran.Payload.Get(0) - resStr := string(res.AsBytes()) - if _, exists := act[resStr]; exists { - t.Errorf("Received duplicate message: %v", resStr) - } - assertValidMetaData(t, res, expFiles[resStr]) - act[resStr] = struct{}{} - - require.NoError(t, tran.Ack(context.Background(), nil)) - } - - var open bool - select { - case _, open = <-i.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - assert.False(t, open) - - if !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func assertValidMetaData(t *testing.T, res *message.Part, tmpFile *os.File) { - assert.Equal(t, tmpFile.Name(), res.MetaGetStr("path")) - assert.Equal(t, mockTime().Format(time.RFC3339), res.MetaGetStr("mod_time")) - assert.Equal(t, strconv.Itoa(int(mockTime().Unix())), res.MetaGetStr("mod_time_unix")) -} - -func mockTime() time.Time { - return time.Date(2015, 8, 25, 23, 23, 0, 0, time.UTC) -} diff --git a/internal/impl/io/input_http_client.go b/internal/impl/io/input_http_client.go deleted file mode 100644 index 4939bca891..0000000000 --- a/internal/impl/io/input_http_client.go +++ /dev/null @@ -1,287 +0,0 @@ -package io - -import ( - "context" - "errors" - "io" - "strings" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/httpclient" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" -) - -func httpClientInputSpec() *service.ConfigSpec { - streamFields := []*service.ConfigField{ - service.NewBoolField("enabled").Description("Enables streaming mode.").Default(false), - service.NewBoolField("reconnect").Description("Sets whether to re-establish the connection once it is lost.").Default(true), - } - streamFields = append(streamFields, codec.DeprecatedCodecFields("lines")...) - - streamField := service.NewObjectField("stream", streamFields...). - Description("Allows you to set streaming mode, where requests are kept open and messages are processed line-by-line."). - Optional() - - return service.NewConfigSpec(). - Stable(). - Categories("Network"). - Summary("Connects to a server and continuously performs requests for a single message."). - Description(` -The URL and header values of this type can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. - -== Streaming - -If you enable streaming then Benthos will consume the body of the response as a continuous stream of data, breaking messages out following a chosen scanner. This allows you to consume APIs that provide long lived streamed data feeds (such as Twitter). - -== Pagination - -This input supports interpolation functions in the `+"`url` and `headers`"+` fields where data from the previous successfully consumed message (if there was one) can be referenced. This can be used in order to support basic levels of pagination. However, in cases where pagination depends on logic it is recommended that you use an `+"xref:components:processors/http.adoc[`http` processor] instead, often combined with a xref:components:inputs/generate.adoc[`generate` input]"+` in order to schedule the processor.`). - Example( - "Basic Pagination", - "Interpolation functions within the `url` and `headers` fields can be used to reference the previously consumed message, which allows simple pagination.", - ` -input: - http_client: - url: >- - https://api.example.com/search?query=allmyfoos&start_time=${! ( - (timestamp_unix()-300).ts_format("2006-01-02T15:04:05Z","UTC").escape_url_query() - ) }${! ("&next_token="+this.meta.next_token.not_null()) | "" } - verb: GET - rate_limit: foo_searches - oauth2: - enabled: true - token_url: https://api.example.com/oauth2/token - client_key: "${EXAMPLE_KEY}" - client_secret: "${EXAMPLE_SECRET}" - -rate_limit_resources: - - label: foo_searches - local: - count: 1 - interval: 30s -`, - ). - Field(httpclient.ConfigField("GET", false, - service.NewInterpolatedStringField("payload").Description("An optional payload to deliver for each request.").Optional(), - service.NewBoolField("drop_empty_bodies").Description("Whether empty payloads received from the target server should be dropped.").Default(true).Advanced(), - streamField, - )). - Field(service.NewAutoRetryNacksToggleField()) -} - -func init() { - err := service.RegisterBatchInput( - "http_client", httpClientInputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - rdr, err := newHTTPClientInputFromParsed(conf, mgr) - if err != nil { - return nil, err - } - return service.AutoRetryNacksBatchedToggled(conf, rdr) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type httpClientInput struct { - client *httpclient.Client - prevResponse service.MessageBatch - - codecCtor codec.DeprecatedFallbackCodec - reconnectStream bool - dropEmptyBodies bool - - codecMut sync.Mutex - codec codec.DeprecatedFallbackStream -} - -func newHTTPClientInputFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (*httpClientInput, error) { - oldConf, err := httpclient.ConfigFromParsed(conf) - if err != nil { - return nil, err - } - - var codecCtor codec.DeprecatedFallbackCodec - - streamEnabled, err := conf.FieldBool("stream", "enabled") - if err != nil { - return nil, err - } - if streamEnabled { - // Timeout should be left at zero if we are streaming. - oldConf.Timeout = 0 - if codecCtor, err = codec.DeprecatedCodecFromParsed(conf.Namespace("stream")); err != nil { - return nil, err - } - } - reconnectStream, _ := conf.FieldBool("stream", "reconnect") - - var payloadExpr *service.InterpolatedString - if payloadStr, _ := conf.FieldString("payload"); payloadStr != "" { - if payloadExpr, err = conf.FieldInterpolatedString("payload"); err != nil { - return nil, err - } - } - - dropEmpty, err := conf.FieldBool("drop_empty_bodies") - if err != nil { - return nil, err - } - - client, err := httpclient.NewClientFromOldConfig(oldConf, mgr, httpclient.WithExplicitBody(payloadExpr)) - if err != nil { - return nil, err - } - - return &httpClientInput{ - prevResponse: nil, - client: client, - - dropEmptyBodies: dropEmpty, - reconnectStream: reconnectStream, - - codecCtor: codecCtor, - }, nil -} - -func (h *httpClientInput) Connect(ctx context.Context) (err error) { - if h.codecCtor == nil { - return nil - } - - h.codecMut.Lock() - defer h.codecMut.Unlock() - - if h.codec != nil { - return nil - } - - res, err := h.client.SendToResponse(context.Background(), h.prevResponse) - if err != nil { - if strings.Contains(err.Error(), "(Client.Timeout exceeded while awaiting headers)") { - err = component.ErrTimeout - } - return err - } - - p := service.NewMessage(nil) - for k, values := range res.Header { - if len(values) > 0 { - p.MetaSetMut(strings.ToLower(k), values[0]) - } - } - h.prevResponse = service.MessageBatch{p} - - if h.codec, err = h.codecCtor.Create(res.Body, func(ctx context.Context, err error) error { - return nil - }, service.NewScannerSourceDetails()); err != nil { - res.Body.Close() - return err - } - return nil -} - -func (h *httpClientInput) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - if h.codecCtor != nil { - return h.readStreamed(ctx) - } - return h.readNotStreamed(ctx) -} - -func (h *httpClientInput) readStreamed(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - h.codecMut.Lock() - defer h.codecMut.Unlock() - - if h.codec == nil { - return nil, nil, service.ErrNotConnected - } - - parts, codecAckFn, err := h.codec.NextBatch(ctx) - if err != nil { - if errors.Is(err, context.Canceled) || - errors.Is(err, context.DeadlineExceeded) { - err = component.ErrTimeout - } - if err != component.ErrTimeout { - h.codec.Close(ctx) - h.codec = nil - } - if errors.Is(err, io.EOF) { - if !h.reconnectStream { - return nil, nil, service.ErrEndOfInput - } - return nil, nil, component.ErrTimeout - } - return nil, nil, err - } - - if len(parts) == 1 { - if mBytes, _ := parts[0].AsBytes(); len(mBytes) == 0 && h.dropEmptyBodies { - _ = codecAckFn(ctx, nil) - return nil, nil, component.ErrTimeout - } - } - if len(parts) == 0 { - _ = codecAckFn(ctx, nil) - return nil, nil, component.ErrTimeout - } - - meta := map[string]string{} - if len(h.prevResponse) > 0 { - _ = h.prevResponse[0].MetaWalk(func(k, v string) error { - meta[k] = v - return nil - }) - } - - h.prevResponse = make(service.MessageBatch, len(parts)) - for i, v := range parts { - h.prevResponse[i] = v.Copy() - } - - return parts, func(rctx context.Context, res error) error { - return codecAckFn(rctx, res) - }, nil -} - -func (h *httpClientInput) readNotStreamed(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - msg, err := h.client.Send(ctx, h.prevResponse) - if err != nil { - if strings.Contains(err.Error(), "(Client.Timeout exceeded while awaiting headers)") { - err = component.ErrTimeout - } - return nil, nil, err - } - - if len(msg) == 0 { - return nil, nil, component.ErrTimeout - } - - mBytes, _ := msg[0].AsBytes() - if len(msg) == 1 && len(mBytes) == 0 && h.dropEmptyBodies { - return nil, nil, component.ErrTimeout - } - - h.prevResponse = msg - return msg.Copy(), func(context.Context, error) error { - return nil - }, nil -} - -func (h *httpClientInput) Close(ctx context.Context) (err error) { - _ = h.client.Close(ctx) - - h.codecMut.Lock() - defer h.codecMut.Unlock() - - if h.codec != nil { - err = h.codec.Close(ctx) - h.codec = nil - } - return -} diff --git a/internal/impl/io/input_http_client_test.go b/internal/impl/io/input_http_client_test.go deleted file mode 100644 index c8075130e2..0000000000 --- a/internal/impl/io/input_http_client_test.go +++ /dev/null @@ -1,898 +0,0 @@ -package io_test - -import ( - "bytes" - "context" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/textproto" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/io" -) - -func parseYAMLInputConf(t testing.TB, formatStr string, args ...any) (conf input.Config) { - t.Helper() - var err error - conf, err = testutil.InputFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return -} - -func TestHTTPClientGET(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - inputs := []string{ - "foo1", - "foo2", - "foo3", - "foo4", - "foo5", - } - - var reqCount uint32 - index := 0 - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "GET", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - - reqBytes, err := io.ReadAll(r.Body) - require.NoError(t, err) - assert.Empty(t, reqBytes) - - atomic.AddUint32(&reqCount, 1) - _, _ = w.Write([]byte(inputs[index%len(inputs)])) - index++ - })) - defer ts.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms -`, ts.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - var tr message.Transaction - var open bool - - for _, expPart := range inputs { - select { - case tr, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := 1, tr.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - if exp, act := expPart, string(tr.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, tr.Ack(tCtx, nil)) - } - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(tCtx)) - - if exp, act := uint32(len(inputs)), atomic.LoadUint32(&reqCount); exp != act && exp+1 != act { - t.Errorf("Wrong count of HTTP attempts: %v != %v", act, exp) - } -} - -func TestHTTPClientPagination(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - var paths []string - var pathsLock sync.Mutex - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "hello%v", len(paths)) - pathsLock.Lock() - paths = append(paths, r.URL.Path) - pathsLock.Unlock() - })) - defer ts.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: "%v/${!content()}" - retry_period: 1ms -`, ts.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - var tr message.Transaction - var open bool - - for i := 0; i < 10; i++ { - exp := fmt.Sprintf("hello%v", i) - select { - case tr, open = <-h.TransactionChan(): - require.True(t, open) - require.Equal(t, 1, tr.Payload.Len()) - assert.Equal(t, exp, string(tr.Payload.Get(0).AsBytes())) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - require.NoError(t, tr.Ack(tCtx, nil)) - } - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(tCtx)) - - pathsLock.Lock() - defer pathsLock.Unlock() - for i, url := range paths { - expURL := "/" - if i > 0 { - expURL = fmt.Sprintf("/hello%v", i-1) - } - assert.Equal(t, expURL, url) - } -} - -func TestHTTPClientGETError(t *testing.T) { - t.Parallel() - - requestChan := make(chan struct{}) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "nah", http.StatusBadGateway) - select { - case requestChan <- struct{}{}: - default: - } - })) - defer ts.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: "%v/testpost" - retry_period: 1ms -`, ts.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for i := 0; i < 3; i++ { - select { - case <-requestChan: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientGETNotExist(t *testing.T) { - t.Parallel() - - conf := parseYAMLInputConf(t, ` -http_client: - url: "jgljksdfhjgkldfjglkf" - retry_period: 1ms -`) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - <-time.After(time.Millisecond * 500) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientGETStreamNotExist(t *testing.T) { - t.Parallel() - - conf := parseYAMLInputConf(t, ` -http_client: - url: jgljksdfhjgkldfjglkf - retry_period: 1ms - stream: - enabled: true -`) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - <-time.After(time.Millisecond * 500) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientGETStreamError(t *testing.T) { - t.Parallel() - - requestChan := make(chan struct{}) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "nah", http.StatusBadGateway) - select { - case requestChan <- struct{}{}: - default: - } - })) - defer ts.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms - stream: - enabled: true -`, ts.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - select { - case <-requestChan: - case <-time.After(time.Second): - t.Error("Timed out") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientPOST(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - var reqCount uint32 - inputs := []string{ - "foo1", - "foo2", - "foo3", - "foo4", - "foo5", - } - - index := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "POST", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - defer r.Body.Close() - - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Error(err) - } - - if exp, act := "foobar", string(bodyBytes); exp != act { - t.Errorf("Wrong post body: %v != %v", act, exp) - } - - atomic.AddUint32(&reqCount, 1) - _, _ = w.Write([]byte(inputs[index%len(inputs)])) - index++ - })) - defer ts.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - verb: POST - payload: foobar - retry_period: 1ms -`, ts.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for _, expPart := range inputs { - var ts message.Transaction - var open bool - - select { - case ts, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := 1, ts.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - if exp, act := expPart, string(ts.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(tCtx)) - - if exp, act := uint32(len(inputs)), atomic.LoadUint32(&reqCount); exp != act && exp+1 != act { - t.Errorf("Wrong count of HTTP attempts: %v != %v", act, exp) - } -} - -func TestHTTPClientGETMultipart(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - var reqCount uint32 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "GET", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - atomic.AddUint32(&reqCount, 1) - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - parts := []string{ - "hello", "http", "world", - } - for _, p := range parts { - var err error - var part io.Writer - if part, err = writer.CreatePart(textproto.MIMEHeader{ - "Content-Type": []string{"application/octet-stream"}, - }); err == nil { - _, err = io.Copy(part, bytes.NewReader([]byte(p))) - } - if err != nil { - t.Fatal(err) - } - } - - writer.Close() - w.Header().Add("Content-Type", writer.FormDataContentType()) - _, _ = w.Write(body.Bytes()) - })) - defer ts.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms -`, ts.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - var tr message.Transaction - var open bool - - select { - case tr, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := 3, tr.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - if exp, act := "hello", string(tr.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - if exp, act := "http", string(tr.Payload.Get(1).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - if exp, act := "world", string(tr.Payload.Get(2).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, tr.Ack(tCtx, nil)) - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(tCtx)) - - if exp, act := uint32(1), atomic.LoadUint32(&reqCount); exp != act && exp+1 != act { - t.Errorf("Wrong count of HTTP attempts: %v != %v", act, exp) - } -} - -func TestHTTPClientGETMultipartLoop(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - tests := [][]string{ - { - "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - }, - { - "Tristique et egestas quis ipsum suspendisse ultrices. Quis enim lobortis scelerisque fermentum dui faucibus.", - }, - { - "Lorem donec massa sapien faucibus et molestie ac. Lectus proin nibh nisl condimentum id venenatis a.", - "Ultricies mi eget mauris pharetra et ultrices neque ornare aenean.", - }, - { - "Amet tellus cras adipiscing enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Venenatis cras sed felis eget velit aliquet sagittis.", - "Ac feugiat sed lectus vestibulum mattis ullamcorper velit. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet.", - "Odio ut sem nulla pharetra diam sit. Neque vitae tempus quam pellentesque nec nam aliquam sem.", - "Scelerisque eu ultrices vitae auctor eu augue. Ut eu sem integer vitae justo eget. Purus in massa tempor nec feugiat nisl pretium fusce id.", - }, - } - - var reqMut sync.Mutex - - var index int - tserve := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqMut.Lock() - defer reqMut.Unlock() - - if exp, act := "GET", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - parts := tests[index%len(tests)] - for _, p := range parts { - var err error - var part io.Writer - if part, err = writer.CreatePart(textproto.MIMEHeader{ - "Content-Type": []string{"application/octet-stream"}, - }); err == nil { - _, err = io.Copy(part, bytes.NewReader([]byte(p))) - } - if err != nil { - t.Fatal(err) - } - } - index++ - - writer.Close() - w.Header().Add("Content-Type", writer.FormDataContentType()) - _, _ = w.Write(body.Bytes()) - })) - defer tserve.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms -`, tserve.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - reqMut.Lock() - for _, test := range tests { - var ts message.Transaction - var open bool - - reqMut.Unlock() - select { - case ts, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := len(test), ts.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - for i, part := range test { - if exp, act := part, string(ts.Payload.Get(i).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - - reqMut.Lock() - require.NoError(t, ts.Ack(tCtx, nil)) - } - - h.TriggerStopConsuming() - reqMut.Unlock() - - select { - case <-h.TransactionChan(): - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPClientStreamGETMultipartLoop(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - tests := [][]string{ - { - "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - }, - { - "Tristique et egestas quis ipsum suspendisse ultrices. Quis enim lobortis scelerisque fermentum dui faucibus.", - }, - { - "Lorem donec massa sapien faucibus et molestie ac. Lectus proin nibh nisl condimentum id venenatis a.", - "Ultricies mi eget mauris pharetra et ultrices neque ornare aenean.", - }, - { - "Amet tellus cras adipiscing enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Venenatis cras sed felis eget velit aliquet sagittis.", - "Ac feugiat sed lectus vestibulum mattis ullamcorper velit. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet.", - "Odio ut sem nulla pharetra diam sit. Neque vitae tempus quam pellentesque nec nam aliquam sem.", - "Scelerisque eu ultrices vitae auctor eu augue. Ut eu sem integer vitae justo eget. Purus in massa tempor nec feugiat nisl pretium fusce id.", - }, - } - - tserve := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "GET", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - - body := &bytes.Buffer{} - - for _, test := range tests { - for _, part := range test { - body.WriteString(part) - body.WriteByte('\n') - } - body.WriteByte('\n') - } - body.WriteString("A msg that we won't read\nsecond part\n\n") - - w.Header().Add("Content-Type", "application/octet-stream") - _, _ = w.Write(body.Bytes()) - })) - defer tserve.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms - stream: - enabled: true - codec: "lines/multipart" -`, tserve.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for _, test := range tests { - var ts message.Transaction - var open bool - - select { - case ts, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := len(test), ts.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - for i, part := range test { - if exp, act := part, string(ts.Payload.Get(i).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(tCtx)) -} - -func TestHTTPClientStreamGETMultiRecover(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - msgs := [][]string{ - {"foo", "bar"}, - {"foo", "baz"}, - } - - tserve := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "GET", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - - body := &bytes.Buffer{} - for _, msg := range msgs { - for _, part := range msg { - body.WriteString(part) - body.WriteByte('\n') - } - body.WriteByte('\n') - } - - w.Header().Add("Content-Type", "application/octet-stream") - _, _ = w.Write(body.Bytes()) - })) - defer tserve.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms - stream: - enabled: true - codec: "lines/multipart" -`, tserve.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - for _, testMsg := range msgs { - var ts message.Transaction - var open bool - select { - case ts, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := len(testMsg), ts.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - for j, part := range testMsg { - if exp, act := part, string(ts.Payload.Get(j).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - } - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPClientStreamGETRecover(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - msgs := []string{"foo", "bar"} - - tserve := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "GET", r.Method; exp != act { - t.Errorf("Wrong method: %v != %v", act, exp) - } - - body := &bytes.Buffer{} - for _, msg := range msgs { - body.WriteString(msg) - body.WriteByte('\n') - } - - w.Header().Add("Content-Type", "application/octet-stream") - _, _ = w.Write(body.Bytes()) - })) - defer tserve.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms - stream: - enabled: true -`, tserve.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - for _, testMsg := range msgs { - var ts message.Transaction - var open bool - select { - case ts, open = <-h.TransactionChan(): - if !open { - t.Fatal("Chan not open") - } - if exp, act := 1, ts.Payload.Len(); exp != act { - t.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - if exp, act := testMsg, string(ts.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong part: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - } - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPClientStreamGETTokenization(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - msgs := []string{`{"token":"foo"}`, `{"token":"bar"}`} - - var tokensLock sync.Mutex - updateTokens := true - tokens := []string{} - - tserve := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "GET", r.Method) - - tokensLock.Lock() - if updateTokens { - tokens = append(tokens, r.URL.Query().Get("token")) - } - tokensLock.Unlock() - - body := &bytes.Buffer{} - for _, msg := range msgs { - body.WriteString(msg) - body.WriteByte('\n') - } - - w.Header().Add("Content-Type", "application/octet-stream") - _, _ = w.Write(body.Bytes()) - })) - defer tserve.Close() - - conf := parseYAMLInputConf(t, ` -http_client: - url: '%v/testpost?token=${!json("token").or(null)}' - retry_period: 1ms - stream: - enabled: true -`, tserve.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - if i == 9 { - tokensLock.Lock() - updateTokens = false - tokensLock.Unlock() - } - - for _, testMsg := range msgs { - var ts message.Transaction - var open bool - select { - case ts, open = <-h.TransactionChan(): - require.True(t, open) - require.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, testMsg, string(ts.Payload.Get(0).AsBytes())) - case <-time.After(time.Second): - t.Errorf("Action timed out") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - } - - tokensLock.Lock() - assert.Equal(t, []string{ - "null", "bar", "bar", "bar", "bar", "bar", "bar", "bar", "bar", - }, tokens) - tokensLock.Unlock() - - h.TriggerStopConsuming() - require.NoError(t, h.WaitForClose(tCtx)) -} - -func BenchmarkHTTPClientGETMultipart(b *testing.B) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - parts := []string{ - "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - } - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - for _, p := range parts { - var err error - var part io.Writer - if part, err = writer.CreatePart(textproto.MIMEHeader{ - "Content-Type": []string{"application/octet-stream"}, - }); err == nil { - _, err = io.Copy(part, bytes.NewReader([]byte(p))) - } - if err != nil { - b.Fatal(err) - } - } - writer.Close() - header := writer.FormDataContentType() - - tserve := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp, act := "GET", r.Method; exp != act { - b.Errorf("Wrong method: %v != %v", act, exp) - } - - w.Header().Add("Content-Type", header) - _, _ = w.Write(body.Bytes()) - })) - defer tserve.Close() - - conf := parseYAMLInputConf(b, ` -http_client: - url: %v/testpost - retry_period: 1ms -`, tserve.URL) - - h, err := mock.NewManager().NewInput(conf) - require.NoError(b, err) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - ts, open := <-h.TransactionChan() - if !open { - b.Fatal("Chan not open") - } - if exp, act := 3, ts.Payload.Len(); exp != act { - b.Fatalf("Wrong count of parts: %v != %v", act, exp) - } - for i, part := range parts { - if exp, act := part, string(ts.Payload.Get(i).AsBytes()); exp != act { - b.Errorf("Wrong part: %v != %v", act, exp) - } - } - require.NoError(b, ts.Ack(tCtx, nil)) - } - - b.StopTimer() - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - b.Error(err) - } -} diff --git a/internal/impl/io/input_http_server.go b/internal/impl/io/input_http_server.go deleted file mode 100644 index 2639a80680..0000000000 --- a/internal/impl/io/input_http_server.go +++ /dev/null @@ -1,960 +0,0 @@ -package io - -import ( - "bytes" - "context" - "crypto/tls" - "errors" - "fmt" - "io" - "mime" - "mime/multipart" - "net" - "net/http" - "net/textproto" - "strconv" - "strings" - "sync" - "time" - - "github.com/gorilla/mux" - "github.com/gorilla/websocket" - "github.com/klauspost/compress/gzip" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/httpserver" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/old/util/throttle" - "github.com/benthosdev/benthos/v4/internal/tracing" - "github.com/benthosdev/benthos/v4/internal/transaction" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - hsiFieldAddress = "address" - hsiFieldPath = "path" - hsiFieldWSPath = "ws_path" - hsiFieldWSWelcomeMessage = "ws_welcome_message" - hsiFieldWSRateLimitMessage = "ws_rate_limit_message" - hsiFieldAllowedVerbs = "allowed_verbs" - hsiFieldTimeout = "timeout" - hsiFieldRateLimit = "rate_limit" - hsiFieldCertFile = "cert_file" - hsiFieldKeyFile = "key_file" - hsiFieldCORS = "cors" - hsiFieldCORSEnabled = "enabled" - hsiFieldCORSAllowedOrigins = "allowed_origins" - hsiFieldResponse = "sync_response" - hsiFieldResponseStatus = "status" - hsiFieldResponseHeaders = "headers" - hsiFieldResponseExtractMetadata = "metadata_headers" -) - -type hsiConfig struct { - Address string - Path string - WSPath string - WSWelcomeMessage string - WSRateLimitMessage string - AllowedVerbs map[string]struct{} - Timeout time.Duration - RateLimit string - CertFile string - KeyFile string - CORS httpserver.CORSConfig - Response hsiResponseConfig -} - -type hsiResponseConfig struct { - Status *service.InterpolatedString - Headers map[string]*service.InterpolatedString - ExtractMetadata *service.MetadataFilter -} - -func hsiConfigFromParsed(pConf *service.ParsedConfig) (conf hsiConfig, err error) { - if conf.Address, err = pConf.FieldString(hsiFieldAddress); err != nil { - return - } - if conf.Path, err = pConf.FieldString(hsiFieldPath); err != nil { - return - } - if conf.WSPath, err = pConf.FieldString(hsiFieldWSPath); err != nil { - return - } - if conf.WSWelcomeMessage, err = pConf.FieldString(hsiFieldWSWelcomeMessage); err != nil { - return - } - if conf.WSRateLimitMessage, err = pConf.FieldString(hsiFieldWSRateLimitMessage); err != nil { - return - } - { - var verbsList []string - if verbsList, err = pConf.FieldStringList(hsiFieldAllowedVerbs); err != nil { - return - } - if len(verbsList) == 0 { - err = errors.New("must specify at least one allowed verb") - return - } - conf.AllowedVerbs = map[string]struct{}{} - for _, v := range verbsList { - conf.AllowedVerbs[v] = struct{}{} - } - } - if conf.Timeout, err = pConf.FieldDuration(hsiFieldTimeout); err != nil { - return - } - if conf.RateLimit, err = pConf.FieldString(hsiFieldRateLimit); err != nil { - return - } - if conf.CertFile, err = pConf.FieldString(hsiFieldCertFile); err != nil { - return - } - if conf.KeyFile, err = pConf.FieldString(hsiFieldKeyFile); err != nil { - return - } - if conf.CORS, err = corsConfigFromParsed(pConf.Namespace(hsiFieldCORS)); err != nil { - return - } - if conf.Response, err = hsiResponseConfigFromParsed(pConf.Namespace(hsiFieldResponse)); err != nil { - return - } - return -} - -func corsConfigFromParsed(pConf *service.ParsedConfig) (conf httpserver.CORSConfig, err error) { - if conf.Enabled, err = pConf.FieldBool(hsiFieldCORSEnabled); err != nil { - return - } - if conf.AllowedOrigins, err = pConf.FieldStringList(hsiFieldCORSAllowedOrigins); err != nil { - return - } - return -} - -func hsiResponseConfigFromParsed(pConf *service.ParsedConfig) (conf hsiResponseConfig, err error) { - if conf.Status, err = pConf.FieldInterpolatedString(hsiFieldResponseStatus); err != nil { - return - } - if conf.Headers, err = pConf.FieldInterpolatedStringMap(hsiFieldResponseHeaders); err != nil { - return - } - if conf.ExtractMetadata, err = pConf.FieldMetadataFilter(hsiFieldResponseExtractMetadata); err != nil { - return - } - return -} - -func hsiSpec() *service.ConfigSpec { - corsSpec := httpserver.ServerCORSFieldSpec() - corsSpec.Description += " Only valid with a custom `address`." - - return service.NewConfigSpec(). - Stable(). - Categories("Network"). - Summary(`Receive messages POSTed over HTTP(S). HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified.`). - Description(` -If the `+"`address`"+` config field is left blank the xref:components:http/about.adoc[service-wide HTTP server] will be used. - -The field `+"`rate_limit`"+` allows you to specify an optional `+"xref:components:rate_limits/about.adoc[`rate_limit` resource]"+`, which will be applied to each HTTP request made and each websocket payload received. - -When the rate limit is breached HTTP requests will have a 429 response returned with a Retry-After header. Websocket payloads will be dropped and an optional response payload will be sent as per `+"`ws_rate_limit_message`"+`. - -== Responses - -It's possible to return a response for each message received using xref:guides:sync_responses.adoc[synchronous responses]. When doing so you can customize headers with the `+"`sync_response` field `headers`"+`, which can also use xref:configuration:interpolation.adoc#bloblang-queries[function interpolation] in the value based on the response message contents. - -== Endpoints - -The following fields specify endpoints that are registered for sending messages, and support path parameters of the form `+"`/\\{foo}`"+`, which are added to ingested messages as metadata. A path ending in `+"`/`"+` will match against all extensions of that path: - -=== `+"`path` (defaults to `/post`)"+` - -This endpoint expects POST requests where the entire request body is consumed as a single message. - -If the request contains a multipart `+"`content-type`"+` header as per https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[rfc1341] then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. - -=== `+"`ws_path` (defaults to `/post/ws`)"+` - -Creates a websocket connection, where payloads received on the socket are passed through the pipeline as a batch of one message. - -`+api.EndpointCaveats()+` - -You may specify an optional `+"`ws_welcome_message`"+`, which is a static payload to be sent to all clients once a websocket connection is first established. - -It's also possible to specify a `+"`ws_rate_limit_message`"+`, which is a static payload to be sent to clients that have triggered the servers rate limit. - -== Metadata - -This input adds the following metadata fields to each message: - -`+"```text"+` -- http_server_user_agent -- http_server_request_path -- http_server_verb -- http_server_remote_ip -- All headers (only first values are taken) -- All query parameters -- All path parameters -- All cookies -`+"```"+` - -If HTTPS is enabled, the following fields are added as well: -`+"```text"+` -- http_server_tls_version -- http_server_tls_subject -- http_server_tls_cipher_suite -`+"```"+` - -You can access these metadata fields using xref:configuration:interpolation.adoc#bloblang-queries[function interpolation].`). - Fields( - service.NewStringField(hsiFieldAddress). - Description("An alternative address to host from. If left empty the service wide address is used."). - Default(""), - service.NewStringField(hsiFieldPath). - Description("The endpoint path to listen for POST requests."). - Default("/post"), - service.NewStringField(hsiFieldWSPath). - Description("The endpoint path to create websocket connections from."). - Default("/post/ws"), - service.NewStringField(hsiFieldWSWelcomeMessage). - Description("An optional message to deliver to fresh websocket connections."). - Advanced(). - Default(""), - service.NewStringField(hsiFieldWSRateLimitMessage). - Description("An optional message to delivery to websocket connections that are rate limited."). - Advanced(). - Default(""), - service.NewStringListField(hsiFieldAllowedVerbs). - Description("An array of verbs that are allowed for the `path` endpoint."). - Version("3.33.0"). - Default([]any{"POST"}), - service.NewDurationField(hsiFieldTimeout). - Description("Timeout for requests. If a consumed messages takes longer than this to be delivered the connection is closed, but the message may still be delivered."). - Default("5s"), - service.NewStringField(hsiFieldRateLimit). - Description("An optional xref:components:rate_limits/about.adoc[rate limit] to throttle requests by."). - Default(""), - service.NewStringField(hsiFieldCertFile). - Description("Enable TLS by specifying a certificate and key file. Only valid with a custom `address`."). - Advanced(). - Default(""), - service.NewStringField(hsiFieldKeyFile). - Description("Enable TLS by specifying a certificate and key file. Only valid with a custom `address`."). - Advanced(). - Default(""), - service.NewInternalField(corsSpec), - service.NewObjectField(hsiFieldResponse, - service.NewInterpolatedStringField(hsiFieldResponseStatus). - Description("Specify the status code to return with synchronous responses. This is a string value, which allows you to customize it based on resulting payloads and their metadata."). - Examples(`${! json("status") }`, `${! meta("status") }`). - Default("200"), - service.NewInterpolatedStringMapField(hsiFieldResponseHeaders). - Description("Specify headers to return with synchronous responses."). - Default(map[string]any{ - "Content-Type": "application/octet-stream", - }), - service.NewMetadataFilterField(hsiFieldResponseExtractMetadata). - Description("Specify criteria for which metadata values are added to the response as headers."), - ). - Description("Customize messages returned via xref:guides:sync_responses.adoc[synchronous responses]."). - Advanced(), - ). - Example( - "Path Switching", - "This example shows an `http_server` input that captures all requests and processes them by switching on that path:", ` -input: - http_server: - path: / - allowed_verbs: [ GET, POST ] - sync_response: - headers: - Content-Type: application/json - - processors: - - switch: - - check: '@http_server_request_path == "/foo"' - processors: - - mapping: | - root.title = "You Got Fooed!" - root.result = content().string().uppercase() - - - check: '@http_server_request_path == "/bar"' - processors: - - mapping: 'root.title = "Bar Is Slow"' - - sleep: # Simulate a slow endpoint - duration: 1s -`). - Example( - "Mock OAuth 2.0 Server", - "This example shows an `http_server` input that mocks an OAuth 2.0 Client Credentials flow server at the endpoint `/oauth2_test`:", ` -input: - http_server: - path: /oauth2_test - allowed_verbs: [ GET, POST ] - sync_response: - headers: - Content-Type: application/json - - processors: - - log: - message: "Received request" - level: INFO - fields_mapping: | - root = @ - root.body = content().string() - - - mapping: | - root.access_token = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3" - root.token_type = "Bearer" - root.expires_in = 3600 - - - sync_response: {} - - mapping: 'root = deleted()' -`) -} - -func init() { - err := service.RegisterBatchInput( - "http_server", hsiSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - hsiConf, err := hsiConfigFromParsed(conf) - if err != nil { - return nil, err - } - - // TODO: If we refactor this input to implement ReadBatch then we - // can return a proper service.BatchInput implementation. - - oldMgr := interop.UnwrapManagement(mgr) - i, err := newHTTPServerInput(hsiConf, oldMgr) - if err != nil { - return nil, err - } - - return interop.NewUnwrapInternalInput(i), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type httpServerInput struct { - conf hsiConfig - log log.Modular - mgr bundle.NewManagement - - mux *mux.Router - server *http.Server - - handlerWG sync.WaitGroup - transactions chan message.Transaction - - shutSig *shutdown.Signaller - - mPostRcvd metrics.StatCounter - mWSRcvd metrics.StatCounter - mLatency metrics.StatTimer -} - -func newHTTPServerInput(conf hsiConfig, mgr bundle.NewManagement) (input.Streamed, error) { - var gMux *mux.Router - var server *http.Server - - var err error - if conf.Address != "" { - gMux = mux.NewRouter() - server = &http.Server{Addr: conf.Address} - if server.Handler, err = conf.CORS.WrapHandler(gMux); err != nil { - return nil, fmt.Errorf("bad CORS configuration: %w", err) - } - } - - mRcvd := mgr.Metrics().GetCounter("input_received") - h := httpServerInput{ - shutSig: shutdown.NewSignaller(), - conf: conf, - log: mgr.Logger(), - mgr: mgr, - mux: gMux, - server: server, - transactions: make(chan message.Transaction), - - mLatency: mgr.Metrics().GetTimer("input_latency_ns"), - mWSRcvd: mRcvd, - mPostRcvd: mRcvd, - } - - postHdlr := gzipHandler(h.postHandler) - wsHdlr := gzipHandler(h.wsHandler) - if gMux != nil { - if h.conf.Path != "" { - api.GetMuxRoute(gMux, h.conf.Path).Handler(postHdlr) - } - if h.conf.WSPath != "" { - api.GetMuxRoute(gMux, h.conf.WSPath).Handler(wsHdlr) - } - } else { - if h.conf.Path != "" { - mgr.RegisterEndpoint( - h.conf.Path, "Post a message into Benthos.", postHdlr, - ) - } - if h.conf.WSPath != "" { - mgr.RegisterEndpoint( - h.conf.WSPath, "Post messages via websocket into Benthos.", wsHdlr, - ) - } - } - - if h.conf.RateLimit != "" { - if !h.mgr.ProbeRateLimit(h.conf.RateLimit) { - return nil, fmt.Errorf("rate limit resource '%v' was not found", h.conf.RateLimit) - } - } - - go h.loop() - return &h, nil -} - -//------------------------------------------------------------------------------ - -func (h *httpServerInput) extractMessageFromRequest(r *http.Request) (message.Batch, error) { - msg := message.QuickBatch(nil) - - contentType := r.Header.Get("Content-Type") - if contentType == "" { - contentType = "application/octet-stream" - } - - mediaType, params, err := mime.ParseMediaType(contentType) - if err != nil { - return nil, err - } - - if strings.HasPrefix(mediaType, "multipart/") { - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - var p *multipart.Part - if p, err = mr.NextPart(); err != nil { - if errors.Is(err, io.EOF) { - break - } - return nil, err - } - var msgBytes []byte - if msgBytes, err = io.ReadAll(p); err != nil { - return nil, err - } - msg = append(msg, message.NewPart(msgBytes)) - } - } else { - var msgBytes []byte - if msgBytes, err = io.ReadAll(r.Body); err != nil { - return nil, err - } - msg = append(msg, message.NewPart(msgBytes)) - } - - _ = msg.Iter(func(i int, p *message.Part) error { - p.MetaSetMut("http_server_user_agent", r.UserAgent()) - p.MetaSetMut("http_server_request_path", r.URL.Path) - p.MetaSetMut("http_server_verb", r.Method) - if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { - p.MetaSetMut("http_server_remote_ip", host) - } - - if r.TLS != nil { - var tlsVersion string - switch r.TLS.Version { - case tls.VersionTLS10: - tlsVersion = "TLSv1.0" - case tls.VersionTLS11: - tlsVersion = "TLSv1.1" - case tls.VersionTLS12: - tlsVersion = "TLSv1.2" - case tls.VersionTLS13: - tlsVersion = "TLSv1.3" - } - p.MetaSetMut("http_server_tls_version", tlsVersion) - if len(r.TLS.VerifiedChains) > 0 && len(r.TLS.VerifiedChains[0]) > 0 { - p.MetaSetMut("http_server_tls_subject", r.TLS.VerifiedChains[0][0].Subject.String()) - } - p.MetaSetMut("http_server_tls_cipher_suite", tls.CipherSuiteName(r.TLS.CipherSuite)) - } - for k, v := range r.Header { - if len(v) > 0 { - p.MetaSetMut(k, v[0]) - } - } - for k, v := range r.URL.Query() { - if len(v) > 0 { - p.MetaSetMut(k, v[0]) - } - } - for k, v := range mux.Vars(r) { - p.MetaSetMut(k, v) - } - for _, c := range r.Cookies() { - p.MetaSetMut(c.Name, c.Value) - } - return nil - }) - - textMapGeneric := map[string]any{} - for k, vals := range r.Header { - for _, v := range vals { - textMapGeneric[k] = v - } - } - - _ = tracing.InitSpansFromParentTextMap(h.mgr.Tracer(), "input_http_server_post", textMapGeneric, msg) - return msg, nil -} - -func (h *httpServerInput) postHandler(w http.ResponseWriter, r *http.Request) { - if h.shutSig.IsSoftStopSignalled() { - http.Error(w, "Server closing", http.StatusServiceUnavailable) - return - } - - h.handlerWG.Add(1) - defer h.handlerWG.Done() - defer r.Body.Close() - - if _, exists := h.conf.AllowedVerbs[r.Method]; !exists { - http.Error(w, "Incorrect method", http.StatusMethodNotAllowed) - return - } - - if h.conf.RateLimit != "" { - var tUntil time.Duration - var err error - if rerr := h.mgr.AccessRateLimit(r.Context(), h.conf.RateLimit, func(rl ratelimit.V1) { - tUntil, err = rl.Access(r.Context()) - }); rerr != nil { - http.Error(w, "Server error", http.StatusBadGateway) - h.log.Warn("Failed to access rate limit: %v\n", rerr) - return - } - if err != nil { - http.Error(w, "Server error", http.StatusBadGateway) - h.log.Warn("Failed to access rate limit: %v\n", err) - return - } else if tUntil > 0 { - w.Header().Add("Retry-After", strconv.Itoa(int(tUntil.Seconds()))) - http.Error(w, "Too Many Requests", http.StatusTooManyRequests) - return - } - } - - msg, err := h.extractMessageFromRequest(r) - if err != nil { - http.Error(w, "Bad request", http.StatusBadRequest) - h.log.Warn("Request read failed: %v\n", err) - return - } - defer tracing.FinishSpans(msg) - - startedAt := time.Now() - - store := transaction.NewResultStore() - transaction.AddResultStore(msg, store) - - h.mPostRcvd.Incr(int64(msg.Len())) - h.log.Trace("Consumed %v messages from POST to '%v'.\n", msg.Len(), h.conf.Path) - - resChan := make(chan error, 1) - select { - case h.transactions <- message.NewTransaction(msg, resChan): - case <-time.After(h.conf.Timeout): - http.Error(w, "Request timed out", http.StatusRequestTimeout) - return - case <-r.Context().Done(): - http.Error(w, "Request timed out", http.StatusRequestTimeout) - return - case <-h.shutSig.SoftStopChan(): - http.Error(w, "Server closing", http.StatusServiceUnavailable) - return - } - - select { - case res, open := <-resChan: - if !open { - http.Error(w, "Server closing", http.StatusServiceUnavailable) - return - } else if res != nil { - http.Error(w, res.Error(), http.StatusBadGateway) - return - } - tTaken := time.Since(startedAt).Nanoseconds() - h.mLatency.Timing(tTaken) - case <-time.After(h.conf.Timeout): - http.Error(w, "Request timed out", http.StatusRequestTimeout) - return - case <-r.Context().Done(): - http.Error(w, "Request timed out", http.StatusRequestTimeout) - return - case <-h.shutSig.HardStopChan(): - http.Error(w, "Server closing", http.StatusServiceUnavailable) - return - } - - var svcBatch service.MessageBatch - for _, resMsg := range store.Get() { - for i := 0; i < len(resMsg); i++ { - svcBatch = append(svcBatch, service.NewInternalMessage(resMsg[i])) - } - } - if len(svcBatch) > 0 { - for k, v := range h.conf.Response.Headers { - headerStr, err := svcBatch.TryInterpolatedString(0, v) - if err != nil { - h.log.Error("Interpolation of response header %v error: %v", k, err) - continue - } - w.Header().Set(k, headerStr) - } - - statusCode := 200 - statusCodeStr, err := svcBatch.TryInterpolatedString(0, h.conf.Response.Status) - if err != nil { - h.log.Error("Interpolation of response status code error: %v", err) - w.WriteHeader(http.StatusBadGateway) - return - } - if statusCodeStr != "200" { - if statusCode, err = strconv.Atoi(statusCodeStr); err != nil { - h.log.Error("Failed to parse sync response status code expression: %v\n", err) - w.WriteHeader(http.StatusBadGateway) - return - } - } - - if plen := len(svcBatch); plen == 1 { - part := svcBatch[0] - _ = h.conf.Response.ExtractMetadata.Walk(part, func(k, v string) error { - w.Header().Set(k, v) - return nil - }) - payload, err := part.AsBytes() - if err != nil { - h.log.Error("Failed to extract message bytes for sync response: %v\n", err) - w.WriteHeader(http.StatusBadGateway) - return - } - if w.Header().Get("Content-Type") == "" { - w.Header().Set("Content-Type", http.DetectContentType(payload)) - } - w.WriteHeader(statusCode) - _, _ = w.Write(payload) - } else if plen > 1 { - customContentType, customContentTypeExists := h.conf.Response.Headers["content-type"] - - var buf bytes.Buffer - writer := multipart.NewWriter(&buf) - - var merr error - for i := 0; i < plen && merr == nil; i++ { - part := svcBatch[i] - _ = h.conf.Response.ExtractMetadata.Walk(part, func(k, v string) error { - w.Header().Set(k, v) - return nil - }) - payload, err := part.AsBytes() - if err != nil { - h.log.Error("Failed to extract message bytes for sync response: %v\n", err) - continue - } - - mimeHeader := textproto.MIMEHeader{} - if customContentTypeExists { - contentTypeStr, err := svcBatch.TryInterpolatedString(i, customContentType) - if err != nil { - h.log.Error("Interpolation of content-type header error: %v", err) - mimeHeader.Set("Content-Type", http.DetectContentType(payload)) - } else { - mimeHeader.Set("Content-Type", contentTypeStr) - } - } else { - mimeHeader.Set("Content-Type", http.DetectContentType(payload)) - } - - var partWriter io.Writer - if partWriter, merr = writer.CreatePart(mimeHeader); merr == nil { - _, merr = io.Copy(partWriter, bytes.NewReader(payload)) - } - } - - merr = writer.Close() - if merr == nil { - w.Header().Del("Content-Type") - w.Header().Add("Content-Type", writer.FormDataContentType()) - w.WriteHeader(statusCode) - _, _ = buf.WriteTo(w) - } else { - h.log.Error("Failed to return sync response: %v\n", merr) - w.WriteHeader(http.StatusBadGateway) - } - } - } -} - -func (h *httpServerInput) wsHandler(w http.ResponseWriter, r *http.Request) { - if h.shutSig.IsSoftStopSignalled() { - http.Error(w, "Server closing", http.StatusServiceUnavailable) - return - } - - h.handlerWG.Add(1) - defer h.handlerWG.Done() - - var err error - defer func() { - if err != nil { - http.Error(w, "Bad request", http.StatusBadRequest) - h.log.Warn("Websocket request failed: %v\n", err) - } - }() - - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - defer ws.Close() - - resChan := make(chan error, 1) - throt := throttle.New(throttle.OptCloseChan(h.shutSig.SoftStopChan())) - - if welMsg := h.conf.WSWelcomeMessage; welMsg != "" { - if err = ws.WriteMessage(websocket.BinaryMessage, []byte(welMsg)); err != nil { - h.log.Error("Failed to send welcome message: %v\n", err) - } - } - - var msgBytes []byte - for !h.shutSig.IsSoftStopSignalled() { - if msgBytes == nil { - if _, msgBytes, err = ws.ReadMessage(); err != nil { - return - } - h.mWSRcvd.Incr(1) - } - - if h.conf.RateLimit != "" { - var tUntil time.Duration - if rerr := h.mgr.AccessRateLimit(r.Context(), h.conf.RateLimit, func(rl ratelimit.V1) { - tUntil, err = rl.Access(r.Context()) - }); rerr != nil { - h.log.Warn("Failed to access rate limit: %v\n", rerr) - err = rerr - } - if err != nil || tUntil > 0 { - if err != nil { - h.log.Warn("Failed to access rate limit: %v\n", err) - } - if rlMsg := h.conf.WSRateLimitMessage; rlMsg != "" { - if err = ws.WriteMessage(websocket.BinaryMessage, []byte(rlMsg)); err != nil { - h.log.Error("Failed to send rate limit message: %v\n", err) - } - } - continue - } - } - - msg := message.QuickBatch([][]byte{msgBytes}) - startedAt := time.Now() - - part := msg.Get(0) - part.MetaSetMut("http_server_user_agent", r.UserAgent()) - for k, v := range r.Header { - if len(v) > 0 { - part.MetaSetMut(k, v[0]) - } - } - for k, v := range r.URL.Query() { - if len(v) > 0 { - part.MetaSetMut(k, v[0]) - } - } - for k, v := range mux.Vars(r) { - part.MetaSetMut(k, v) - } - for _, c := range r.Cookies() { - part.MetaSetMut(c.Name, c.Value) - } - tracing.InitSpans(h.mgr.Tracer(), "input_http_server_websocket", msg) - - store := transaction.NewResultStore() - transaction.AddResultStore(msg, store) - - select { - case h.transactions <- message.NewTransaction(msg, resChan): - case <-h.shutSig.SoftStopChan(): - return - } - select { - case res, open := <-resChan: - if !open { - return - } - if res != nil { - throt.Retry() - } else { - tTaken := time.Since(startedAt).Nanoseconds() - h.mLatency.Timing(tTaken) - msgBytes = nil - throt.Reset() - } - case <-h.shutSig.HardStopChan(): - return - } - - for _, responseMsg := range store.Get() { - if err := responseMsg.Iter(func(i int, part *message.Part) error { - return ws.WriteMessage(websocket.TextMessage, part.AsBytes()) - }); err != nil { - h.log.Error("Failed to send sync response over websocket: %v\n", err) - } - } - - tracing.FinishSpans(msg) - } -} - -//------------------------------------------------------------------------------ - -func (h *httpServerInput) loop() { - defer func() { - if h.server != nil { - if err := h.server.Shutdown(context.Background()); err != nil { - h.log.Error("Failed to gracefully terminate http_server: %v\n", err) - } - } else { - // We are using the service-wide HTTP server. In order to prevent - // situations where a slow shutdown results in serving an abundance - // of 503 responses we wait until either the current requests are - // handled and shutdown can commence, or we've been instructed to - // close immediately, which prevents these requests from - // indefinitely blocking shutdown. - go func() { - select { - case <-h.shutSig.HasStoppedChan(): - case <-h.shutSig.HardStopChan(): - } - - if h.conf.Path != "" { - h.mgr.RegisterEndpoint(h.conf.Path, "Endpoint disabled.", func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "Service unavailable", http.StatusServiceUnavailable) - }) - } - if h.conf.WSPath != "" { - h.mgr.RegisterEndpoint(h.conf.WSPath, "Endpoint disabled.", func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "Service unavailable", http.StatusServiceUnavailable) - }) - } - }() - } - - h.handlerWG.Wait() - - close(h.transactions) - h.shutSig.TriggerHasStopped() - }() - - if h.server != nil { - go func() { - if h.conf.KeyFile != "" || h.conf.CertFile != "" { - h.log.Info( - "Receiving HTTPS messages at: https://%s\n", - h.conf.Address+h.conf.Path, - ) - if err := h.server.ListenAndServeTLS( - h.conf.CertFile, h.conf.KeyFile, - ); err != http.ErrServerClosed { - h.log.Error("Server error: %v\n", err) - } - } else { - h.log.Info( - "Receiving HTTP messages at: http://%s\n", - h.conf.Address+h.conf.Path, - ) - if err := h.server.ListenAndServe(); err != http.ErrServerClosed { - h.log.Error("Server error: %v\n", err) - } - } - }() - } - - <-h.shutSig.SoftStopChan() -} - -// TransactionChan returns a transactions channel for consuming messages from -// this input. -func (h *httpServerInput) TransactionChan() <-chan message.Transaction { - return h.transactions -} - -// Connected returns a boolean indicating whether this input is currently -// connected to its target. -func (h *httpServerInput) Connected() bool { - return true -} - -func (h *httpServerInput) TriggerStopConsuming() { - h.shutSig.TriggerSoftStop() -} - -func (h *httpServerInput) TriggerCloseNow() { - h.shutSig.TriggerHardStop() -} - -func (h *httpServerInput) WaitForClose(ctx context.Context) error { - select { - case <-h.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -//------------------------------------------------------------------------------ - -type gzipResponseWriter struct { - io.Writer - http.ResponseWriter -} - -func (w gzipResponseWriter) Write(b []byte) (int, error) { - if w.Header().Get("Content-Type") == "" { - // If no content type, apply sniffing algorithm to un-gzipped body. - w.Header().Set("Content-Type", http.DetectContentType(b)) - } - return w.Writer.Write(b) -} - -func gzipHandler(fn http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - fn(w, r) - return - } - w.Header().Set("Content-Encoding", "gzip") - gz := gzip.NewWriter(w) - defer gz.Close() - gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w} - fn(gzr, r) - } -} diff --git a/internal/impl/io/input_http_server_test.go b/internal/impl/io/input_http_server_test.go deleted file mode 100644 index 62ce273ccd..0000000000 --- a/internal/impl/io/input_http_server_test.go +++ /dev/null @@ -1,1338 +0,0 @@ -package io_test - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "mime" - "mime/multipart" - "net" - "net/http" - "net/http/httptest" - "net/textproto" - "net/url" - "sync" - "testing" - "time" - - "github.com/gorilla/mux" - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type apiRegGorillaMutWrapper struct { - mut *mux.Router -} - -func (a apiRegGorillaMutWrapper) RegisterEndpoint(path, desc string, h http.HandlerFunc) { - api.GetMuxRoute(a.mut, path).Handler(h) -} - -func TestHTTPBasic(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - nTestLoops := 100 - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - require.NoError(t, err) - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - // Test both single and multipart messages. - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - testResponse := fmt.Sprintf("response%v", i) - // Send it as single part - go func(input, output string) { - res, err := http.Post( - server.URL+"/testpost", - "application/octet-stream", - bytes.NewBufferString(input), - ) - if err != nil { - t.Error(err) - } else if res.StatusCode != 200 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - } - resBytes, err := io.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - if exp, act := output, string(resBytes); exp != act { - t.Errorf("Wrong sync response: %v != %v", act, exp) - } - }(testStr, testResponse) - - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - if res := string(ts.Payload.Get(0).AsBytes()); res != testStr { - t.Errorf("Wrong result, %v != %v", ts.Payload, res) - } - ts.Payload.Get(0).SetBytes([]byte(testResponse)) - require.NoError(t, transaction.SetAsResponse(ts.Payload)) - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - // Test MIME multipart parsing, as defined in RFC 2046 - for i := 0; i < nTestLoops; i++ { - partOne := fmt.Sprintf("test%v part one", i) - partTwo := fmt.Sprintf("test%v part two", i) - - testStr := fmt.Sprintf( - "--foo\r\n"+ - "Content-Type: application/octet-stream\r\n\r\n"+ - "%v\r\n"+ - "--foo\r\n"+ - "Content-Type: application/octet-stream\r\n\r\n"+ - "%v\r\n"+ - "--foo--\r\n", - partOne, partTwo) - - // Send it as multi part - go func() { - if res, err := http.Post( - server.URL+"/testpost", - "multipart/mixed; boundary=foo", - bytes.NewBufferString(testStr), - ); err != nil { - t.Error(err) - } else if res.StatusCode != 200 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - } - }() - - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - if exp, actual := 2, ts.Payload.Len(); exp != actual { - t.Errorf("Wrong number of parts: %v != %v", actual, exp) - } else if exp, actual := partOne, string(ts.Payload.Get(0).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", actual, exp) - } else if exp, actual := partTwo, string(ts.Payload.Get(1).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", actual, exp) - } - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - // Test requests without content-type - client := &http.Client{} - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - testResponse := fmt.Sprintf("response%v", i) - // Send it as single part - go func(input, output string) { - req, err := http.NewRequest( - "POST", server.URL+"/testpost", bytes.NewBufferString(input)) - if err != nil { - t.Error(err) - } - res, err := client.Do(req) - if err != nil { - t.Error(err) - } else if res.StatusCode != 200 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - } - resBytes, err := io.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - if exp, act := output, string(resBytes); exp != act { - t.Errorf("Wrong sync response: %v != %v", act, exp) - } - }(testStr, testResponse) - - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - if res := string(ts.Payload.Get(0).AsBytes()); res != testStr { - t.Errorf("Wrong result, %v != %v", ts.Payload, res) - } - ts.Payload.Get(0).SetBytes([]byte(testResponse)) - require.NoError(t, transaction.SetAsResponse(ts.Payload)) - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - } - - h.TriggerStopConsuming() -} - -func getFreePort(t testing.TB) int { - t.Helper() - - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - require.NoError(t, err) - - listener, err := net.ListenTCP("tcp", addr) - require.NoError(t, err) - - port := listener.Addr().(*net.TCPAddr).Port - require.NoError(t, listener.Close()) - return port -} - -func TestHTTPServerLifecycle(t *testing.T) { - t.Skip("This test seems to break on many systems") - - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - freePort := getFreePort(t) - - apiConf := api.NewConfig() - apiConf.Address = fmt.Sprintf("0.0.0.0:%v", freePort) - apiConf.Enabled = true - - testURL := fmt.Sprintf("http://localhost:%v/foo/bar", freePort) - - apiImpl, err := api.New("", "", apiConf, nil, log.Noop(), metrics.Noop()) - require.NoError(t, err) - - go func() { - _ = apiImpl.ListenAndServe() - }() - defer func() { - _ = apiImpl.Shutdown(context.Background()) - }() - - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(apiImpl)) - require.NoError(t, err) - - conf := parseYAMLInputConf(t, ` -http_server: - path: /foo/bar -`) - - timeout := time.Second * 5 - readNextMsg := func(in input.Streamed) (message.Batch, error) { - t.Helper() - var tran message.Transaction - select { - case tran = <-in.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(timeout): - return nil, errors.New("timed out 2") - } - return tran.Payload, nil - } - - server, err := mgr.NewInput(conf) - require.NoError(t, err) - - dummyData := []byte("a bunch of jolly leprechauns await") - go func() { - resp, cerr := http.Post(testURL, "text/plain", bytes.NewReader(dummyData)) - if assert.NoError(t, cerr) { - resp.Body.Close() - } - }() - - msg, err := readNextMsg(server) - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - - res, err := http.Post(testURL, "text/plain", bytes.NewReader(dummyData)) - assert.NoError(t, err) - assert.Equal(t, 503, res.StatusCode) - - serverTwo, err := mgr.NewInput(conf) - require.NoError(t, err) - - go func() { - resp, cerr := http.Post(testURL, "text/plain", bytes.NewReader(dummyData)) - if assert.NoError(t, cerr) { - resp.Body.Close() - } - }() - - msg, err = readNextMsg(serverTwo) - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - serverTwo.TriggerStopConsuming() - assert.NoError(t, serverTwo.WaitForClose(tCtx)) -} - -func TestHTTPServerMetadata(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - require.NoError(t, err) - - conf := parseYAMLInputConf(t, ` -http_server: - path: /across/the/rainbow/bridge -`) - - server, err := mgr.NewInput(conf) - require.NoError(t, err) - - defer func() { - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - }() - - testServer := httptest.NewServer(reg.mut) - defer testServer.Close() - - dummyPath := "/across/the/rainbow/bridge" - dummyQuery := url.Values{"foo": []string{"bar"}} - serverURL, err := url.Parse(testServer.URL) - require.NoError(t, err) - - serverURL.Path = dummyPath - serverURL.RawQuery = dummyQuery.Encode() - - dummyData := []byte("a bunch of jolly leprechauns await") - go func() { - resp, cerr := http.Post(serverURL.String(), "text/plain", bytes.NewReader(dummyData)) - require.NoError(t, cerr) - defer resp.Body.Close() - }() - - timeout := time.Second * 5 - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-server.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(timeout): - return nil, errors.New("timed out 2") - } - return tran.Payload, nil - } - - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - part := msg.Get(0) - assert.Equal(t, dummyPath, part.MetaGetStr("http_server_request_path")) - assert.Equal(t, "POST", part.MetaGetStr("http_server_verb")) - assert.Regexp(t, "^Go-http-client/", part.MetaGetStr("http_server_user_agent")) - assert.Equal(t, "127.0.0.1", part.MetaGetStr("http_server_remote_ip")) - // Make sure query params are set in the metadata - assert.Contains(t, "bar", part.MetaGetStr("foo")) -} - -func TestHTTPServerPathParameters(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - require.NoError(t, err) - - conf := parseYAMLInputConf(t, ` -http_server: - path: /test/{foo}/{bar} - allowed_verbs: [ "POST", "PUT" ] -`) - - server, err := mgr.NewInput(conf) - require.NoError(t, err) - - defer func() { - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - }() - - testServer := httptest.NewServer(reg.mut) - defer testServer.Close() - - dummyPath := "/test/foo1/bar1" - dummyQuery := url.Values{"mylove": []string{"will go on"}} - serverURL, err := url.Parse(testServer.URL) - require.NoError(t, err) - - serverURL.Path = dummyPath - serverURL.RawQuery = dummyQuery.Encode() - - dummyData := []byte("a bunch of jolly leprechauns await") - go func() { - req, cerr := http.NewRequest("PUT", serverURL.String(), bytes.NewReader(dummyData)) - require.NoError(t, cerr) - req.Header.Set("Content-Type", "text/plain") - resp, cerr := http.DefaultClient.Do(req) - require.NoError(t, cerr) - defer resp.Body.Close() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-server.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - part := msg.Get(0) - - assert.Equal(t, dummyPath, part.MetaGetStr("http_server_request_path")) - assert.Equal(t, "PUT", part.MetaGetStr("http_server_verb")) - assert.Equal(t, "foo1", part.MetaGetStr("foo")) - assert.Equal(t, "bar1", part.MetaGetStr("bar")) - assert.Equal(t, "will go on", part.MetaGetStr("mylove")) -} - -func TestHTTPServerPathIsPrefix(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - require.NoError(t, err) - - conf := parseYAMLInputConf(t, ` -http_server: - path: /test/{foo}/{bar}/ - allowed_verbs: [ "POST", "PUT" ] -`) - server, err := mgr.NewInput(conf) - require.NoError(t, err) - - defer func() { - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - }() - - testServer := httptest.NewServer(reg.mut) - defer testServer.Close() - - dummyPath := "/test/foo1/bar1/baz1" - dummyQuery := url.Values{"mylove": []string{"will go on"}} - serverURL, err := url.Parse(testServer.URL) - require.NoError(t, err) - - serverURL.Path = dummyPath - serverURL.RawQuery = dummyQuery.Encode() - - dummyData := []byte("a bunch of jolly leprechauns await") - go func() { - req, cerr := http.NewRequest("PUT", serverURL.String(), bytes.NewReader(dummyData)) - require.NoError(t, cerr) - req.Header.Set("Content-Type", "text/plain") - resp, cerr := http.DefaultClient.Do(req) - require.NoError(t, cerr) - defer resp.Body.Close() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-server.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - part := msg.Get(0) - - assert.Equal(t, dummyPath, part.MetaGetStr("http_server_request_path")) - assert.Equal(t, "PUT", part.MetaGetStr("http_server_verb")) - assert.Equal(t, "foo1", part.MetaGetStr("foo")) - assert.Equal(t, "bar1", part.MetaGetStr("bar")) - assert.Equal(t, "will go on", part.MetaGetStr("mylove")) -} - -func TestHTTPServerPathParametersCustomServer(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - freePort := getFreePort(t) - - conf := parseYAMLInputConf(t, ` -http_server: - address: 0.0.0.0:%v - path: /test/{foo}/{bar} -`, freePort) - - server, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - defer func() { - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - }() - - dummyPath := "/test/foo1/bar1" - dummyQuery := url.Values{"mylove": []string{"will go on"}} - serverURL, err := url.Parse(fmt.Sprintf("http://localhost:%v", freePort)) - require.NoError(t, err) - - serverURL.Path = dummyPath - serverURL.RawQuery = dummyQuery.Encode() - - dummyData := []byte("a bunch of jolly leprechauns await") - go func() { - assert.Eventually(t, func() (succeeded bool) { - req, cerr := http.NewRequest("POST", serverURL.String(), bytes.NewReader(dummyData)) - require.NoError(t, cerr) - req.Header.Set("Content-Type", "text/plain") - - if resp, cerr := http.DefaultClient.Do(req); cerr == nil { - succeeded = true - _ = resp.Body.Close() - } - return - }, time.Second, 50*time.Millisecond) - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-server.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - part := msg.Get(0) - - assert.Equal(t, dummyPath, part.MetaGetStr("http_server_request_path")) - assert.Equal(t, "POST", part.MetaGetStr("http_server_verb")) - assert.Equal(t, "foo1", part.MetaGetStr("foo")) - assert.Equal(t, "bar1", part.MetaGetStr("bar")) - assert.Equal(t, "will go on", part.MetaGetStr("mylove")) -} - -func TestHTTPServerPathParametersCustomServerPathIsPrefix(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - freePort := getFreePort(t) - - conf := parseYAMLInputConf(t, ` -http_server: - address: 0.0.0.0:%v - path: /test/{foo}/{bar}/ -`, freePort) - - server, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - defer func() { - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - }() - - dummyPath := "/test/foo1/bar1/baz1" - dummyQuery := url.Values{"mylove": []string{"will go on"}} - serverURL, err := url.Parse(fmt.Sprintf("http://localhost:%v", freePort)) - require.NoError(t, err) - - serverURL.Path = dummyPath - serverURL.RawQuery = dummyQuery.Encode() - - dummyData := []byte("a bunch of jolly leprechauns await") - go func() { - require.Eventually(t, func() (succeeded bool) { - req, cerr := http.NewRequest("POST", serverURL.String(), bytes.NewReader(dummyData)) - require.NoError(t, cerr) - req.Header.Set("Content-Type", "text/plain") - - if resp, cerr := http.DefaultClient.Do(req); cerr == nil { - succeeded = true - _ = resp.Body.Close() - } - return - }, time.Second, 50*time.Millisecond) - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-server.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, dummyData, message.GetAllBytes(msg)[0]) - - part := msg.Get(0) - - assert.Equal(t, dummyPath, part.MetaGetStr("http_server_request_path")) - assert.Equal(t, "POST", part.MetaGetStr("http_server_verb")) - assert.Equal(t, "foo1", part.MetaGetStr("foo")) - assert.Equal(t, "bar1", part.MetaGetStr("bar")) - assert.Equal(t, "will go on", part.MetaGetStr("mylove")) -} - -func TestHTTPBadRequests(t *testing.T) { - t.Parallel() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - res, err := http.Get(server.URL + "/testpost") - if err != nil { - t.Error(err) - return - } - if exp, act := http.StatusMethodNotAllowed, res.StatusCode; exp != act { - t.Errorf("unexpected HTTP response code: %v != %v", exp, act) - } - - h.TriggerStopConsuming() - assert.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPTimeout(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost - timeout: 1ms -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - var res *http.Response - res, err = http.Post( - server.URL+"/testpost", - "application/octet-stream", - bytes.NewBufferString("hello world"), - ) - if err != nil { - t.Fatal(err) - } - if exp, act := http.StatusRequestTimeout, res.StatusCode; exp != act { - t.Errorf("Unexpected status code: %v != %v", exp, act) - } - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPRateLimit(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - - mgrConf, err := testutil.ManagerFromYAML(` -rate_limit_resources: - - label: foorl - local: - count: 1 - interval: 60s -`) - require.NoError(t, err) - - mgr, err := manager.New(mgrConf, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost - rate_limit: foorl -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - go func() { - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - var res *http.Response - res, err = http.Post( - server.URL+"/testpost", - "application/octet-stream", - bytes.NewBufferString("hello world"), - ) - if err != nil { - t.Fatal(err) - } - if exp, act := http.StatusOK, res.StatusCode; exp != act { - t.Errorf("Unexpected status code: %v != %v", exp, act) - } - - res, err = http.Post( - server.URL+"/testpost", - "application/octet-stream", - bytes.NewBufferString("hello world"), - ) - if err != nil { - t.Fatal(err) - } - if exp, act := http.StatusTooManyRequests, res.StatusCode; exp != act { - t.Errorf("Unexpected status code: %v != %v", exp, act) - } - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPServerWebsockets(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - ws_path: /testws -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - purl, err := url.Parse(server.URL + "/testws") - if err != nil { - t.Fatal(err) - } - purl.Scheme = "ws" - - var client *websocket.Conn - if client, _, err = websocket.DefaultDialer.Dial(purl.String(), http.Header{}); err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - if clientErr := client.WriteMessage( - websocket.BinaryMessage, []byte("hello world 1"), - ); clientErr != nil { - t.Error(clientErr) - } - wg.Done() - }() - - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - if exp, act := `[hello world 1]`, fmt.Sprintf("%s", message.GetAllBytes(ts.Payload)); exp != act { - t.Errorf("Unexpected message: %v != %v", act, exp) - } - require.NoError(t, ts.Ack(tCtx, nil)) - wg.Wait() - - wg.Add(1) - go func() { - if closeErr := client.WriteMessage( - websocket.BinaryMessage, []byte("hello world 2"), - ); closeErr != nil { - t.Error(closeErr) - } - wg.Done() - }() - - select { - case ts = <-h.TransactionChan(): - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - if exp, act := `[hello world 2]`, fmt.Sprintf("%s", message.GetAllBytes(ts.Payload)); exp != act { - t.Errorf("Unexpected message: %v != %v", act, exp) - } - require.NoError(t, ts.Ack(tCtx, nil)) - wg.Wait() - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPServerWSRateLimit(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - - mgrConf, err := testutil.ManagerFromYAML(` -rate_limit_resources: - - label: foorl - local: - count: 1 - interval: 60s -`) - require.NoError(t, err) - - mgr, err := manager.New(mgrConf, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - ws_path: /testws - ws_welcome_message: test welcome - ws_rate_limit_message: test rate limited - rate_limit: foorl -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - purl, err := url.Parse(server.URL + "/testws") - if err != nil { - t.Fatal(err) - } - purl.Scheme = "ws" - - var client *websocket.Conn - if client, _, err = websocket.DefaultDialer.Dial(purl.String(), http.Header{}); err != nil { - t.Fatal(err) - } - - go func() { - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - var msgBytes []byte - if _, msgBytes, err = client.ReadMessage(); err != nil { - t.Fatal(err) - } - if exp, act := "test welcome", string(msgBytes); exp != act { - t.Errorf("Unexpected welcome message: %v != %v", act, exp) - } - - if err = client.WriteMessage( - websocket.BinaryMessage, []byte("hello world"), - ); err != nil { - t.Fatal(err) - } - - if err = client.WriteMessage( - websocket.BinaryMessage, []byte("hello world"), - ); err != nil { - t.Fatal(err) - } - - if _, msgBytes, err = client.ReadMessage(); err != nil { - t.Fatal(err) - } - if exp, act := "test rate limited", string(msgBytes); exp != act { - t.Errorf("Unexpected rate limit message: %v != %v", act, exp) - } - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} - -func TestHTTPSyncResponseHeaders(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost - sync_response: - headers: - Content-Type: application/json - foo: '${!json("field1")}' - metadata_headers: - include_prefixes: [ 'Loca' ] - include_patterns: [ 'name' ] -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - input := `{"foo":"test message","field1":"bar"}` - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - - req, err := http.NewRequest(http.MethodPost, server.URL+"/testpost", bytes.NewBufferString(input)) - if err != nil { - t.Error(err) - } - req.Header.Set("Content-Type", "application/octet-stream") - req.Header.Set("Location", "Asgard") - req.Header.Set("Username", "Thor") - req.Header.Set("Language", "Norse") - res, err := http.DefaultClient.Do(req) - if err != nil { - t.Error(err) - } else if res.StatusCode != 200 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - } - resBytes, err := io.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - assert.JSONEq(t, input, string(resBytes)) - if exp, act := "application/json", res.Header.Get("Content-Type"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - if exp, act := "bar", res.Header.Get("foo"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - if exp, act := "Asgard", res.Header.Get("Location"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - if exp, act := "Thor", res.Header.Get("Username"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - if exp, act := "", res.Header.Get("Language"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - }() - - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - if res := string(ts.Payload.Get(0).AsBytes()); res != input { - t.Errorf("Wrong result, %v != %v", ts.Payload, res) - } - require.NoError(t, transaction.SetAsResponse(ts.Payload)) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } - - wg.Wait() -} - -func createMultipart(payloads []string, contentType string) (hdr string, bodyBytes []byte, err error) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - for i := 0; i < len(payloads) && err == nil; i++ { - var part io.Writer - if part, err = writer.CreatePart(textproto.MIMEHeader{ - "Content-Type": []string{contentType}, - }); err == nil { - _, err = io.Copy(part, bytes.NewReader([]byte(payloads[i]))) - } - } - - if err != nil { - return "", nil, err - } - - writer.Close() - return writer.FormDataContentType(), body.Bytes(), nil -} - -func readMultipart(res *http.Response) ([]string, error) { - var params map[string]string - var err error - if contentType := res.Header.Get("Content-Type"); contentType != "" { - if _, params, err = mime.ParseMediaType(contentType); err != nil { - return nil, err - } - } - - var buffer bytes.Buffer - var output []string - - mr := multipart.NewReader(res.Body, params["boundary"]) - var bufferIndex int64 - for { - var p *multipart.Part - if p, err = mr.NextPart(); err != nil { - if err == io.EOF { - break - } - return nil, err - } - - var bytesRead int64 - if bytesRead, err = buffer.ReadFrom(p); err != nil { - return nil, err - } - - output = append(output, string(buffer.Bytes()[bufferIndex:bufferIndex+bytesRead])) - bufferIndex += bytesRead - } - - return output, nil -} - -func TestHTTPSyncResponseMultipart(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - require.NoError(t, err) - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost - sync_response: - headers: - Content-Type: application/json -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - t.Cleanup(func() { - server.Close() - }) - - input := []string{ - `{"foo":"test message 1","field1":"bar"}`, - `{"foo":"test message 2","field1":"baz"}`, - `{"foo":"test message 3","field1":"buz"}`, - } - output := []string{ - `{"foo":"test message 4","field1":"bar"}`, - `{"foo":"test message 5","field1":"baz"}`, - `{"foo":"test message 6","field1":"buz"}`, - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - - hdr, body, err := createMultipart(input, "application/octet-stream") - require.NoError(t, err) - - res, err := http.Post(server.URL+"/testpost", hdr, bytes.NewReader(body)) - require.NoError(t, err) - require.Equal(t, 200, res.StatusCode) - - act, err := readMultipart(res) - require.NoError(t, err) - assert.Equal(t, output, act) - }() - - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - for i, in := range input { - assert.Equal(t, in, string(ts.Payload.Get(i).AsBytes())) - } - for i, o := range output { - ts.Payload.Get(i).SetBytes([]byte(o)) - } - require.NoError(t, transaction.SetAsResponse(ts.Payload)) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - h.TriggerStopConsuming() - err = h.WaitForClose(tCtx) - require.NoError(t, err) - - wg.Wait() -} - -func TestHTTPSyncResponseHeadersStatus(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - reg := apiRegGorillaMutWrapper{mut: mux.NewRouter()} - mgr, err := manager.New(manager.ResourceConfig{}, manager.OptSetAPIReg(reg)) - if err != nil { - t.Fatal(err) - } - - conf := parseYAMLInputConf(t, ` -http_server: - path: /testpost - sync_response: - status: '${! meta("status").or("200") }' - headers: - Content-Type: application/json - foo: '${!json("field1")}' -`) - - h, err := mgr.NewInput(conf) - require.NoError(t, err) - - server := httptest.NewServer(reg.mut) - defer server.Close() - - input := `{"foo":"test message","field1":"bar"}` - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - - res, err := http.Post( - server.URL+"/testpost", - "application/octet-stream", - bytes.NewBufferString(input), - ) - if err != nil { - t.Error(err) - } else if res.StatusCode != 200 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - } - resBytes, err := io.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - assert.JSONEq(t, input, string(resBytes)) - if exp, act := "application/json", res.Header.Get("Content-Type"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - if exp, act := "bar", res.Header.Get("foo"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - - res, err = http.Post( - server.URL+"/testpost", - "application/octet-stream", - bytes.NewBufferString(input), - ) - if err != nil { - t.Error(err) - } else if res.StatusCode != 400 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - } - resBytes, err = io.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - assert.JSONEq(t, input, string(resBytes)) - if exp, act := "application/json", res.Header.Get("Content-Type"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - if exp, act := "bar", res.Header.Get("foo"); exp != act { - t.Errorf("Wrong sync response header: %v != %v", act, exp) - } - }() - - // Non errored message - var ts message.Transaction - select { - case ts = <-h.TransactionChan(): - if res := string(ts.Payload.Get(0).AsBytes()); res != input { - t.Errorf("Wrong result, %v != %v", ts.Payload, res) - } - require.NoError(t, transaction.SetAsResponse(ts.Payload)) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - // Errored message - select { - case ts = <-h.TransactionChan(): - if res := string(ts.Payload.Get(0).AsBytes()); res != input { - t.Errorf("Wrong result, %v != %v", ts.Payload, res) - } - ts.Payload.Get(0).MetaSetMut("status", "400") - require.NoError(t, transaction.SetAsResponse(ts.Payload)) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for message") - } - require.NoError(t, ts.Ack(tCtx, nil)) - - h.TriggerStopConsuming() - if err := h.WaitForClose(tCtx); err != nil { - t.Error(err) - } - - wg.Wait() -} - -func TestHTTPServerInputEnableCORSOrigins(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - freePort := getFreePort(t) - - conf := parseYAMLInputConf(t, ` -http_server: - address: 0.0.0.0:%v - path: /test/{foo}/{bar} - allowed_verbs: [ POST ] - cors: - enabled: true - allowed_origins: [ foo, bar ] -`, freePort) - - server, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - defer func() { - server.TriggerStopConsuming() - assert.NoError(t, server.WaitForClose(tCtx)) - }() - - var resp *http.Response - require.Eventually(t, func() (succeeded bool) { - req, cerr := http.NewRequest("OPTIONS", fmt.Sprintf("http://localhost:%v/test/foo1/bar1", freePort), http.NoBody) - require.NoError(t, cerr) - - req.Header.Add("Origin", "foo") - req.Header.Add("Access-Control-Request-Method", "POST") - req.Header.Set("Content-Type", "text/plain") - - if resp, cerr = http.DefaultClient.Do(req); cerr == nil { - succeeded = true - resp.Body.Close() - } - return - }, time.Second, 50*time.Millisecond) - - assert.Equal(t, "200 OK", resp.Status) - assert.Equal(t, "foo", resp.Header.Get("Access-Control-Allow-Origin")) -} diff --git a/internal/impl/io/input_socket.go b/internal/impl/io/input_socket.go deleted file mode 100644 index f23f4634d3..0000000000 --- a/internal/impl/io/input_socket.go +++ /dev/null @@ -1,151 +0,0 @@ -package io - -import ( - "context" - "errors" - "io" - "net" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" -) - -const ( - isFieldNetwork = "network" - isFieldAddress = "address" -) - -func socketInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary(`Connects to a tcp or unix socket and consumes a continuous stream of messages.`). - Categories("Network"). - Fields( - service.NewStringEnumField(isFieldNetwork, "unix", "tcp"). - Description("A network type to assume (unix|tcp)."), - service.NewStringField(isFieldAddress). - Description("The address to connect to."). - Examples("/tmp/benthos.sock", "127.0.0.1:6000"), - service.NewAutoRetryNacksToggleField(), - ). - Fields(codec.DeprecatedCodecFields("lines")...) -} - -func init() { - err := service.RegisterBatchInput("socket", socketInputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - i, err := newSocketReaderFromParsed(conf, mgr) - if err != nil { - return nil, err - } - // TODO: Inject async cut off? - return service.AutoRetryNacksBatchedToggled(conf, i) - }) - if err != nil { - panic(err) - } -} - -type socketReader struct { - log *service.Logger - - address string - network string - codecCtor codec.DeprecatedFallbackCodec - - codecMut sync.Mutex - codec codec.DeprecatedFallbackStream -} - -func newSocketReaderFromParsed(pConf *service.ParsedConfig, mgr *service.Resources) (rdr *socketReader, err error) { - rdr = &socketReader{ - log: mgr.Logger(), - } - if rdr.address, err = pConf.FieldString(isFieldAddress); err != nil { - return - } - if rdr.network, err = pConf.FieldString(isFieldNetwork); err != nil { - return - } - if rdr.codecCtor, err = codec.DeprecatedCodecFromParsed(pConf); err != nil { - return - } - return -} - -func (s *socketReader) Connect(ctx context.Context) error { - s.codecMut.Lock() - defer s.codecMut.Unlock() - - if s.codec != nil { - return nil - } - - conn, err := net.Dial(s.network, s.address) - if err != nil { - return err - } - - if s.codec, err = s.codecCtor.Create(conn, func(ctx context.Context, err error) error { - return nil - }, service.NewScannerSourceDetails()); err != nil { - conn.Close() - return err - } - return nil -} - -func (s *socketReader) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - s.codecMut.Lock() - codec := s.codec - s.codecMut.Unlock() - - if codec == nil { - return nil, nil, service.ErrNotConnected - } - - parts, codecAckFn, err := codec.NextBatch(ctx) - if err != nil { - if errors.Is(err, context.Canceled) || - errors.Is(err, context.DeadlineExceeded) { - err = component.ErrTimeout - } - if err != component.ErrTimeout { - s.codecMut.Lock() - if s.codec != nil && s.codec == codec { - s.codec.Close(ctx) - s.codec = nil - } - s.codecMut.Unlock() - } - if errors.Is(err, io.EOF) { - return nil, nil, component.ErrTimeout - } - return nil, nil, err - } - - // We simply bounce rejected messages in a loop downstream so there's no - // benefit to aggregating acks. - _ = codecAckFn(context.Background(), nil) - - if len(parts) == 0 { - return nil, nil, component.ErrTimeout - } - - return parts, func(rctx context.Context, res error) error { - return nil - }, nil -} - -func (s *socketReader) Close(ctx context.Context) (err error) { - s.codecMut.Lock() - defer s.codecMut.Unlock() - - if s.codec != nil { - err = s.codec.Close(ctx) - s.codec = nil - } - - return -} diff --git a/internal/impl/io/input_socket_server.go b/internal/impl/io/input_socket_server.go deleted file mode 100644 index 23aa877bd9..0000000000 --- a/internal/impl/io/input_socket_server.go +++ /dev/null @@ -1,377 +0,0 @@ -package io - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "io" - "math/big" - "net" - "strings" - "sync" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" -) - -const ( - issFieldNetwork = "network" - issFieldAddress = "address" - issFieldAddressCache = "address_cache" - issFieldTLS = "tls" - issFieldTLSCertFile = "cert_file" - issFieldTLSKeyFile = "key_file" - issFieldTLSSelfSigned = "self_signed" -) - -func socketServerInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary(`Creates a server that receives a stream of messages over a TCP, UDP or Unix socket.`). - Categories("Network"). - Fields( - service.NewStringEnumField(issFieldNetwork, "unix", "tcp", "udp", "tls"). - Description("A network type to accept."), - service.NewStringField(isFieldAddress). - Description("The address to listen from."). - Examples("/tmp/benthos.sock", "0.0.0.0:6000"), - service.NewStringField(issFieldAddressCache). - Description("An optional xref:components:caches/about.adoc[`cache`] within which this input should write it's bound address once known. The key of the cache item containing the address will be the label of the component suffixed with `_address` (e.g. `foo_address`), or `socket_server_address` when a label has not been provided. This is useful in situations where the address is dynamically allocated by the server (`127.0.0.1:0`) and you want to store the allocated address somewhere for reference by other systems and components."). - Optional(). - Version("4.25.0"), - service.NewObjectField(issFieldTLS, - service.NewStringField(issFieldTLSCertFile). - Description("PEM encoded certificate for use with TLS."). - Optional(), - service.NewStringField(issFieldTLSKeyFile). - Description("PEM encoded private key for use with TLS."). - Optional(), - service.NewBoolField(issFieldTLSSelfSigned). - Description("Whether to generate self signed certificates."). - Default(false), - ). - Description("TLS specific configuration, valid when the `network` is set to `tls`."). - Optional(), - service.NewAutoRetryNacksToggleField(), - ). - Fields(codec.DeprecatedCodecFields("lines")...) -} - -func init() { - err := service.RegisterBatchInput("socket_server", socketServerInputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - i, err := newSocketServerInputFromParsed(conf, mgr) - if err != nil { - return nil, err - } - return service.AutoRetryNacksBatchedToggled(conf, i) - }) - if err != nil { - panic(err) - } -} - -type wrapPacketConn struct { - net.PacketConn -} - -func (w *wrapPacketConn) Read(p []byte) (n int, err error) { - n, _, err = w.ReadFrom(p) - return -} - -type socketServerInput struct { - log *service.Logger - mgr *service.Resources - - network string - address string - addressCache string - tlsCert string - tlsKey string - tlsSelfSigned bool - codecCtor codec.DeprecatedFallbackCodec - - messages chan service.MessageBatch - shutSig *shutdown.Signaller -} - -func newSocketServerInputFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (i *socketServerInput, err error) { - t := socketServerInput{ - log: mgr.Logger(), - mgr: mgr, - shutSig: shutdown.NewSignaller(), - messages: make(chan service.MessageBatch), - } - - if t.network, err = conf.FieldString(issFieldNetwork); err != nil { - return - } - if t.address, err = conf.FieldString(issFieldAddress); err != nil { - return - } - t.addressCache, _ = conf.FieldString(issFieldAddressCache) - - tlsConf := conf.Namespace(issFieldTLS) - t.tlsCert, _ = tlsConf.FieldString(issFieldTLSCertFile) - t.tlsKey, _ = tlsConf.FieldString(issFieldTLSKeyFile) - t.tlsSelfSigned, _ = tlsConf.FieldBool(issFieldTLSSelfSigned) - - if t.codecCtor, err = codec.DeprecatedCodecFromParsed(conf); err != nil { - return - } - return &t, nil -} - -func (t *socketServerInput) Connect(ctx context.Context) error { - var ln net.Listener - var cn net.PacketConn - - var err error - switch t.network { - case "tcp", "unix": - ln, err = net.Listen(t.network, t.address) - case "tls": - var cert tls.Certificate - if cert, err = loadOrCreateCertificate(t.tlsCert, t.tlsKey, t.tlsSelfSigned); err != nil { - return err - } - config := &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - ln, err = tls.Listen("tcp", t.address, config) - case "udp": - cn, err = net.ListenPacket(t.network, t.address) - default: - return fmt.Errorf("socket network '%v' is not supported by this input", t.network) - } - if err != nil { - return err - } - - if ln == nil { - go t.udpLoop(cn) - } else { - go t.loop(ln) - } - - var addr net.Addr - if ln != nil { - addr = ln.Addr() - t.log.Infof("Receiving %v socket messages from address: %v", t.network, addr.String()) - } else { - addr = cn.LocalAddr() - t.log.Infof("Receiving udp socket messages from address: %v", addr.String()) - } - if t.addressCache != "" { - key := "socket_server_address" - if l := t.mgr.Label(); l != "" { - key = l + "_address" - } - _ = t.mgr.AccessCache(ctx, t.addressCache, func(c service.Cache) { - if err := c.Set(ctx, key, []byte(addr.String()), nil); err != nil { - t.log.Errorf("Failed to set address in cache: %v", err) - } - }) - } - return nil -} - -func (t *socketServerInput) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - select { - case b, open := <-t.messages: - if open { - return b, func(ctx context.Context, err error) error { - return nil - }, nil - } - return nil, nil, service.ErrEndOfInput - case <-ctx.Done(): - return nil, nil, ctx.Err() - } -} - -func (t *socketServerInput) loop(listener net.Listener) { - var wg sync.WaitGroup - - defer func() { - wg.Wait() - _ = listener.Close() - close(t.messages) - t.shutSig.TriggerHasStopped() - }() - - go func() { - <-t.shutSig.SoftStopChan() - _ = listener.Close() - }() - - closeCtx, done := t.shutSig.SoftStopCtx(context.Background()) - defer done() - -acceptLoop: - for { - conn, err := listener.Accept() - if err != nil { - if !strings.Contains(err.Error(), "use of closed network connection") { - t.log.Errorf("Failed to accept Socket connection: %v", err) - } - select { - case <-time.After(time.Second): - continue acceptLoop - case <-t.shutSig.SoftStopChan(): - return - } - } - - go func() { - <-t.shutSig.SoftStopChan() - _ = conn.Close() - }() - - wg.Add(1) - go func(c net.Conn) { - defer func() { - _ = c.Close() - wg.Done() - }() - - codec, err := t.codecCtor.Create(c, func(ctx context.Context, err error) error { - return nil - }, service.NewScannerSourceDetails()) - if err != nil { - t.log.Errorf("Failed to create codec for new connection: %v", err) - return - } - - for { - parts, ackFn, err := codec.NextBatch(closeCtx) - if err != nil { - if !errors.Is(err, io.EOF) { - t.log.Errorf("Connection dropped due to: %v\n", err) - } - return - } - - // We simply bounce rejected messages in a loop downstream so - // there's no benefit to aggregating acks. - _ = ackFn(closeCtx, nil) - - select { - case t.messages <- parts: - case <-t.shutSig.SoftStopChan(): - return - } - } - }(conn) - } -} - -func (t *socketServerInput) udpLoop(conn net.PacketConn) { - defer func() { - _ = conn.Close() - close(t.messages) - t.shutSig.TriggerHasStopped() - }() - - go func() { - <-t.shutSig.SoftStopChan() - _ = conn.Close() - }() - - closeCtx, done := t.shutSig.SoftStopCtx(context.Background()) - defer done() - - codec, err := t.codecCtor.Create(&wrapPacketConn{PacketConn: conn}, func(ctx context.Context, err error) error { - return nil - }, service.NewScannerSourceDetails()) - if err != nil { - t.log.Errorf("Connection error due to: %v", err) - return - } - - for { - parts, ackFn, err := codec.NextBatch(closeCtx) - if err != nil { - if err != io.EOF && err != component.ErrTimeout { - t.log.Errorf("Connection dropped due to: %v", err) - } - return - } - - // We simply bounce rejected messages in a loop downstream so - // there's no benefit to aggregating acks. - _ = ackFn(closeCtx, nil) - - select { - case t.messages <- parts: - case <-t.shutSig.SoftStopChan(): - return - } - } -} - -func (t *socketServerInput) Close(ctx context.Context) error { - t.shutSig.TriggerSoftStop() - select { - case <-t.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -//------------------------------------------------------------------------------ - -func createSelfSignedCertificate() (tls.Certificate, error) { - priv, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - certOptions := &x509.Certificate{ - SerialNumber: &big.Int{}, - } - - certBytes, _ := x509.CreateCertificate(rand.Reader, certOptions, certOptions, &priv.PublicKey, priv) - pemcert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) - key, err := x509.MarshalECPrivateKey(priv) - if err != nil { - return tls.Certificate{}, err - } - - keyBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: key}) - cert, err := tls.X509KeyPair(pemcert, keyBytes) - if err != nil { - return tls.Certificate{}, err - } - - return cert, nil -} - -func loadOrCreateCertificate(certFile, keyFile string, selfSigned bool) (tls.Certificate, error) { - var cert tls.Certificate - var err error - if certFile != "" && keyFile != "" { - cert, err = tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return tls.Certificate{}, err - } - return cert, nil - } - if !selfSigned { - return tls.Certificate{}, errors.New("must specify either a certificate file or enable self signed") - } - - // Either CertFile or KeyFile was not specified, so make our own certificate - cert, err = createSelfSignedCertificate() - if err != nil { - return tls.Certificate{}, err - } - return cert, nil -} diff --git a/internal/impl/io/input_socket_server_test.go b/internal/impl/io/input_socket_server_test.go deleted file mode 100644 index a9bf885c6e..0000000000 --- a/internal/impl/io/input_socket_server_test.go +++ /dev/null @@ -1,1272 +0,0 @@ -package io_test - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "path/filepath" - "sort" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func socketServerInputFromConf(t testing.TB, confStr string, bits ...any) (input.Streamed, string) { - t.Helper() - - mgr := mock.NewManager() - mgr.Caches["testcache"] = map[string]mock.CacheItem{} - - conf, err := testutil.InputFromYAML(fmt.Sprintf(confStr+"\n address_cache: testcache", bits...)) - require.NoError(t, err) - - s, err := mgr.NewInput(conf) - require.NoError(t, err) - - addr := "" - require.Eventually(t, func() bool { - _ = mgr.AccessCache(context.Background(), "testcache", func(v cache.V1) { - res, _ := v.Get(context.Background(), "socket_server_address") - addr = string(res) - }) - return addr != "" - }, time.Second, time.Millisecond*10) - - return s, addr -} - -func TestSocketServerBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v -`, filepath.Join(tmpDir, "benthos.sock")) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestSocketServerRetries(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v -`, filepath.Join(tmpDir, "benthos.sock")) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - wg.Done() - }() - - readNextMsg := func(reject bool) (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - var res error - if reject { - res = errors.New("test err") - } - require.NoError(t, tran.Ack(ctx, res)) - case <-time.After(time.Second * 5): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg(false) - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg(true) - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - expRemaining := []string{"bar", "baz"} - actRemaining := []string{} - - msg, err = readNextMsg(false) - require.NoError(t, err) - require.Equal(t, 1, msg.Len()) - actRemaining = append(actRemaining, string(msg.Get(0).AsBytes())) - - msg, err = readNextMsg(false) - require.NoError(t, err) - require.Equal(t, 1, msg.Len()) - actRemaining = append(actRemaining, string(msg.Get(0).AsBytes())) - - sort.Strings(actRemaining) - assert.Equal(t, expRemaining, actRemaining) - - wg.Wait() - conn.Close() -} - -func TestSocketServerWriteClosed(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v -`, filepath.Join(tmpDir, "benthos.sock")) - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - - _, cerr := conn.Write([]byte("bar\n")) - require.Error(t, cerr) - - _, open := <-rdr.TransactionChan() - assert.False(t, open) - - conn.Close() -} - -func TestSocketServerRecon(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v -`, filepath.Join(tmpDir, "benthos.sock")) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - conn.Close() - conn, cerr = net.Dial("unix", addr) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - expMsgs := map[string]struct{}{ - "foo": {}, - "bar": {}, - "baz": {}, - } - - for i := 0; i < 3; i++ { - msg, err := readNextMsg() - require.NoError(t, err) - - act := string(msg.Get(0).AsBytes()) - assert.Contains(t, expMsgs, act) - - delete(expMsgs, act) - } - - wg.Wait() - conn.Close() -} - -func TestSocketServerMpart(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v - codec: lines/multipart -`, filepath.Join(tmpDir, "benthos.sock")) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestSocketServerMpartCDelim(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v - codec: delim:@/multipart -`, filepath.Join(tmpDir, "b.sock")) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n@@")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz\n")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestSocketServerMpartSdown(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: unix - address: %v - codec: lines/multipart -`, filepath.Join(tmpDir, "b.sock")) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("unix", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - conn.Close() - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() -} - -func TestSocketUDPServerBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: udp - address: 127.0.0.1:0 -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("udp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestSocketUDPServerRetries(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: udp - address: 127.0.0.1:0 -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("udp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func(reject bool) (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - var res error - if reject { - res = errors.New("test err") - } - require.NoError(t, tran.Ack(ctx, res)) - case <-time.After(time.Second * 5): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg(false) - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg(true) - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - expRemaining := []string{"bar", "baz"} - actRemaining := []string{} - - msg, err = readNextMsg(false) - require.NoError(t, err) - require.Equal(t, 1, msg.Len()) - actRemaining = append(actRemaining, string(msg.Get(0).AsBytes())) - - msg, err = readNextMsg(false) - require.NoError(t, err) - require.Equal(t, 1, msg.Len()) - actRemaining = append(actRemaining, string(msg.Get(0).AsBytes())) - - sort.Strings(actRemaining) - assert.Equal(t, expRemaining, actRemaining) - - wg.Wait() - conn.Close() -} - -func TestUDPServerWriteToClosed(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: udp - address: 127.0.0.1:0 -`) - - conn, err := net.Dial("udp", addr) - require.NoError(t, err) - - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - - // Just make sure data written doesn't panic - _, _ = conn.Write([]byte("bar\n")) - - _, open := <-rdr.TransactionChan() - assert.False(t, open) - - conn.Close() -} - -func TestSocketUDPServerReconnect(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: udp - address: 127.0.0.1:0 -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("udp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - conn.Close() - - conn, cerr = net.Dial("udp", addr) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestSocketUDPServerCustomDelim(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: udp - address: 127.0.0.1:0 - codec: delim:@ -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("udp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n@@")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz\n")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestSocketUDPServerShutdown(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: udp - address: 127.0.0.1:0 -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("udp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - conn.Close() - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() -} - -func TestTCPSocketServerBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: tcp - address: 127.0.0.1:0 -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("tcp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestTCPSocketServerReconnect(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: tcp - address: 127.0.0.1:0 -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("tcp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - conn.Close() - - conn, cerr = net.Dial("tcp", addr) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - expMsgs := map[string]struct{}{ - "foo": {}, - "bar": {}, - "baz": {}, - } - - for i := 0; i < 3; i++ { - msg, err := readNextMsg() - require.NoError(t, err) - - act := string(msg.Get(0).AsBytes()) - assert.Contains(t, expMsgs, act) - delete(expMsgs, act) - } - - wg.Wait() - conn.Close() -} - -func TestTCPSocketServerMultipart(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: tcp - address: 127.0.0.1:0 - codec: lines/multipart -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("tcp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestTCPSocketServerMultipartCustomDelim(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: tcp - address: 127.0.0.1:0 - codec: delim:@/multipart -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("tcp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("@")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n@@")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz\n")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} - -func TestTCPSocketServerMultipartShutdown(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: tcp - address: 127.0.0.1:0 - codec: lines/multipart -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }() - - conn, err := net.Dial("tcp", addr) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - conn.Close() - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() -} - -func TestTLSSocketServerBasic(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - rdr, addr := socketServerInputFromConf(t, ` -socket_server: - network: tls - address: 127.0.0.1:0 - tls: - self_signed: true -`) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(tCtx)) - }() - - conn, err := tls.Dial("tcp", addr, &tls.Config{ - InsecureSkipVerify: true, - }) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - - _, cerr := conn.Write([]byte("foo\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("bar\n")) - require.NoError(t, cerr) - - _, cerr = conn.Write([]byte("baz\n")) - require.NoError(t, cerr) - - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var tran message.Transaction - select { - case tran = <-rdr.TransactionChan(): - require.NoError(t, tran.Ack(tCtx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return tran.Payload, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("bar")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - exp = [][]byte{[]byte("baz")} - msg, err = readNextMsg() - require.NoError(t, err) - assert.Equal(t, exp, message.GetAllBytes(msg)) - - wg.Wait() - conn.Close() -} diff --git a/internal/impl/io/input_socket_test.go b/internal/impl/io/input_socket_test.go deleted file mode 100644 index 5ba5c65a6c..0000000000 --- a/internal/impl/io/input_socket_test.go +++ /dev/null @@ -1,1021 +0,0 @@ -package io - -import ( - "context" - "errors" - "fmt" - "net" - "path/filepath" - "reflect" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func inputFromConf(t testing.TB, confStr string, bits ...any) input.Streamed { - t.Helper() - - conf, err := testutil.InputFromYAML(fmt.Sprintf(confStr, bits...)) - require.NoError(t, err) - - s, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - return s -} - -func TestSocketInputBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - ln, err := net.Listen("unix", filepath.Join(tmpDir, "benthos.sock")) - if err != nil { - t.Fatalf("failed to listen on a address: %v", err) - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: %v - address: %v -`, ln.Addr().Network(), ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("bar")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestSocketInputReconnect(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - ln, err := net.Listen("unix", filepath.Join(tmpDir, "benthos.sock")) - if err != nil { - t.Fatalf("failed to listen on address: %v", err) - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: %v - address: %v -`, ln.Addr().Network(), ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - if cerr != nil { - t.Error(cerr) - } - conn.Close() - conn, cerr = ln.Accept() - require.NoError(t, cerr) - - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("bar")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestSocketInputMultipart(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - ln, err := net.Listen("unix", filepath.Join(tmpDir, "benthos.sock")) - if err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: %v - address: %v - codec: lines/multipart -`, ln.Addr().Network(), ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n\n")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestSocketMultipartCustomDelim(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - ln, err := net.Listen("unix", filepath.Join(tmpDir, "b.sock")) - if err != nil { - t.Fatalf("failed to listen on address: %v", err) - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: %v - address: %v - codec: delim:@/multipart -`, ln.Addr().Network(), ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo@")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar@")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("@")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n@@")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz\n")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestSocketMultipartShutdown(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - tmpDir := t.TempDir() - - ln, err := net.Listen("unix", filepath.Join(tmpDir, "benthos.sock")) - if err != nil { - t.Fatalf("failed to listen on address: %v", err) - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: %v - address: %v - codec: lines/multipart -`, ln.Addr().Network(), ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n")); cerr != nil { - t.Error(cerr) - } - conn.Close() - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out on read") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() -} - -func TestTCPSocketInputBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: tcp - address: %v -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("bar")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestTCPSocketReconnect(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: tcp - address: %v -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _, cerr := conn.Write([]byte("foo\n")) - if cerr != nil { - t.Error(cerr) - } - conn.Close() - conn, cerr = ln.Accept() - if cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("bar")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestTCPSocketInputMultipart(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: tcp - address: %v - codec: lines/multipart -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n\n")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestTCPSocketMultipartCustomDelim(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: tcp - address: %v - codec: delim:@/multipart -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo@")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar@")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("@")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n@@")); cerr != nil { - t.Error(cerr) - } - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz\n")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() - conn.Close() -} - -func TestTCPSocketMultipartShutdown(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer ln.Close() - - rdr := inputFromConf(t, ` -socket: - network: tcp - address: %v - codec: lines/multipart -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - if err := rdr.WaitForClose(ctx); err != nil { - t.Error(err) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - if _, cerr := conn.Write([]byte("foo\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("bar\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("\n")); cerr != nil { - t.Error(cerr) - } - if _, cerr := conn.Write([]byte("baz\n")); cerr != nil { - t.Error(cerr) - } - conn.Close() - wg.Done() - }() - - readNextMsg := func() (message.Batch, error) { - var msg message.Batch - select { - case tran := <-rdr.TransactionChan(): - msg = tran.Payload.DeepCopy() - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Second): - return nil, errors.New("timed out on read") - } - return msg, nil - } - - exp := [][]byte{[]byte("foo"), []byte("bar")} - msg, err := readNextMsg() - if err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - exp = [][]byte{[]byte("baz")} - if msg, err = readNextMsg(); err != nil { - t.Fatal(err) - } - if act := message.GetAllBytes(msg); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message contents: %s != %s", act, exp) - } - - wg.Wait() -} - -func BenchmarkTCPSocketWithCutOff(b *testing.B) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - ln, err = net.Listen("tcp6", "[::1]:0") - require.NoError(b, err) - } - b.Cleanup(func() { - ln.Close() - }) - - rdr := inputFromConf(b, ` -socket: - network: tcp - address: %v -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(b, rdr.WaitForClose(ctx)) - }() - - conn, err := ln.Accept() - require.NoError(b, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 60)) - for i := 0; i < b.N; i++ { - _, cerr := fmt.Fprintf(conn, "hello world this is message %v\n", i) - assert.NoError(b, cerr) - } - wg.Done() - }() - - readNextMsg := func() (string, error) { - var payload string - select { - case tran := <-rdr.TransactionChan(): - payload = string(tran.Payload.Get(0).AsBytes()) - go func() { - require.NoError(b, tran.Ack(ctx, nil)) - }() - case <-time.After(time.Second): - return "", errors.New("timed out") - } - return payload, nil - } - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - exp := fmt.Sprintf("hello world this is message %v", i) - act, err := readNextMsg() - assert.NoError(b, err) - assert.Equal(b, exp, act) - } - - wg.Wait() - conn.Close() -} - -func BenchmarkTCPSocketNoCutOff(b *testing.B) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - ln, err = net.Listen("tcp6", "[::1]:0") - require.NoError(b, err) - } - b.Cleanup(func() { - ln.Close() - }) - - rdr := inputFromConf(b, ` -socket: - network: tcp - address: %v -`, ln.Addr().String()) - - defer func() { - rdr.TriggerStopConsuming() - assert.NoError(b, rdr.WaitForClose(ctx)) - }() - - conn, err := ln.Accept() - require.NoError(b, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 60)) - for i := 0; i < b.N; i++ { - _, cerr := fmt.Fprintf(conn, "hello world this is message %v\n", i) - assert.NoError(b, cerr) - } - wg.Done() - }() - - readNextMsg := func() (string, error) { - var payload string - select { - case tran := <-rdr.TransactionChan(): - payload = string(tran.Payload.Get(0).AsBytes()) - go func() { - require.NoError(b, tran.Ack(ctx, nil)) - }() - case <-time.After(time.Second): - return "", errors.New("timed out") - } - return payload, nil - } - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - exp := fmt.Sprintf("hello world this is message %v", i) - act, err := readNextMsg() - assert.NoError(b, err) - assert.Equal(b, exp, act) - } - - wg.Wait() - conn.Close() -} diff --git a/internal/impl/io/input_stdin.go b/internal/impl/io/input_stdin.go deleted file mode 100644 index 9e7834a1cb..0000000000 --- a/internal/impl/io/input_stdin.go +++ /dev/null @@ -1,92 +0,0 @@ -package io - -import ( - "context" - "errors" - "io" - "os" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" -) - -// TODO: Fan this out when appropriate? -func getStdinReader() io.ReadCloser { - return io.NopCloser(os.Stdin) -} - -func init() { - err := service.RegisterBatchInput( - "stdin", service.NewConfigSpec(). - Stable(). - Categories("Local"). - Summary(`Consumes data piped to stdin, chopping it into individual messages according to the specified scanner.`). - Fields(codec.DeprecatedCodecFields("lines")...).Field(service.NewAutoRetryNacksToggleField()), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - rdr, err := newStdinConsumerFromParsed(conf) - if err != nil { - return nil, err - } - return service.AutoRetryNacksBatchedToggled(conf, rdr) - }) - if err != nil { - panic(err) - } -} - -type stdinConsumer struct { - scanner codec.DeprecatedFallbackStream -} - -func newStdinConsumerFromParsed(conf *service.ParsedConfig) (*stdinConsumer, error) { - c, err := codec.DeprecatedCodecFromParsed(conf) - if err != nil { - return nil, err - } - - s, err := c.Create(getStdinReader(), func(_ context.Context, err error) error { - return nil - }, service.NewScannerSourceDetails()) - if err != nil { - return nil, err - } - return &stdinConsumer{scanner: s}, nil -} - -func (s *stdinConsumer) Connect(ctx context.Context) error { - return nil -} - -func (s *stdinConsumer) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - parts, codecAckFn, err := s.scanner.NextBatch(ctx) - if err != nil { - if errors.Is(err, context.Canceled) || - errors.Is(err, context.DeadlineExceeded) { - err = component.ErrTimeout - } - if err != component.ErrTimeout { - s.scanner.Close(ctx) - } - if errors.Is(err, io.EOF) { - return nil, nil, service.ErrEndOfInput - } - return nil, nil, err - } - _ = codecAckFn(ctx, nil) - - if len(parts) == 0 { - return nil, nil, component.ErrTimeout - } - - return parts, func(rctx context.Context, res error) error { - return nil - }, nil -} - -func (s *stdinConsumer) Close(ctx context.Context) (err error) { - if s.scanner != nil { - err = s.scanner.Close(ctx) - } - return -} diff --git a/internal/impl/io/input_stdin_test.go b/internal/impl/io/input_stdin_test.go deleted file mode 100644 index 90059dff83..0000000000 --- a/internal/impl/io/input_stdin_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package io_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -func TestSTDINClose(t *testing.T) { - conf := input.NewConfig() - conf.Type = "stdin" - s, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - s.TriggerStopConsuming() - require.NoError(t, s.WaitForClose(ctx)) -} diff --git a/internal/impl/io/input_subprocess.go b/internal/impl/io/input_subprocess.go deleted file mode 100644 index 0d8aea30a2..0000000000 --- a/internal/impl/io/input_subprocess.go +++ /dev/null @@ -1,252 +0,0 @@ -package io - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "os/exec" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - spiFieldName = "name" - spiFieldArgs = "args" - spiFieldCodec = "codec" - spiFieldRestartOnExit = "restart_on_exit" - spiFieldMaxBuffer = "max_buffer" -) - -func subprocInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Beta(). - Categories("Utility"). - Summary("Executes a command, runs it as a subprocess, and consumes messages from it over stdout."). - Description(` -Messages are consumed according to a specified codec. The command is executed once and if it terminates the input also closes down gracefully. Alternatively, the field `+"`restart_on_close` can be set to `true`"+` in order to have Benthos re-execute the command each time it stops. - -The field `+"`max_buffer`"+` defines the maximum message size able to be read from the subprocess. This value should be set significantly above the real expected maximum message size. - -The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory.`). - Fields( - service.NewStringField(spiFieldName). - Description("The command to execute as a subprocess."). - Examples("cat", "sed", "awk"), - service.NewStringListField(spiFieldArgs). - Description("A list of arguments to provide the command."). - Default([]any{}), - service.NewStringEnumField(spiFieldCodec, "lines"). - Description("The way in which messages should be consumed from the subprocess."). - Default("lines"), - service.NewBoolField(spiFieldRestartOnExit). - Description("Whether the command should be re-executed each time the subprocess ends."). - Default(false), - service.NewIntField(spiFieldMaxBuffer). - Description("The maximum expected size of an individual message."). - Advanced(). - Default(bufio.MaxScanTokenSize), - ) -} - -func init() { - err := service.RegisterBatchInput("subprocess", subprocInputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - return newSubprocessReaderFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type inputSubprocScanner interface { - Bytes() []byte - Text() string - Err() error - Scan() bool -} - -func linesSubprocInputCodec(maxBuf int, stdout, stderr io.Reader) (outScanner, errScanner inputSubprocScanner) { - outScanner = bufio.NewScanner(stdout) - errScanner = bufio.NewScanner(stderr) - if maxBuf != bufio.MaxScanTokenSize { - outScanner.(*bufio.Scanner).Buffer([]byte{}, maxBuf) - errScanner.(*bufio.Scanner).Buffer([]byte{}, maxBuf) - } - return outScanner, errScanner -} - -type subprocInputCodec func(int, io.Reader, io.Reader) (inputSubprocScanner, inputSubprocScanner) - -func subprocInputCodecFromStr(codec string) (subprocInputCodec, error) { - // TODO: Flesh this out with more options based on s.conf.Codec. - if codec == "lines" { - return linesSubprocInputCodec, nil - } - return nil, fmt.Errorf("codec not recognised: %v", codec) -} - -//------------------------------------------------------------------------------ - -type subprocessReader struct { - name string - args []string - restartOnExit bool - maxBuf int - codec subprocInputCodec - - msgChan chan []byte - errChan chan error - - close func() - ctx context.Context -} - -func newSubprocessReaderFromParsed(conf *service.ParsedConfig) (s *subprocessReader, err error) { - s = &subprocessReader{} - s.ctx, s.close = context.WithCancel(context.Background()) - - if s.name, err = conf.FieldString(spiFieldName); err != nil { - return - } - if s.args, err = conf.FieldStringList(spiFieldArgs); err != nil { - return - } - if s.restartOnExit, err = conf.FieldBool(spiFieldRestartOnExit); err != nil { - return - } - if s.maxBuf, err = conf.FieldInt(spiFieldMaxBuffer); err != nil { - return - } - - var codecStr string - if codecStr, err = conf.FieldString(spiFieldCodec); err != nil { - return nil, err - } - - if s.codec, err = subprocInputCodecFromStr(codecStr); err != nil { - return nil, err - } - return s, nil -} - -func (s *subprocessReader) Connect(ctx context.Context) error { - if s.msgChan != nil { - return nil - } - - cmd := exec.CommandContext(s.ctx, s.name, s.args...) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - if err := cmd.Start(); err != nil { - return err - } - - msgChan := make(chan []byte) - errChan := make(chan error) - - outScanner, errScanner := s.codec(s.maxBuf, stdout, stderr) - - go func() { - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - - for outScanner.Scan() { - data := outScanner.Bytes() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - - select { - case msgChan <- dataCopy: - case <-s.ctx.Done(): - } - } - - if err := outScanner.Err(); err != nil { - select { - case errChan <- err: - case <-s.ctx.Done(): - } - } - }() - - go func() { - defer wg.Done() - - for errScanner.Scan() { - select { - case errChan <- errors.New(errScanner.Text()): - case <-s.ctx.Done(): - } - } - - if err := errScanner.Err(); err != nil { - select { - case errChan <- err: - case <-s.ctx.Done(): - } - } - }() - - wg.Wait() - close(msgChan) - close(errChan) - }() - - s.msgChan = msgChan - s.errChan = errChan - return nil -} - -func (s *subprocessReader) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - msgChan, errChan := s.msgChan, s.errChan - if msgChan == nil { - return nil, nil, service.ErrNotConnected - } - - select { - case b, open := <-msgChan: - if !open { - if s.restartOnExit { - s.msgChan = nil - s.errChan = nil - return nil, nil, service.ErrNotConnected - } - return nil, nil, service.ErrEndOfInput - } - msg := service.MessageBatch{service.NewMessage(b)} - return msg, func(context.Context, error) error { return nil }, nil - case err, open := <-errChan: - if !open { - if s.restartOnExit { - s.msgChan = nil - s.errChan = nil - return nil, nil, service.ErrNotConnected - } - return nil, nil, component.ErrTypeClosed - } - return nil, nil, err - case <-ctx.Done(): - } - - return nil, nil, component.ErrTimeout -} - -func (s *subprocessReader) Close(ctx context.Context) (err error) { - s.close() - return -} diff --git a/internal/impl/io/input_subprocess_test.go b/internal/impl/io/input_subprocess_test.go deleted file mode 100644 index 4902355319..0000000000 --- a/internal/impl/io/input_subprocess_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/io" -) - -func readMsg(t *testing.T, tranChan <-chan message.Transaction) message.Batch { - t.Helper() - - tCtx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - select { - case tran := <-tranChan: - require.NoError(t, tran.Ack(tCtx, nil)) - return tran.Payload - case <-time.After(time.Second * 5): - } - t.Fatal("timed out") - return nil -} - -func testInput(t testing.TB, confPattern string, args ...any) input.Streamed { - iConf, err := testutil.InputFromYAML(fmt.Sprintf(confPattern, args...)) - require.NoError(t, err) - - i, err := mock.NewManager().NewInput(iConf) - require.NoError(t, err) - - return i -} - -func TestSubprocessBasic(t *testing.T) { - filePath := testProgram(t, `package main - -import ( - "fmt" -) - -func main() { - fmt.Println("foo") - fmt.Println("bar") - fmt.Println("baz") -} -`) - - i := testInput(t, ` -subprocess: - name: go - args: [ "run", "%v" ] -`, filePath) - - msg := readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "foo", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "bar", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "baz", string(msg.Get(0).AsBytes())) - - select { - case _, open := <-i.TransactionChan(): - assert.False(t, open) - case <-time.After(time.Second): - t.Error("timed out") - } -} - -func TestSubprocessRestarted(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - filePath := testProgram(t, `package main - -import ( - "fmt" -) - -func main() { - fmt.Println("foo") - fmt.Println("bar") - fmt.Println("baz") -} -`) - - i := testInput(t, ` -subprocess: - name: go - args: [ "run", "%v" ] - restart_on_exit: true -`, filePath) - - msg := readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "foo", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "bar", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "baz", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "foo", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "bar", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "baz", string(msg.Get(0).AsBytes())) - - i.TriggerStopConsuming() - require.NoError(t, i.WaitForClose(ctx)) -} - -func TestSubprocessCloseInBetween(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*20) - defer done() - - filePath := testProgram(t, `package main - -import ( - "fmt" -) - -func main() { - i := 0 - for { - fmt.Printf("foo:%v\n", i) - i++ - } -} -`) - - i := testInput(t, ` -subprocess: - name: go - args: [ "run", "%v" ] -`, filePath) - - msg := readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "foo:0", string(msg.Get(0).AsBytes())) - - msg = readMsg(t, i.TransactionChan()) - assert.Equal(t, 1, msg.Len()) - assert.Equal(t, "foo:1", string(msg.Get(0).AsBytes())) - - i.TriggerStopConsuming() - require.NoError(t, i.WaitForClose(ctx)) -} diff --git a/internal/impl/io/input_websocket.go b/internal/impl/io/input_websocket.go deleted file mode 100644 index 8cd714acdf..0000000000 --- a/internal/impl/io/input_websocket.go +++ /dev/null @@ -1,232 +0,0 @@ -package io - -import ( - "context" - "crypto/tls" - "fmt" - "io/fs" - "net/http" - "net/url" - "sync" - - "github.com/gorilla/websocket" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/input/config" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -type wsOpenMsgType string - -const ( - // wsOpenMsgTypeBinary sets the type of open_message to binary. - wsOpenMsgTypeBinary wsOpenMsgType = "binary" - // wsOpenMsgTypeText sets the type of open_message to text (UTF-8 encoded text data). - wsOpenMsgTypeText wsOpenMsgType = "text" -) - -func websocketInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Network"). - Summary("Connects to a websocket server and continuously receives messages."). - Description(`It is possible to configure an `+"`open_message`"+`, which when set to a non-empty string will be sent to the websocket server each time a connection is first established.`). - Fields( - service.NewURLField("url"). - Description("The URL to connect to."). - Example("ws://localhost:4195/get/ws"), - service.NewStringField("open_message"). - Description("An optional message to send to the server upon connection."). - Advanced().Optional(), - service.NewStringAnnotatedEnumField("open_message_type", map[string]string{ - string(wsOpenMsgTypeBinary): "Binary data open_message.", - string(wsOpenMsgTypeText): "Text data open_message. The text message payload is interpreted as UTF-8 encoded text data.", - }).Description("An optional flag to indicate the data type of open_message."). - Advanced().Default(string(wsOpenMsgTypeBinary)), - service.NewAutoRetryNacksToggleField(), - service.NewTLSToggledField("tls"), - ). - Fields(config.AsyncOptsFields()...). - Fields(service.NewHTTPRequestAuthSignerFields()...) -} - -func init() { - err := service.RegisterBatchInput( - "websocket", websocketInputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (in service.BatchInput, err error) { - oldMgr := interop.UnwrapManagement(mgr) - var r input.Async - if r, err = newWebsocketReaderFromParsed(conf, oldMgr); err != nil { - return - } - - var opts []func(*input.AsyncReader) - if opts, err = config.AsyncOptsFromParsed(conf); err != nil { - return - } - - if autoRetry, _ := conf.FieldBool(service.AutoRetryNacksToggleFieldName); autoRetry { - r = input.NewAsyncPreserver(r) - } - - var i input.Streamed - if i, err = input.NewAsyncReader("websocket", r, oldMgr, opts...); err != nil { - return - } - in = interop.NewUnwrapInternalInput(i) - return - }) - if err != nil { - panic(err) - } -} - -type websocketReader struct { - log log.Modular - mgr bundle.NewManagement - - lock *sync.Mutex - - client *websocket.Conn - urlParsed *url.URL - urlStr string - tlsEnabled bool - tlsConf *tls.Config - reqSigner func(f fs.FS, req *http.Request) error - - openMsgType wsOpenMsgType - openMsg []byte -} - -func newWebsocketReaderFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (*websocketReader, error) { - ws := &websocketReader{ - log: mgr.Logger(), - mgr: mgr, - lock: &sync.Mutex{}, - } - var err error - if ws.urlParsed, err = conf.FieldURL("url"); err != nil { - return nil, err - } - if ws.urlStr, err = conf.FieldString("url"); err != nil { - return nil, err - } - if ws.tlsConf, ws.tlsEnabled, err = conf.FieldTLSToggled("tls"); err != nil { - return nil, err - } - if ws.reqSigner, err = conf.HTTPRequestAuthSignerFromParsed(); err != nil { - return nil, err - } - var openMsgStr, openMsgTypeStr string - if openMsgTypeStr, err = conf.FieldString("open_message_type"); err != nil { - return nil, err - } - ws.openMsgType = wsOpenMsgType(openMsgTypeStr) - if openMsgStr, _ = conf.FieldString("open_message"); openMsgStr != "" { - ws.openMsg = []byte(openMsgStr) - } - return ws, nil -} - -func (w *websocketReader) getWS() *websocket.Conn { - w.lock.Lock() - ws := w.client - w.lock.Unlock() - return ws -} - -func (w *websocketReader) Connect(ctx context.Context) error { - w.lock.Lock() - defer w.lock.Unlock() - - if w.client != nil { - return nil - } - - headers := http.Header{} - - err := w.reqSigner(w.mgr.FS(), &http.Request{ - URL: w.urlParsed, - Header: headers, - }) - if err != nil { - return err - } - - var ( - client *websocket.Conn - res *http.Response - ) - - defer func() { - if res != nil { - res.Body.Close() - } - }() - - if w.tlsEnabled { - dialer := websocket.Dialer{ - TLSClientConfig: w.tlsConf, - } - if client, res, err = dialer.Dial(w.urlStr, headers); err != nil { - return err - } - } else if client, res, err = websocket.DefaultDialer.Dial(w.urlStr, headers); err != nil { - return err - } - - var openMsgType int - switch w.openMsgType { - case wsOpenMsgTypeBinary: - openMsgType = websocket.BinaryMessage - case wsOpenMsgTypeText: - openMsgType = websocket.TextMessage - default: - return fmt.Errorf("unrecognised open_message_type: %s", w.openMsgType) - } - - if len(w.openMsg) > 0 { - if err := client.WriteMessage(openMsgType, w.openMsg); err != nil { - return err - } - } - - w.client = client - return nil -} - -func (w *websocketReader) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - client := w.getWS() - if client == nil { - return nil, nil, component.ErrNotConnected - } - - _, data, err := client.ReadMessage() - if err != nil { - w.lock.Lock() - w.client = nil - w.lock.Unlock() - err = component.ErrNotConnected - return nil, nil, err - } - - return message.QuickBatch([][]byte{data}), func(ctx context.Context, err error) error { - return nil - }, nil -} - -func (w *websocketReader) Close(ctx context.Context) (err error) { - w.lock.Lock() - defer w.lock.Unlock() - - if w.client != nil { - err = w.client.Close() - w.client = nil - } - return -} diff --git a/internal/impl/io/input_websocket_test.go b/internal/impl/io/input_websocket_test.go deleted file mode 100644 index 60977b837a..0000000000 --- a/internal/impl/io/input_websocket_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package io - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "sync" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestWebsocketBasic(t *testing.T) { - expMsgs := []string{ - "foo", - "bar", - "baz", - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - - defer ws.Close() - - for _, msg := range expMsgs { - if err = ws.WriteMessage(websocket.BinaryMessage, []byte(msg)); err != nil { - t.Error(err) - } - } - })) - - wsURL, err := url.Parse(server.URL) - require.NoError(t, err) - - wsURL.Scheme = "ws" - - pConf, err := websocketInputSpec().ParseYAML(fmt.Sprintf(` -url: %v -`, wsURL.String()), nil) - require.NoError(t, err) - - m, err := newWebsocketReaderFromParsed(pConf, mock.NewManager()) - require.NoError(t, err) - - ctx := context.Background() - - if err = m.Connect(ctx); err != nil { - t.Fatal(err) - } - - for _, exp := range expMsgs { - var actMsg message.Batch - if actMsg, _, err = m.ReadBatch(ctx); err != nil { - t.Error(err) - } else if act := string(actMsg.Get(0).AsBytes()); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } - } - - require.NoError(t, m.Close(ctx)) -} - -func TestWebsocketOpenMsg(t *testing.T) { - expMsgs := []string{ - "foo", - "bar", - "baz", - } - - testHandler := func(expMsgType int, w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - - defer ws.Close() - - msgType, data, err := ws.ReadMessage() - if err != nil { - t.Fatal(err) - } - if exp, act := "hello world", string(data); exp != act { - t.Errorf("Wrong open message: %v != %v", act, exp) - } - if msgType != expMsgType { - t.Errorf("Wrong open message type: %v != %v", msgType, expMsgType) - } - - for _, msg := range expMsgs { - if err = ws.WriteMessage(websocket.BinaryMessage, []byte(msg)); err != nil { - t.Error(err) - } - } - } - - tests := []struct { - handler func(expMsgType int, w http.ResponseWriter, r *http.Request) - openMsgType wsOpenMsgType - wsOpenMsgType int - errStr string - }{ - { - handler: testHandler, - openMsgType: wsOpenMsgTypeBinary, - wsOpenMsgType: websocket.BinaryMessage, - }, - { - handler: testHandler, - openMsgType: wsOpenMsgTypeText, - wsOpenMsgType: websocket.TextMessage, - }, - { - // Use a simplified handler to avoid the blocking call to `ws.ReadMessage()` when no OpenMsg gets sent - handler: func(_ int, w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - - ws.Close() - }, - openMsgType: "foobar", - errStr: "unrecognised open_message_type: foobar", - }, - } - - for id, test := range tests { - t.Run(strconv.Itoa(id), func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { test.handler(test.wsOpenMsgType, w, r) })) - t.Cleanup(server.Close) - - wsURL, err := url.Parse(server.URL) - require.NoError(t, err) - - wsURL.Scheme = "ws" - - pConf, err := websocketInputSpec().ParseYAML(fmt.Sprintf(` -url: %v -open_message: "hello world" -open_message_type: %v -`, wsURL.String(), test.openMsgType), nil) - require.NoError(t, err) - - m, err := newWebsocketReaderFromParsed(pConf, mock.NewManager()) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), 100*time.Millisecond) - t.Cleanup(func() { require.NoError(t, m.Close(ctx)) }) - t.Cleanup(done) - - if err = m.Connect(ctx); err != nil { - if test.errStr != "" { - require.ErrorContains(t, err, test.errStr) - return - } - - t.Fatal(err) - } - - for _, exp := range expMsgs { - var actMsg message.Batch - if actMsg, _, err = m.ReadBatch(ctx); err != nil { - t.Error(err) - } else if act := string(actMsg.Get(0).AsBytes()); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } - } - - require.NoError(t, m.Close(ctx)) - }) - } -} - -func TestWebsocketClose(t *testing.T) { - closeChan := make(chan struct{}) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - - defer ws.Close() - <-closeChan - })) - - wsURL, err := url.Parse(server.URL) - require.NoError(t, err) - - wsURL.Scheme = "ws" - - pConf, err := websocketInputSpec().ParseYAML(fmt.Sprintf(` -url: %v -`, wsURL.String()), nil) - require.NoError(t, err) - - m, err := newWebsocketReaderFromParsed(pConf, mock.NewManager()) - require.NoError(t, err) - - ctx := context.Background() - - if err = m.Connect(ctx); err != nil { - t.Fatal(err) - } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - require.NoError(t, m.Close(ctx)) - wg.Done() - }() - - if _, _, err = m.ReadBatch(ctx); err != component.ErrTypeClosed && err != component.ErrNotConnected { - t.Errorf("Wrong error: %v != %v", err, component.ErrTypeClosed) - } - - wg.Wait() - close(closeChan) -} diff --git a/internal/impl/io/metrics_json_api.go b/internal/impl/io/metrics_json_api.go deleted file mode 100644 index 5a80194e67..0000000000 --- a/internal/impl/io/metrics_json_api.go +++ /dev/null @@ -1,96 +0,0 @@ -package io - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterMetricsExporter("json_api", service.NewConfigSpec(). - Stable(). - Summary(`Serves metrics as JSON object with the service wide HTTP service at the endpoints `+"`/stats` and `/metrics`"+`.`). - Description(`This metrics type is useful for debugging as it provides a human readable format that you can parse with tools such as `+"`jq`"+``). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, log *service.Logger) (service.MetricsExporter, error) { - return newJSONAPI(log) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type jsonAPIMetrics struct { - local *metrics.Local - timestamp time.Time -} - -func newJSONAPI(logger *service.Logger) (*jsonAPIMetrics, error) { - return &jsonAPIMetrics{ - local: metrics.NewLocal(), - timestamp: time.Now(), - }, nil -} - -//------------------------------------------------------------------------------ - -func (h *jsonAPIMetrics) HandlerFunc() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - values := map[string]any{} - for k, v := range h.local.GetCounters() { - values[k] = v - } - for k, v := range h.local.GetTimings() { - ps := v.Percentiles([]float64{0.5, 0.9, 0.99}) - values[k] = struct { - P50 float64 `json:"p50"` - P90 float64 `json:"p90"` - P99 float64 `json:"p99"` - }{ - P50: ps[0], - P90: ps[1], - P99: ps[2], - } - } - - jBytes, err := json.Marshal(values) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(jBytes) - } -} - -func (h *jsonAPIMetrics) NewCounterCtor(path string, n ...string) service.MetricsExporterCounterCtor { - tmp := h.local.GetCounterVec(path, n...) - return func(labelValues ...string) service.MetricsExporterCounter { - return tmp.With(labelValues...) - } -} - -func (h *jsonAPIMetrics) NewTimerCtor(path string, n ...string) service.MetricsExporterTimerCtor { - tmp := h.local.GetTimerVec(path, n...) - return func(labelValues ...string) service.MetricsExporterTimer { - return tmp.With(labelValues...) - } -} - -func (h *jsonAPIMetrics) NewGaugeCtor(path string, n ...string) service.MetricsExporterGaugeCtor { - tmp := h.local.GetGaugeVec(path, n...) - return func(labelValues ...string) service.MetricsExporterGauge { - return tmp.With(labelValues...) - } -} - -func (h *jsonAPIMetrics) Close(context.Context) error { - return nil -} diff --git a/internal/impl/io/output_dynamic.go b/internal/impl/io/output_dynamic.go deleted file mode 100644 index ff1e874733..0000000000 --- a/internal/impl/io/output_dynamic.go +++ /dev/null @@ -1,210 +0,0 @@ -package io - -import ( - "context" - "path" - "sync" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/impl/pure" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - doFieldPrefix = "prefix" - doFieldOutputs = "outputs" -) - -func dynOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`A special broker type where the outputs are identified by unique labels and can be created, changed and removed during runtime via a REST API.`). - Description(`The broker pattern used is always `+"`fan_out`"+`, meaning each message will be delivered to each dynamic output.`). - Footnotes(` -== Endpoints - -=== GET `+"`/outputs`"+` - -Returns a JSON object detailing all dynamic outputs, providing information such as their current uptime and configuration. - -=== GET `+"`/outputs/\\{id}`"+` - -Returns the configuration of an output. - -=== POST `+"`/outputs/\\{id}`"+` - -Creates or updates an output with a configuration provided in the request body (in YAML or JSON format). - -=== DELETE `+"`/outputs/\\{id}`"+` - -Stops and removes an output. - -=== GET `+"`/outputs/\\{id}/uptime`"+` - -Returns the uptime of an output as a duration string (of the form "72h3m0.5s").`). - Fields( - service.NewOutputMapField(doFieldOutputs). - Description("A map of outputs to statically create."). - Default(map[string]any{}), - service.NewStringField(doFieldPrefix). - Description("A path prefix for HTTP endpoints that are registered."). - Default(""), - ) -} - -func init() { - err := service.RegisterBatchOutput("dynamic", dynOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - maxInFlight = 1 - - var o output.Streamed - if o, err = newDynamicOutputFromParsed(conf, mgr); err != nil { - return - } - - out = interop.NewUnwrapInternalOutput(o) - return - }) - if err != nil { - panic(err) - } -} - -func newDynamicOutputFromParsed(conf *service.ParsedConfig, res *service.Resources) (output.Streamed, error) { - mgr := interop.UnwrapManagement(res) - - prefix, err := conf.FieldString(doFieldPrefix) - if err != nil { - return nil, err - } - - outputsAnyMap, err := conf.FieldAnyMap(doFieldOutputs) - if err != nil { - return nil, err - } - - wOutsMap, err := conf.FieldOutputMap(doFieldOutputs) - if err != nil { - return nil, err - } - - dynAPI := api.NewDynamic() - - outputs := map[string]output.Streamed{} - for k, v := range wOutsMap { - newOutput := interop.UnwrapOwnedOutput(v) - if outputs[k], err = pure.RetryOutputIndefinitely(mgr, newOutput); err != nil { - return nil, err - } - } - - var outputConfigsMut sync.RWMutex - outputYAMLConfs := map[string][]byte{} - for k, v := range outputsAnyMap { - a, _ := v.FieldAny() - outputYAMLConfs[k] = dynOutputAnyToYAMLConf(a) - } - - fanOut, err := newDynamicFanOutOutputBroker(outputs, mgr.Logger(), - func(l string) { - outputConfigsMut.Lock() - defer outputConfigsMut.Unlock() - - confBytes, exists := outputYAMLConfs[l] - if !exists { - return - } - - dynAPI.Started(l, confBytes) - delete(outputYAMLConfs, l) - }, - func(l string) { - dynAPI.Stopped(l) - }, - ) - if err != nil { - return nil, err - } - - dynAPI.OnUpdate(func(ctx context.Context, id string, c []byte) error { - confNode, err := docs.UnmarshalYAML(c) - if err != nil { - return err - } - - newConf, err := output.FromAny(bundle.GlobalEnvironment, confNode) - if err != nil { - return err - } - - oMgr := mgr.IntoPath("dynamic", "outputs", id) - newOutput, err := oMgr.NewOutput(newConf) - if err != nil { - return err - } - if newOutput, err = pure.RetryOutputIndefinitely(mgr, newOutput); err != nil { - return err - } - - outputConfigsMut.Lock() - outputYAMLConfs[id] = dynOutputAnyToYAMLConf(newConf) - outputConfigsMut.Unlock() - if err = fanOut.SetOutput(ctx, id, newOutput); err != nil { - mgr.Logger().Error("Failed to set output '%v': %v", id, err) - outputConfigsMut.Lock() - delete(outputYAMLConfs, id) - outputConfigsMut.Unlock() - } - return err - }) - dynAPI.OnDelete(func(ctx context.Context, id string) error { - err := fanOut.SetOutput(ctx, id, nil) - if err != nil { - mgr.Logger().Error("Failed to close output '%v': %v", id, err) - } - return err - }) - - mgr.RegisterEndpoint( - path.Join(prefix, "/outputs/{id}/uptime"), - `Returns the uptime of a specific output as a duration string.`, - dynAPI.HandleUptime, - ) - mgr.RegisterEndpoint( - path.Join(prefix, "/outputs/{id}"), - "Perform CRUD operations on the configuration of dynamic outputs. For"+ - " more information read the `dynamic` output type documentation.", - dynAPI.HandleCRUD, - ) - mgr.RegisterEndpoint( - path.Join(prefix, "/outputs"), - "Get a map of running output identifiers with their current uptimes.", - dynAPI.HandleList, - ) - - return fanOut, nil -} - -func dynOutputAnyToYAMLConf(v any) []byte { - var node yaml.Node - if err := node.Encode(v); err != nil { - return nil - } - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - if err := docs.FieldOutput("output", "").SanitiseYAML(&node, sanitConf); err != nil { - return nil - } - - confBytes, _ := yaml.Marshal(node) - return confBytes -} diff --git a/internal/impl/io/output_dynamic_fan_out.go b/internal/impl/io/output_dynamic_fan_out.go deleted file mode 100644 index 333c8115a4..0000000000 --- a/internal/impl/io/output_dynamic_fan_out.go +++ /dev/null @@ -1,307 +0,0 @@ -package io - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// wrappedOutput is a struct that wraps a DynamicOutput with an identifying -// name. -type wrappedOutput struct { - Ctx context.Context - Name string - Output output.Streamed - ResChan chan<- error -} - -// outputWithTSChan is a struct containing both an output and the transaction -// chan it reads from. -type outputWithTSChan struct { - tsChan chan message.Transaction - output output.Streamed - ctx context.Context - done func() -} - -type dynamicFanOutOutputBroker struct { - log log.Modular - - onAdd func(label string) - onRemove func(label string) - - transactions <-chan message.Transaction - - outputsMut sync.RWMutex - newOutputChan chan wrappedOutput - outputs map[string]outputWithTSChan - - shutSig *shutdown.Signaller -} - -func newDynamicFanOutOutputBroker( - outputs map[string]output.Streamed, - logger log.Modular, - onAdd func(label string), - onRemove func(label string), -) (*dynamicFanOutOutputBroker, error) { - d := &dynamicFanOutOutputBroker{ - log: logger, - transactions: nil, - newOutputChan: make(chan wrappedOutput), - outputs: make(map[string]outputWithTSChan, len(outputs)), - shutSig: shutdown.NewSignaller(), - onAdd: onAdd, - onRemove: onRemove, - } - if d.onAdd == nil { - d.onAdd = func(l string) {} - } - if d.onRemove == nil { - d.onRemove = func(l string) {} - } - - for k, v := range outputs { - if err := d.addOutput(k, v); err != nil { - return nil, fmt.Errorf("failed to initialise dynamic output '%v': %v", k, err) - } - d.onAdd(k) - } - return d, nil -} - -// SetOutput attempts to add a new output to the dynamic output broker. If an -// output already exists with the same identifier it will be closed and removed. -// If either action takes longer than the timeout period an error will be -// returned. -// -// A nil output argument is safe and will simply remove the previous output -// under the indentifier, if there was one. -func (d *dynamicFanOutOutputBroker) SetOutput(ctx context.Context, ident string, output output.Streamed) error { - resChan := make(chan error, 1) - select { - case d.newOutputChan <- wrappedOutput{ - Name: ident, - Output: output, - ResChan: resChan, - Ctx: ctx, - }: - case <-ctx.Done(): - return component.ErrTimeout - } - select { - case err := <-resChan: - return err - case <-ctx.Done(): - } - return component.ErrTimeout -} - -func (d *dynamicFanOutOutputBroker) Consume(transactions <-chan message.Transaction) error { - if d.transactions != nil { - return component.ErrAlreadyStarted - } - d.transactions = transactions - - go d.loop() - return nil -} - -func (d *dynamicFanOutOutputBroker) addOutput(ident string, output output.Streamed) error { - if _, exists := d.outputs[ident]; exists { - return fmt.Errorf("output key '%v' already exists", ident) - } - - ow := outputWithTSChan{ - tsChan: make(chan message.Transaction), - output: output, - } - - if err := output.Consume(ow.tsChan); err != nil { - output.TriggerCloseNow() - return err - } - ow.ctx, ow.done = context.WithCancel(context.Background()) - - d.outputs[ident] = ow - return nil -} - -func (d *dynamicFanOutOutputBroker) removeOutput(ctx context.Context, ident string) error { - ow, exists := d.outputs[ident] - if !exists { - return nil - } - - ow.output.TriggerCloseNow() - err := ow.output.WaitForClose(ctx) - - ow.done() - close(ow.tsChan) - delete(d.outputs, ident) - - return err -} - -func (d *dynamicFanOutOutputBroker) loop() { - apiWG := sync.WaitGroup{} - - ackInterruptChan := make(chan struct{}) - var ackPending int64 - - defer func() { - // Wait for pending acks to be resolved, or forceful termination - ackWaitLoop: - for atomic.LoadInt64(&ackPending) > 0 { - select { - case <-ackInterruptChan: - case <-time.After(time.Millisecond * 100): - // Just incase an interrupt doesn't arrive. - case <-d.shutSig.SoftStopChan(): - break ackWaitLoop - } - } - - for _, ow := range d.outputs { - ow.output.TriggerCloseNow() - close(ow.tsChan) - } - for _, ow := range d.outputs { - ow.output.TriggerCloseNow() - } - for _, ow := range d.outputs { - _ = ow.output.WaitForClose(context.Background()) - } - - d.shutSig.TriggerHardStop() - apiWG.Wait() - - d.outputs = map[string]outputWithTSChan{} - d.shutSig.TriggerHasStopped() - }() - - apiWG.Add(1) - go func() { - defer apiWG.Done() - for { - select { - case wrappedOutput, open := <-d.newOutputChan: - if !open { - return - } - func() { - d.outputsMut.Lock() - defer d.outputsMut.Unlock() - - // First, always remove the previous output if it exists. - if _, exists := d.outputs[wrappedOutput.Name]; exists { - if err := d.removeOutput(wrappedOutput.Ctx, wrappedOutput.Name); err != nil { - d.log.Error("Failed to stop old copy of dynamic output '%v' in time: %v, the output will continue to shut down in the background.\n", wrappedOutput.Name, err) - } - d.onRemove(wrappedOutput.Name) - } - - // Next, attempt to create a new output (if specified). - if wrappedOutput.Output == nil { - wrappedOutput.ResChan <- nil - } else { - err := d.addOutput(wrappedOutput.Name, wrappedOutput.Output) - if err != nil { - d.log.Error("Failed to start new dynamic output '%v': %v\n", wrappedOutput.Name, err) - } else { - d.onAdd(wrappedOutput.Name) - } - wrappedOutput.ResChan <- err - } - }() - case <-d.shutSig.SoftStopChan(): - return - } - } - }() - - for { - var ts message.Transaction - var open bool - select { - case ts, open = <-d.transactions: - if !open { - return - } - case <-d.shutSig.SoftStopChan(): - return - } - - d.outputsMut.RLock() - for len(d.outputs) == 0 { - // Assuming this isn't a common enough occurrence that it - // won't be busy enough to require a sync.Cond, looping with - // a sleep is fine for now. - d.outputsMut.RUnlock() - select { - case <-time.After(time.Millisecond * 10): - case <-d.shutSig.SoftStopChan(): - return - } - d.outputsMut.RLock() - } - - _ = atomic.AddInt64(&ackPending, 1) - pendingResponses := int64(len(d.outputs)) - - outputsLoop: - for _, output := range d.outputs { - select { - case output.tsChan <- message.NewTransactionFunc(ts.Payload.ShallowCopy(), func(ctx context.Context, err error) error { - if atomic.AddInt64(&pendingResponses, -1) == 0 || err != nil { - atomic.StoreInt64(&pendingResponses, 0) - ackErr := ts.Ack(ctx, err) - _ = atomic.AddInt64(&ackPending, -1) - select { - case ackInterruptChan <- struct{}{}: - default: - } - return ackErr - } - return nil - }): - case <-d.shutSig.SoftStopChan(): - break outputsLoop // This signal will be caught again in the next loop - } - } - d.outputsMut.RUnlock() - } -} - -func (d *dynamicFanOutOutputBroker) Connected() bool { - d.outputsMut.RLock() - defer d.outputsMut.RUnlock() - for _, out := range d.outputs { - if !out.output.Connected() { - return false - } - } - return true -} - -func (d *dynamicFanOutOutputBroker) TriggerCloseNow() { - d.shutSig.TriggerHardStop() -} - -func (d *dynamicFanOutOutputBroker) WaitForClose(ctx context.Context) error { - select { - case <-d.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/io/output_dynamic_fan_out_test.go b/internal/impl/io/output_dynamic_fan_out_test.go deleted file mode 100644 index 57a9d0a4a0..0000000000 --- a/internal/impl/io/output_dynamic_fan_out_test.go +++ /dev/null @@ -1,437 +0,0 @@ -package io - -import ( - "bytes" - "context" - "errors" - "fmt" - "reflect" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ output.Streamed = &dynamicFanOutOutputBroker{} - -func TestBasicDynamicFanOut(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nOutputs, nMsgs := 10, 1000 - - outputs := map[string]output.Streamed{} - mockOutputs := []*mock.OutputChanneled{} - - for i := 0; i < nOutputs; i++ { - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - outputs[fmt.Sprintf("out-%v", i)] = mockOutputs[i] - } - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newDynamicFanOutOutputBroker(outputs, log.Noop(), nil, nil) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - wg := sync.WaitGroup{} - for j := 0; j < nOutputs; j++ { - wg.Add(1) - go func(index int) { - defer wg.Done() - var ts message.Transaction - select { - case ts = <-mockOutputs[index].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second): - t.Error("Timed out waiting for broker propagate", index) - } - require.NoError(t, ts.Ack(tCtx, nil)) - }(j) - } - wg.Wait() - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestDynamicFanOutChangeOutputs(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nOutputs := 10 - - outputs := map[string]*mock.OutputChanneled{} - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newDynamicFanOutOutputBroker(nil, log.Noop(), nil, nil) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - for i := 0; i < nOutputs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - - newOutput := &mock.OutputChanneled{} - newOutputName := fmt.Sprintf("output-%v", i) - - outputs[newOutputName] = newOutput - require.NoError(t, oTM.SetOutput(context.Background(), newOutputName, newOutput)) - - wg := sync.WaitGroup{} - wg.Add(len(outputs)) - for k, v := range outputs { - go func(name string, out *mock.OutputChanneled) { - defer wg.Done() - var ts message.Transaction - select { - case ts = <-out.TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned for output '%v': %s != %s", name, ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second): - t.Error("Timed out waiting for broker propagate") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }(k, v) - } - - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - - wg.Wait() - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - for i := 0; i < nOutputs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - - wg := sync.WaitGroup{} - wg.Add(len(outputs)) - for k, v := range outputs { - go func(name string, out *mock.OutputChanneled) { - defer wg.Done() - var ts message.Transaction - select { - case ts = <-out.TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned for output '%v': %s != %s", name, ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second): - t.Error("Timed out waiting for broker propagate") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }(k, v) - } - - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - - wg.Wait() - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - - oldOutputName := fmt.Sprintf("output-%v", i) - require.NoError(t, oTM.SetOutput(context.Background(), oldOutputName, nil)) - delete(outputs, oldOutputName) - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestDynamicFanOutAtLeastOnce(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockOne := mock.OutputChanneled{} - mockTwo := mock.OutputChanneled{} - - outputs := map[string]output.Streamed{ - "first": &mockOne, - "second": &mockTwo, - } - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newDynamicFanOutOutputBroker(outputs, log.Noop(), nil, nil) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - assert.Error(t, oTM.Consume(readChan), "Expected error on duplicate receive call") - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - var ts message.Transaction - select { - case ts = <-mockOne.TChan: - case <-time.After(time.Second): - t.Error("Timed out waiting for mockOne") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - go func() { - defer wg.Done() - var ts message.Transaction - select { - case ts = <-mockTwo.TChan: - case <-time.After(time.Second): - t.Error("Timed out waiting for mockOne") - return - } - require.NoError(t, ts.Ack(tCtx, errors.New("this is a test"))) - }() - - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("hello world")}), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - - wg.Wait() - - select { - case res := <-resChan: - require.EqualError(t, res, "this is a test") - case <-time.After(time.Second): - t.Error("Timed out responding to broker") - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestDynamicFanOutStartEmpty(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockOne := mock.OutputChanneled{} - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - outputs := map[string]output.Streamed{} - - oTM, err := newDynamicFanOutOutputBroker(outputs, log.Noop(), nil, nil) - require.NoError(t, err) - - require.NoError(t, oTM.Consume(readChan)) - assert.Error(t, oTM.Consume(readChan), "Expected error on duplicate receive call") - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("hello world")}), resChan): - case <-time.After(time.Second): - t.Error("Timed out waiting for broker send") - } - }() - - require.NoError(t, oTM.SetOutput(context.Background(), "first", &mockOne)) - - go func() { - defer wg.Done() - var ts message.Transaction - select { - case ts = <-mockOne.TChan: - case <-time.After(time.Second): - t.Error("Timed out waiting for mockOne") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - wg.Wait() - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Error("Timed out responding to broker") - } - - close(readChan) - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestDynamicFanOutShutDownFromErrorResponse(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mockOutput := &mock.OutputChanneled{} - outputs := map[string]output.Streamed{ - "test": mockOutput, - } - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - outputAddedList := []string{} - outputRemovedList := []string{} - - oTM, err := newDynamicFanOutOutputBroker( - outputs, log.Noop(), - func(label string) { - outputAddedList = append(outputAddedList, label) - }, - func(label string) { - outputRemovedList = append(outputRemovedList, label) - }, - ) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - var ts message.Transaction - var open bool - select { - case ts, open = <-mockOutput.TChan: - require.True(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - - require.NoError(t, ts.Ack(tCtx, errors.New("test"))) - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) - - select { - case _, open := <-mockOutput.TChan: - require.False(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - - if exp, act := []string{"test"}, outputAddedList; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong list of added outputs: %v != %v", act, exp) - } - if exp, act := []string{}, outputRemovedList; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong list of removed outputs: %v != %v", act, exp) - } -} - -func TestDynamicFanOutShutDownFromReceive(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutput := &mock.OutputChanneled{} - outputs := map[string]output.Streamed{ - "test": mockOutput, - } - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newDynamicFanOutOutputBroker(outputs, log.Noop(), nil, nil) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - select { - case _, open := <-mockOutput.TChan: - require.True(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) - - select { - case _, open := <-mockOutput.TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out waiting for msg rcv") - } -} - -func TestDynamicFanOutShutDownFromSend(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutput := &mock.OutputChanneled{} - outputs := map[string]output.Streamed{ - "test": mockOutput, - } - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newDynamicFanOutOutputBroker(outputs, log.Noop(), nil, nil) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) - - select { - case _, open := <-mockOutput.TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out waiting for msg rcv") - } -} diff --git a/internal/impl/io/output_dynamic_test.go b/internal/impl/io/output_dynamic_test.go deleted file mode 100644 index 01688dc304..0000000000 --- a/internal/impl/io/output_dynamic_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package io - -import ( - "bytes" - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - bmock "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestDynamicOutputAPI(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - gMux := mux.NewRouter() - - mgr := bmock.NewManager() - mgr.OnRegisterEndpoint = func(path string, h http.HandlerFunc) { - gMux.HandleFunc(path, h) - } - - conf := output.NewConfig() - conf.Type = "dynamic" - - o, err := mgr.NewOutput(conf) - require.NoError(t, err) - - tChan := make(chan message.Transaction) - resChan := make(chan error, 1) - require.NoError(t, o.Consume(tChan)) - - req := httptest.NewRequest(http.MethodGet, "/outputs", http.NoBody) - res := httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - assert.Equal(t, `{}`, res.Body.String()) - - fooConf := `drop: {}` - req = httptest.NewRequest("POST", "/outputs/foo", bytes.NewBufferString(fooConf)) - res = httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo")}), resChan): - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - select { - case err := <-resChan: - require.NoError(t, err) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - - req = httptest.NewRequest(http.MethodGet, "/outputs/foo", http.NoBody) - res = httptest.NewRecorder() - gMux.ServeHTTP(res, req) - - assert.Equal(t, 200, res.Code) - assert.Equal(t, `label: "" -drop: {} -`, res.Body.String()) - - o.TriggerCloseNow() - require.NoError(t, o.WaitForClose(ctx)) -} diff --git a/internal/impl/io/output_file.go b/internal/impl/io/output_file.go deleted file mode 100644 index 7da1dde0ae..0000000000 --- a/internal/impl/io/output_file.go +++ /dev/null @@ -1,192 +0,0 @@ -package io - -import ( - "context" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "sync" - - "github.com/benthosdev/benthos/v4/internal/codec" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - fileOutputFieldPath = "path" - fileOutputFieldCodec = "codec" -) - -func fileOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Local"). - Summary(`Writes messages to files on disk based on a chosen codec.`). - Description(`Messages can be written to different files by using xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions] in the path field. However, only one file is ever open at a given time, and therefore when the path changes the previously open file is closed.`). - Fields( - service.NewInterpolatedStringField(fileOutputFieldPath). - Description("The file to write to, if the file does not yet exist it will be created."). - Examples( - "/tmp/data.txt", - "/tmp/${! timestamp_unix() }.txt", - `/tmp/${! json("document.id") }.json`, - ). - Version("3.33.0"), - service.NewInternalField(codec.NewWriterDocs(fileOutputFieldCodec)).Version("3.33.0").Default("lines"), - ) -} - -type fileOutputConfig struct { - Path *service.InterpolatedString - Codec string -} - -func fileOutputConfigFromParsed(pConf *service.ParsedConfig) (conf fileOutputConfig, err error) { - if conf.Path, err = pConf.FieldInterpolatedString(fileOutputFieldPath); err != nil { - return - } - if conf.Codec, err = pConf.FieldString(fileOutputFieldCodec); err != nil { - return - } - return -} - -func init() { - err := service.RegisterOutput("file", fileOutputSpec(), - func(pConf *service.ParsedConfig, res *service.Resources) (out service.Output, mif int, err error) { - var conf fileOutputConfig - if conf, err = fileOutputConfigFromParsed(pConf); err != nil { - return - } - - mif = 1 - out, err = newFileWriter(conf.Path, conf.Codec, res) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type fileWriter struct { - log *service.Logger - nm *service.Resources - - path *service.InterpolatedString - suffixFn codec.SuffixFn - appendMode bool - - handleMut sync.Mutex - handlePath string - handle io.WriteCloser -} - -func newFileWriter(path *service.InterpolatedString, codecStr string, mgr *service.Resources) (*fileWriter, error) { - codec, appendMode, err := codec.GetWriter(codecStr) - if err != nil { - return nil, err - } - return &fileWriter{ - suffixFn: codec, - appendMode: appendMode, - path: path, - log: mgr.Logger(), - nm: mgr, - }, nil -} - -//------------------------------------------------------------------------------ - -func (w *fileWriter) Connect(ctx context.Context) error { - return nil -} - -func (w *fileWriter) writeTo(wtr io.Writer, p *service.Message) error { - mBytes, err := p.AsBytes() - if err != nil { - return err - } - - suffix, addSuffix := w.suffixFn(mBytes) - - if _, err := wtr.Write(mBytes); err != nil { - return err - } - if addSuffix { - if _, err := wtr.Write(suffix); err != nil { - return err - } - } - return nil -} - -func (w *fileWriter) Write(ctx context.Context, msg *service.Message) error { - path, err := w.path.TryString(msg) - if err != nil { - return fmt.Errorf("path interpolation error: %w", err) - } - path = filepath.Clean(path) - - w.handleMut.Lock() - defer w.handleMut.Unlock() - - if w.handle != nil && path == w.handlePath { - return w.writeTo(w.handle, msg) - } - if w.handle != nil { - if err := w.handle.Close(); err != nil { - return err - } - } - - flag := os.O_CREATE | os.O_RDWR - if w.appendMode { - flag |= os.O_APPEND - } else { - flag |= os.O_TRUNC - } - - if err := w.nm.FS().MkdirAll(filepath.Dir(path), fs.FileMode(0o777)); err != nil { - return err - } - - file, err := w.nm.FS().OpenFile(path, flag, fs.FileMode(0o666)) - if err != nil { - return err - } - - handle, ok := file.(io.WriteCloser) - if !ok { - _ = file.Close() - return errors.New("failed to open file for writing") - } - - w.handlePath = path - if err := w.writeTo(handle, msg); err != nil { - _ = handle.Close() - return err - } - - if w.appendMode { - w.handle = handle - } else { - _ = handle.Close() - } - return nil -} - -func (w *fileWriter) Close(ctx context.Context) error { - w.handleMut.Lock() - defer w.handleMut.Unlock() - - var err error - if w.handle != nil { - err = w.handle.Close() - w.handle = nil - } - return err -} diff --git a/internal/impl/io/output_http_client.go b/internal/impl/io/output_http_client.go deleted file mode 100644 index f70f93e677..0000000000 --- a/internal/impl/io/output_http_client.go +++ /dev/null @@ -1,178 +0,0 @@ -package io - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/httpclient" - "github.com/benthosdev/benthos/v4/public/service" -) - -func httpClientOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Network"). - Summary("Sends messages to an HTTP server."). - Description(` -When the number of retries expires the output will reject the message, the behavior after this will depend on the pipeline but usually this simply means the send is attempted again until successful whilst applying back pressure. - -The URL and header values of this type can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. - -The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be disabled by setting the field ` + "<> to `false`" + `. - -== Propagate responses - -It's possible to propagate the response from each HTTP request back to the input source by setting ` + "`propagate_response` to `true`" + `. Only inputs that support xref:guides:sync_responses.adoc[synchronous responses] are able to make use of these propagated responses.` + service.OutputPerformanceDocs(true, true)). - Field(httpclient.ConfigField("POST", true, - service.NewBoolField("batch_as_multipart"). - Description("Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. If disabled messages in batches will be sent as individual requests."). - Advanced().Default(false), - service.NewBoolField("propagate_response"). - Description("Whether responses from the server should be xref:guides:sync_responses.adoc[propagated back] to the input."). - Advanced().Default(false), - service.NewIntField("max_in_flight"). - Description("The maximum number of parallel message batches to have in flight at any given time."). - Default(64), - service.NewBatchPolicyField("batching"), - service.NewObjectListField("multipart", - service.NewInterpolatedStringField("content_type"). - Description("The content type of the individual message part."). - Example("application/bin"). - Default(""), - service.NewInterpolatedStringField("content_disposition"). - Description("The content disposition of the individual message part."). - Example(`form-data; name="bin"; filename='${! @AttachmentName }`). - Default(""), - service.NewInterpolatedStringField("body"). - Description("The body of the individual message part."). - Example(`${! this.data.part1 }`). - Default(""), - ).Description("EXPERIMENTAL: Create explicit multipart HTTP requests by specifying an array of parts to add to the request, each part specified consists of content headers and a data field that can be populated dynamically. If this field is populated it will override the default request creation behavior."). - Advanced().Version("3.63.0").Default([]any{}), - )) -} - -func init() { - err := service.RegisterBatchOutput( - "http_client", httpClientOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (bo service.BatchOutput, b service.BatchPolicy, mIF int, err error) { - if mIF, err = conf.FieldInt("max_in_flight"); err != nil { - return - } - - if b, err = conf.FieldBatchPolicy("batching"); err != nil { - return - } - - bo, err = newHTTPClientOutputFromParsed(conf, mgr) - return - }) - if err != nil { - panic(err) - } -} - -type httpClientWriter struct { - client *httpclient.Client - log *service.Logger - - logURL string - propResponse bool - batchAsMultipart bool -} - -func newHTTPClientOutputFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (*httpClientWriter, error) { - opts := []httpclient.RequestOpt{} - - logURL, _ := conf.FieldString("url") - propResponse, err := conf.FieldBool("propagate_response") - if err != nil { - return nil, err - } - - if multiPartObjs, _ := conf.FieldObjectList("multipart"); len(multiPartObjs) > 0 { - parts := make([]httpclient.MultipartExpressions, len(multiPartObjs)) - for i, p := range multiPartObjs { - var exprPart httpclient.MultipartExpressions - if exprPart.ContentDisposition, err = p.FieldInterpolatedString("content_disposition"); err != nil { - return nil, err - } - if exprPart.ContentType, err = p.FieldInterpolatedString("content_type"); err != nil { - return nil, err - } - if exprPart.Body, err = p.FieldInterpolatedString("body"); err != nil { - return nil, err - } - parts[i] = exprPart - } - opts = append(opts, httpclient.WithExplicitMultipart(parts)) - } - - oldHTTPConf, err := httpclient.ConfigFromParsed(conf) - if err != nil { - return nil, err - } - - client, err := httpclient.NewClientFromOldConfig(oldHTTPConf, mgr, opts...) - if err != nil { - return nil, err - } - - batchAsMultipart, err := conf.FieldBool("batch_as_multipart") - if err != nil { - return nil, err - } - - return &httpClientWriter{ - client: client, - log: mgr.Logger(), - logURL: logURL, - propResponse: propResponse, - batchAsMultipart: batchAsMultipart, - }, nil -} - -func (h *httpClientWriter) Connect(ctx context.Context) error { - return nil -} - -func (h *httpClientWriter) WriteBatch(ctx context.Context, msg service.MessageBatch) error { - if len(msg) > 1 && !h.batchAsMultipart { - for _, v := range msg { - if err := h.WriteBatch(ctx, service.MessageBatch{v}); err != nil { - return err - } - } - return nil - } - - resultMsg, err := h.client.Send(ctx, msg) - if err == nil && h.propResponse { - parts := make(service.MessageBatch, len(resultMsg)) - for i, p := range resultMsg { - if i < len(msg) { - parts[i] = msg[i] - } else { - parts[i] = msg[0].Copy() - } - - mBytes, err := p.AsBytes() - if err != nil { - return err - } - parts[i].SetBytes(mBytes) - - _ = p.MetaWalkMut(func(k string, v any) error { - parts[i].MetaSetMut(k, v) - return nil - }) - } - if err := parts.AddSyncResponse(); err != nil { - h.log.Warnf("Unable to propagate response to input: %v", err) - } - } - return err -} - -func (h *httpClientWriter) Close(ctx context.Context) error { - return h.client.Close(ctx) -} diff --git a/internal/impl/io/output_http_client_test.go b/internal/impl/io/output_http_client_test.go deleted file mode 100644 index e0a9a31540..0000000000 --- a/internal/impl/io/output_http_client_test.go +++ /dev/null @@ -1,664 +0,0 @@ -package io - -import ( - "context" - "encoding/json" - "fmt" - "io" - "mime" - "mime/multipart" - "net/http" - "net/http/httptest" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" -) - -func parseYAMLOutputConf(t testing.TB, formatStr string, args ...any) (conf output.Config) { - t.Helper() - var err error - conf, err = testutil.OutputFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return -} - -func TestHTTPClientMultipartEnabled(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - resultChan := make(chan string, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - require.NoError(t, err) - require.True(t, strings.HasPrefix(mediaType, "multipart/")) - - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - require.NoError(t, err) - - msgBytes, err := io.ReadAll(p) - require.NoError(t, err) - - resultChan <- string(msgBytes) - } - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - batch_as_multipart: true -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - resChan := make(chan error) - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{ - []byte("PART-A"), - []byte("PART-B"), - []byte("PART-C"), - }), resChan): - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - for _, exp := range []string{ - "PART-A", - "PART-B", - "PART-C", - } { - select { - case resMsg := <-resultChan: - assert.Equal(t, exp, resMsg) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } - - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientMultipartDisabled(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - resultChan := make(chan string, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - resBytes, err := io.ReadAll(r.Body) - require.NoError(t, err) - resultChan <- string(resBytes) - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - max_in_flight: 1 -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - resChan := make(chan error) - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{ - []byte("PART-A"), - []byte("PART-B"), - []byte("PART-C"), - }), resChan): - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - for _, exp := range []string{ - "PART-A", - "PART-B", - "PART-C", - } { - select { - case resMsg := <-resultChan: - assert.Equal(t, exp, resMsg) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } - - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func writeBatchToStreamed(ctx context.Context, t *testing.T, batch message.Batch, out output.Streamed) (err error) { - t.Helper() - - tChan := make(chan message.Transaction) - require.NoError(t, out.Consume(tChan)) - - return writeBatchToChan(ctx, t, batch, tChan) -} - -func writeBatchToChan(ctx context.Context, t *testing.T, batch message.Batch, tChan chan message.Transaction) (err error) { - t.Helper() - - resChan := make(chan error) - select { - case tChan <- message.NewTransaction(batch, resChan): - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - - select { - case err = <-resChan: - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - return -} - -func TestHTTPClientRetries(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - var reqCount uint32 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddUint32(&reqCount, 1) - http.Error(w, "test error", http.StatusForbidden) - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - retry_period: 1ms - retries: 3 -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - require.Error(t, writeBatchToStreamed(ctx, t, message.QuickBatch([][]byte{[]byte("test")}), h)) - if exp, act := uint32(4), atomic.LoadUint32(&reqCount); exp != act { - t.Errorf("Wrong count of HTTP attempts: %v != %v", exp, act) - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nTestLoops := 1000 - - resultChan := make(chan message.Batch, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - msg := message.QuickBatch(nil) - defer func() { - resultChan <- msg - }() - - b, err := io.ReadAll(r.Body) - if err != nil { - t.Error(err) - return - } - msg = append(msg, message.NewPart(b)) - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - testMsg := message.QuickBatch([][]byte{[]byte(testStr)}) - - if err = writeBatchToChan(ctx, t, testMsg, tChan); err != nil { - t.Error(err) - } - - select { - case resMsg := <-resultChan: - if resMsg.Len() != 1 { - t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 1) - return - } - if exp, actual := testStr, string(resMsg.Get(0).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", exp, actual) - return - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - return - } - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientSyncResponse(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nTestLoops := 1000 - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := io.ReadAll(r.Body) - if err != nil { - t.Error(err) - return - } - w.Header().Add("fooheader", "foovalue") - _, _ = w.Write([]byte("echo: ")) - _, _ = w.Write(b) - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - propagate_response: true -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - - resultStore := transaction.NewResultStore() - testMsg := message.QuickBatch([][]byte{[]byte(testStr)}) - transaction.AddResultStore(testMsg, resultStore) - - require.NoError(t, writeBatchToChan(ctx, t, testMsg, tChan)) - resMsgs := resultStore.Get() - require.Len(t, resMsgs, 1) - - resMsg := resMsgs[0] - require.Equal(t, 1, resMsg.Len()) - assert.Equal(t, "echo: "+testStr, string(resMsg.Get(0).AsBytes())) - assert.Equal(t, "", resMsg.Get(0).MetaGetStr("fooheader")) - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientSyncResponseCopyHeaders(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nTestLoops := 1000 - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := io.ReadAll(r.Body) - if err != nil { - t.Error(err) - return - } - w.Header().Add("fooheader", "foovalue") - _, _ = w.Write([]byte("echo: ")) - _, _ = w.Write(b) - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - propagate_response: true - extract_headers: - include_patterns: [ ".*" ] -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - - resultStore := transaction.NewResultStore() - testMsg := message.QuickBatch([][]byte{[]byte(testStr)}) - transaction.AddResultStore(testMsg, resultStore) - - require.NoError(t, writeBatchToChan(ctx, t, testMsg, tChan)) - resMsgs := resultStore.Get() - require.Len(t, resMsgs, 1) - - resMsg := resMsgs[0] - require.Equal(t, 1, resMsg.Len()) - assert.Equal(t, "echo: "+testStr, string(resMsg.Get(0).AsBytes())) - assert.Equal(t, "foovalue", resMsg.Get(0).MetaGetStr("fooheader")) - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPClientMultipart(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nTestLoops := 1000 - - resultChan := make(chan message.Batch, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - msg := message.QuickBatch(nil) - defer func() { - resultChan <- msg - }() - - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil { - t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err) - return - } - - if strings.HasPrefix(mediaType, "multipart/") { - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - if err != nil { - t.Error(err) - return - } - msgBytes, err := io.ReadAll(p) - if err != nil { - t.Error(err) - return - } - msg = append(msg, message.NewPart(msgBytes)) - } - } else { - b, err := io.ReadAll(r.Body) - if err != nil { - t.Error(err) - return - } - msg = append(msg, message.NewPart(b)) - } - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - batch_as_multipart: true -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - testMsg := message.QuickBatch([][]byte{ - []byte(testStr + "PART-A"), - []byte(testStr + "PART-B"), - }) - - require.NoError(t, writeBatchToChan(ctx, t, testMsg, tChan)) - - select { - case resMsg := <-resultChan: - if resMsg.Len() != 2 { - t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2) - return - } - if exp, actual := testStr+"PART-A", string(resMsg.Get(0).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", exp, actual) - return - } - if exp, actual := testStr+"PART-B", string(resMsg.Get(1).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", exp, actual) - return - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - return - } - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPOutputClientMultipartBody(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nTestLoops := 1000 - resultChan := make(chan message.Batch, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - msg := message.QuickBatch(nil) - defer func() { - resultChan <- msg - }() - - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil { - t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err) - return - } - - if strings.HasPrefix(mediaType, "multipart/") { - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - - if err == io.EOF { - break - } - if err != nil { - t.Error(err) - return - } - msgBytes, err := io.ReadAll(p) - if err != nil { - t.Error(err) - return - } - msg = append(msg, message.NewPart(msgBytes)) - } - } - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - multipart: - - content_disposition: 'form-data; name="text"' - content_type: 'text/plain' - body: PART-A - - content_disposition: 'form-data; name="file1"; filename="a.txt"' - content_type: 'text/plain' - body: PART-B -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - require.NoError(t, h.Consume(tChan)) - - for i := 0; i < nTestLoops; i++ { - require.NoError(t, writeBatchToChan(ctx, t, message.QuickBatch([][]byte{[]byte("test")}), tChan)) - - select { - case resMsg := <-resultChan: - if resMsg.Len() != 2 { - t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2) - return - } - if exp, actual := "PART-A", string(resMsg.Get(0).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", exp, actual) - return - } - if exp, actual := "PART-B", string(resMsg.Get(1).AsBytes()); exp != actual { - t.Errorf("Wrong result, %v != %v", exp, actual) - return - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - return - } - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPOutputClientMultipartHeaders(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - resultChan := make(chan message.Batch, 1) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - msg := message.QuickBatch(nil) - defer func() { - resultChan <- msg - }() - - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil { - t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err) - return - } - - if strings.HasPrefix(mediaType, "multipart/") { - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - - if err == io.EOF { - break - } - if err != nil { - t.Error(err) - return - } - a, err := json.Marshal(p.Header) - if err != nil { - t.Error(err) - return - } - msg = append(msg, message.NewPart(a)) - } - } - })) - defer ts.Close() - - conf := parseYAMLOutputConf(t, ` -http_client: - url: %v/testpost - multipart: - - content_disposition: 'form-data; name="text"' - content_type: 'text/plain' - body: PART-A - - content_disposition: 'form-data; name="file1"; filename="a.txt"' - content_type: 'text/plain' - body: PART-B -`, ts.URL) - - h, err := mock.NewManager().NewOutput(conf) - if err != nil { - t.Fatal(err) - } - - require.NoError(t, writeBatchToStreamed(ctx, t, message.QuickBatch([][]byte{[]byte("test")}), h)) - - expHeaders := [][2]string{ - {`form-data; name="text"`, "text/plain"}, - {`form-data; name="file1"; filename="a.txt"`, "text/plain"}, - } - - select { - case resMsg := <-resultChan: - for i, exp := range expHeaders { - if resMsg.Len() != 2 { - t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2) - return - } - mp := make(map[string][]string) - err := json.Unmarshal(resMsg.Get(i).AsBytes(), &mp) - if err != nil { - t.Error(err) - } - assert.Equal(t, []string{exp[0]}, mp["Content-Disposition"]) - assert.Equal(t, []string{exp[1]}, mp["Content-Type"]) - } - case <-time.After(time.Second): - t.Errorf("Action timed out") - return - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} diff --git a/internal/impl/io/output_http_server.go b/internal/impl/io/output_http_server.go deleted file mode 100644 index 7e2d82f81c..0000000000 --- a/internal/impl/io/output_http_server.go +++ /dev/null @@ -1,498 +0,0 @@ -package io - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/textproto" - "sync" - "time" - - "github.com/gorilla/mux" - "github.com/gorilla/websocket" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/httpserver" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - hsoFieldAddress = "address" - hsoFieldPath = "path" - hsoFieldStreamPath = "stream_path" - hsoFieldWSPath = "ws_path" - hsoFieldAllowedVerbs = "allowed_verbs" - hsoFieldTimeout = "timeout" - hsoFieldCertFile = "cert_file" - hsoFieldKeyFile = "key_file" - hsoFieldCORS = "cors" - hsoFieldCORSEnabled = "enabled" - hsoFieldCORSAllowedOrigins = "allowed_origins" -) - -type hsoConfig struct { - Address string - Path string - StreamPath string - WSPath string - AllowedVerbs map[string]struct{} - Timeout time.Duration - CertFile string - KeyFile string - CORS httpserver.CORSConfig -} - -func hsoConfigFromParsed(pConf *service.ParsedConfig) (conf hsoConfig, err error) { - if conf.Address, err = pConf.FieldString(hsoFieldAddress); err != nil { - return - } - if conf.Path, err = pConf.FieldString(hsoFieldPath); err != nil { - return - } - if conf.StreamPath, err = pConf.FieldString(hsoFieldStreamPath); err != nil { - return - } - if conf.WSPath, err = pConf.FieldString(hsoFieldWSPath); err != nil { - return - } - { - var verbsList []string - if verbsList, err = pConf.FieldStringList(hsoFieldAllowedVerbs); err != nil { - return - } - if len(verbsList) == 0 { - err = errors.New("must specify at least one allowed verb") - return - } - conf.AllowedVerbs = map[string]struct{}{} - for _, v := range verbsList { - conf.AllowedVerbs[v] = struct{}{} - } - } - if conf.Timeout, err = pConf.FieldDuration(hsoFieldTimeout); err != nil { - return - } - if conf.CertFile, err = pConf.FieldString(hsoFieldCertFile); err != nil { - return - } - if conf.KeyFile, err = pConf.FieldString(hsoFieldKeyFile); err != nil { - return - } - if conf.CORS, err = corsConfigFromParsed(pConf.Namespace(hsoFieldCORS)); err != nil { - return - } - return -} - -func hsoSpec() *service.ConfigSpec { - corsSpec := httpserver.ServerCORSFieldSpec() - corsSpec.Description += " Only valid with a custom `address`." - - return service.NewConfigSpec(). - Stable(). - Categories("Network"). - Summary(`Sets up an HTTP server that will send messages over HTTP(S) GET requests. HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified.`). - Description(`Sets up an HTTP server that will send messages over HTTP(S) GET requests. If the `+"`address`"+` config field is left blank the xref:components:http/about.adoc[service-wide HTTP server] will be used. - -Three endpoints will be registered at the paths specified by the fields `+"`path`, `stream_path` and `ws_path`"+`. Which allow you to consume a single message batch, a continuous stream of line delimited messages, or a websocket of messages for each request respectively. - -When messages are batched the `+"`path`"+` endpoint encodes the batch according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be overridden by xref:configuration:batching.adoc#post-batch-processing[archiving your batches]. - -Please note, messages are considered delivered as soon as the data is written to the client. There is no concept of at least once delivery on this output. - -`+api.EndpointCaveats()+` -`). - Fields( - service.NewStringField(hsoFieldAddress). - Description("An alternative address to host from. If left empty the service wide address is used."). - Default(""), - service.NewStringField(hsoFieldPath). - Description("The path from which discrete messages can be consumed."). - Default("/get"), - service.NewStringField(hsoFieldStreamPath). - Description("The path from which a continuous stream of messages can be consumed."). - Default("/get/stream"), - service.NewStringField(hsoFieldWSPath). - Description("The path from which websocket connections can be established."). - Default("/get/ws"), - service.NewStringListField(hsoFieldAllowedVerbs). - Description("An array of verbs that are allowed for the `path` and `stream_path` HTTP endpoint."). - Default([]any{"GET"}), - service.NewDurationField(hsoFieldTimeout). - Description("The maximum time to wait before a blocking, inactive connection is dropped (only applies to the `path` endpoint)."). - Default("5s"). - Advanced(), - service.NewStringField(hsoFieldCertFile). - Description("Enable TLS by specifying a certificate and key file. Only valid with a custom `address`."). - Advanced(). - Default(""), - service.NewStringField(hsoFieldKeyFile). - Description("Enable TLS by specifying a certificate and key file. Only valid with a custom `address`."). - Advanced(). - Default(""), - service.NewInternalField(corsSpec), - ) -} - -func init() { - err := service.RegisterBatchOutput( - "http_server", hsoSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, pol service.BatchPolicy, mif int, err error) { - var hsoConf hsoConfig - if hsoConf, err = hsoConfigFromParsed(conf); err != nil { - return - } - - // TODO: If we refactor this input to implement WriteBatch then we - // can return a proper service.BatchOutput implementation. - - oldMgr := interop.UnwrapManagement(mgr) - - var outStrm output.Streamed - if outStrm, err = newHTTPServerOutput(hsoConf, oldMgr); err != nil { - return - } - - out = interop.NewUnwrapInternalOutput(outStrm) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type httpServerOutput struct { - conf hsoConfig - log log.Modular - - mux *mux.Router - server *http.Server - - transactions <-chan message.Transaction - - mGetSent metrics.StatCounter - mGetBatchSent metrics.StatCounter - - mWSSent metrics.StatCounter - mWSBatchSent metrics.StatCounter - mWSError metrics.StatCounter - - mStreamSent metrics.StatCounter - mStreamBatchSent metrics.StatCounter - mStreamError metrics.StatCounter - - closeServerOnce sync.Once - shutSig *shutdown.Signaller -} - -func newHTTPServerOutput(conf hsoConfig, mgr bundle.NewManagement) (output.Streamed, error) { - var gMux *mux.Router - var server *http.Server - - var err error - if conf.Address != "" { - gMux = mux.NewRouter() - server = &http.Server{Addr: conf.Address} - if server.Handler, err = conf.CORS.WrapHandler(gMux); err != nil { - return nil, fmt.Errorf("bad CORS configuration: %w", err) - } - } - - stats := mgr.Metrics() - mSent := stats.GetCounter("output_sent") - mBatchSent := stats.GetCounter("output_batch_sent") - mError := stats.GetCounter("output_error") - - h := httpServerOutput{ - shutSig: shutdown.NewSignaller(), - conf: conf, - log: mgr.Logger(), - mux: gMux, - server: server, - - mGetSent: mSent, - mGetBatchSent: mBatchSent, - - mWSSent: mSent, - mWSBatchSent: mBatchSent, - mWSError: mError, - - mStreamSent: mSent, - mStreamBatchSent: mBatchSent, - mStreamError: mError, - } - - if gMux != nil { - if h.conf.Path != "" { - api.GetMuxRoute(gMux, h.conf.Path).HandlerFunc(h.getHandler) - } - if h.conf.StreamPath != "" { - api.GetMuxRoute(gMux, h.conf.StreamPath).HandlerFunc(h.streamHandler) - } - if h.conf.WSPath != "" { - api.GetMuxRoute(gMux, h.conf.WSPath).HandlerFunc(h.wsHandler) - } - } else { - if h.conf.Path != "" { - mgr.RegisterEndpoint( - h.conf.Path, "Read a single message from Benthos.", - h.getHandler, - ) - } - if h.conf.StreamPath != "" { - mgr.RegisterEndpoint( - h.conf.StreamPath, - "Read a continuous stream of messages from Benthos.", - h.streamHandler, - ) - } - if h.conf.WSPath != "" { - mgr.RegisterEndpoint( - h.conf.WSPath, - "Read messages from Benthos via websockets.", - h.wsHandler, - ) - } - } - - return &h, nil -} - -//------------------------------------------------------------------------------ - -func (h *httpServerOutput) getHandler(w http.ResponseWriter, r *http.Request) { - if h.shutSig.IsSoftStopSignalled() { - http.Error(w, "Server closed", http.StatusServiceUnavailable) - return - } - - ctx, done := h.shutSig.SoftStopCtx(r.Context()) - defer done() - - if _, exists := h.conf.AllowedVerbs[r.Method]; !exists { - http.Error(w, "Incorrect method", http.StatusMethodNotAllowed) - return - } - - tStart := time.Now() - - var ts message.Transaction - var open bool - var err error - - select { - case ts, open = <-h.transactions: - if !open { - http.Error(w, "Server closed", http.StatusServiceUnavailable) - go h.TriggerCloseNow() - return - } - case <-time.After(h.conf.Timeout - time.Since(tStart)): - http.Error(w, "Timed out waiting for message", http.StatusRequestTimeout) - return - } - - if ts.Payload.Len() > 1 { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - for i := 0; i < ts.Payload.Len() && err == nil; i++ { - var part io.Writer - if part, err = writer.CreatePart(textproto.MIMEHeader{ - "Content-Type": []string{"application/octet-stream"}, - }); err == nil { - _, err = io.Copy(part, bytes.NewReader(ts.Payload.Get(i).AsBytes())) - } - } - - writer.Close() - w.Header().Add("Content-Type", writer.FormDataContentType()) - _, _ = w.Write(body.Bytes()) - } else { - w.Header().Add("Content-Type", "application/octet-stream") - _, _ = w.Write(ts.Payload.Get(0).AsBytes()) - } - - h.mGetBatchSent.Incr(1) - h.mGetSent.Incr(int64(batch.MessageCollapsedCount(ts.Payload))) - - _ = ts.Ack(ctx, nil) -} - -func (h *httpServerOutput) streamHandler(w http.ResponseWriter, r *http.Request) { - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Server error", http.StatusInternalServerError) - h.log.Error("Failed to cast response writer to flusher") - return - } - - if _, exists := h.conf.AllowedVerbs[r.Method]; !exists { - http.Error(w, "Incorrect method", http.StatusMethodNotAllowed) - return - } - - ctx, done := h.shutSig.SoftStopCtx(r.Context()) - defer done() - - for !h.shutSig.IsSoftStopSignalled() { - var ts message.Transaction - var open bool - - select { - case ts, open = <-h.transactions: - if !open { - go h.TriggerCloseNow() - return - } - case <-r.Context().Done(): - return - } - - var data []byte - if ts.Payload.Len() == 1 { - data = ts.Payload.Get(0).AsBytes() - } else { - data = append(bytes.Join(message.GetAllBytes(ts.Payload), []byte("\n")), byte('\n')) - } - - _, err := w.Write(data) - _ = ts.Ack(ctx, err) - if err != nil { - h.mStreamError.Incr(1) - return - } - - _, _ = w.Write([]byte("\n")) - flusher.Flush() - h.mStreamSent.Incr(int64(batch.MessageCollapsedCount(ts.Payload))) - h.mStreamBatchSent.Incr(1) - } -} - -func (h *httpServerOutput) wsHandler(w http.ResponseWriter, r *http.Request) { - var err error - defer func() { - if err != nil { - http.Error(w, "Bad request", http.StatusBadRequest) - h.log.Warn("Websocket request failed: %v\n", err) - return - } - }() - - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - defer ws.Close() - - ctx, done := h.shutSig.SoftStopCtx(r.Context()) - defer done() - - for !h.shutSig.IsSoftStopSignalled() { - var ts message.Transaction - var open bool - - select { - case ts, open = <-h.transactions: - if !open { - go h.TriggerCloseNow() - return - } - case <-r.Context().Done(): - return - case <-h.shutSig.SoftStopChan(): - return - } - - var werr error - for _, msg := range message.GetAllBytes(ts.Payload) { - if werr = ws.WriteMessage(websocket.BinaryMessage, msg); werr != nil { - break - } - h.mWSBatchSent.Incr(1) - h.mWSSent.Incr(int64(batch.MessageCollapsedCount(ts.Payload))) - } - if werr != nil { - h.mWSError.Incr(1) - } - _ = ts.Ack(ctx, werr) - } -} - -func (h *httpServerOutput) Consume(ts <-chan message.Transaction) error { - if h.transactions != nil { - return component.ErrAlreadyStarted - } - h.transactions = ts - - if h.server != nil { - go func() { - if h.conf.KeyFile != "" || h.conf.CertFile != "" { - h.log.Info( - "Serving messages through HTTPS GET request at: https://%s\n", - h.conf.Address+h.conf.Path, - ) - if err := h.server.ListenAndServeTLS( - h.conf.CertFile, h.conf.KeyFile, - ); err != http.ErrServerClosed { - h.log.Error("Server error: %v\n", err) - } - } else { - h.log.Info( - "Serving messages through HTTP GET request at: http://%s\n", - h.conf.Address+h.conf.Path, - ) - if err := h.server.ListenAndServe(); err != http.ErrServerClosed { - h.log.Error("Server error: %v\n", err) - } - } - - h.shutSig.TriggerSoftStop() - h.shutSig.TriggerHasStopped() - }() - } - return nil -} - -func (h *httpServerOutput) Connected() bool { - // Always return true as this is fuzzy right now. - return true -} - -func (h *httpServerOutput) TriggerCloseNow() { - h.shutSig.TriggerHardStop() - h.closeServerOnce.Do(func() { - if h.server != nil { - _ = h.server.Shutdown(context.Background()) - } - h.shutSig.TriggerHasStopped() - }) -} - -func (h *httpServerOutput) WaitForClose(ctx context.Context) error { - select { - case <-h.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/io/output_http_server_test.go b/internal/impl/io/output_http_server_test.go deleted file mode 100644 index a053fc62d3..0000000000 --- a/internal/impl/io/output_http_server_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func parseYAMLOutputConf(t testing.TB, formatStr string, args ...any) (conf output.Config) { - t.Helper() - var err error - conf, err = testutil.OutputFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return -} - -func TestHTTPServerOutputBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nTestLoops := 10 - - port := getFreePort(t) - conf := parseYAMLOutputConf(t, ` -http_server: - address: localhost:%v - path: /testpost -`, port) - - h, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - msgChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = h.Consume(msgChan); err != nil { - t.Error(err) - return - } - if err = h.Consume(msgChan); err == nil { - t.Error("Expected error from double listen") - } - - <-time.After(time.Millisecond * 100) - - // Test both single and multipart messages. - for i := 0; i < nTestLoops; i++ { - testStr := fmt.Sprintf("test%v", i) - - go func() { - testMsg := message.QuickBatch([][]byte{[]byte(testStr)}) - select { - case msgChan <- message.NewTransaction(testMsg, resChan): - case <-time.After(time.Second): - t.Error("Timed out waiting for message") - return - } - select { - case resMsg := <-resChan: - if resMsg != nil { - t.Error(resMsg) - } - case <-time.After(time.Second): - t.Error("Timed out waiting for response") - } - }() - - res, err := http.Get(fmt.Sprintf("http://localhost:%v/testpost", port)) - if err != nil { - t.Error(err) - return - } - res.Body.Close() - if res.StatusCode != 200 { - t.Errorf("Wrong error code returned: %v", res.StatusCode) - return - } - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} - -func TestHTTPServerOutputBadRequests(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - port := getFreePort(t) - conf := parseYAMLOutputConf(t, ` -http_server: - address: localhost:%v - path: /testpost -`, port) - - h, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - msgChan := make(chan message.Transaction) - - if err = h.Consume(msgChan); err != nil { - t.Error(err) - return - } - - <-time.After(time.Millisecond * 100) - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) - - _, err = http.Get(fmt.Sprintf("http://localhost:%v/testpost", port)) - if err == nil { - t.Error("request success when service should be closed") - } -} - -func TestHTTPServerOutputTimeout(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - port := getFreePort(t) - conf := parseYAMLOutputConf(t, ` -http_server: - address: localhost:%v - path: /testpost - timeout: 1ms -`, port) - - h, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - msgChan := make(chan message.Transaction) - - if err = h.Consume(msgChan); err != nil { - t.Error(err) - return - } - - <-time.After(time.Millisecond * 100) - - var res *http.Response - res, err = http.Get(fmt.Sprintf("http://localhost:%v/testpost", port)) - if err != nil { - t.Error(err) - return - } - if exp, act := http.StatusRequestTimeout, res.StatusCode; exp != act { - t.Errorf("Unexpected status code: %v != %v", exp, act) - } - - h.TriggerCloseNow() - require.NoError(t, h.WaitForClose(ctx)) -} diff --git a/internal/impl/io/output_socket.go b/internal/impl/io/output_socket.go deleted file mode 100644 index cf0dc99f48..0000000000 --- a/internal/impl/io/output_socket.go +++ /dev/null @@ -1,140 +0,0 @@ -package io - -import ( - "context" - "io" - "net" - "sync" - - "github.com/benthosdev/benthos/v4/internal/codec" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - osFieldNetwork = "network" - osFieldAddress = "address" -) - -func socketOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary(`Connects to a (tcp/udp/unix) server and sends a continuous stream of data, dividing messages according to the specified codec.`). - Categories("Network"). - Fields( - service.NewStringEnumField(osFieldNetwork, "unix", "tcp", "udp"). - Description("A network type to connect as."), - service.NewStringField(osFieldAddress). - Description("The address to connect to."). - Examples("/tmp/benthos.sock", "127.0.0.1:6000"), - service.NewInternalField(codec.NewWriterDocs("codec").HasDefault("lines")), - ) -} - -func init() { - err := service.RegisterOutput("socket", socketOutputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - maxInFlight = 1 - out, err = newSocketWriterFromParsed(conf, mgr) - return - }) - if err != nil { - panic(err) - } -} - -type socketWriter struct { - network string - address string - suffixFn codec.SuffixFn - appendMode bool - - log *service.Logger - - writer io.WriteCloser - writerMut sync.Mutex -} - -func newSocketWriterFromParsed(pConf *service.ParsedConfig, mgr *service.Resources) (w *socketWriter, err error) { - w = &socketWriter{ - log: mgr.Logger(), - } - if w.address, err = pConf.FieldString(osFieldAddress); err != nil { - return - } - if w.network, err = pConf.FieldString(osFieldNetwork); err != nil { - return - } - - var codecStr string - if codecStr, err = pConf.FieldString("codec"); err != nil { - return - } - if w.suffixFn, w.appendMode, err = codec.GetWriter(codecStr); err != nil { - return - } - return -} - -func (s *socketWriter) Connect(ctx context.Context) error { - s.writerMut.Lock() - defer s.writerMut.Unlock() - if s.writer != nil { - return nil - } - - var err error - if s.writer, err = net.Dial(s.network, s.address); err != nil { - return err - } - return nil -} - -func (s *socketWriter) writeTo(wtr io.Writer, p *service.Message) error { - mBytes, err := p.AsBytes() - if err != nil { - return err - } - - suffix, addSuffix := s.suffixFn(mBytes) - - if _, err := wtr.Write(mBytes); err != nil { - return err - } - if addSuffix { - if _, err := wtr.Write(suffix); err != nil { - return err - } - } - return nil -} - -func (s *socketWriter) Write(ctx context.Context, msg *service.Message) error { - s.writerMut.Lock() - w := s.writer - s.writerMut.Unlock() - - if w == nil { - return component.ErrNotConnected - } - - serr := s.writeTo(w, msg) - if serr != nil || !s.appendMode { - s.writerMut.Lock() - _ = s.writer.Close() - s.writer = nil - s.writerMut.Unlock() - } - return serr -} - -func (s *socketWriter) Close(ctx context.Context) error { - s.writerMut.Lock() - defer s.writerMut.Unlock() - - var err error - if s.writer != nil { - err = s.writer.Close() - s.writer = nil - } - return err -} diff --git a/internal/impl/io/output_socket_test.go b/internal/impl/io/output_socket_test.go deleted file mode 100644 index 8abb819812..0000000000 --- a/internal/impl/io/output_socket_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package io - -import ( - "bytes" - "context" - "fmt" - "net" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func socketWriterFromConf(t testing.TB, confStr string, bits ...any) *socketWriter { - t.Helper() - - conf, err := socketOutputSpec().ParseYAML(fmt.Sprintf(confStr, bits...), nil) - require.NoError(t, err) - - w, err := newSocketWriterFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - return w -} - -func TestSocketBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tmpDir := t.TempDir() - - ln, err := net.Listen("unix", filepath.Join(tmpDir, "benthos.sock")) - if err != nil { - t.Fatalf("failed to listen on address: %v", err) - } - defer ln.Close() - - wtr := socketWriterFromConf(t, ` -network: %v -address: %v -`, ln.Addr().Network(), ln.Addr().String()) - - defer func() { - if err := wtr.Close(ctx); err != nil { - t.Error(err) - } - }() - - go func() { - if cerr := wtr.Connect(context.Background()); cerr != nil { - t.Error(cerr) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - var buf bytes.Buffer - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetReadDeadline(time.Now().Add(time.Second * 5)) - _, _ = buf.ReadFrom(conn) - wg.Done() - }() - - if err = wtr.Write(context.Background(), service.NewMessage([]byte("foo"))); err != nil { - t.Error(err) - } - if err = wtr.Write(context.Background(), service.NewMessage([]byte("bar\n"))); err != nil { - t.Error(err) - } - if err = wtr.Write(context.Background(), service.NewMessage([]byte("baz"))); err != nil { - t.Error(err) - } - - require.NoError(t, wtr.Close(ctx)) - wg.Wait() - - exp := "foo\nbar\nbaz\n" - if act := buf.String(); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - conn.Close() -} - -type testOutputWrapPacketConn struct { - r net.PacketConn -} - -func (w *testOutputWrapPacketConn) Read(p []byte) (n int, err error) { - n, _, err = w.r.ReadFrom(p) - return -} - -func TestUDPSocketBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - conn, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - if conn, err = net.ListenPacket("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer conn.Close() - - wtr := socketWriterFromConf(t, ` -network: udp -address: %v -`, conn.LocalAddr().String()) - - defer func() { - if err := wtr.Close(ctx); err != nil { - t.Error(err) - } - }() - - if cerr := wtr.Connect(context.Background()); cerr != nil { - t.Fatal(cerr) - } - - var buf bytes.Buffer - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetReadDeadline(time.Now().Add(time.Second * 5)) - _, _ = buf.ReadFrom(&testOutputWrapPacketConn{r: conn}) - wg.Done() - }() - - if err = wtr.Write(context.Background(), service.NewMessage([]byte("foo"))); err != nil { - t.Error(err) - } - if err = wtr.Write(context.Background(), service.NewMessage([]byte("bar\n"))); err != nil { - t.Error(err) - } - if err = wtr.Write(context.Background(), service.NewMessage([]byte("baz"))); err != nil { - t.Error(err) - } - - require.NoError(t, wtr.Close(ctx)) - wg.Wait() - - exp := "foo\nbar\nbaz\n" - if act := buf.String(); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - conn.Close() -} - -func TestTCPSocketBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil { - t.Fatalf("failed to listen on a port: %v", err) - } - } - defer ln.Close() - - wtr := socketWriterFromConf(t, ` -network: tcp -address: %v -`, ln.Addr().String()) - - defer func() { - if err := wtr.Close(ctx); err != nil { - t.Error(err) - } - }() - - go func() { - if cerr := wtr.Connect(context.Background()); cerr != nil { - t.Error(cerr) - } - }() - - conn, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - - var buf bytes.Buffer - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - _ = conn.SetReadDeadline(time.Now().Add(time.Second * 5)) - _, _ = buf.ReadFrom(conn) - wg.Done() - }() - - if err = wtr.Write(context.Background(), service.NewMessage([]byte("foo"))); err != nil { - t.Error(err) - } - if err = wtr.Write(context.Background(), service.NewMessage([]byte("bar\n"))); err != nil { - t.Error(err) - } - if err = wtr.Write(context.Background(), service.NewMessage([]byte("baz"))); err != nil { - t.Error(err) - } - - require.NoError(t, wtr.Close(ctx)) - wg.Wait() - - exp := "foo\nbar\nbaz\n" - if act := buf.String(); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - conn.Close() -} diff --git a/internal/impl/io/output_stdout.go b/internal/impl/io/output_stdout.go deleted file mode 100644 index 1d93698f0b..0000000000 --- a/internal/impl/io/output_stdout.go +++ /dev/null @@ -1,82 +0,0 @@ -package io - -import ( - "context" - "io" - "os" - - "github.com/benthosdev/benthos/v4/internal/codec" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterOutput( - "stdout", service.NewConfigSpec(). - Stable(). - Categories("Local"). - Summary(`Prints messages to stdout as a continuous stream of data.`). - Fields(service.NewInternalField(codec.NewWriterDocs("codec").AtVersion("3.46.0").HasDefault("lines"))), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Output, int, error) { - w, err := newStdoutWriterFromParsed(conf) - if err != nil { - return nil, 0, err - } - return w, 1, nil - }) - if err != nil { - panic(err) - } -} - -type stdoutWriter struct { - suffixFn codec.SuffixFn - handle io.WriteCloser -} - -func newStdoutWriterFromParsed(conf *service.ParsedConfig) (*stdoutWriter, error) { - codecStr, err := conf.FieldString("codec") - if err != nil { - return nil, err - } - - codec, _, err := codec.GetWriter(codecStr) - if err != nil { - return nil, err - } - - return &stdoutWriter{ - suffixFn: codec, - handle: os.Stdout, - }, nil -} - -func (w *stdoutWriter) Connect(ctx context.Context) error { - return nil -} - -func (w *stdoutWriter) writeTo(wtr io.Writer, p *service.Message) error { - mBytes, err := p.AsBytes() - if err != nil { - return err - } - - suffix, addSuffix := w.suffixFn(mBytes) - - if _, err := wtr.Write(mBytes); err != nil { - return err - } - if addSuffix { - if _, err := wtr.Write(suffix); err != nil { - return err - } - } - return nil -} - -func (w *stdoutWriter) Write(ctx context.Context, msg *service.Message) error { - return w.writeTo(w.handle, msg) -} - -func (w *stdoutWriter) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/io/output_subprocess.go b/internal/impl/io/output_subprocess.go deleted file mode 100644 index eadd2bd0df..0000000000 --- a/internal/impl/io/output_subprocess.go +++ /dev/null @@ -1,175 +0,0 @@ -package io - -import ( - "context" - "fmt" - "io" - "os/exec" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - soFieldName = "name" - soFieldArgs = "args" - soFieldCodec = "codec" -) - -func subprocOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Beta(). - Categories("Utility"). - Summary("Executes a command, runs it as a subprocess, and writes messages to it over stdin."). - Description(` -Messages are written according to a specified codec. The process is expected to terminate gracefully when stdin is closed. - -If the subprocess exits unexpectedly then Benthos will log anything printed to stderr and will log the exit code, and will attempt to execute the command again until success. - -The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory.`). - Fields( - service.NewStringField(soFieldName). - Description("The command to execute as a subprocess."), - service.NewStringListField(soFieldArgs). - Description("A list of arguments to provide the command."). - Default([]any{}), - service.NewStringEnumField(soFieldCodec, "lines"). - Description("The way in which messages should be written to the subprocess."). - Default("lines"), - ) -} - -func init() { - err := service.RegisterBatchOutput( - "subprocess", subprocOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - maxInFlight = 1 - out, err = newSubprocessWriterFromParsed(conf, mgr.Logger()) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -func subprocOutputLinesCodec(w io.Writer, b []byte) error { - _, err := fmt.Fprintln(w, string(b)) - return err -} - -type subprocOutputCodec func(io.Writer, []byte) error - -func subprocOutputCodecFromStr(codec string) (subprocOutputCodec, error) { - // TODO: Flesh this out with more options based on s.conf.Codec. - if codec == "lines" { - return subprocOutputLinesCodec, nil - } - return nil, fmt.Errorf("codec not recognised: %v", codec) -} - -//------------------------------------------------------------------------------ - -type subprocessWriter struct { - log *service.Logger - name string - args []string - - codec subprocOutputCodec - - cmdMut sync.Mutex - stdin io.WriteCloser -} - -func newSubprocessWriterFromParsed(conf *service.ParsedConfig, log *service.Logger) (s *subprocessWriter, err error) { - s = &subprocessWriter{log: log} - if s.name, err = conf.FieldString(soFieldName); err != nil { - return - } - if s.args, err = conf.FieldStringList(soFieldArgs); err != nil { - return - } - - var codecStr string - if codecStr, err = conf.FieldString(soFieldCodec); err != nil { - return - } - if s.codec, err = subprocOutputCodecFromStr(codecStr); err != nil { - return nil, err - } - return s, nil -} - -func (s *subprocessWriter) Connect(ctx context.Context) error { - s.cmdMut.Lock() - defer s.cmdMut.Unlock() - - if s.stdin != nil { - return nil - } - - cmd := exec.Command(s.name, s.args...) - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - - go func() { - stdout, err := cmd.Output() - if len(stdout) > 0 { - s.log.Debugf("Process exited with: %s\n", stdout) - } else { - s.log.Debug("Process exited") - } - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - if len(exitErr.Stderr) > 0 { - s.log.Errorf("Process exited with error: %s\n", exitErr.Stderr) - } else if !exitErr.Success() { - s.log.Errorf("Process exited with code %v: %v\n", exitErr.ExitCode(), exitErr.String()) - } - } else { - s.log.Errorf("Process error: %v\n", err) - } - } - s.cmdMut.Lock() - if s.stdin != nil { - s.stdin.Close() - s.stdin = nil - } - s.cmdMut.Unlock() - }() - - s.stdin = stdin - return nil -} - -func (s *subprocessWriter) WriteBatch(ctx context.Context, b service.MessageBatch) error { - s.cmdMut.Lock() - defer s.cmdMut.Unlock() - if s.stdin == nil { - return component.ErrNotConnected - } - - return b.WalkWithBatchedErrors(func(i int, m *service.Message) error { - mBytes, err := m.AsBytes() - if err != nil { - return err - } - return s.codec(s.stdin, mBytes) - }) -} - -func (s *subprocessWriter) Close(ctx context.Context) error { - s.cmdMut.Lock() - defer s.cmdMut.Unlock() - - var err error - if s.stdin != nil { - err = s.stdin.Close() - s.stdin = nil - } - return err -} diff --git a/internal/impl/io/output_subprocess_test.go b/internal/impl/io/output_subprocess_test.go deleted file mode 100644 index 460e034308..0000000000 --- a/internal/impl/io/output_subprocess_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "os" - "path" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service/integration" - - _ "github.com/benthosdev/benthos/v4/internal/impl/io" -) - -func sendMsg(t *testing.T, msg string, tChan chan message.Transaction) { - t.Helper() - - m := message.Batch{message.NewPart([]byte(msg))} - - resChan := make(chan error) - - select { - case tChan <- message.NewTransaction(m, resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("timed out") - } -} - -func TestSubprocessOutputBasic(t *testing.T) { - integration.CheckSkip(t) - - t.Parallel() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - dir := t.TempDir() - - filePath := testProgram(t, fmt.Sprintf(`package main - -import ( - "fmt" - "bufio" - "os" - "strings" - "bytes" -) - -func main() { - var buf bytes.Buffer - - target := "%v/output.txt" - - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - fmt.Fprintln(&buf, strings.ToUpper(scanner.Text())) - } - - if err := scanner.Err(); err != nil { - panic(err) - } - - if err := os.WriteFile(target, buf.Bytes(), 0o644); err != nil { - panic(err) - } -} -`, dir)) - - conf := output.NewConfig() - conf.Type = "subprocess" - conf.Plugin = map[string]any{ - "name": "go", - "args": []any{"run", filePath}, - } - - o, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - tranChan := make(chan message.Transaction) - require.NoError(t, o.Consume(tranChan)) - - sendMsg(t, "foo", tranChan) - sendMsg(t, "bar", tranChan) - sendMsg(t, "baz", tranChan) - - o.TriggerCloseNow() - o.TriggerCloseNow() // No panic on double close - - require.NoError(t, o.WaitForClose(ctx)) - - assert.Eventually(t, func() bool { - resBytes, err := os.ReadFile(path.Join(dir, "output.txt")) - if err != nil { - return false - } - return string(resBytes) == "FOO\nBAR\nBAZ\n" - }, time.Second, time.Millisecond*100) -} - -func TestSubprocessOutputEarlyExit(t *testing.T) { - t.Skip() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - dir := t.TempDir() - - filePath := testProgram(t, fmt.Sprintf(`package main - -import ( - "fmt" - "bufio" - "os" - "strings" -) - -func main() { - scanner := bufio.NewScanner(os.Stdin) - if scanner.Scan() { - f, err := os.OpenFile("%v/"+scanner.Text()+".txt", os.O_RDWR|os.O_CREATE, 0o644) - if err != nil { - panic(err) - } - fmt.Fprintln(f, strings.ToUpper(scanner.Text())) - f.Sync() - } else if err := scanner.Err(); err != nil { - panic(err) - } -} -`, dir)) - - conf := output.NewConfig() - conf.Type = "subprocess" - conf.Plugin = map[string]any{ - "name": "go", - "args": []any{"run", filePath}, - } - - o, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - tranChan := make(chan message.Transaction) - require.NoError(t, o.Consume(tranChan)) - - sendMsg(t, "foo", tranChan) - - assert.Eventually(t, func() bool { - sendMsg(t, "bar", tranChan) - - resBytes, err := os.ReadFile(path.Join(dir, "bar.txt")) - if err != nil { - return false - } - return string(resBytes) == "BAR\n" - }, time.Second, time.Millisecond*100) - - assert.Eventually(t, func() bool { - sendMsg(t, "baz", tranChan) - - resBytes, err := os.ReadFile(path.Join(dir, "baz.txt")) - if err != nil { - return false - } - return string(resBytes) == "BAZ\n" - }, time.Second, time.Millisecond*100) - - o.TriggerCloseNow() - require.NoError(t, o.WaitForClose(ctx)) - - resBytes, err := os.ReadFile(path.Join(dir, "foo.txt")) - require.NoError(t, err) - assert.Equal(t, "FOO\n", string(resBytes)) - - resBytes, err = os.ReadFile(path.Join(dir, "bar.txt")) - require.NoError(t, err) - assert.Equal(t, "BAR\n", string(resBytes)) - - resBytes, err = os.ReadFile(path.Join(dir, "baz.txt")) - require.NoError(t, err) - assert.Equal(t, "BAZ\n", string(resBytes)) -} diff --git a/internal/impl/io/output_websocket.go b/internal/impl/io/output_websocket.go deleted file mode 100644 index ff992abda1..0000000000 --- a/internal/impl/io/output_websocket.go +++ /dev/null @@ -1,188 +0,0 @@ -package io - -import ( - "context" - "crypto/tls" - "errors" - "io/fs" - "net/http" - "net/url" - "sync" - - "github.com/gorilla/websocket" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func websocketOutputSpec() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Categories("Network"). - Summary("Sends messages to an HTTP server via a websocket connection."). - Field(service.NewURLField("url").Description("The URL to connect to.")). - Field(service.NewTLSToggledField("tls")) - - for _, f := range service.NewHTTPRequestAuthSignerFields() { - spec = spec.Field(f) - } - - return spec -} - -func init() { - err := service.RegisterBatchOutput( - "websocket", websocketOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - maxInFlight = 1 - oldMgr := interop.UnwrapManagement(mgr) - var w *websocketWriter - if w, err = newWebsocketWriterFromParsed(conf, oldMgr); err != nil { - return - } - var o output.Streamed - if o, err = output.NewAsyncWriter("websocket", 1, w, oldMgr); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(o) - return - }) - if err != nil { - panic(err) - } -} - -type websocketWriter struct { - log log.Modular - mgr bundle.NewManagement - - lock *sync.Mutex - - client *websocket.Conn - urlParsed *url.URL - urlStr string - tlsEnabled bool - tlsConf *tls.Config - reqSigner func(f fs.FS, req *http.Request) error -} - -func newWebsocketWriterFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (*websocketWriter, error) { - ws := &websocketWriter{ - log: mgr.Logger(), - mgr: mgr, - lock: &sync.Mutex{}, - } - - var err error - if ws.urlParsed, err = conf.FieldURL("url"); err != nil { - return nil, err - } - if ws.urlStr, err = conf.FieldString("url"); err != nil { - return nil, err - } - if ws.tlsConf, ws.tlsEnabled, err = conf.FieldTLSToggled("tls"); err != nil { - return nil, err - } - if ws.reqSigner, err = conf.HTTPRequestAuthSignerFromParsed(); err != nil { - return nil, err - } - return ws, nil -} - -func (w *websocketWriter) getWS() *websocket.Conn { - w.lock.Lock() - ws := w.client - w.lock.Unlock() - return ws -} - -func (w *websocketWriter) Connect(ctx context.Context) error { - w.lock.Lock() - defer w.lock.Unlock() - - if w.client != nil { - return nil - } - - headers := http.Header{} - - err := w.reqSigner(w.mgr.FS(), &http.Request{ - URL: w.urlParsed, - Header: headers, - }) - if err != nil { - return err - } - - var ( - client *websocket.Conn - res *http.Response - ) - - defer func() { - if res != nil { - res.Body.Close() - } - }() - - if w.tlsEnabled { - dialer := websocket.Dialer{ - TLSClientConfig: w.tlsConf, - } - if client, res, err = dialer.Dial(w.urlStr, headers); err != nil { - return err - } - } else if client, res, err = websocket.DefaultDialer.Dial(w.urlStr, headers); err != nil { - return err - } - - go func(c *websocket.Conn) { - for { - if _, _, cerr := c.NextReader(); cerr != nil { - c.Close() - break - } - } - }(client) - - w.client = client - return nil -} - -func (w *websocketWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - client := w.getWS() - if client == nil { - return component.ErrNotConnected - } - - err := msg.Iter(func(i int, p *message.Part) error { - return client.WriteMessage(websocket.BinaryMessage, p.AsBytes()) - }) - if err != nil { - w.lock.Lock() - w.client = nil - w.lock.Unlock() - if errors.Is(err, websocket.ErrCloseSent) { - return component.ErrNotConnected - } - return err - } - return nil -} - -func (w *websocketWriter) Close(ctx context.Context) error { - w.lock.Lock() - defer w.lock.Unlock() - - var err error - if w.client != nil { - err = w.client.Close() - w.client = nil - } - return err -} diff --git a/internal/impl/io/output_websocket_test.go b/internal/impl/io/output_websocket_test.go deleted file mode 100644 index 66a8227d9b..0000000000 --- a/internal/impl/io/output_websocket_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package io - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestWebsocketOutputBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - expMsgs := []string{ - "foo", - "bar", - "baz", - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - - defer ws.Close() - - var actBytes []byte - for _, exp := range expMsgs { - if _, actBytes, err = ws.ReadMessage(); err != nil { - t.Error(err) - } else if act := string(actBytes); act != exp { - t.Errorf("Wrong msg contents: %v != %v", act, exp) - } - } - })) - - wsURL, err := url.Parse(server.URL) - require.NoError(t, err) - - wsURL.Scheme = "ws" - - conf := parseYAMLOutputConf(t, ` -websocket: - url: %v -`, wsURL.String()) - - m, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - tChan := make(chan message.Transaction) - require.NoError(t, m.Consume(tChan)) - - for _, msg := range expMsgs { - require.NoError(t, writeBatchToChan(ctx, t, message.QuickBatch([][]byte{[]byte(msg)}), tChan)) - } - - m.TriggerCloseNow() - require.NoError(t, m.WaitForClose(ctx)) -} - -func TestWebsocketOutputClose(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var ws *websocket.Conn - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - - ws.Close() - })) - - wsURL, err := url.Parse(server.URL) - require.NoError(t, err) - - wsURL.Scheme = "ws" - - conf := parseYAMLOutputConf(t, ` -websocket: - url: %v -`, wsURL.String()) - - m, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - tChan := make(chan message.Transaction) - require.NoError(t, m.Consume(tChan)) - - m.TriggerCloseNow() - require.NoError(t, m.WaitForClose(ctx)) -} diff --git a/internal/impl/io/package.go b/internal/impl/io/package.go deleted file mode 100644 index ad1d249e4b..0000000000 --- a/internal/impl/io/package.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package io contains component implementations that have a small dependency -// footprint (mostly standard library) and interact with external systems via -// the filesystem and/or network sockets. -package io diff --git a/internal/impl/io/processor_command.go b/internal/impl/io/processor_command.go deleted file mode 100644 index 47e7cfa0f4..0000000000 --- a/internal/impl/io/processor_command.go +++ /dev/null @@ -1,164 +0,0 @@ -package io - -import ( - "bytes" - "context" - "fmt" - "os/exec" - - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - cpNameField = "name" - cpArgsField = "args_mapping" -) - -func commandProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Version("4.21.0"). - Categories("Integration"). - Summary("Executes a command for each message."). - Description(` -The specified command is executed for each message processed, with the raw bytes of the message being fed into the stdin of the command process, and the resulting message having its contents replaced with the stdout of it. - -== Performance - -Since this processor executes a new process for each message performance will likely be an issue for high throughput streams. If this is the case then consider using the xref:components:processors/subprocess.adoc[`+"`subprocess` processor"+`] instead as it keeps the underlying process alive long term and uses codecs to insert and extract inputs and outputs to it via stdin/stdout. - -== Error handling - -If a non-zero error code is returned by the command then an error containing the entirety of stderr (or a generic message if nothing is written) is set on the message. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about xref:configuration:error_handling.adoc[these patterns]. - -If the command is successful but stderr is written to then a metadata field `+"`command_stderr`"+` is populated with its contents. -`). - Fields( - service.NewInterpolatedStringField(cpNameField). - Description("The name of the command to execute."). - Examples("bash", "go", "${! @command }"), - service.NewBloblangField(cpArgsField). - Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that, when specified, should resolve into an array of arguments to pass to the command. Command arguments are expressed this way in order to support dynamic behavior."). - Optional(). - Examples(`[ "-c", this.script_path ]`), - ). - Example( - "Cron Scheduled Command", - `This example uses a xref:components:inputs/generate.adoc[`+"`generate`"+` input] to trigger a command on a cron schedule:`, - ` -input: - generate: - interval: '0,30 */2 * * * *' - mapping: 'root = ""' # Empty string as we do not need to pipe anything to stdin - processors: - - command: - name: df - args_mapping: '[ "-h" ]' -`, - ). - Example( - "Dynamic Command Execution", - `This example config takes structured messages of the form `+"`"+`{"command":"echo","args":["foo"]}`+"`"+` and uses their contents to execute the contained command and arguments dynamically, replacing its contents with the command result printed to stdout:`, - ` -pipeline: - processors: - - command: - name: ${! this.command } - args_mapping: 'this.args' -`, - ) -} - -func init() { - err := service.RegisterProcessor( - "command", commandProcSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return newCommandProcFromParsed(conf, mgr) - }) - if err != nil { - panic(err) - } -} - -type commandProc struct { - name *service.InterpolatedString - argsMapping *bloblang.Executor -} - -func newCommandProcFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (proc *commandProc, err error) { - proc = &commandProc{} - - if proc.name, err = conf.FieldInterpolatedString(cpNameField); err != nil { - return - } - - if conf.Contains(cpArgsField) { - if proc.argsMapping, err = conf.FieldBloblang(cpArgsField); err != nil { - return - } - } - - return proc, nil -} - -func (c *commandProc) Process(ctx context.Context, msg *service.Message) (service.MessageBatch, error) { - name, err := c.name.TryString(msg) - if err != nil { - return nil, fmt.Errorf("name interpolation error: %w", err) - } - - var args []string - - if c.argsMapping != nil { - mapRes, err := msg.BloblangQuery(c.argsMapping) - if err != nil { - return nil, fmt.Errorf("args mapping error: %w", err) - } - - mapResI, err := mapRes.AsStructured() - if err != nil { - return nil, fmt.Errorf("args mapping error: %w", err) - } - - switch t := mapResI.(type) { - case []any: - args = make([]string, len(t)) - for i, v := range t { - args[i] = value.IToString(v) - } - case []string: - args = t - default: - return nil, fmt.Errorf("args mapping result error: %w", value.NewTypeError(mapResI, value.TArray)) - } - } - - msgBytes, err := msg.AsBytes() - if err != nil { - return nil, err - } - - var stdout, stderr bytes.Buffer - - cmd := exec.CommandContext(ctx, name, args...) - cmd.Stdin = bytes.NewReader(msgBytes) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err = cmd.Run() - outBytes, errBytes := stdout.Bytes(), stderr.Bytes() - if err != nil { - return nil, fmt.Errorf("execution error: %w: %s", err, errBytes) - } - - msg.SetBytes(outBytes) - if len(errBytes) > 0 { - msg.MetaSet("command_stderr", string(errBytes)) - } - return service.MessageBatch{msg}, nil -} - -func (c *commandProc) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/io/processor_command_test.go b/internal/impl/io/processor_command_test.go deleted file mode 100644 index e5ca1f4d81..0000000000 --- a/internal/impl/io/processor_command_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package io - -import ( - "context" - "testing" - "time" - - "github.com/benthosdev/benthos/v4/public/service" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCommand(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tests := []struct { - name string - config string - input string - outputContains string - errContains string - }{ - { - name: "static with args", - config: ` -name: go -args_mapping: '[ "help" ]' -`, - outputContains: `Go is a tool for managing Go source code.`, - input: "", - }, - { - name: "static no args", - config: ` -name: cat -`, - outputContains: `foo`, - input: "foo", - }, - { - name: "error command", - config: ` -name: go -`, - input: "", - errContains: "exit status 2", - }, - { - name: "dynamic command", - config: ` -name: ${! this.name } -args_mapping: '[ "help" ]' -`, - input: `{"name":"go"}`, - outputContains: `Go is a tool for managing Go source code.`, - }, - { - name: "dynamic args", - config: ` -name: ${! this.name } -args_mapping: 'this.args' -`, - input: `{"name":"go","args":["help"]}`, - outputContains: `Go is a tool for managing Go source code.`, - }, - { - name: "static capture stdout", - config: ` -name: cat -args_mapping: '[ "-n" ]' -`, - input: "hello world", - outputContains: "1\thello world", - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - pConf, err := commandProcSpec().ParseYAML(test.config, nil) - require.NoError(t, err) - - cmdProc, err := newCommandProcFromParsed(pConf, service.MockResources()) - require.NoError(t, err) - - res, err := cmdProc.Process(tCtx, service.NewMessage([]byte(test.input))) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - require.Len(t, res, 1) - - resBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Contains(t, string(resBytes), test.outputContains) - } - }) - } -} diff --git a/internal/impl/io/processor_http.go b/internal/impl/io/processor_http.go deleted file mode 100644 index a471a76c96..0000000000 --- a/internal/impl/io/processor_http.go +++ /dev/null @@ -1,240 +0,0 @@ -package io - -import ( - "context" - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/httpclient" - "github.com/benthosdev/benthos/v4/public/service" -) - -func httpProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Integration"). - Summary("Performs an HTTP request using a message batch as the request body, and replaces the original message parts with the body of the response."). - Description(` -The `+"`rate_limit`"+` field can be used to specify a rate limit xref:components:rate_limits/about.adoc[resource] to cap the rate of requests across all parallel components service wide. - -The URL and header values of this type can be dynamically set using function interpolations described xref:configuration:interpolation.adoc#bloblang-queries[here]. - -In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the `+"xref:components:processors/branch.adoc[`branch` processor]"+`. - -== Response codes - -Benthos considers any response code between 200 and 299 inclusive to indicate a successful response, you can add more success status codes with the field `+"`successful_on`"+`. - -When a request returns a response code within the `+"`backoff_on`"+` field it will be retried after increasing intervals. - -When a request returns a response code within the `+"`drop_on`"+` field it will not be reattempted and is immediately considered a failed request. - -== Add metadata - -If the request returns an error response code this processor sets a metadata field `+"`http_status_code`"+` on the resulting message. - -Use the field `+"`extract_headers`"+` to specify rules for which other headers should be copied into the resulting message from the response. - -== Error handling - -When all retry attempts for a message are exhausted the processor cancels the attempt. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about xref:configuration:error_handling.adoc[these patterns].`). - Example( - "Branched Request", - `This example uses a `+"xref:components:processors/branch.adoc[`branch` processor]"+` to strip the request message into an empty body, grab an HTTP payload, and place the result back into the original message at the path `+"`repo.status`"+`:`, - ` -pipeline: - processors: - - branch: - request_map: 'root = ""' - processors: - - http: - url: https://hub.docker.com/v2/repositories/jeffail/benthos - verb: GET - headers: - Content-Type: application/json - result_map: 'root.repo.status = this' -`, - ). - Field(httpclient.ConfigField("POST", false, - service.NewBoolField("batch_as_multipart").Description("Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341].").Advanced().Default(false), - service.NewBoolField("parallel").Description("When processing batched messages, whether to send messages of the batch in parallel, otherwise they are sent serially.").Default(false)), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "http", httpProcSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - return newHTTPProcFromParsed(conf, mgr) - }) - if err != nil { - panic(err) - } -} - -type httpProc struct { - client *httpclient.Client - asMultipart bool - parallel bool - rawURL string - log *service.Logger -} - -func newHTTPProcFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (*httpProc, error) { - oldConf, err := httpclient.ConfigFromParsed(conf) - if err != nil { - return nil, err - } - - asMultipart, err := conf.FieldBool("batch_as_multipart") - if err != nil { - return nil, err - } - - parallel, err := conf.FieldBool("parallel") - if err != nil { - return nil, err - } - - rawURL, _ := conf.FieldString("url") - - g := &httpProc{ - rawURL: rawURL, - log: mgr.Logger(), - asMultipart: asMultipart, - parallel: parallel, - } - if g.client, err = httpclient.NewClientFromOldConfig(oldConf, mgr); err != nil { - return nil, err - } - return g, nil -} - -func (h *httpProc) ProcessBatch(ctx context.Context, msg service.MessageBatch) ([]service.MessageBatch, error) { - var responseMsg service.MessageBatch - - if h.asMultipart || len(msg) == 1 { - // Easy, just do a single request. - resultMsg, err := h.client.Send(context.Background(), msg) - if err != nil { - var code int - var hErr httpclient.ErrUnexpectedHTTPRes - if ok := errors.As(err, &hErr); ok { - code = hErr.Code - } - h.log.Errorf("HTTP request to '%v' failed: %v", h.rawURL, err) - responseMsg = msg.Copy() - for _, p := range responseMsg { - if code > 0 { - p.MetaSetMut("http_status_code", code) - } - p.SetError(err) - } - } else { - parts := make(service.MessageBatch, len(resultMsg)) - for i, p := range resultMsg { - if i < len(msg) { - parts[i] = msg[i].Copy() - } else { - parts[i] = msg[0].Copy() - } - mBytes, err := p.AsBytes() - if err != nil { - return nil, err - } - parts[i].SetBytes(mBytes) - _ = p.MetaWalkMut(func(k string, v any) error { - parts[i].MetaSetMut(k, v) - return nil - }) - } - responseMsg = parts - } - } else if !h.parallel { - for _, p := range msg { - tmpMsg := service.MessageBatch{p} - result, err := h.client.Send(context.Background(), tmpMsg) - if err != nil { - h.log.Errorf("HTTP request to '%v' failed: %v", h.rawURL, err) - - errPart := p.Copy() - var hErr httpclient.ErrUnexpectedHTTPRes - if ok := errors.As(err, &hErr); ok { - errPart.MetaSetMut("http_status_code", hErr.Code) - } - errPart.SetError(err) - responseMsg = append(responseMsg, errPart) - } - - for _, rp := range result { - tmpPart := p.Copy() - mBytes, err := rp.AsBytes() - if err != nil { - return nil, err - } - tmpPart.SetBytes(mBytes) - _ = rp.MetaWalkMut(func(k string, v any) error { - tmpPart.MetaSetMut(k, v) - return nil - }) - responseMsg = append(responseMsg, tmpPart) - } - } - } else { - // Hard, need to do parallel requests limited by max parallelism. - results := make(service.MessageBatch, len(msg)) - for i, p := range msg { - results[i] = p.Copy() - } - reqChan, resChan := make(chan int), make(chan error) - - for i := 0; i < len(msg); i++ { - go func() { - for index := range reqChan { - tmpMsg := service.MessageBatch{msg[index]} - result, err := h.client.Send(context.Background(), tmpMsg) - if err == nil && len(result) != 1 { - err = fmt.Errorf("unexpected response size: %v", len(result)) - } - if err == nil { - mBytes, _ := result[0].AsBytes() - results[index].SetBytes(mBytes) - _ = result[0].MetaWalkMut(func(k string, v any) error { - results[index].MetaSetMut(k, v) - return nil - }) - } else { - var hErr httpclient.ErrUnexpectedHTTPRes - if ok := errors.As(err, &hErr); ok { - results[index].MetaSetMut("http_status_code", hErr.Code) - } - results[index].SetError(err) - } - resChan <- err - } - }() - } - go func() { - for i := 0; i < len(msg); i++ { - reqChan <- i - } - }() - for i := 0; i < len(msg); i++ { - if err := <-resChan; err != nil { - h.log.Errorf("HTTP parallel request to '%v' failed: %v", h.rawURL, err) - } - } - - close(reqChan) - responseMsg = results - } - - if len(responseMsg) < 1 { - return nil, fmt.Errorf("HTTP response from '%v' was empty", h.rawURL) - } - return []service.MessageBatch{responseMsg}, nil -} - -func (h *httpProc) Close(ctx context.Context) error { - return h.client.Close(ctx) -} diff --git a/internal/impl/io/processor_http_test.go b/internal/impl/io/processor_http_test.go deleted file mode 100644 index f7084f3c18..0000000000 --- a/internal/impl/io/processor_http_test.go +++ /dev/null @@ -1,434 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "sync" - "sync/atomic" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func parseYAMLProcConf(t testing.TB, formatStr string, args ...any) (conf processor.Config) { - t.Helper() - var err error - conf, err = testutil.ProcessorFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return -} - -func TestHTTPClientRetries(t *testing.T) { - var reqCount uint32 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddUint32(&reqCount, 1) - http.Error(w, "test error", http.StatusForbidden) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost - retry_period: 1ms - retries: 3 -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("test")})) - if res != nil { - t.Fatal(res) - } - if len(msgs) != 1 { - t.Fatal("Wrong count of error messages") - } - if msgs[0].Len() != 1 { - t.Fatal("Wrong count of error message parts") - } - if exp, act := "test", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message contents: %v != %v", act, exp) - } - assert.Error(t, msgs[0].Get(0).ErrorGet()) - if exp, act := "403", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } - - if exp, act := uint32(4), atomic.LoadUint32(&reqCount); exp != act { - t.Errorf("Wrong count of HTTP attempts: %v != %v", exp, act) - } -} - -func TestHTTPClientBasic(t *testing.T) { - i := 0 - expPayloads := []string{"foo", "bar", "baz"} - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - if exp, act := expPayloads[i], string(reqBytes); exp != act { - t.Errorf("Wrong payload value: %v != %v", act, exp) - } - i++ - w.Header().Add("foobar", "baz") - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("foobar")) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost - retry_period: 1ms - retries: 3 -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo")})) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "201", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } else if exp, act := "", msgs[0].Get(0).MetaGetStr("foobar"); exp != act { - t.Errorf("Wrong metadata value: %v != %v", act, exp) - } - - msgs, res = h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "201", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } else if exp, act := "", msgs[0].Get(0).MetaGetStr("foobar"); exp != act { - t.Errorf("Wrong metadata value: %v != %v", act, exp) - } - - // Check metadata persists. - msg := message.QuickBatch([][]byte{[]byte("baz")}) - msg.Get(0).MetaSetMut("foo", "bar") - msgs, res = h.ProcessBatch(context.Background(), msg) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "bar", msgs[0].Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Metadata not preserved: %v != %v", act, exp) - } else if exp, act := "201", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } else if exp, act := "", msgs[0].Get(0).MetaGetStr("foobar"); exp != act { - t.Errorf("Wrong metadata value: %v != %v", act, exp) - } -} - -func TestHTTPClientEmptyResponse(t *testing.T) { - i := 0 - expPayloads := []string{"foo", "bar", "baz"} - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - if exp, act := expPayloads[i], string(reqBytes); exp != act { - t.Errorf("Wrong payload value: %v != %v", act, exp) - } - i++ - w.WriteHeader(http.StatusOK) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo")})) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "200", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } - - msgs, res = h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "200", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } - - // Check metadata persists. - msg := message.QuickBatch([][]byte{[]byte("baz")}) - msg.Get(0).MetaSetMut("foo", "bar") - msgs, res = h.ProcessBatch(context.Background(), msg) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "200", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } -} - -func TestHTTPClientEmpty404Response(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo")})) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foo", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "404", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } else { - assert.Error(t, msgs[0].Get(0).ErrorGet()) - } -} - -func TestHTTPClientBasicWithMetadata(t *testing.T) { - i := 0 - expPayloads := []string{"foo", "bar", "baz"} - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - if exp, act := expPayloads[i], string(reqBytes); exp != act { - t.Errorf("Wrong payload value: %v != %v", act, exp) - } - i++ - w.Header().Add("foobar", "baz") - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("foobar")) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost - extract_headers: - include_patterns: [ ".*" ] -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo")})) - if res != nil { - t.Error(res) - } else if expC, actC := 1, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "201", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } else if exp, act := "baz", msgs[0].Get(0).MetaGetStr("foobar"); exp != act { - t.Errorf("Wrong metadata value: %v != %v", act, exp) - } -} - -func TestHTTPClientSerial(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - bodyBytes, err := io.ReadAll(r.Body) - require.NoError(t, err) - - if string(bodyBytes) == "bar" { - w.WriteHeader(http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("foobar " + string(bodyBytes))) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost - retry_period: 1ms -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - inputMsg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("qux"), - []byte("quz"), - }) - inputMsg.Get(0).MetaSetMut("foo", "bar") - msgs, res := h.ProcessBatch(context.Background(), inputMsg) - require.NoError(t, res) - require.Len(t, msgs, 1) - require.Equal(t, 5, msgs[0].Len()) - - assert.Equal(t, "foobar foo", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "bar", string(msgs[0].Get(1).AsBytes())) - require.Error(t, msgs[0].Get(1).ErrorGet()) - assert.Contains(t, msgs[0].Get(1).ErrorGet().Error(), "request returned unexpected response code") - assert.Equal(t, "foobar baz", string(msgs[0].Get(2).AsBytes())) - assert.Equal(t, "foobar qux", string(msgs[0].Get(3).AsBytes())) - assert.Equal(t, "foobar quz", string(msgs[0].Get(4).AsBytes())) -} - -func TestHTTPClientParallel(t *testing.T) { - wg := sync.WaitGroup{} - wg.Add(5) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wg.Done() - wg.Wait() - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("foobar")) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost - parallel: true -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - inputMsg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("qux"), - []byte("quz"), - }) - inputMsg.Get(0).MetaSetMut("foo", "bar") - msgs, res := h.ProcessBatch(context.Background(), inputMsg) - if res != nil { - t.Error(res) - } else if expC, actC := 5, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } else if exp, act := "bar", msgs[0].Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Metadata not preserved: %v != %v", act, exp) - } else if exp, act := "201", msgs[0].Get(0).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } -} - -func TestHTTPClientParallelError(t *testing.T) { - wg := sync.WaitGroup{} - wg.Add(5) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wg.Done() - wg.Wait() - reqBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - if string(reqBytes) == "baz" { - http.Error(w, "test error", http.StatusForbidden) - return - } - _, _ = w.Write([]byte("foobar")) - })) - defer ts.Close() - - conf := parseYAMLProcConf(t, ` -http: - url: %v/testpost - parallel: true - retries: 0 -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("qux"), - []byte("quz"), - })) - if res != nil { - t.Error(res) - } - if expC, actC := 5, msgs[0].Len(); actC != expC { - t.Fatalf("Wrong result count: %v != %v", actC, expC) - } - if exp, act := "baz", string(msgs[0].Get(2).AsBytes()); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } - assert.Error(t, msgs[0].Get(2).ErrorGet()) - if exp, act := "403", msgs[0].Get(2).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } - for _, i := range []int{0, 1, 3, 4} { - if exp, act := "foobar", string(msgs[0].Get(i).AsBytes()); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } - assert.NoError(t, msgs[0].Get(i).ErrorGet()) - if exp, act := "200", msgs[0].Get(i).MetaGetStr("http_status_code"); exp != act { - t.Errorf("Wrong response code metadata: %v != %v", act, exp) - } - } -} diff --git a/internal/impl/io/processor_subprocess.go b/internal/impl/io/processor_subprocess.go deleted file mode 100644 index fea4c08cd4..0000000000 --- a/internal/impl/io/processor_subprocess.go +++ /dev/null @@ -1,523 +0,0 @@ -package io - -import ( - "bufio" - "bytes" - "context" - "encoding/binary" - "errors" - "fmt" - "io" - "math/bits" - "os/exec" - "strconv" - "sync" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - spFieldName = "name" - spFieldArgs = "args" - spFieldMaxBuffer = "max_buffer" - spFieldCodecSend = "codec_send" - spFieldCodecRecv = "codec_recv" -) - -type subprocConfig struct { - Name string - Args []string - MaxBuffer int - CodecSend string - CodecRecv string -} - -func subProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Integration"). - Stable(). - Summary("Executes a command as a subprocess and, for each message, will pipe its contents to the stdin stream of the process followed by a newline."). - Description(` -[NOTE] -==== -This processor keeps the subprocess alive and requires very specific behavior from the command executed. If you wish to simply execute a command for each message take a look at the xref:components:processors/command.adoc[`+"`command`"+` processor] instead. -==== - -The subprocess must then either return a line over stdout or stderr. If a response is returned over stdout then its contents will replace the message. If a response is instead returned from stderr it will be logged and the message will continue unchanged and will be xref:configuration:error_handling.adoc[marked as failed]. - -Rather than separating data by a newline it's possible to specify alternative `+"<> and <>"+` values, which allow binary messages to be encoded for logical separation. - -The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. - -The field `+"`max_buffer`"+` defines the maximum response size able to be read from the subprocess. This value should be set significantly above the real expected maximum response size. - -== Subprocess requirements - -It is required that subprocesses flush their stdout and stderr pipes for each line. Benthos will attempt to keep the process alive for as long as the pipeline is running. If the process exits early it will be restarted. - -== Messages containing line breaks - -If a message contains line breaks each line of the message is piped to the subprocess and flushed, and a response is expected from the subprocess before another line is fed in.`). - Fields( - service.NewStringField(spFieldName). - Description("The command to execute as a subprocess."). - Examples("cat", "sed", "awk"), - service.NewStringListField(spFieldArgs). - Description("A list of arguments to provide the command."). - Default([]any{}), - service.NewIntField(spFieldMaxBuffer). - Description("The maximum expected response size."). - Advanced(). - Default(bufio.MaxScanTokenSize), - service.NewStringEnumField(spFieldCodecSend, "lines", "length_prefixed_uint32_be", "netstring"). - Description("Determines how messages written to the subprocess are encoded, which allows them to be logically separated."). - Version("3.37.0"). - Advanced(). - Default("lines"), - service.NewStringEnumField(spFieldCodecRecv, "lines", "length_prefixed_uint32_be", "netstring"). - Description("Determines how messages read from the subprocess are decoded, which allows them to be logically separated."). - Version("3.37.0"). - Advanced(). - Default("lines"), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "subprocess", subProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - var sConf subprocConfig - var err error - if sConf.Name, err = conf.FieldString(spFieldName); err != nil { - return nil, err - } - if sConf.Args, err = conf.FieldStringList(spFieldArgs); err != nil { - return nil, err - } - if sConf.MaxBuffer, err = conf.FieldInt(spFieldMaxBuffer); err != nil { - return nil, err - } - if sConf.CodecSend, err = conf.FieldString(spFieldCodecSend); err != nil { - return nil, err - } - if sConf.CodecRecv, err = conf.FieldString(spFieldCodecRecv); err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newSubprocess(sConf, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("subprocess", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type subprocessProc struct { - log log.Modular - - subproc *subprocWrapper - procFunc func(part *message.Part) error - mut sync.Mutex -} - -func newSubprocess(conf subprocConfig, mgr bundle.NewManagement) (*subprocessProc, error) { - e := &subprocessProc{ - log: mgr.Logger(), - } - var err error - if e.subproc, err = newSubprocWrapper(conf.Name, conf.Args, conf.MaxBuffer, conf.CodecRecv, mgr.Logger()); err != nil { - return nil, err - } - if e.procFunc, err = e.getSendSubprocessorFunc(conf.CodecSend); err != nil { - return nil, err - } - return e, nil -} - -func (e *subprocessProc) getSendSubprocessorFunc(codec string) (func(part *message.Part) error, error) { - switch codec { - case "length_prefixed_uint32_be": - return func(part *message.Part) error { - const prefixBytes int = 4 - - lenBuf := make([]byte, prefixBytes) - m := part.AsBytes() - binary.BigEndian.PutUint32(lenBuf, uint32(len(m))) - - res, err := e.subproc.Send(lenBuf, m, nil) - if err != nil { - e.log.Error("Failed to send message to subprocess: %v\n", err) - return err - } - res2 := make([]byte, len(res)) - copy(res2, res) - part.SetBytes(res2) - return nil - }, nil - case "netstring": - return func(part *message.Part) error { - lenBuf := make([]byte, 0) - m := part.AsBytes() - lenBuf = append(strconv.AppendUint(lenBuf, uint64(len(m)), 10), ':') - res, err := e.subproc.Send(lenBuf, m, commaBytes) - if err != nil { - e.log.Error("Failed to send message to subprocess: %v\n", err) - return err - } - res2 := make([]byte, len(res)) - copy(res2, res) - part.SetBytes(res2) - return nil - }, nil - case "lines": - return func(part *message.Part) error { - results := [][]byte{} - splitMsg := bytes.Split(part.AsBytes(), newLineBytes) - for j, p := range splitMsg { - if len(p) == 0 && len(splitMsg) > 1 && j == (len(splitMsg)-1) { - results = append(results, []byte("")) - continue - } - res, err := e.subproc.Send(nil, p, newLineBytes) - if err != nil { - e.log.Error("Failed to send message to subprocess: %v\n", err) - return err - } - results = append(results, res) - } - part.SetBytes(bytes.Join(results, newLineBytes)) - return nil - }, nil - } - return nil, fmt.Errorf("unrecognised codec_send value: %v", codec) -} - -type subprocWrapper struct { - name string - args []string - maxBuf int - - splitFunc bufio.SplitFunc - logger log.Modular - - cmdMut sync.Mutex - cmdExitChan chan struct{} - stdoutChan chan []byte - stderrChan chan []byte - - cmd *exec.Cmd - cmdStdin io.WriteCloser - cmdCancelFn func() - - shutSig *shutdown.Signaller -} - -func newSubprocWrapper(name string, args []string, maxBuf int, codecRecv string, log log.Modular) (*subprocWrapper, error) { - s := &subprocWrapper{ - name: name, - args: args, - maxBuf: maxBuf, - logger: log, - shutSig: shutdown.NewSignaller(), - } - switch codecRecv { - case "lines": - s.splitFunc = bufio.ScanLines - case "length_prefixed_uint32_be": - s.splitFunc = lengthPrefixedUInt32BESplitFunc - case "netstring": - s.splitFunc = netstringSplitFunc - default: - return nil, fmt.Errorf("invalid codec_recv option: %v", codecRecv) - } - if err := s.start(); err != nil { - return nil, err - } - go func() { - defer func() { - _ = s.stop() - s.shutSig.TriggerHasStopped() - }() - for { - select { - case <-s.cmdExitChan: - log.Warn("Subprocess exited") - _ = s.stop() - - // Flush channels - var msgBytes []byte - for stdoutMsg := range s.stdoutChan { - msgBytes = append(msgBytes, stdoutMsg...) - } - if len(msgBytes) > 0 { - log.Info(string(msgBytes)) - } - msgBytes = nil - for stderrMsg := range s.stderrChan { - msgBytes = append(msgBytes, stderrMsg...) - } - if len(msgBytes) > 0 { - log.Error(string(msgBytes)) - } - - _ = s.start() - case <-s.shutSig.SoftStopChan(): - return - } - } - }() - return s, nil -} - -var maxInt = (1< (uint32(maxInt) - uint32(prefixBytes)) { - return 0, nil, errors.New("number of bytes to read exceeds representable range of go int datatype") - } - bytesToRead := int(l) - - if len(data)-prefixBytes >= bytesToRead { - return prefixBytes + bytesToRead, data[prefixBytes : prefixBytes+bytesToRead], nil - } - return 0, nil, nil -} - -func netstringSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF { - return 0, nil, nil - } - - if i := bytes.IndexByte(data, ':'); i >= 0 { - if i == 0 { - return 0, nil, errors.New("encountered invalid netstring: netstring starts with colon (':')") - } - l, err := strconv.ParseUint(string(data[0:i]), 10, bits.UintSize-1) - if err != nil { - return 0, nil, fmt.Errorf("encountered invalid netstring: unable to decode length '%v'", string(data[0:i])) - } - bytesToRead := int(l) - - if len(data) > i+1+bytesToRead { - if data[i+1+bytesToRead] != ',' { - return 0, nil, errors.New("encountered invalid netstring: trailing comma-character is missing") - } - return i + 1 + bytesToRead + 1, data[i+1 : i+1+bytesToRead], nil - } - } - // request more data - return 0, nil, nil -} - -func (s *subprocWrapper) start() (err error) { - s.cmdMut.Lock() - defer s.cmdMut.Unlock() - - cmdCtx, cmdCancelFn := context.WithCancel(context.Background()) - defer func() { - if err != nil { - cmdCancelFn() - } - }() - - cmd := exec.CommandContext(cmdCtx, s.name, s.args...) - var cmdStdin io.WriteCloser - if cmdStdin, err = cmd.StdinPipe(); err != nil { - return err - } - var cmdStdout, cmdStderr io.ReadCloser - if cmdStdout, err = cmd.StdoutPipe(); err != nil { - return err - } - if cmdStderr, err = cmd.StderrPipe(); err != nil { - return err - } - if err := cmd.Start(); err != nil { - return err - } - - s.cmd = cmd - s.cmdStdin = cmdStdin - s.cmdCancelFn = cmdCancelFn - - cmdExitChan := make(chan struct{}) - stdoutChan := make(chan []byte) - stderrChan := make(chan []byte) - - go func() { - defer func() { - s.cmdMut.Lock() - if cmdExitChan != nil { - close(cmdExitChan) - cmdExitChan = nil - } - close(stdoutChan) - s.cmdMut.Unlock() - }() - - scanner := bufio.NewScanner(cmdStdout) - scanner.Split(s.splitFunc) - if s.maxBuf != bufio.MaxScanTokenSize { - scanner.Buffer(nil, s.maxBuf) - } - for scanner.Scan() { - data := scanner.Bytes() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - - stdoutChan <- dataCopy - } - if err := scanner.Err(); err != nil { - s.logger.Error("Failed to read subprocess output: %v\n", err) - } - }() - go func() { - defer func() { - s.cmdMut.Lock() - if cmdExitChan != nil { - close(cmdExitChan) - cmdExitChan = nil - } - close(stderrChan) - s.cmdMut.Unlock() - }() - - scanner := bufio.NewScanner(cmdStderr) - if s.maxBuf != bufio.MaxScanTokenSize { - scanner.Buffer(nil, s.maxBuf) - } - for scanner.Scan() { - data := scanner.Bytes() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - - stderrChan <- dataCopy - } - if err := scanner.Err(); err != nil { - s.logger.Error("Failed to read subprocess error output: %v\n", err) - } - }() - - s.cmdExitChan = cmdExitChan - s.stdoutChan = stdoutChan - s.stderrChan = stderrChan - s.logger.Info("Subprocess started") - return nil -} - -func (s *subprocWrapper) stop() error { - s.cmdMut.Lock() - var err error - if s.cmd != nil { - s.cmdCancelFn() - err = s.cmd.Wait() - s.cmd = nil - s.cmdStdin = nil - s.cmdCancelFn = func() {} - } - s.cmdMut.Unlock() - return err -} - -func (s *subprocWrapper) Send(prolog, payload, epilog []byte) ([]byte, error) { - s.cmdMut.Lock() - stdin := s.cmdStdin - outChan := s.stdoutChan - errChan := s.stderrChan - s.cmdMut.Unlock() - - if stdin == nil { - return nil, component.ErrTypeClosed - } - if prolog != nil { - if _, err := stdin.Write(prolog); err != nil { - return nil, err - } - } - if _, err := stdin.Write(payload); err != nil { - return nil, err - } - if epilog != nil { - if _, err := stdin.Write(epilog); err != nil { - return nil, err - } - } - - var outBytes, errBytes []byte - var open bool - select { - case outBytes, open = <-outChan: - case errBytes, open = <-errChan: - tout := time.After(time.Second) - var errBuf bytes.Buffer - _, _ = errBuf.Write(errBytes) - flushErrLoop: - for open { - select { - case errBytes, open = <-errChan: - _, _ = errBuf.Write(errBytes) - case <-tout: - break flushErrLoop - } - } - errBytes = errBuf.Bytes() - } - - if !open { - return nil, component.ErrTypeClosed - } - if len(errBytes) > 0 { - return nil, errors.New(string(errBytes)) - } - return outBytes, nil -} - -//------------------------------------------------------------------------------ - -var ( - newLineBytes = []byte("\n") - commaBytes = []byte(",") -) - -// ProcessMessage logs an event and returns the message unchanged. -func (e *subprocessProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - e.mut.Lock() - defer e.mut.Unlock() - - if err := e.procFunc(msg); err != nil { - return nil, err - } - return []*message.Part{msg}, nil -} - -func (e *subprocessProc) Close(ctx context.Context) error { - e.subproc.shutSig.TriggerHardStop() - select { - case <-e.subproc.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/io/processor_subprocess_test.go b/internal/impl/io/processor_subprocess_test.go deleted file mode 100644 index 57fb95d198..0000000000 --- a/internal/impl/io/processor_subprocess_test.go +++ /dev/null @@ -1,305 +0,0 @@ -package io_test - -import ( - "context" - "fmt" - "os" - "path" - "reflect" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestSubprocessWithSed(t *testing.T) { - t.Skip("disabled for now") - - conf, err := testutil.ProcessorFromYAML(` -subprocess: - name: sed - args: [ "s/foo/bar/g", "-u" ] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Skipf("Not sure if this is due to missing executable: %v", err) - } - - exp := [][]byte{ - []byte(`hello bar world`), - []byte(`hello baz world`), - []byte(`bar`), - } - msgIn := message.QuickBatch([][]byte{ - []byte(`hello foo world`), - []byte(`hello baz world`), - []byte(`foo`), - }) - msgs, res := proc.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatal("Wrong count of messages") - } - if res != nil { - t.Fatalf("Non-nil result: %v", res) - } - - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - require.NoError(t, proc.Close(ctx)) -} - -func TestSubprocessWithCat(t *testing.T) { - t.Skip("disabled for now") - - conf, err := testutil.ProcessorFromYAML(` -subprocess: - name: cat -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Skipf("Not sure if this is due to missing executable: %v", err) - } - - exp := [][]byte{ - []byte(`hello bar world`), - []byte(`hello baz world`), - []byte(`bar`), - } - msgIn := message.QuickBatch([][]byte{ - []byte(`hello bar world`), - []byte(`hello baz world`), - []byte(`bar`), - }) - msgs, res := proc.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatal("Wrong count of messages") - } - if res != nil { - t.Fatalf("Non-nil result: %v", res) - } - - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - require.NoError(t, proc.Close(ctx)) -} - -func TestSubprocessLineBreaks(t *testing.T) { - t.Skip("disabled for now") - - conf, err := testutil.ProcessorFromYAML(` -subprocess: - name: sed - args: [ "s/\\(^$\\)\\|\\(foo\\)/bar/", "-u" ] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Skipf("Not sure if this is due to missing executable: %v", err) - } - - exp := [][]byte{ - []byte("hello bar\nbar world"), - []byte("hello bar bar world"), - []byte("hello bar\nbar world\n"), - []byte("bar"), - []byte("hello bar\nbar\nbar world\n"), - } - msgIn := message.QuickBatch([][]byte{ - []byte("hello foo\nfoo world"), - []byte("hello foo bar world"), - []byte("hello foo\nfoo world\n"), - []byte(""), - []byte("hello foo\n\nfoo world\n"), - }) - msgs, res := proc.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatalf("Wrong count of messages %d", len(msgs)) - } - if res != nil { - t.Fatalf("Non-nil result: %v", res) - } - - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - require.NoError(t, proc.Close(ctx)) -} - -func TestSubprocessWithErrors(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -subprocess: - name: sh - args: [ "-c", "cat 1>&2" ] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Skipf("Not sure if this is due to missing executable: %v", err) - } - - msgIn := message.QuickBatch([][]byte{ - []byte(`hello bar world`), - }) - - msgs, _ := proc.ProcessBatch(context.Background(), msgIn) - - assert.Error(t, msgs[0].Get(0).ErrorGet()) - - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - require.NoError(t, proc.Close(ctx)) -} - -func testProgram(t *testing.T, program string) string { - t.Helper() - - dir := t.TempDir() - - pathStr := path.Join(dir, "main.go") - require.NoError(t, os.WriteFile(pathStr, []byte(program), 0o666)) - - return pathStr -} - -func TestSubprocessHappy(t *testing.T) { - filePath := testProgram(t, `package main - -import ( - "bufio" - "encoding/binary" - "flag" - "fmt" - "log" - "os" - "strings" -) - -func lengthPrefixedUInt32BESplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { - const prefixBytes int = 4 - if atEOF { - return 0, nil, nil - } - if len(data) < prefixBytes { - // request more data - return 0, nil, nil - } - l := binary.BigEndian.Uint32(data) - bytesToRead := int(l) - - if len(data)-prefixBytes >= bytesToRead { - return prefixBytes + bytesToRead, data[prefixBytes : prefixBytes+bytesToRead], nil - } else { - // request more data - return 0, nil, nil - } -} - -var stdinCodec *string = flag.String("stdinCodec", "lines", "format to use for input") -var stdoutCodec *string = flag.String("stdoutCodec", "lines", "format for use for output") - -func main() { - flag.Parse() - - scanner := bufio.NewScanner(os.Stdin) - if *stdinCodec == "length_prefixed_uint32_be" { - scanner.Split(lengthPrefixedUInt32BESplitFunc) - } - - lenBuf := make([]byte, 4) - for scanner.Scan() { - res := strings.ToUpper(scanner.Text()) - switch *stdoutCodec { - case "length_prefixed_uint32_be": - buf := []byte(res) - binary.BigEndian.PutUint32(lenBuf, uint32(len(buf))) - _, _ = os.Stdout.Write(lenBuf) - _, _ = os.Stdout.Write(buf) - break - case "netstring": - fmt.Printf("%d:%s,",len(res),res) - break - default: - fmt.Println(res) - } - } - - if err := scanner.Err(); err != nil { - log.Println(err) - } -} -`) - f := func(formatSend, formatRecv string, extra bool) { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -subprocess: - name: go - args: %v - codec_send: %v - codec_recv: %v -`, gabs.Wrap([]string{"run", filePath, "-stdinCodec", formatSend, "-stdoutCodec", formatRecv}).String(), formatSend, formatRecv)) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - exp := [][]byte{ - []byte(`FOO`), - []byte(`FOÖ`), - []byte(`BAR`), - []byte(`BAZ`), - } - if extra { - exp = append(exp, []byte(``), []byte("|{O\n\r\nO}|")) - } - - msgIn := message.QuickBatch([][]byte{ - []byte(`foo`), - []byte(`foö`), - []byte(`bar`), - []byte(`baz`), - }) - if extra { - msgIn = append(msgIn, message.NewPart([]byte(``)), message.NewPart([]byte("|{o\n\r\no}|"))) - } - - msgs, res := proc.ProcessBatch(context.Background(), msgIn) - require.Len(t, msgs, 1) - require.NoError(t, res) - - for i := 0; i < msgIn.Len(); i++ { - assert.NoError(t, msgs[0].Get(i).ErrorGet()) - } - assert.Equal(t, exp, message.GetAllBytes(msgs[0])) - - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - require.NoError(t, proc.Close(ctx)) - } - f("lines", "lines", false) - f("length_prefixed_uint32_be", "lines", false) - f("lines", "length_prefixed_uint32_be", false) - f("length_prefixed_uint32_be", "netstring", true) - f("length_prefixed_uint32_be", "length_prefixed_uint32_be", true) -} diff --git a/internal/impl/kafka/aws/aws.go b/internal/impl/kafka/aws/aws.go index 59867e0417..5d5227cd77 100644 --- a/internal/impl/kafka/aws/aws.go +++ b/internal/impl/kafka/aws/aws.go @@ -3,13 +3,14 @@ package aws import ( "context" - "github.com/benthosdev/benthos/v4/internal/impl/kafka" "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/connect/v4/internal/impl/kafka" + "github.com/twmb/franz-go/pkg/sasl" kaws "github.com/twmb/franz-go/pkg/sasl/aws" - sess "github.com/benthosdev/benthos/v4/internal/impl/aws" + sess "github.com/redpanda-data/connect/v4/internal/impl/aws" ) func init() { diff --git a/internal/impl/kafka/integration_sarama_test.go b/internal/impl/kafka/integration_sarama_test.go index b469aedd19..8a6e831e98 100644 --- a/internal/impl/kafka/integration_sarama_test.go +++ b/internal/impl/kafka/integration_sarama_test.go @@ -14,9 +14,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/kafka" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" + + "github.com/redpanda-data/connect/v4/internal/impl/kafka" ) func TestIntegrationSaramaCheckpointOneLockUp(t *testing.T) { diff --git a/internal/impl/kafka/sasl.go b/internal/impl/kafka/sasl.go index 88b78bdfb7..c3edbe6d17 100644 --- a/internal/impl/kafka/sasl.go +++ b/internal/impl/kafka/sasl.go @@ -7,9 +7,10 @@ import ( "github.com/IBM/sarama" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" + "github.com/twmb/franz-go/pkg/sasl" "github.com/twmb/franz-go/pkg/sasl/oauth" "github.com/twmb/franz-go/pkg/sasl/plain" diff --git a/internal/impl/kafka/sasl_test.go b/internal/impl/kafka/sasl_test.go index 656239a969..42fcb215fb 100644 --- a/internal/impl/kafka/sasl_test.go +++ b/internal/impl/kafka/sasl_test.go @@ -7,7 +7,7 @@ import ( "github.com/IBM/sarama" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/kafka" + "github.com/redpanda-data/connect/v4/internal/impl/kafka" _ "github.com/benthosdev/benthos/v4/public/components/pure" "github.com/benthosdev/benthos/v4/public/service" diff --git a/internal/impl/mongodb/output.go b/internal/impl/mongodb/output.go index 17ef52a382..2383d75e80 100644 --- a/internal/impl/mongodb/output.go +++ b/internal/impl/mongodb/output.go @@ -9,8 +9,9 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/mongodb/processor.go b/internal/impl/mongodb/processor.go index de97019190..81953e5bda 100644 --- a/internal/impl/mongodb/processor.go +++ b/internal/impl/mongodb/processor.go @@ -9,8 +9,9 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/internal/retries" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/retries" ) const ( diff --git a/internal/impl/mongodb/processor_test.go b/internal/impl/mongodb/processor_test.go index 6f6943f909..7bc07606fe 100644 --- a/internal/impl/mongodb/processor_test.go +++ b/internal/impl/mongodb/processor_test.go @@ -14,9 +14,10 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/internal/impl/mongodb" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" + + "github.com/redpanda-data/connect/v4/internal/impl/mongodb" ) func TestProcessorIntegration(t *testing.T) { diff --git a/internal/impl/opensearch/aws/aws.go b/internal/impl/opensearch/aws/aws.go index 0fc3a4f8dc..7801a8a71f 100644 --- a/internal/impl/opensearch/aws/aws.go +++ b/internal/impl/opensearch/aws/aws.go @@ -6,9 +6,10 @@ import ( "github.com/opensearch-project/opensearch-go/v3/opensearchapi" "github.com/opensearch-project/opensearch-go/v3/signer/awsv2" - baws "github.com/benthosdev/benthos/v4/internal/impl/aws" - "github.com/benthosdev/benthos/v4/internal/impl/opensearch" "github.com/benthosdev/benthos/v4/public/service" + + baws "github.com/redpanda-data/connect/v4/internal/impl/aws" + "github.com/redpanda-data/connect/v4/internal/impl/opensearch" ) func init() { diff --git a/internal/impl/opensearch/integration_test.go b/internal/impl/opensearch/integration_test.go index 9d22fda715..4b38888133 100644 --- a/internal/impl/opensearch/integration_test.go +++ b/internal/impl/opensearch/integration_test.go @@ -17,10 +17,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/opensearch" _ "github.com/benthosdev/benthos/v4/public/components/pure" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" + + "github.com/redpanda-data/connect/v4/internal/impl/opensearch" ) func outputFromConf(t testing.TB, confStr string, args ...any) *opensearch.Output { diff --git a/internal/impl/opensearch/output.go b/internal/impl/opensearch/output.go index d86d7e7005..c3be2e68ed 100644 --- a/internal/impl/opensearch/output.go +++ b/internal/impl/opensearch/output.go @@ -14,8 +14,9 @@ import ( "github.com/opensearch-project/opensearch-go/v3/opensearchapi" "github.com/opensearch-project/opensearch-go/v3/opensearchutil" - "github.com/benthosdev/benthos/v4/internal/impl/aws/config" "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) const ( diff --git a/internal/impl/pure/algorithms.go b/internal/impl/pure/algorithms.go deleted file mode 100644 index 6e616e9e33..0000000000 --- a/internal/impl/pure/algorithms.go +++ /dev/null @@ -1,298 +0,0 @@ -package pure - -import ( - "bytes" - "compress/bzip2" - "fmt" - "io" - "sort" - "sync" - - "github.com/klauspost/compress/flate" - "github.com/klauspost/compress/gzip" - "github.com/klauspost/compress/snappy" - "github.com/klauspost/compress/zlib" - "github.com/klauspost/pgzip" - "github.com/pierrec/lz4/v4" -) - -type ( - CompressFunc func(level int, b []byte) ([]byte, error) - CompressWriter func(level int, w io.Writer) (io.Writer, error) - DecompressFunc func(b []byte) ([]byte, error) - DecompressReader func(r io.Reader) (io.Reader, error) -) - -type KnownCompressionAlgorithm struct { - CompressFunc CompressFunc - CompressWriter CompressWriter - DecompressFunc DecompressFunc - DecompressReader DecompressReader -} - -var knownCompressionAlgorithms = map[string]KnownCompressionAlgorithm{} - -var knownCompressionAlgorithmsLock sync.Mutex - -func AddKnownCompressionAlgorithm(name string, a KnownCompressionAlgorithm) struct{} { - if a.CompressFunc == nil && a.CompressWriter != nil { - a.CompressFunc = func(level int, b []byte) ([]byte, error) { - var buf bytes.Buffer - wtr, err := a.CompressWriter(level, &buf) - if err != nil { - return nil, err - } - _, err = io.Copy(wtr, bytes.NewReader(b)) - if c, ok := wtr.(io.Closer); ok { - if cerr := c.Close(); cerr != nil { - return nil, cerr - } - } - return buf.Bytes(), err - } - } - - if a.DecompressFunc == nil && a.DecompressReader != nil { - a.DecompressFunc = func(b []byte) ([]byte, error) { - rdr, err := a.DecompressReader(bytes.NewReader(b)) - if err != nil { - return nil, err - } - mBytes, err := io.ReadAll(rdr) - if c, ok := rdr.(io.Closer); ok { - if cerr := c.Close(); cerr != nil { - return nil, cerr - } - } - return mBytes, err - } - } - - knownCompressionAlgorithmsLock.Lock() - knownCompressionAlgorithms[name] = a - knownCompressionAlgorithmsLock.Unlock() - return struct{}{} -} - -func CompressionAlgsList() (v []string) { - knownCompressionAlgorithmsLock.Lock() - v = make([]string, 0, len(knownCompressionAlgorithms)) - for k, a := range knownCompressionAlgorithms { - if a.CompressFunc != nil { - v = append(v, k) - } - } - knownCompressionAlgorithmsLock.Unlock() - sort.Strings(v) - return v -} - -func DecompressionAlgsList() (v []string) { - knownCompressionAlgorithmsLock.Lock() - v = make([]string, 0, len(knownCompressionAlgorithms)) - for k, a := range knownCompressionAlgorithms { - if a.DecompressFunc != nil { - v = append(v, k) - } - } - knownCompressionAlgorithmsLock.Unlock() - sort.Strings(v) - return v -} - -func strToCompressAlg(str string) (KnownCompressionAlgorithm, error) { - fn, exists := knownCompressionAlgorithms[str] - if !exists { - return KnownCompressionAlgorithm{}, fmt.Errorf("compression type not recognised: %v", str) - } - return fn, nil -} - -func strToCompressFunc(str string) (CompressFunc, error) { - alg, err := strToCompressAlg(str) - if err != nil { - return nil, err - } - if alg.CompressFunc == nil { - return nil, fmt.Errorf("compression type not recognised: %v", str) - } - return alg.CompressFunc, nil -} - -func strToDecompressFunc(str string) (DecompressFunc, error) { - alg, err := strToCompressAlg(str) - if err != nil { - return nil, err - } - if alg.DecompressFunc == nil { - return nil, fmt.Errorf("decompression type not recognised: %v", str) - } - return alg.DecompressFunc, nil -} - -func strToDecompressReader(str string) (DecompressReader, error) { - alg, err := strToCompressAlg(str) - if err != nil { - return nil, err - } - if alg.DecompressReader == nil { - return nil, fmt.Errorf("decompression type not recognised: %v", str) - } - return alg.DecompressReader, nil -} - -//------------------------------------------------------------------------------ - -// The Primary is written to and closed first. The Sink is closed second. -type CombinedWriteCloser struct { - Primary, Sink io.Writer -} - -func (c *CombinedWriteCloser) Write(b []byte) (int, error) { - return c.Primary.Write(b) -} - -func (c *CombinedWriteCloser) Close() error { - if closer, ok := c.Primary.(io.Closer); ok { - if err := closer.Close(); err != nil { - return err - } - } - if closer, ok := c.Sink.(io.Closer); ok { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -// The Primary is read from and closed second. The Source is closed first. -type CombinedReadCloser struct { - Primary, Source io.Reader -} - -func (c *CombinedReadCloser) Read(b []byte) (int, error) { - return c.Primary.Read(b) -} - -func (c *CombinedReadCloser) Close() error { - if closer, ok := c.Source.(io.Closer); ok { - if err := closer.Close(); err != nil { - return err - } - } - if closer, ok := c.Primary.(io.Closer); ok { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -//------------------------------------------------------------------------------ - -var _ = AddKnownCompressionAlgorithm("gzip", KnownCompressionAlgorithm{ - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw, err := gzip.NewWriterLevel(w, level) - if err != nil { - return nil, err - } - return &CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar, err := gzip.NewReader(r) - if err != nil { - return nil, err - } - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) - -var _ = AddKnownCompressionAlgorithm("pgzip", KnownCompressionAlgorithm{ - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw, err := pgzip.NewWriterLevel(w, level) - if err != nil { - return nil, err - } - return &CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar, err := pgzip.NewReader(r) - if err != nil { - return nil, err - } - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) - -var _ = AddKnownCompressionAlgorithm("zlib", KnownCompressionAlgorithm{ - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw, err := zlib.NewWriterLevel(w, level) - if err != nil { - return nil, err - } - return &CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar, err := zlib.NewReader(r) - if err != nil { - return nil, err - } - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) - -var _ = AddKnownCompressionAlgorithm("flate", KnownCompressionAlgorithm{ - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw, err := flate.NewWriter(w, level) - if err != nil { - return nil, err - } - return &CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar := flate.NewReader(r) - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) - -var _ = AddKnownCompressionAlgorithm("bzip2", KnownCompressionAlgorithm{ - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar := bzip2.NewReader(r) - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) - -var _ = AddKnownCompressionAlgorithm("lz4", KnownCompressionAlgorithm{ - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw := lz4.NewWriter(w) - if level > 0 { - // The default compression level is 0 (lz4.Fast) - if err := aw.Apply(lz4.CompressionLevelOption(lz4.CompressionLevel(1 << (8 + level)))); err != nil { - return nil, err - } - } - return &CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar := lz4.NewReader(r) - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) - -var _ = AddKnownCompressionAlgorithm("snappy", KnownCompressionAlgorithm{ - CompressFunc: func(level int, b []byte) ([]byte, error) { - return snappy.Encode(nil, b), nil - }, - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw := snappy.NewBufferedWriter(w) - return &CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressFunc: func(b []byte) ([]byte, error) { - return snappy.Decode(nil, b) - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar := snappy.NewReader(r) - return &CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) diff --git a/internal/impl/pure/bloblang_encoding.go b/internal/impl/pure/bloblang_encoding.go deleted file mode 100644 index 12df724bb1..0000000000 --- a/internal/impl/pure/bloblang_encoding.go +++ /dev/null @@ -1,84 +0,0 @@ -package pure - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func init() { - if err := bloblang.RegisterMethodV2("compress", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryEncoding). - Description(`Compresses a string or byte array value according to a specified algorithm.`). - Param(bloblang.NewStringParam("algorithm").Description("One of `flate`, `gzip`, `pgzip`, `lz4`, `snappy`, `zlib`, `zstd`.")). - Param(bloblang.NewInt64Param("level").Description("The level of compression to use. May not be applicable to all algorithms.").Default(-1)). - Example("", `let long_content = range(0, 1000).map_each(content()).join(" ") -root.a_len = $long_content.length() -root.b_len = $long_content.compress("gzip").length() -`, - [2]string{ - `hello world this is some content`, - `{"a_len":32999,"b_len":161}`, - }, - ). - Example("", `root.compressed = content().compress("lz4").encode("base64")`, - [2]string{ - `hello world I love space`, - `{"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - level, err := args.GetInt64("level") - if err != nil { - return nil, err - } - algStr, err := args.GetString("algorithm") - if err != nil { - return nil, err - } - algFn, err := strToCompressFunc(algStr) - if err != nil { - return nil, err - } - return bloblang.BytesMethod(func(data []byte) (any, error) { - return algFn(int(level), data) - }), nil - }); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("decompress", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryEncoding). - Description(`Decompresses a string or byte array value according to a specified algorithm. The result of decompression `). - Param(bloblang.NewStringParam("algorithm").Description("One of `gzip`, `pgzip`, `zlib`, `bzip2`, `flate`, `snappy`, `lz4`, `zstd`.")). - Example("", `root = this.compressed.decode("base64").decompress("lz4")`, - [2]string{ - `{"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="}`, - `hello world I love space`, - }, - ). - Example( - "Use the `.string()` method in order to coerce the result into a string, this makes it possible to place the data within a JSON document without automatic base64 encoding.", - `root.result = this.compressed.decode("base64").decompress("lz4").string()`, - [2]string{ - `{"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="}`, - `{"result":"hello world I love space"}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - algStr, err := args.GetString("algorithm") - if err != nil { - return nil, err - } - algFn, err := strToDecompressFunc(algStr) - if err != nil { - return nil, err - } - return bloblang.BytesMethod(func(data []byte) (any, error) { - return algFn(data) - }), nil - }); err != nil { - panic(err) - } -} diff --git a/internal/impl/pure/bloblang_encoding_test.go b/internal/impl/pure/bloblang_encoding_test.go deleted file mode 100644 index 76fabff9bd..0000000000 --- a/internal/impl/pure/bloblang_encoding_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package pure - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestCompressionDecompression(t *testing.T) { - seen := map[string]struct{}{} - for _, alg := range []string{`flate`, `gzip`, `pgzip`, `lz4`, `snappy`, `zlib`} { - exec, err := bloblang.Parse(fmt.Sprintf(`root = this.compress(algorithm: "%v")`, alg)) - require.NoError(t, err) - - input := []byte("hello world this is a really long string") - - compressed, err := exec.Query(input) - require.NoError(t, err) - - compressedBytes, ok := compressed.([]byte) - require.True(t, ok) - - _, exists := seen[string(compressedBytes)] - require.False(t, exists) - seen[string(compressedBytes)] = struct{}{} - - assert.NotEqual(t, input, compressed) - assert.Greater(t, len(compressedBytes), 1) - - exec, err = bloblang.Parse(fmt.Sprintf(`root = this.decompress(algorithm: "%v")`, alg)) - require.NoError(t, err) - - decompressed, err := exec.Query(compressed) - require.NoError(t, err) - - assert.Equal(t, input, decompressed) - } -} diff --git a/internal/impl/pure/bloblang_general.go b/internal/impl/pure/bloblang_general.go deleted file mode 100644 index dd26bfae36..0000000000 --- a/internal/impl/pure/bloblang_general.go +++ /dev/null @@ -1,162 +0,0 @@ -package pure - -import ( - "fmt" - "sync" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func init() { - maxUint := ^uint64(0) - maxInt := maxUint >> 1 - - if err := bloblang.RegisterAdvancedFunction("counter", - bloblang.NewPluginSpec(). - Category(query.FunctionCategoryGeneral). - Experimental(). - Description("Returns a non-negative integer that increments each time it is resolved, yielding the minimum (`1` by default) as the first value. Each instantiation of `counter` has its own independent count. Once the maximum integer (or `max` argument) is reached the counter resets back to the minimum."). - Param(bloblang.NewQueryParam("min", true). - Default(1). - Description("The minimum value of the counter, this is the first value that will be yielded. If this parameter is dynamic it will be resolved only once during the lifetime of the mapping.")). - Param(bloblang.NewQueryParam("max", true). - Default(maxInt). - Description("The maximum value of the counter, once this value is yielded the counter will reset back to the min. If this parameter is dynamic it will be resolved only once during the lifetime of the mapping.")). - Param(bloblang.NewQueryParam("set", false). - Optional(). - Description("An optional mapping that when specified will be executed each time the counter is resolved. When this mapping resolves to a non-negative integer value it will cause the counter to reset to this value and yield it. If this mapping is omitted or doesn't resolve to anything then the counter will increment and yield the value as normal. If this mapping resolves to `null` then the counter is not incremented and the current value is yielded. If this mapping resolves to a deletion then the counter is reset to the `min` value.")). - Example("", `root.id = counter()`, - [2]string{ - `{}`, - `{"id":1}`, - }, - [2]string{ - `{}`, - `{"id":2}`, - }, - ). - Example("It's possible to increment a counter multiple times within a single mapping invocation using a map.", - ` -map foos { - root = counter() -} - -root.meow_id = null.apply("foos") -root.woof_id = null.apply("foos") -`, - [2]string{ - `{}`, - `{"meow_id":1,"woof_id":2}`, - }, - [2]string{ - `{}`, - `{"meow_id":3,"woof_id":4}`, - }, - ). - Example( - "By specifying an optional `set` parameter it is possible to dynamically reset the counter based on input data.", - `root.consecutive_doggos = counter(min: 1, set: if !this.sound.lowercase().contains("woof") { 0 })`, - [2]string{ - `{"sound":"woof woof"}`, - `{"consecutive_doggos":1}`, - }, - [2]string{ - `{"sound":"woofer wooooo"}`, - `{"consecutive_doggos":2}`, - }, - [2]string{ - `{"sound":"meow"}`, - `{"consecutive_doggos":0}`, - }, - [2]string{ - `{"sound":"uuuuh uh uh woof uhhhhhh"}`, - `{"consecutive_doggos":1}`, - }, - ). - Example( - "The `set` parameter can also be utilized to peek at the counter without mutating it by returning `null`.", - `root.things = counter(set: if this.id == null { null })`, - [2]string{`{"id":"a"}`, `{"things":1}`}, - [2]string{`{"id":"b"}`, `{"things":2}`}, - [2]string{`{"what":"just checking"}`, `{"things":2}`}, - [2]string{`{"id":"c"}`, `{"things":3}`}, - ), - func(args *bloblang.ParsedParams) (bloblang.AdvancedFunction, error) { - minFunc, err := args.GetQuery("min") - if err != nil { - return nil, err - } - - maxFunc, err := args.GetOptionalQuery("max") - if err != nil { - return nil, err - } - - setFunc, err := args.GetOptionalQuery("set") - if err != nil { - return nil, err - } - - var min, max int64 - var i *int64 - - var mut sync.Mutex - - return func(ctx *bloblang.ExecContext) (any, error) { - mut.Lock() - defer mut.Unlock() - - if i == nil { - var err error - if min, err = ctx.ExecToInt64(minFunc); err != nil { - return nil, fmt.Errorf("failed to resolve min argument: %w", err) - } - if min < 0 { - return nil, fmt.Errorf("min argument must be >0, got %v", min) - } - if max, err = ctx.ExecToInt64(maxFunc); err != nil { - return nil, fmt.Errorf("failed to resolve max argument: %w", err) - } - if max < 0 || max <= min { - return nil, fmt.Errorf("max argument must be >0 and >min, got %v", max) - } - - iV := min - 1 - i = &iV - } - - if setFunc != nil { - setV, err := ctx.Exec(setFunc) - if err != nil { - return nil, fmt.Errorf("failed to resolve set argument: %w", err) - } - if setV == nil { - return *i, nil - } - switch setV.(type) { - case bloblang.ExecResultDelete: - *i = min - 1 - case bloblang.ExecResultNothing: - default: - iv, err := value.IGetInt(setV) - if err != nil { - return nil, fmt.Errorf("failed to resolve set argument: %w", err) - } - *i = iv - return iv, nil - } - } - - *i++ - v := *i - if v >= max { - *i = min - 1 - } - return v, nil - }, nil - }); err != nil { - panic(err) - } -} diff --git a/internal/impl/pure/bloblang_general_test.go b/internal/impl/pure/bloblang_general_test.go deleted file mode 100644 index 9e449a6d86..0000000000 --- a/internal/impl/pure/bloblang_general_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package pure - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestBloblangCounter(t *testing.T) { - exec, err := bloblang.Parse(` -map map_wide_counter { - root = counter(min: this.length()) -} - -root.a = counter() -root.b = counter(min: this.min) -root.c = counter(min: this.min, max: this.max) -root.d = "foo".apply("map_wide_counter") -root.e = "foobar".apply("map_wide_counter") -root.f = "f".apply("map_wide_counter") -`) - require.NoError(t, err) - - v, err := exec.Query(map[string]any{ - "min": 10, - "max": 20, - }) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "a": int64(1), - "b": int64(10), - "c": int64(10), - "d": int64(3), - "e": int64(4), - "f": int64(5), - }, v) - - v, err = exec.Query(map[string]any{ - "min": 100, - "max": 200, - }) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "a": int64(2), - "b": int64(11), - "c": int64(11), - "d": int64(6), - "e": int64(7), - "f": int64(8), - }, v) - - for i := 0; i < 9; i++ { - v, err = exec.Query(nil) - require.NoError(t, err) - - vObj, ok := v.(map[string]any) - require.True(t, ok) - - assert.Equal(t, int64(3+i), vObj["a"]) - assert.Equal(t, int64(12+i), vObj["b"]) - assert.Equal(t, int64(12+i), vObj["c"]) - } - - v, err = exec.Query(nil) - require.NoError(t, err) - - vObj, ok := v.(map[string]any) - require.True(t, ok) - - assert.Equal(t, int64(10), vObj["c"]) -} - -func TestBloblangCounterBadParams(t *testing.T) { - exec, err := bloblang.Parse(` -root.a = counter(min: this.min, max: this.max) -`) - require.NoError(t, err) - - _, err = exec.Query(map[string]any{ - "min": -1, - "max": 20, - }) - require.Error(t, err) - - _, err = exec.Query(map[string]any{ - "min": 5, - "max": 2, - }) - require.Error(t, err) - - _, err = exec.Query(nil) - require.Error(t, err) -} diff --git a/internal/impl/pure/bloblang_numbers.go b/internal/impl/pure/bloblang_numbers.go deleted file mode 100644 index d2192188ae..0000000000 --- a/internal/impl/pure/bloblang_numbers.go +++ /dev/null @@ -1,176 +0,0 @@ -package pure - -import ( - "math" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func registerIntMethod(name, longName, exampleIn, exampleOut string, method func(input any) (any, error)) { - replacer := strings.NewReplacer("$NAME", name, "$LONGNAME", longName) - - exampleOneBody := replacer.Replace(` -root.a = this.a.$NAME() -root.b = this.b.round().$NAME() -root.c = this.c.$NAME() -root.d = this.d.$NAME().catch(0) -`) - exampleOneIO := [2]string{ - `{"a":12,"b":12.34,"c":"12","d":-12}`, - `{"a":12,"b":12,"c":12,"d":-12}`, - } - if name[0] == 'u' { - exampleOneIO[1] = `{"a":12,"b":12,"c":12,"d":0}` - } - - if err := bloblang.RegisterMethodV2(name, - bloblang.NewPluginSpec(). - Category(query.MethodCategoryNumbers). - Description(replacer.Replace(` -Converts a numerical type into a $LONGNAME, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a $LONGNAME. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`+"`strconv.ParseInt`"+` documentation] for details regarding the supported formats.`)). - Example("", exampleOneBody, exampleOneIO). - Example("", replacer.Replace(` -root = this.$NAME() -`), - [2]string{exampleIn, exampleOut}, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return method, nil - }); err != nil { - panic(err) - } -} - -func init() { - registerIntMethod( - "int64", "64-bit signed integer", - `"0xDEADBEEF"`, "3735928559", - func(input any) (any, error) { - return value.IToInt(input) - }) - - registerIntMethod( - "int32", "32-bit signed integer", - `"0xDEAD"`, "57005", - func(input any) (any, error) { - return value.IToInt32(input) - }) - - registerIntMethod( - "int16", "16-bit signed integer", - `"0xDE"`, "222", - func(input any) (any, error) { - return value.IToInt16(input) - }) - - registerIntMethod( - "int8", "8-bit signed integer", - `"0xD"`, "13", - func(input any) (any, error) { - return value.IToInt8(input) - }) - - registerIntMethod( - "uint64", "64-bit unsigned integer", - `"0xDEADBEEF"`, "3735928559", - func(input any) (any, error) { - return value.IToUint(input) - }) - - registerIntMethod( - "uint32", "32-bit unsigned integer", - `"0xDEAD"`, "57005", - func(input any) (any, error) { - return value.IToUint32(input) - }) - - registerIntMethod( - "uint16", "16-bit unsigned integer", - `"0xDE"`, "222", - func(input any) (any, error) { - return value.IToUint16(input) - }) - - registerIntMethod( - "uint8", "8-bit unsigned integer", - `"0xD"`, "13", - func(input any) (any, error) { - return value.IToUint8(input) - }) - - if err := bloblang.RegisterMethodV2("float64", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryNumbers). - Description(` -Converts a numerical type into a 64-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`+"`strconv.ParseFloat`"+` documentation] for details regarding the supported formats.`). - Example("", ` -root.out = this.in.float64() -`, - [2]string{`{"in":"6.674282313423543523453425345e-11"}`, `{"out":6.674282313423544e-11}`}, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return func(input any) (any, error) { - return value.IToFloat64(input) - }, nil - }); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("float32", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryNumbers). - Description(` -Converts a numerical type into a 32-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`+"`strconv.ParseFloat`"+` documentation] for details regarding the supported formats.`). - Example("", ` -root.out = this.in.float32() -`, - [2]string{`{"in":"6.674282313423543523453425345e-11"}`, `{"out":6.674283e-11}`}, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return func(input any) (any, error) { - return value.IToFloat32(input) - }, nil - }); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("abs", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryNumbers). - Description(`Returns the absolute value of an int64 or float64 number. As a special case, when an integer is provided that is the minimum value it is converted to the maximum value.`). - Example("", ` -root.outs = this.ins.map_each(ele -> ele.abs()) -`, - [2]string{`{"ins":[9,-18,1.23,-4.56]}`, `{"outs":[9,18,1.23,4.56]}`}, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return func(input any) (any, error) { - sanitInput := value.ISanitize(input) - switch v := sanitInput.(type) { - case float64: - return math.Abs(v), nil - case int64: - switch { - case v >= 0: - return v, nil - case v == value.MinInt: - return value.MaxInt, nil - default: - return -v, nil - } - } - return nil, value.NewTypeError(input, value.TNumber, value.TInt) - }, nil - }); err != nil { - panic(err) - } -} diff --git a/internal/impl/pure/bloblang_objects.go b/internal/impl/pure/bloblang_objects.go deleted file mode 100644 index 4c63600310..0000000000 --- a/internal/impl/pure/bloblang_objects.go +++ /dev/null @@ -1,180 +0,0 @@ -package pure - -import ( - "errors" - "fmt" - - "github.com/Jeffail/gabs/v2" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func init() { - if err := bloblang.RegisterMethodV2("squash", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryObjectAndArray). - Description("Squashes an array of objects into a single object, where key collisions result in the values being merged (following similar rules as the `.merge()` method)"). - Example("", `root.locations = this.locations.map_each(loc -> {loc.state: [loc.name]}).squash()`, - [2]string{ - `{"locations":[{"name":"Seattle","state":"WA"},{"name":"New York","state":"NY"},{"name":"Bellevue","state":"WA"},{"name":"Olympia","state":"WA"}]}`, - `{"locations":{"NY":["New York"],"WA":["Seattle","Bellevue","Olympia"]}}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.ArrayMethod(func(i []any) (any, error) { - root := gabs.New() - for _, v := range i { - if err := root.Merge(gabs.Wrap(v)); err != nil { - return nil, err - } - } - return root.Data(), nil - }), nil - }); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("with", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryObjectAndArray). - Variadic(). - Description(`Returns an object where all but one or more xref:configuration:field_paths.adoc[field path] arguments are removed. Each path specifies a specific field to be retained from the input object, allowing for nested fields. - -If a key within a nested path does not exist then it is ignored.`). - Example("", `root = this.with("inner.a","inner.c","d")`, - [2]string{ - `{"inner":{"a":"first","b":"second","c":"third"},"d":"fourth","e":"fifth"}`, - `{"d":"fourth","inner":{"a":"first","c":"third"}}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - includeList := make([][]string, 0, len(args.AsSlice())) - for i, argVal := range args.AsSlice() { - argStr, err := value.IGetString(argVal) - if err != nil { - return nil, fmt.Errorf("argument %v: %w", i, err) - } - includeList = append(includeList, gabs.DotPathToSlice(argStr)) - } - return bloblang.ObjectMethod(func(i map[string]any) (any, error) { - return mapWith(i, includeList), nil - }), nil - }); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("concat", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryObjectAndArray). - Variadic(). - Description("Concatenates an array value with one or more argument arrays."). - Example("", `root.foo = this.foo.concat(this.bar, this.baz)`, - [2]string{ - `{"foo":["a","b"],"bar":["c"],"baz":["d","e","f"]}`, - `{"foo":["a","b","c","d","e","f"]}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - argAnys := args.AsSlice() - argSlices := make([][]any, len(argAnys)) - tally := 0 - for i, a := range argAnys { - var ok bool - if argSlices[i], ok = a.([]any); !ok { - return nil, value.NewTypeError(a, value.TArray) - } - tally += len(argSlices[i]) - } - - return bloblang.ArrayMethod(func(i []any) (any, error) { - resSlice := make([]any, 0, len(i)+tally) - resSlice = append(resSlice, i...) - for _, s := range argSlices { - resSlice = append(resSlice, s...) - } - return resSlice, nil - }), nil - }); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("zip", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryObjectAndArray). - Variadic(). - Description("Zip an array value with one or more argument arrays. Each array must match in length."). - Example("", `root.foo = this.foo.zip(this.bar, this.baz)`, - [2]string{ - `{"foo":["a","b","c"],"bar":[1,2,3],"baz":[4,5,6]}`, - `{"foo":[["a",1,4],["b",2,5],["c",3,6]]}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - sizeError := errors.New("can't zip different length array values") - argError := errors.New("zip requires at least one argument") - - argAnys := args.AsSlice() - argSlices := make([][]any, len(argAnys)) - for i, a := range argAnys { - var ok bool - if argSlices[i], ok = a.([]any); !ok { - return nil, value.NewTypeError(a, value.TArray) - } - if len(argSlices[i]) != len(argSlices[0]) { - return nil, sizeError - } - } - - return bloblang.ArrayMethod(func(i []any) (any, error) { - if len(argSlices) == 0 { - return nil, argError - } - if len(i) != len(argSlices[0]) { - return nil, sizeError - } - resSlice := make([]any, 0, len(i)) - for offset, value := range i { - zipValue := make([]any, 0, len(argSlices)+1) - zipValue = append(zipValue, value) - for _, argSlice := range argSlices { - zipValue = append(zipValue, argSlice[offset]) - } - resSlice = append(resSlice, zipValue) - } - return resSlice, nil - }), nil - }); err != nil { - panic(err) - } -} - -func mapWith(m map[string]any, paths [][]string) map[string]any { - newMap := make(map[string]any, len(m)) - for k, v := range m { - included := false - var nestedInclude [][]string - for _, p := range paths { - if p[0] == k { - included = true - if len(p) > 1 { - nestedInclude = append(nestedInclude, p[1:]) - } - } - } - if included { - if len(nestedInclude) > 0 { - vMap, ok := v.(map[string]any) - if ok { - newMap[k] = mapWith(vMap, nestedInclude) - } else { - newMap[k] = v - } - } else { - newMap[k] = v - } - } - } - return newMap -} diff --git a/internal/impl/pure/bloblang_objects_test.go b/internal/impl/pure/bloblang_objects_test.go deleted file mode 100644 index 9daec154d4..0000000000 --- a/internal/impl/pure/bloblang_objects_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package pure - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestConcatMethod(t *testing.T) { - testCases := []struct { - name string - mapping string - input any - output any - execErr string - }{ - { - name: "nested arrays", - mapping: `root.foo = this.foo.concat(this.bar, this.baz)`, - input: map[string]any{ - "foo": []any{[]any{"first"}}, - "bar": []any{ - []any{"second", "third"}, - []any{"fourth"}, - }, - "baz": []any{"fifth", "sixth"}, - }, - output: map[string]any{ - "foo": []any{ - []any{"first"}, - []any{"second", "third"}, - []any{"fourth"}, - "fifth", "sixth", - }, - }, - }, - { - name: "non array arg", - mapping: `root.foo = this.foo.concat(this.bar)`, - input: map[string]any{ - "foo": []any{"first"}, - "bar": "second", - }, - execErr: "expected array value, got string", - }, - { - name: "non array target", - mapping: `root.foo = this.foo.concat(this.bar)`, - input: map[string]any{ - "bar": []any{"first"}, - "foo": "second", - }, - execErr: "expected array value, got string", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - exec, err := bloblang.Parse(test.mapping) - require.NoError(t, err) - - res, err := exec.Query(test.input) - - if test.execErr == "" { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.execErr) - } - }) - } -} - -func TestZipMethod(t *testing.T) { - testCases := []struct { - name string - mapping string - input any - output any - execErr string - }{ - { - name: "three arrays", - mapping: `root.foo = this.foo.zip(this.bar, this.baz)`, - input: map[string]any{ - "foo": []any{"a", "b", "c"}, - "bar": []any{1, 2, 3}, - "baz": []any{ - []any{"x"}, - []any{"y"}, - []any{"z"}, - }, - }, - output: map[string]any{ - "foo": []any{ - []any{"a", 1, []any{"x"}}, - []any{"b", 2, []any{"y"}}, - []any{"c", 3, []any{"z"}}, - }, - }, - }, - { - name: "non array arg", - mapping: `root.foo = this.foo.zip(this.bar)`, - input: map[string]any{ - "foo": []any{"first"}, - "bar": "second", - }, - execErr: "expected array value, got string", - }, - { - name: "non array target", - mapping: `root.foo = this.foo.zip(this.bar)`, - input: map[string]any{ - "bar": []any{"first"}, - "foo": "second", - }, - execErr: "expected array value, got string", - }, - { - name: "jagged array value parameters", - mapping: `root.foo = this.foo.zip(this.bar, this.baz)`, - input: map[string]any{ - "baz": []any{"first", "second"}, - "bar": []any{"third"}, - "foo": []any{"forth"}, - }, - execErr: "can't zip different length array values", - }, - { - name: "jagged array value and parameters", - mapping: `root.foo = this.foo.zip(this.bar, this.baz)`, - input: map[string]any{ - "baz": []any{"first", "second"}, - "bar": []any{"third", "forth"}, - "foo": []any{"fifth"}, - }, - execErr: "can't zip different length array values", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - exec, err := bloblang.Parse(test.mapping) - require.NoError(t, err) - - res, err := exec.Query(test.input) - - if test.execErr == "" { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.execErr) - } - }) - } -} diff --git a/internal/impl/pure/bloblang_string.go b/internal/impl/pure/bloblang_string.go deleted file mode 100644 index 70f95efca6..0000000000 --- a/internal/impl/pure/bloblang_string.go +++ /dev/null @@ -1,53 +0,0 @@ -package pure - -import ( - "fmt" - "net/url" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// var compressAlgorithms = map[string] - -func init() { - if err := bloblang.RegisterMethodV2("parse_form_url_encoded", - bloblang.NewPluginSpec(). - Category(query.MethodCategoryParsing). - Description(`Attempts to parse a url-encoded query string (from an x-www-form-urlencoded request body) and returns a structured result.`). - Example("", `root.values = this.body.parse_form_url_encoded()`, - [2]string{ - `{"body":"noise=meow&animal=cat&fur=orange&fur=fluffy"}`, - `{"values":{"animal":"cat","fur":["orange","fluffy"],"noise":"meow"}}`, - }, - ), - func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.StringMethod(func(data string) (any, error) { - values, err := url.ParseQuery(data) - if err != nil { - return nil, fmt.Errorf("failed to parse value as url-encoded data: %w", err) - } - return urlValuesToMap(values), nil - }), nil - }); err != nil { - panic(err) - } -} - -func urlValuesToMap(values url.Values) map[string]any { - root := make(map[string]any, len(values)) - - for k, v := range values { - if len(v) == 1 { - root[k] = v[0] - } else { - elements := make([]any, 0, len(v)) - for _, e := range v { - elements = append(elements, e) - } - root[k] = elements - } - } - - return root -} diff --git a/internal/impl/pure/bloblang_string_test.go b/internal/impl/pure/bloblang_string_test.go deleted file mode 100644 index 6285d4639e..0000000000 --- a/internal/impl/pure/bloblang_string_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package pure - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" -) - -func TestParseUrlencoded(t *testing.T) { - testCases := []struct { - name string - method string - target any - args []any - exp any - }{ - { - name: "simple parsing", - method: "parse_form_url_encoded", - target: "username=example", - args: []any{}, - exp: map[string]any{"username": "example"}, - }, - { - name: "parsing multiple values under the same key", - method: "parse_form_url_encoded", - target: "usernames=userA&usernames=userB", - args: []any{}, - exp: map[string]any{"usernames": []any{"userA", "userB"}}, - }, - { - name: "decodes data correctly", - method: "parse_form_url_encoded", - target: "email=example%40email.com", - args: []any{}, - exp: map[string]any{"email": "example@email.com"}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - targetClone := value.IClone(test.target) - argsClone := value.IClone(test.args).([]any) - - fn, err := query.InitMethodHelper(test.method, query.NewLiteralFunction("", targetClone), argsClone...) - require.NoError(t, err) - - res, err := fn.Exec(query.FunctionContext{ - Maps: map[string]query.Function{}, - Index: 0, - MsgBatch: nil, - }) - require.NoError(t, err) - - assert.Equal(t, test.exp, res) - assert.Equal(t, test.target, targetClone) - assert.Equal(t, test.args, argsClone) - }) - } -} diff --git a/internal/impl/pure/bloblang_time.go b/internal/impl/pure/bloblang_time.go deleted file mode 100644 index 8ffd58fec3..0000000000 --- a/internal/impl/pure/bloblang_time.go +++ /dev/null @@ -1,620 +0,0 @@ -package pure - -import ( - "fmt" - "time" - - "github.com/itchyny/timefmt-go" - "github.com/rickb777/date/period" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func asDeprecated(s *bloblang.PluginSpec) *bloblang.PluginSpec { - tmpSpec := *s - newSpec := &tmpSpec - newSpec = newSpec.Deprecated() - return newSpec -} - -func init() { - // Note: The examples are run and tested from within - // ./internal/bloblang/query/parsed_test.go - - tsRoundSpec := bloblang.NewPluginSpec(). - Beta(). - Static(). - Category(query.MethodCategoryTime). - Description(`Returns the result of rounding a timestamp to the nearest multiple of the argument duration (nanoseconds). The rounding behavior for halfway values is to round up. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The `+"<>"+` method can be used in order to parse different timestamp formats.`). - Param(bloblang.NewInt64Param("duration").Description("A duration measured in nanoseconds to round by.")). - Version("4.2.0"). - Example("Use the method `parse_duration` to convert a duration string into an integer argument.", - `root.created_at_hour = this.created_at.ts_round("1h".parse_duration())`, - [2]string{ - `{"created_at":"2020-08-14T05:54:23Z"}`, - `{"created_at_hour":"2020-08-14T06:00:00Z"}`, - }) - - tsRoundCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - iDur, err := args.GetInt64("duration") - if err != nil { - return nil, err - } - dur := time.Duration(iDur) - return bloblang.TimestampMethod(func(t time.Time) (any, error) { - return t.Round(dur), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_round", tsRoundSpec, tsRoundCtor); err != nil { - panic(err) - } - - tsTZSpec := bloblang.NewPluginSpec(). - Beta(). - Static(). - Category(query.MethodCategoryTime). - Description(`Returns the result of converting a timestamp to a specified timezone. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The `+"<>"+` method can be used in order to parse different timestamp formats.`). - Param(bloblang.NewStringParam("tz").Description(`The timezone to change to. If set to "UTC" then the timezone will be UTC. If set to "Local" then the local timezone will be used. Otherwise, the argument is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York".`)). - Version("4.3.0"). - Example("", - `root.created_at_utc = this.created_at.ts_tz("UTC")`, - [2]string{ - `{"created_at":"2021-02-03T17:05:06+01:00"}`, - `{"created_at_utc":"2021-02-03T16:05:06Z"}`, - }) - - tsTZCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - timezoneStr, err := args.GetString("tz") - if err != nil { - return nil, err - } - timezone, err := time.LoadLocation(timezoneStr) - if err != nil { - return nil, fmt.Errorf("failed to parse timezone location name: %w", err) - } - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - return target.In(timezone), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_tz", tsTZSpec, tsTZCtor); err != nil { - panic(err) - } - - tsAddISOSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Parse parameter string as ISO 8601 period and add it to value with high precision for units larger than an hour."). - Param(bloblang.NewStringParam("duration").Description(`Duration in ISO 8601 format`)) - - tsSubISOSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Parse parameter string as ISO 8601 period and subtract it from value with high precision for units larger than an hour."). - Param(bloblang.NewStringParam("duration").Description(`Duration in ISO 8601 format`)) - - tsModifyISOCtor := func(callback func(d period.Period, t time.Time) time.Time) func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return func(args *bloblang.ParsedParams) (bloblang.Method, error) { - s, err := args.GetString("duration") - if err != nil { - return nil, err - } - dur, err := period.Parse(s) - if err != nil { - return nil, err - } - return bloblang.TimestampMethod(func(t time.Time) (any, error) { - return callback(dur, t), nil - }), nil - } - } - - if err := bloblang.RegisterMethodV2("ts_add_iso8601", tsAddISOSpec, - tsModifyISOCtor(func(d period.Period, t time.Time) time.Time { - r, _ := d.AddTo(t) - return r - })); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("ts_sub_iso8601", tsSubISOSpec, - tsModifyISOCtor(func(d period.Period, t time.Time) time.Time { - r, _ := d.Negate().AddTo(t) - return r - })); err != nil { - panic(err) - } - - //-------------------------------------------------------------------------- - - parseDurSpec := bloblang.NewPluginSpec(). - Static(). - Category(query.MethodCategoryTime). - Description(`Attempts to parse a string as a duration and returns an integer of nanoseconds. A duration string is a possibly signed sequence of decimal numbers, each with an optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`). - Example("", - `root.delay_for_ns = this.delay_for.parse_duration()`, - [2]string{ - `{"delay_for":"50us"}`, - `{"delay_for_ns":50000}`, - }, - ). - Example("", - `root.delay_for_s = this.delay_for.parse_duration() / 1000000000`, - [2]string{ - `{"delay_for":"2h"}`, - `{"delay_for_s":7200}`, - }, - ) - - parseDurCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.StringMethod(func(s string) (any, error) { - d, err := time.ParseDuration(s) - if err != nil { - return nil, err - } - return d.Nanoseconds(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("parse_duration", parseDurSpec, parseDurCtor); err != nil { - panic(err) - } - - parseDurISOSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description(`Attempts to parse a string using ISO-8601 rules as a duration and returns an integer of nanoseconds. A duration string is represented by the format "P[n]Y[n]M[n]DT[n]H[n]M[n]S" or "P[n]W". In these representations, the "[n]" is replaced by the value for each of the date and time elements that follow the "[n]". For example, "P3Y6M4DT12H30M5S" represents a duration of "three years, six months, four days, twelve hours, thirty minutes, and five seconds". The last field of the format allows fractions with one decimal place, so "P3.5S" will return 3500000000ns. Any additional decimals will be truncated.`). - Example("Arbitrary ISO-8601 duration string to nanoseconds:", - `root.delay_for_ns = this.delay_for.parse_duration_iso8601()`, - [2]string{ - `{"delay_for":"P3Y6M4DT12H30M5S"}`, - `{"delay_for_ns":110839937000000000}`, - }, - ). - Example("Two hours ISO-8601 duration string to seconds:", - `root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000`, - [2]string{ - `{"delay_for":"PT2H"}`, - `{"delay_for_s":7200}`, - }, - ). - Example("Two and a half seconds ISO-8601 duration string to seconds:", - `root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000`, - [2]string{ - `{"delay_for":"PT2.5S"}`, - `{"delay_for_s":2.5}`, - }, - ) - - parseDurISOCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.StringMethod(func(s string) (any, error) { - // No need to normalise the output since we need it expressed as nanoseconds. - d, err := period.Parse(s, false) - if err != nil { - return nil, err - } - // The conversion is likely imprecise when the period specifies years, months and days. - // See method documentation for details on precision. - return d.DurationApprox().Nanoseconds(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("parse_duration_iso8601", parseDurISOSpec, parseDurISOCtor); err != nil { - panic(err) - } - - //-------------------------------------------------------------------------- - - parseTSSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description(`Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as ` + "<>" + `. - -The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the ` + "<>" + ` method.`). - Param(bloblang.NewStringParam("format").Description("The format of the target string.")) - - parseTSSpecDep := asDeprecated(parseTSSpec) - - parseTSSpec = parseTSSpec. - Example("", - `root.doc.timestamp = this.doc.timestamp.ts_parse("2006-Jan-02")`, - [2]string{ - `{"doc":{"timestamp":"2020-Aug-14"}}`, - `{"doc":{"timestamp":"2020-08-14T00:00:00Z"}}`, - }, - ) - - parseTSCtor := func(deprecated bool) bloblang.MethodConstructorV2 { - return func(args *bloblang.ParsedParams) (bloblang.Method, error) { - layout, err := args.GetString("format") - if err != nil { - return nil, err - } - return bloblang.StringMethod(func(s string) (any, error) { - ut, err := time.Parse(layout, s) - if err != nil { - return nil, err - } - if deprecated { - return ut.Format(time.RFC3339Nano), nil - } - return ut, nil - }), nil - } - } - - if err := bloblang.RegisterMethodV2("ts_parse", parseTSSpec, parseTSCtor(false)); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("parse_timestamp", parseTSSpecDep, parseTSCtor(true)); err != nil { - panic(err) - } - - parseTSStrptimeSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into <>."). - Param(bloblang.NewStringParam("format").Description("The format of the target string.")) - - parseTSStrptimeSpecDep := asDeprecated(parseTSStrptimeSpec) - - parseTSStrptimeSpec = parseTSStrptimeSpec. - Example( - "The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strptime[man 3 strptime] for the list of format specifiers.", - `root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d")`, - [2]string{ - `{"doc":{"timestamp":"2020-Aug-14"}}`, - `{"doc":{"timestamp":"2020-08-14T00:00:00Z"}}`, - }, - ). - Example( - "As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported.", - `root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d %H:%M:%S.%f")`, - [2]string{ - `{"doc":{"timestamp":"2020-Aug-14 11:50:26.371000"}}`, - `{"doc":{"timestamp":"2020-08-14T11:50:26.371Z"}}`, - }, - ) - - parseTSStrptimeCtor := func(deprecated bool) bloblang.MethodConstructorV2 { - return func(args *bloblang.ParsedParams) (bloblang.Method, error) { - layout, err := args.GetString("format") - if err != nil { - return nil, err - } - return bloblang.StringMethod(func(s string) (any, error) { - ut, err := timefmt.Parse(s, layout) - if err != nil { - return nil, err - } - if deprecated { - return ut.Format(time.RFC3339Nano), nil - } - return ut, nil - }), nil - } - } - - if err := bloblang.RegisterMethodV2("ts_strptime", parseTSStrptimeSpec, parseTSStrptimeCtor(false)); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("parse_timestamp_strptime", parseTSStrptimeSpecDep, parseTSStrptimeCtor(true)); err != nil { - panic(err) - } - - //-------------------------------------------------------------------------- - - formatTSSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description(`Attempts to format a timestamp value as a string according to a specified format, or RFC 3339 by default. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. - -The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the ` + "<>" + ` method.`). - Param(bloblang.NewStringParam("format").Description("The output format to use.").Default(time.RFC3339Nano)). - Param(bloblang.NewStringParam("tz").Description("An optional timezone to use, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used.").Optional()) - - formatTSSpecDep := asDeprecated(formatTSSpec) - - formatTSSpec = formatTSSpec. - Example("", - `root.something_at = (this.created_at + 300).ts_format()`, - // `{"created_at":1597405526}`, - // `{"something_at":"2020-08-14T11:50:26.371Z"}`, - ). - Example( - "An optional string argument can be used in order to specify the output format of the timestamp. The format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value.", - `root.something_at = (this.created_at + 300).ts_format("2006-Jan-02 15:04:05")`, - // `{"created_at":1597405526}`, - // `{"something_at":"2020-Aug-14 11:50:26"}`, - ). - Example( - "A second optional string argument can also be used in order to specify a timezone, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used.", - `root.something_at = this.created_at.ts_format(format: "2006-Jan-02 15:04:05", tz: "UTC")`, - [2]string{ - `{"created_at":1597405526}`, - `{"something_at":"2020-Aug-14 11:45:26"}`, - }, - [2]string{ - `{"created_at":"2020-08-14T11:50:26.371Z"}`, - `{"something_at":"2020-Aug-14 11:50:26"}`, - }, - ). - Example( - "And `ts_format` supports up to nanosecond precision with floating point timestamp values.", - `root.something_at = this.created_at.ts_format("2006-Jan-02 15:04:05.999999", "UTC")`, - [2]string{ - `{"created_at":1597405526.123456}`, - `{"something_at":"2020-Aug-14 11:45:26.123456"}`, - }, - [2]string{ - `{"created_at":"2020-08-14T11:50:26.371Z"}`, - `{"something_at":"2020-Aug-14 11:50:26.371"}`, - }, - ) - - formatTSCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - layout, err := args.GetString("format") - if err != nil { - return nil, err - } - var timezone *time.Location - tzOpt, err := args.GetOptionalString("tz") - if err != nil { - return nil, err - } - if tzOpt != nil { - if timezone, err = time.LoadLocation(*tzOpt); err != nil { - return nil, fmt.Errorf("failed to parse timezone location name: %w", err) - } - } - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - if timezone != nil { - target = target.In(timezone) - } - return target.Format(layout), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_format", formatTSSpec, formatTSCtor); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("format_timestamp", formatTSSpecDep, formatTSCtor); err != nil { - panic(err) - } - - formatTSStrftimeSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Attempts to format a timestamp value as a string according to a specified strftime-compatible format. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format."). - Param(bloblang.NewStringParam("format").Description("The output format to use.")). - Param(bloblang.NewStringParam("tz").Description("An optional timezone to use, otherwise the timezone of the input string is used.").Optional()) - - formatTSStrftimeSpecDep := asDeprecated(formatTSStrftimeSpec) - - formatTSStrftimeSpec = formatTSStrftimeSpec. - Example( - "The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strftime[man 3 strftime] for the list of format specifiers.", - `root.something_at = (this.created_at + 300).ts_strftime("%Y-%b-%d %H:%M:%S")`, - // `{"created_at":1597405526}`, - // `{"something_at":"2020-Aug-14 11:50:26"}`, - ). - Example( - "A second optional string argument can also be used in order to specify a timezone, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used.", - `root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S", "UTC")`, - [2]string{ - `{"created_at":1597405526}`, - `{"something_at":"2020-Aug-14 11:45:26"}`, - }, - [2]string{ - `{"created_at":"2020-08-14T11:50:26.371Z"}`, - `{"something_at":"2020-Aug-14 11:50:26"}`, - }, - ). - Example( - "As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported.", - `root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S.%f", "UTC")`, - [2]string{ - `{"created_at":1597405526}`, - `{"something_at":"2020-Aug-14 11:45:26.000000"}`, - }, - [2]string{ - `{"created_at":"2020-08-14T11:50:26.371Z"}`, - `{"something_at":"2020-Aug-14 11:50:26.371000"}`, - }, - ) - - formatTSStrftimeCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - layout, err := args.GetString("format") - if err != nil { - return nil, err - } - var timezone *time.Location - tzOpt, err := args.GetOptionalString("tz") - if err != nil { - return nil, err - } - if tzOpt != nil { - if timezone, err = time.LoadLocation(*tzOpt); err != nil { - return nil, fmt.Errorf("failed to parse timezone location name: %w", err) - } - } - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - if timezone != nil { - target = target.In(timezone) - } - return timefmt.Format(target, layout), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_strftime", formatTSStrftimeSpec, formatTSStrftimeCtor); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("format_timestamp_strftime", formatTSStrftimeSpecDep, formatTSStrftimeCtor); err != nil { - panic(err) - } - - formatTSUnixSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") - - formatTSUnixSpecDep := asDeprecated(formatTSUnixSpec) - - formatTSUnixSpec = formatTSUnixSpec. - Example("", - `root.created_at_unix = this.created_at.ts_unix()`, - [2]string{ - `{"created_at":"2009-11-10T23:00:00Z"}`, - `{"created_at_unix":1257894000}`, - }, - ) - - formatTSUnixCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - return target.Unix(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_unix", formatTSUnixSpec, formatTSUnixCtor); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("format_timestamp_unix", formatTSUnixSpecDep, formatTSUnixCtor); err != nil { - panic(err) - } - - formatTSUnixMilliSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") - - formatTSUnixMilliSpecDep := asDeprecated(formatTSUnixMilliSpec) - - formatTSUnixMilliSpec = formatTSUnixMilliSpec. - Example("", - `root.created_at_unix = this.created_at.ts_unix_milli()`, - [2]string{ - `{"created_at":"2009-11-10T23:00:00Z"}`, - `{"created_at_unix":1257894000000}`, - }, - ) - - formatTSUnixMilliCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - return target.UnixMilli(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_unix_milli", formatTSUnixMilliSpec, formatTSUnixMilliCtor); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("format_timestamp_unix_milli", formatTSUnixMilliSpecDep, formatTSUnixMilliCtor); err != nil { - panic(err) - } - - formatTSUnixMicroSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") - - formatTSUnixMicroSpecDep := asDeprecated(formatTSUnixMicroSpec) - - formatTSUnixMicroSpec = formatTSUnixMicroSpec. - Example("", - `root.created_at_unix = this.created_at.ts_unix_micro()`, - [2]string{ - `{"created_at":"2009-11-10T23:00:00Z"}`, - `{"created_at_unix":1257894000000000}`, - }, - ) - - formatTSUnixMicroCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - return target.UnixMicro(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_unix_micro", formatTSUnixMicroSpec, formatTSUnixMicroCtor); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("format_timestamp_unix_micro", formatTSUnixMicroSpecDep, formatTSUnixMicroCtor); err != nil { - panic(err) - } - - formatTSUnixNanoSpec := bloblang.NewPluginSpec(). - Category(query.MethodCategoryTime). - Beta(). - Static(). - Description("Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The <> method can be used in order to parse different timestamp formats.") - - formatTSUnixNanoSpecDep := asDeprecated(formatTSUnixNanoSpec) - - formatTSUnixNanoSpec = formatTSUnixNanoSpec. - Example("", - `root.created_at_unix = this.created_at.ts_unix_nano()`, - [2]string{ - `{"created_at":"2009-11-10T23:00:00Z"}`, - `{"created_at_unix":1257894000000000000}`, - }, - ) - - formatTSUnixNanoCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.TimestampMethod(func(target time.Time) (any, error) { - return target.UnixNano(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_unix_nano", formatTSUnixNanoSpec, formatTSUnixNanoCtor); err != nil { - panic(err) - } - - if err := bloblang.RegisterMethodV2("format_timestamp_unix_nano", formatTSUnixNanoSpecDep, formatTSUnixNanoCtor); err != nil { - panic(err) - } - - tsSubSpec := bloblang.NewPluginSpec(). - Beta(). - Static(). - Category(query.MethodCategoryTime). - Description(`Returns the difference in nanoseconds between the target timestamp (t1) and the timestamp provided as a parameter (t2). The `+"<>"+` method can be used in order to parse different timestamp formats.`). - Param(bloblang.NewTimestampParam("t2").Description("The second timestamp to be subtracted from the method target.")). - Version("4.23.0"). - Example("Use the `.abs()` method in order to calculate an absolute duration between two timestamps.", - `root.between = this.started_at.ts_sub("2020-08-14T05:54:23Z").abs()`, - [2]string{ - `{"started_at":"2020-08-13T05:54:23Z"}`, - `{"between":86400000000000}`, - }) - - tsSubCtor := func(args *bloblang.ParsedParams) (bloblang.Method, error) { - t2, err := args.GetTimestamp("t2") - if err != nil { - return nil, err - } - return bloblang.TimestampMethod(func(t1 time.Time) (any, error) { - return t1.Sub(t2).Nanoseconds(), nil - }), nil - } - - if err := bloblang.RegisterMethodV2("ts_sub", tsSubSpec, tsSubCtor); err != nil { - panic(err) - } -} diff --git a/internal/impl/pure/bloblang_time_test.go b/internal/impl/pure/bloblang_time_test.go deleted file mode 100644 index d4bac65232..0000000000 --- a/internal/impl/pure/bloblang_time_test.go +++ /dev/null @@ -1,331 +0,0 @@ -package pure - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestTimestampMethods(t *testing.T) { - tests := []struct { - name string - mapping string - input any - output any - parseErrorContains string - execErrorContains string - }{ - { - name: "ts_round by hour", - mapping: `root = this.ts_round("1h".parse_duration()).string()`, - input: "2020-08-14T05:54:23Z", - output: "2020-08-14T06:00:00Z", - }, - { - name: "ts_round by minute", - mapping: `root = this.ts_round("1m".parse_duration()).string()`, - input: "2020-08-14T05:54:23Z", - output: "2020-08-14T05:54:00Z", - }, - { - name: "ts_round bad timestamp", - mapping: `root = this.ts_round("1h".parse_duration()).string()`, - input: "not a timestamp", - execErrorContains: "parsing time \"not a timestamp\" as", - }, - { - name: "ts_round bad timestamp static", - mapping: `root = "not a timestamp".ts_round("1h".parse_duration()).string()`, - parseErrorContains: "parsing time \"not a timestamp\" as", - }, - { - name: "check ts_parse with format", - mapping: `root = "2020-Aug-14".ts_parse("2006-Jan-02").string()`, - output: "2020-08-14T00:00:00Z", - }, - { - name: "check ts_parse invalid", - mapping: `root = this.ts_parse("2006-01-02T15:04:05Z07:00").string()`, - input: "not valid timestamp", - execErrorContains: `parsing time "not valid timestamp" as "2006-01-02T15:04:05Z07:00": cannot parse "not valid timestamp" as "2006"`, - }, - { - name: "check parse_timestamp invalid static", - mapping: `root = "not valid timestamp".parse_timestamp("2006-01-02T15:04:05Z07:00")`, - parseErrorContains: `parsing time "not valid timestamp" as "2006-01-02T15:04:05Z07:00": cannot parse "not valid timestamp" as "2006"`, - }, - { - name: "check ts_parse with invalid format", - mapping: `root = "invalid format".ts_parse("2006-Jan-02")`, - parseErrorContains: `parsing time "invalid format" as "2006-Jan-02": cannot parse "invalid format" as "2006"`, - }, - { - name: "check ts_parse with invalid literal type", - mapping: `root = 1.ts_parse("2006-Jan-02")`, - parseErrorContains: `expected string value, got number (1)`, - }, - { - name: "check ts_strptime with format", - mapping: `root = "2020-Aug-14".ts_strptime("%Y-%b-%d").string()`, - output: "2020-08-14T00:00:00Z", - }, - { - name: "check ts_strptime invalid", - mapping: `root = "not valid timestamp".ts_strptime("%Y-%b-%d")`, - parseErrorContains: `failed to parse "not valid timestamp" with "%Y-%b-%d": cannot parse %Y`, - }, - { - name: "check ts_strptime with invalid format", - mapping: `root = "invalid format".ts_strptime("INVALID_FORMAT")`, - parseErrorContains: `failed to parse "invalid format" with "INVALID_FORMAT": expected 'I'`, - }, - { - name: "check ts_strptime with invalid literal type", - mapping: `root = 1.ts_strptime("%Y-%b-%d")`, - parseErrorContains: `expected string value, got number`, - }, - { - name: "check ts_format string default", - mapping: `root = "2020-08-14T11:45:26.371+01:00".ts_format("2006-01-02T15:04:05.999999999Z07:00")`, - output: "2020-08-14T11:45:26.371+01:00", - }, - { - name: "check ts_format string", - mapping: `root = "2020-08-14T11:45:26.371+00:00".ts_format("2006-Jan-02 15:04:05.999999")`, - output: "2020-Aug-14 11:45:26.371", - }, - { - name: "check ts_format unix float", - mapping: `root = 1597405526.123456.ts_format("2006-Jan-02 15:04:05.999999", "UTC")`, - output: "2020-Aug-14 11:45:26.123456", - }, - { - name: "check ts_format unix", - mapping: `root = 1597405526.ts_format("2006-Jan-02 15:04:05", "UTC")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check ts_unix", - mapping: `root = "2009-11-10T23:00:00Z".ts_unix()`, - output: int64(1257894000), - }, - { - name: "check ts_unix_milli", - mapping: `root = "2009-11-10T23:00:00Z".ts_unix_milli()`, - output: int64(1257894000000), - }, - { - name: "check ts_unix_micro", - mapping: `root = "2009-11-10T23:00:00Z".ts_unix_micro()`, - output: int64(1257894000000000), - }, - { - name: "check ts_unix_nano", - mapping: `root = "2009-11-10T23:00:00Z".ts_unix_nano()`, - output: int64(1257894000000000000), - }, - { - name: "check ts_strftime string", - mapping: `root = "2020-08-14T11:45:26.371+01:00".ts_strftime("%Y-%b-%d %H:%M:%S")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check ts_strftime float", - mapping: `root = 1597405526.123456.ts_strftime("%Y-%b-%d %H:%M:%S", "UTC")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check ts_strftime unix", - mapping: `root = 1597405526.ts_strftime("%Y-%b-%d %H:%M:%S", "UTC")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check parse duration ISO-8601", - mapping: `root = "P3Y6M4DT12H30M5.3S".parse_duration_iso8601()`, - output: int64(110839937300000000), - }, - { - name: "check parse duration ISO-8601 ignore more than one decimal place", - mapping: `root = "P3Y6M4DT12H30M5.33S".parse_duration_iso8601()`, - output: int64(110839937300000000), - }, - { - name: "check parse duration ISO-8601 only allow fractions in the last field", - mapping: `root = "P2.5YT7.5S".parse_duration_iso8601()`, - parseErrorContains: "P2.5YT7.5S: 'Y' & 'S' only the last field can have a fraction", - }, - { - name: "check parse duration ISO-8601 with invalid format", - mapping: `root = "P3S".parse_duration_iso8601()`, - parseErrorContains: "P3S: 'S' designator cannot occur here", - }, - { - name: "check parse duration ISO-8601 with bogus format", - mapping: `root = "gibberish".parse_duration_iso8601()`, - parseErrorContains: "gibberish: expected 'P' period mark at the start", - }, - { - name: "check ts_add_iso8601", - mapping: `root = 1677097265.ts_add_iso8601("P1Y").ts_unix()`, - output: int64(1708633265), - }, - { - name: "check ts_sub_iso8601", - mapping: `root = 1677097265.ts_sub_iso8601("P1Y").ts_unix()`, - output: int64(1645561265), - }, - { - name: "check ts_sub", - mapping: `root = "2023-10-12T14:59:10.12345+03:00".ts_sub("2023-10-02T02:55:03.6789Z")`, - output: int64(896646444550000), - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - m, err := bloblang.Parse(test.mapping) - if test.parseErrorContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.parseErrorContains) - } else { - require.NoError(t, err) - v, err := m.Query(test.input) - if test.execErrorContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.execErrorContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, v) - } - } - }) - } -} - -func TestTimestampMethodsOld(t *testing.T) { - tests := []struct { - name string - mapping string - input any - output any - parseErrorContains string - execErrorContains string - }{ - { - name: "check parse_timestamp with format", - mapping: `root = "2020-Aug-14".parse_timestamp("2006-Jan-02")`, - output: "2020-08-14T00:00:00Z", - }, - { - name: "check parse_timestamp invalid", - mapping: `root = this.parse_timestamp("2006-01-02T15:04:05Z07:00")`, - input: "not valid timestamp", - execErrorContains: `parsing time "not valid timestamp" as "2006-01-02T15:04:05Z07:00": cannot parse "not valid timestamp" as "2006"`, - }, - { - name: "check parse_timestamp invalid static", - mapping: `root = "not valid timestamp".parse_timestamp("2006-01-02T15:04:05Z07:00")`, - parseErrorContains: `parsing time "not valid timestamp" as "2006-01-02T15:04:05Z07:00": cannot parse "not valid timestamp" as "2006"`, - }, - { - name: "check parse_timestamp with invalid format", - mapping: `root = "invalid format".parse_timestamp("2006-Jan-02")`, - parseErrorContains: `parsing time "invalid format" as "2006-Jan-02": cannot parse "invalid format" as "2006"`, - }, - { - name: "check parse_timestamp with invalid literal type", - mapping: `root = 1.parse_timestamp("2006-Jan-02")`, - parseErrorContains: `expected string value, got number (1)`, - }, - { - name: "check parse_timestamp_strptime with format", - mapping: `root = "2020-Aug-14".parse_timestamp_strptime("%Y-%b-%d")`, - output: "2020-08-14T00:00:00Z", - }, - { - name: "check parse_timestamp_strptime invalid", - mapping: `root = "not valid timestamp".parse_timestamp_strptime("%Y-%b-%d")`, - parseErrorContains: `failed to parse "not valid timestamp" with "%Y-%b-%d": cannot parse %Y`, - }, - { - name: "check parse_timestamp_strptime with invalid format", - mapping: `root = "invalid format".parse_timestamp_strptime("INVALID_FORMAT")`, - parseErrorContains: `failed to parse "invalid format" with "INVALID_FORMAT": expected 'I'`, - }, - { - name: "check parse_timestamp_strptime with invalid literal type", - mapping: `root = 1.parse_timestamp_strptime("%Y-%b-%d")`, - parseErrorContains: `expected string value, got number`, - }, - { - name: "check format_timestamp string default", - mapping: `root = "2020-08-14T11:45:26.371+01:00".format_timestamp("2006-01-02T15:04:05.999999999Z07:00")`, - output: "2020-08-14T11:45:26.371+01:00", - }, - { - name: "check format_timestamp string", - mapping: `root = "2020-08-14T11:45:26.371+00:00".format_timestamp("2006-Jan-02 15:04:05.999999")`, - output: "2020-Aug-14 11:45:26.371", - }, - { - name: "check format_timestamp unix float", - mapping: `root = 1597405526.123456.format_timestamp("2006-Jan-02 15:04:05.999999", "UTC")`, - output: "2020-Aug-14 11:45:26.123456", - }, - { - name: "check format_timestamp unix", - mapping: `root = 1597405526.format_timestamp("2006-Jan-02 15:04:05", "UTC")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check format_timestamp_unix", - mapping: `root = "2009-11-10T23:00:00Z".format_timestamp_unix()`, - output: int64(1257894000), - }, - { - name: "check format_timestamp_unix_nano", - mapping: `root = "2009-11-10T23:00:00Z".format_timestamp_unix_nano()`, - output: int64(1257894000000000000), - }, - { - name: "check format_timestamp_strftime string", - mapping: `root = "2020-08-14T11:45:26.371+01:00".format_timestamp_strftime("%Y-%b-%d %H:%M:%S")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check format_timestamp_strftime float", - mapping: `root = 1597405526.123456.format_timestamp_strftime("%Y-%b-%d %H:%M:%S", "UTC")`, - output: "2020-Aug-14 11:45:26", - }, - { - name: "check format_timestamp_strftime unix", - mapping: `root = 1597405526.format_timestamp_strftime("%Y-%b-%d %H:%M:%S", "UTC")`, - output: "2020-Aug-14 11:45:26", - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - m, err := bloblang.Parse(test.mapping) - if test.parseErrorContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.parseErrorContains) - } else { - require.NoError(t, err) - v, err := m.Query(test.input) - if test.execErrorContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.execErrorContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.output, v) - } - } - }) - } -} diff --git a/internal/impl/pure/buffer_memory.go b/internal/impl/pure/buffer_memory.go deleted file mode 100644 index ee69686c89..0000000000 --- a/internal/impl/pure/buffer_memory.go +++ /dev/null @@ -1,291 +0,0 @@ -package pure - -import ( - "context" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/public/service" -) - -func memoryBufferConfig() *service.ConfigSpec { - bs := policy.FieldSpec() - bs.Name = "batch_policy" - bs.Description = "Optionally configure a policy to flush buffered messages in batches." - bs.Examples = nil - newChildren := []docs.FieldSpec{ - docs.FieldBool("enabled", "Whether to batch messages as they are flushed.").HasDefault(false), - } - for _, f := range bs.Children { - if f.Name == "count" { - f = f.HasDefault(0) - } - if !f.IsDeprecated { - newChildren = append(newChildren, f) - } - } - bs.Children = newChildren - - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary("Stores consumed messages in memory and acknowledges them at the input level. During shutdown Benthos will make a best attempt at flushing all remaining messages before exiting cleanly."). - Description(` -This buffer is appropriate when consuming messages from inputs that do not gracefully handle back pressure and where delivery guarantees aren't critical. - -This buffer has a configurable limit, where consumption will be stopped with back pressure upstream if the total size of messages in the buffer reaches this amount. Since this calculation is only an estimate, and the real size of messages in RAM is always higher, it is recommended to set the limit significantly below the amount of RAM available. - -== Delivery guarantees - -This buffer intentionally weakens the delivery guarantees of the pipeline and therefore should never be used in places where data loss is unacceptable. - -== Batching - -It is possible to batch up messages sent from this buffer using a xref:configuration:batching.adoc#batch-policy[batch policy].`). - Field(service.NewIntField("limit"). - Description(`The maximum buffer size (in bytes) to allow before applying backpressure upstream.`). - Default(524288000)). - Field(service.NewInternalField(bs)) -} - -func init() { - err := service.RegisterBatchBuffer( - "memory", memoryBufferConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return newMemoryBufferFromConfig(conf, mgr) - }) - if err != nil { - panic(err) - } -} - -func newMemoryBufferFromConfig(conf *service.ParsedConfig, res *service.Resources) (*memoryBuffer, error) { - limit, err := conf.FieldInt("limit") - if err != nil { - return nil, err - } - - batchingEnabled, err := conf.FieldBool("batch_policy", "enabled") - if err != nil { - return nil, err - } - - var batcher *service.Batcher - if batchingEnabled { - batchPol, err := conf.FieldBatchPolicy("batch_policy") - if err != nil { - return nil, err - } - if batcher, err = batchPol.NewBatcher(res); err != nil { - return nil, err - } - } else { - if batcher, err = (service.BatchPolicy{Count: 1}).NewBatcher(res); err != nil { - return nil, err - } - } - - return newMemoryBuffer(limit, batcher), nil -} - -//------------------------------------------------------------------------------ - -type measuredBatch struct { - b service.MessageBatch - size int -} - -type memoryBuffer struct { - batches []measuredBatch - bytes int - - cap int - cond *sync.Cond - endOfInput bool - closed bool - - batcher *service.Batcher -} - -func newMemoryBuffer(capacity int, batcher *service.Batcher) *memoryBuffer { - return &memoryBuffer{ - cap: capacity, - cond: sync.NewCond(&sync.Mutex{}), - batcher: batcher, - } -} - -//------------------------------------------------------------------------------ - -func (m *memoryBuffer) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - ctx, done := context.WithCancel(ctx) - defer done() - - go func() { - <-ctx.Done() - m.cond.Broadcast() - }() - - var batchReady, timedBatch bool - - triggerTimed := func() { - batchReady = false - timedBatch = false - - timedDur, exists := m.batcher.UntilNext() - if !exists { - return - } - - timer := time.NewTimer(timedDur) - go func() { - defer timer.Stop() - select { - case <-timer.C: - m.cond.L.Lock() - defer m.cond.L.Unlock() - timedBatch = true - batchReady = true - m.cond.Broadcast() - case <-ctx.Done(): - } - }() - } - triggerTimed() - - m.cond.L.Lock() - defer m.cond.L.Unlock() - - // The output batch we're forming from the buffered batches - var outBatch service.MessageBatch - - // The batches that have made up our output batch, this could be multiple - // batches if we have a batching policy - var batchSources []measuredBatch - - // The size of the batches that formed our output batch - var outSize int - - for { - if m.closed { - return nil, nil, service.ErrEndOfBuffer - } - if ctx.Err() != nil { - return nil, nil, ctx.Err() - } - - for len(m.batches) > 0 && !batchReady { - outSize += m.batches[0].size - for _, msg := range m.batches[0].b { - batchReady = m.batcher.Add(msg.Copy()) - } - batchSources = append(batchSources, m.batches[0]) - - m.batches[0] = measuredBatch{} - m.batches = m.batches[1:] - } - - if batchReady || m.endOfInput { - var err error - if outBatch, err = m.batcher.Flush(ctx); err != nil { - return nil, nil, err - } - if m.endOfInput && len(batchSources) == 0 { - return nil, nil, service.ErrEndOfBuffer - } - if timedBatch && len(outBatch) == 0 { - triggerTimed() - continue - } - break - } - - // None of our exit conditions triggered, so exit - m.cond.Wait() - } - - m.cond.Broadcast() - return outBatch, func(ctx context.Context, err error) error { - m.cond.L.Lock() - defer m.cond.L.Unlock() - if err == nil { - m.bytes -= outSize - } else { - m.batches = append(batchSources, m.batches...) - } - m.cond.Broadcast() - return nil - }, nil -} - -// PushMessage adds a new message to the stack. Returns the backlog in bytes. -func (m *memoryBuffer) WriteBatch(ctx context.Context, msgBatch service.MessageBatch, aFn service.AckFunc) error { - // Deep copy before acknowledging in order to avoid vague ownership - msgBatch = msgBatch.DeepCopy() - if err := aFn(ctx, nil); err != nil { - return err - } - - extraBytes := 0 - for _, b := range msgBatch { - bBytes, err := b.AsBytes() - if err != nil { - return err - } - extraBytes += len(bBytes) - } - - if extraBytes > m.cap { - return component.ErrMessageTooLarge - } - - m.cond.L.Lock() - defer m.cond.L.Unlock() - - if m.closed { - return component.ErrTypeClosed - } - - for (m.bytes + extraBytes) > m.cap { - m.cond.Wait() - if m.closed { - return component.ErrTypeClosed - } - } - - m.batches = append(m.batches, measuredBatch{ - b: msgBatch, - size: extraBytes, - }) - m.bytes += extraBytes - - m.cond.Broadcast() - return nil -} - -func (m *memoryBuffer) EndOfInput() { - go func() { - m.cond.L.Lock() - defer m.cond.L.Unlock() - - m.endOfInput = true - m.cond.Broadcast() - - for m.bytes > 0 && !m.closed { - m.cond.Wait() - } - m.closed = true - m.cond.Broadcast() - }() -} - -func (m *memoryBuffer) Close(ctx context.Context) error { - m.cond.L.Lock() - m.closed = true - m.cond.Broadcast() - m.cond.L.Unlock() - return nil -} diff --git a/internal/impl/pure/buffer_memory_test.go b/internal/impl/pure/buffer_memory_test.go deleted file mode 100644 index ce4f6468b0..0000000000 --- a/internal/impl/pure/buffer_memory_test.go +++ /dev/null @@ -1,478 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "math/rand" - "sync" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/public/service" -) - -func msgEqual(t testing.TB, expected string, m *service.Message) { - t.Helper() - - mBytes, err := m.AsBytes() - require.NoError(t, err) - - assert.Equal(t, expected, string(mBytes)) -} - -func memBufFromConf(t *testing.T, conf string) *memoryBuffer { - t.Helper() - - parsedConf, err := memoryBufferConfig().ParseYAML(conf, nil) - require.NoError(t, err) - - buf, err := newMemoryBufferFromConfig(parsedConf, service.MockResources()) - require.NoError(t, err) - - return buf -} - -func TestMemoryBasic(t *testing.T) { - n := 100 - - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 100000 -`) - defer block.Close(ctx) - - for i := 0; i < n; i++ { - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello")), - service.NewMessage([]byte("world")), - service.NewMessage([]byte("12345")), - service.NewMessage([]byte(fmt.Sprintf("test%v", i))), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - } - - for i := 0; i < n; i++ { - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 4) - msgEqual(t, fmt.Sprintf("test%v", i), m[3]) - require.NoError(t, ackFunc(ctx, nil)) - } -} - -func TestMemoryOwnership(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 100000 -`) - defer block.Close(ctx) - - inMsg := service.NewMessage(nil) - inMsg.SetStructuredMut(map[string]any{ - "hello": "world", - }) - - require.NoError(t, block.WriteBatch(ctx, service.MessageBatch{inMsg}, func(ctx context.Context, _ error) error { - inStruct, err := inMsg.AsStructuredMut() - require.NoError(t, err) - _, err = gabs.Wrap(inStruct).Set("quack", "moo") - require.NoError(t, err) - return nil - })) - - outBatch, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, outBatch, 1) - - outStruct, err := outBatch[0].AsStructuredMut() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - }, outStruct) - - require.NoError(t, ackFunc(ctx, nil)) - - _, err = gabs.Wrap(outStruct).Set("woof", "meow") - require.NoError(t, err) - - inStruct, err := inMsg.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - "moo": "quack", - }, inStruct) -} - -func TestMemoryNearLimit(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 2285 -`) - defer block.Close(ctx) - - n, iter := 50, 5 - - for j := 0; j < iter; j++ { - for i := 0; i < n; i++ { - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello")), - service.NewMessage([]byte("world")), - service.NewMessage([]byte("12345")), - service.NewMessage([]byte(fmt.Sprintf("test%v", i))), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - } - - for i := 0; i < n; i++ { - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 4) - msgEqual(t, fmt.Sprintf("test%v", i), m[3]) - require.NoError(t, ackFunc(ctx, nil)) - } - } -} - -func TestMemoryLoopingRandom(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 8000 -`) - defer block.Close(ctx) - - n, iter := 50, 5 - - for j := 0; j < iter; j++ { - for i := 0; i < n; i++ { - b := make([]byte, rand.Int()%100) - for k := range b { - b[k] = '0' - } - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage(b), - service.NewMessage([]byte(fmt.Sprintf("test%v", i))), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - } - - for i := 0; i < n; i++ { - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 2) - msgEqual(t, fmt.Sprintf("test%v", i), m[1]) - require.NoError(t, ackFunc(ctx, nil)) - } - } -} - -func TestMemoryLockStep(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 1000 -`) - defer block.Close(ctx) - - n := 10000 - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - for i := 0; i < n; i++ { - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 4) - msgEqual(t, fmt.Sprintf("test%v", i), m[3]) - require.NoError(t, ackFunc(ctx, nil)) - } - }() - - go func() { - for i := 0; i < n; i++ { - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello")), - service.NewMessage([]byte("world")), - service.NewMessage([]byte("12345")), - service.NewMessage([]byte(fmt.Sprintf("test%v", i))), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - } - }() - - wg.Wait() -} - -func TestMemoryAck(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 1000 -`) - defer block.Close(ctx) - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("2")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "1", m[0]) - - require.NoError(t, ackFunc(ctx, errors.New("nope"))) - - m, ackFunc, err = block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "1", m[0]) - - require.NoError(t, ackFunc(ctx, nil)) - - m, ackFunc, err = block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "2", m[0]) - - require.NoError(t, ackFunc(ctx, nil)) - - block.EndOfInput() - - _, _, err = block.ReadBatch(ctx) - require.Error(t, err) - assert.Equal(t, service.ErrEndOfBuffer, err) -} - -func TestMemoryCloseWithPending(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 1000 -`) - defer block.Close(ctx) - - for i := 0; i < 10; i++ { - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello world")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - } - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - block.EndOfInput() - wg.Done() - }() - - <-time.After(time.Millisecond * 100) - for i := 0; i < 10; i++ { - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello world", m[0]) - require.NoError(t, ackFunc(ctx, nil)) - } - - _, _, err := block.ReadBatch(ctx) - require.Error(t, err) - assert.Equal(t, service.ErrEndOfBuffer, err) - - wg.Wait() -} - -func TestMemoryRejectLargeMessage(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 10 -`) - defer block.Close(ctx) - - err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello world this message is too long!")), - }, func(ctx context.Context, err error) error { return nil }) - require.Error(t, err) - assert.Equal(t, component.ErrMessageTooLarge, err) - - require.NoError(t, block.Close(ctx)) -} - -func TestMemoryBatched(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 100000 -batch_policy: - enabled: true - count: 3 - processors: - - archive: - format: lines -`) - defer block.Close(ctx) - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("world1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("meow1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello1\nworld1\nmeow1", m[0]) - require.NoError(t, ackFunc(ctx, nil)) - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello2")), - service.NewMessage([]byte("world2")), - service.NewMessage([]byte("meow2")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - m, ackFunc, err = block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello2\nworld2\nmeow2", m[0]) - require.NoError(t, ackFunc(ctx, nil)) -} - -func TestMemoryBatchedNack(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 100000 -batch_policy: - enabled: true - count: 3 - processors: - - archive: - format: lines -`) - defer block.Close(ctx) - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("world1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("meow1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello1\nworld1\nmeow1", m[0]) - - require.NoError(t, ackFunc(ctx, errors.New("nope"))) - - m, ackFunc, err = block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello1\nworld1\nmeow1", m[0]) - - require.NoError(t, ackFunc(ctx, nil)) -} - -func TestMemoryBatchedEarlyTerm(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 100000 -batch_policy: - enabled: true - count: 3 - processors: - - archive: - format: lines -`) - defer block.Close(ctx) - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("world1")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - - block.EndOfInput() - - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello1\nworld1", m[0]) - require.NoError(t, ackFunc(ctx, nil)) - - _, _, err = block.ReadBatch(ctx) - assert.Equal(t, service.ErrEndOfBuffer, err) -} - -func TestMemoryBatchedTimed(t *testing.T) { - ctx := context.Background() - block := memBufFromConf(t, ` -limit: 100000 -batch_policy: - enabled: true - count: 2 - period: 50ms -`) - defer block.Close(ctx) - - go func() { - <-time.After(time.Millisecond * 500) - if err := block.WriteBatch(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello")), - }, func(ctx context.Context, err error) error { return nil }); err != nil { - t.Error(err) - } - }() - - m, ackFunc, err := block.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, m, 1) - msgEqual(t, "hello", m[0]) - require.NoError(t, ackFunc(ctx, nil)) -} diff --git a/internal/impl/pure/buffer_none.go b/internal/impl/pure/buffer_none.go deleted file mode 100644 index 10cc04fa07..0000000000 --- a/internal/impl/pure/buffer_none.go +++ /dev/null @@ -1,25 +0,0 @@ -package pure - -import ( - "errors" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchBuffer( - "none", - service.NewConfigSpec(). - Stable(). - Summary(`Do not buffer messages. This is the default and most resilient configuration.`). - Description(`Selecting no buffer means the output layer is directly coupled with the input layer. This is the safest and lowest latency option since acknowledgements from at-least-once protocols can be propagated all the way from the output protocol to the input protocol. - -If the output layer is hit with back pressure it will propagate all the way to the input layer, and further up the data stream. If you need to relieve your pipeline of this back pressure consider using a more robust buffering solution such as Kafka before resorting to alternatives.`). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return nil, errors.New("not implemented") - }) - if err != nil { - panic(err) - } -} diff --git a/internal/impl/pure/buffer_system_window.go b/internal/impl/pure/buffer_system_window.go deleted file mode 100644 index f9e1504257..0000000000 --- a/internal/impl/pure/buffer_system_window.go +++ /dev/null @@ -1,458 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func tumblingWindowBufferConfig() *service.ConfigSpec { - return service.NewConfigSpec(). - Beta(). - Version("3.53.0"). - Categories("Windowing"). - Summary("Chops a stream of messages into tumbling or sliding windows of fixed temporal size, following the system clock."). - Description(` -A window is a grouping of messages that fit within a discrete measure of time following the system clock. Messages are allocated to a window either by the processing time (the time at which they're ingested) or by the event time, and this is controlled via the `+"<>"+`. - -In tumbling mode (default) the beginning of a window immediately follows the end of a prior window. When the buffer is initialized the first window to be created and populated is aligned against the zeroth minute of the zeroth hour of the day by default, and may therefore be open for a shorter period than the specified size. - -A window is flushed only once the system clock surpasses its scheduled end. If an `+"<>"+` is specified then the window will not be flushed until the scheduled end plus that length of time. - -When a message is added to a window it has a metadata field `+"`window_end_timestamp`"+` added to it containing the timestamp of the end of the window as an RFC3339 string. - -== Sliding windows - -Sliding windows begin from an offset of the prior windows' beginning rather than its end, and therefore messages may belong to multiple windows. In order to produce sliding windows specify a `+"<>"+`. - -== Back pressure - -If back pressure is applied to this buffer either due to output services being unavailable or resources being saturated, windows older than the current and last according to the system clock will be dropped in order to prevent unbounded resource usage. This means you should ensure that under the worst case scenario you have enough system memory to store two windows' worth of data at a given time (plus extra for redundancy and other services). - -If messages could potentially arrive with event timestamps in the future (according to the system clock) then you should also factor in these extra messages in memory usage estimates. - -== Delivery guarantees - -This buffer honours the transaction model within Benthos in order to ensure that messages are not acknowledged until they are either intentionally dropped or successfully delivered to outputs. However, since messages belonging to an expired window are intentionally dropped there are circumstances where not all messages entering the system will be delivered. - -When this buffer is configured with a slide duration it is possible for messages to belong to multiple windows, and therefore be delivered multiple times. In this case the first time the message is delivered it will be acked (or nacked) and subsequent deliveries of the same message will be a "best attempt". - -During graceful termination if the current window is partially populated with messages they will be nacked such that they are re-consumed the next time the service starts. -`). - Field(service.NewBloblangField("timestamp_mapping"). - Description(` -A xref:guides:bloblang/about.adoc[Bloblang mapping] applied to each message during ingestion that provides the timestamp to use for allocating it a window. By default the function `+"`now()`"+` is used in order to generate a fresh timestamp at the time of ingestion (the processing time), whereas this mapping can instead extract a timestamp from the message itself (the event time). - -The timestamp value assigned to `+"`root`"+` must either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in ISO 8601 format. If the mapping fails or provides an invalid result the message will be dropped (with logging to describe the problem). -`). - Default("root = now()"). - Example("root = this.created_at").Example(`root = meta("kafka_timestamp_unix").number()`)). - Field(service.NewStringField("size"). - Description("A duration string describing the size of each window. By default windows are aligned to the zeroth minute and zeroth hour on the UTC clock, meaning windows of 1 hour duration will match the turn of each hour in the day, this can be adjusted with the `offset` field."). - Example("30s").Example("10m")). - Field(service.NewStringField("slide"). - Description("An optional duration string describing by how much time the beginning of each window should be offset from the beginning of the previous, and therefore creates sliding windows instead of tumbling. When specified this duration must be smaller than the `size` of the window."). - Default(""). - Example("30s").Example("10m")). - Field(service.NewStringField("offset"). - Description("An optional duration string to offset the beginning of each window by, otherwise they are aligned to the zeroth minute and zeroth hour on the UTC clock. The offset cannot be a larger or equal measure to the window size or the slide."). - Default(""). - Example("-6h").Example("30m")). - Field(service.NewStringField("allowed_lateness"). - Description("An optional duration string describing the length of time to wait after a window has ended before flushing it, allowing late arrivals to be included. Since this windowing buffer uses the system clock an allowed lateness can improve the matching of messages when using event time."). - Default(""). - Example("10s").Example("1m")). - Example("Counting Passengers at Traffic", `Given a stream of messages relating to cars passing through various traffic lights of the form: - -`+"```json"+` -{ - "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", - "created_at": "2021-08-07T09:49:35Z", - "registration_plate": "AB1C DEF", - "passengers": 3 -} -`+"```"+` - -We can use a window buffer in order to create periodic messages summarizing the traffic for a period of time of this form: - -`+"```json"+` -{ - "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", - "created_at": "2021-08-07T10:00:00Z", - "total_cars": 15, - "passengers": 43 -} -`+"```"+` - -With the following config:`, - ` -buffer: - system_window: - timestamp_mapping: root = this.created_at - size: 1h - -pipeline: - processors: - # Group messages of the window into batches of common traffic light IDs - - group_by_value: - value: '${! json("traffic_light") }' - - # Reduce each batch to a single message by deleting indexes > 0, and - # aggregate the car and passenger counts. - - mapping: | - root = if batch_index() == 0 { - { - "traffic_light": this.traffic_light, - "created_at": meta("window_end_timestamp"), - "total_cars": json("registration_plate").from_all().unique().length(), - "passengers": json("passengers").from_all().sum(), - } - } else { deleted() } -`, - ) -} - -func getDuration(conf *service.ParsedConfig, required bool, name string) (time.Duration, error) { - periodStr, err := conf.FieldString(name) - if err != nil { - return 0, err - } - if !required && periodStr == "" { - return 0, nil - } - period, err := time.ParseDuration(periodStr) - if err != nil { - return 0, fmt.Errorf("failed to parse field '%v' as duration: %w", name, err) - } - return period, nil -} - -func init() { - err := service.RegisterBatchBuffer( - "system_window", tumblingWindowBufferConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - size, err := getDuration(conf, true, "size") - if err != nil { - return nil, err - } - slide, err := getDuration(conf, false, "slide") - if err != nil { - return nil, err - } - if slide >= size { - return nil, fmt.Errorf("invalid window slide '%v' must be lower than the size '%v'", slide, size) - } - offset, err := getDuration(conf, false, "offset") - if err != nil { - return nil, err - } - if offset >= size { - return nil, fmt.Errorf("invalid offset '%v' must be lower than the size '%v'", offset, size) - } - if slide > 0 && offset >= slide { - return nil, fmt.Errorf("invalid offset '%v' must be lower than the slide '%v'", offset, slide) - } - allowedLateness, err := getDuration(conf, false, "allowed_lateness") - if err != nil { - return nil, err - } - if allowedLateness >= size { - return nil, fmt.Errorf("invalid allowed_lateness '%v' must be lower than the size '%v'", allowedLateness, size) - } - tsMapping, err := conf.FieldBloblang("timestamp_mapping") - if err != nil { - return nil, err - } - return newSystemWindowBuffer(tsMapping, func() time.Time { - return time.Now().UTC() - }, size, slide, offset, allowedLateness, mgr.Logger()) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type tsMessage struct { - ts time.Time - m *service.Message - ackFn service.AckFunc -} - -type utcNowProvider func() time.Time - -type systemWindowBuffer struct { - logger *service.Logger - - tsMapping *bloblang.Executor - clock utcNowProvider - size, slide, offset, allowedLateness time.Duration - - latestFlushedWindowEnd time.Time - oldestTS time.Time - pending []*tsMessage - pendingMut sync.Mutex - - closedTimerChan <-chan time.Time - - endOfInputChan chan struct{} - closeEndOfInputOnce sync.Once -} - -func newSystemWindowBuffer( - tsMapping *bloblang.Executor, - clock utcNowProvider, - size, slide, offset, allowedLateness time.Duration, - logger *service.Logger, -) (*systemWindowBuffer, error) { - w := &systemWindowBuffer{ - tsMapping: tsMapping, - clock: clock, - size: size, - slide: slide, - allowedLateness: allowedLateness, - offset: offset, - logger: logger, - oldestTS: clock(), - endOfInputChan: make(chan struct{}), - } - - tmpTimerChan := make(chan time.Time) - close(tmpTimerChan) - w.closedTimerChan = tmpTimerChan - return w, nil -} - -func (w *systemWindowBuffer) nextSystemWindow() (prevStart, prevEnd, start, end time.Time) { - now := w.clock() - - windowEpoch := w.size - if w.slide > 0 { - windowEpoch = w.slide - } - - // The start is now, rounded by our window epoch to the UTC clock, and with - // our offset (plus one to avoid overlapping with the previous window) - // added. - // - // If the result is after now then we rounded upwards, so we roll it back by - // the window epoch. - if start = w.clock().Round(windowEpoch).Add(1 + w.offset); start.After(now) { - start = start.Add(-windowEpoch) - } - - // The result is the start of the newest active window. In the case of - // sliding windows this is not the "next" window to be flushed, so we need - // to roll back further. - if w.slide > 0 { - start = start.Add(w.slide - w.size) - } - - // The end is our start plus the window size (minus the nanosecond added to - // the start). - end = start.Add(w.size - 1) - - // Calculate the previous window as well - prevStart, prevEnd = start.Add(-windowEpoch), end.Add(-windowEpoch) - return -} - -func (w *systemWindowBuffer) getTimestamp(i int, batch service.MessageBatch) (ts time.Time, err error) { - var tsValueMsg *service.Message - if tsValueMsg, err = batch.BloblangQuery(i, w.tsMapping); err != nil { - w.logger.Errorf("Timestamp mapping failed for message: %v", err) - err = fmt.Errorf("timestamp mapping failed: %w", err) - return - } - - var tsValue any - if tsValue, err = tsValueMsg.AsStructured(); err != nil { - if tsBytes, _ := tsValueMsg.AsBytes(); len(tsBytes) > 0 { - tsValue = string(tsBytes) - err = nil - } - } - if err != nil { - w.logger.Errorf("Timestamp mapping failed for message: unable to parse result as structured value: %v", err) - err = fmt.Errorf("unable to parse result of timestamp mapping as structured value: %w", err) - return - } - - if ts, err = value.IGetTimestamp(tsValue); err != nil { - w.logger.Errorf("Timestamp mapping failed for message: %v", err) - err = fmt.Errorf("unable to parse result of timestamp mapping as timestamp: %w", err) - } - return -} - -func (w *systemWindowBuffer) WriteBatch(ctx context.Context, msgBatch service.MessageBatch, aFn service.AckFunc) error { - w.pendingMut.Lock() - defer w.pendingMut.Unlock() - - // If our output is blocked and therefore we haven't flushed more than the - // last two windows we purge messages that wouldn't fit within them. - prevStart, _, _, _ := w.nextSystemWindow() - if w.latestFlushedWindowEnd.Before(prevStart) && w.oldestTS.Before(prevStart) { - newOldestTS := w.clock() - newPending := make([]*tsMessage, 0, len(w.pending)) - for _, pending := range w.pending { - if pending.ts.Before(prevStart) { - // Reject messages too old to fit into a window by acknowledging - // them. - _ = pending.ackFn(ctx, nil) - continue - } - newPending = append(newPending, pending) - if pending.ts.Before(newOldestTS) { - newOldestTS = pending.ts - } - } - w.pending = newPending - } - - messageAdded := false - aggregatedAck := batch.NewCombinedAcker(batch.AckFunc(aFn)) - - // And now add new messages. - for i, msg := range msgBatch { - ts, err := w.getTimestamp(i, msgBatch) - if err != nil { - return err - } - - // Don't add messages older than our current window start. - if !ts.After(w.latestFlushedWindowEnd) { //nolint: gocritic - continue - } - - messageAdded = true - w.pending = append(w.pending, &tsMessage{ - ts: ts, m: msg, ackFn: service.AckFunc(aggregatedAck.Derive()), - }) - if ts.Before(w.oldestTS) { - w.oldestTS = ts - } - } - - if !messageAdded { - // If none of the messages have fit into a window we reject them by - // acknowledging the batch. - _ = aFn(ctx, nil) - } - return nil -} - -func (w *systemWindowBuffer) flushWindow(ctx context.Context, start, end time.Time) (service.MessageBatch, service.AckFunc, error) { - w.pendingMut.Lock() - defer w.pendingMut.Unlock() - - // Calculate the next start and purge everything older as we flush. - nextStart := start.Add(w.size) - if w.slide > 0 { - nextStart = start.Add(w.slide) - } - - var flushBatch service.MessageBatch - var flushAcks []service.AckFunc - - newPending := make([]*tsMessage, 0, len(w.pending)) - newOldest := w.clock() - for _, pending := range w.pending { - flush := !pending.ts.Before(start) && !pending.ts.After(end) //nolint: gocritic - preserve := !pending.ts.Before(nextStart) //nolint: gocritic - - if flush { - tmpMsg := pending.m.Copy() - tmpMsg.MetaSet("window_end_timestamp", end.Format(time.RFC3339Nano)) - flushBatch = append(flushBatch, tmpMsg) - flushAcks = append(flushAcks, pending.ackFn) - } - if preserve { - if pending.ts.Before(newOldest) { - newOldest = pending.ts - } - newPending = append(newPending, pending) - } - if !flush && !preserve { - _ = pending.ackFn(ctx, nil) - } - } - - w.pending = newPending - w.latestFlushedWindowEnd = end - w.oldestTS = newOldest - - return flushBatch, func(ctx context.Context, err error) error { - for _, aFn := range flushAcks { - _ = aFn(ctx, err) - } - return nil - }, nil -} - -var errWindowClosed = errors.New("message rejected as window did not complete") - -func (w *systemWindowBuffer) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - prevStart, prevEnd, nextStart, nextEnd := w.nextSystemWindow() - - // We haven't been read since the previous window ended, so create that one - // instead in an attempt to back fill. - // - // Note that we do not need to lock around latestFlushWindowEnd because it's - // only written from the reader, and we only expect one active reader at a - // given time. If this assumption changes we would need to lock around this - // also. - if w.latestFlushedWindowEnd.Before(prevStart) { - if msgBatch, aFn, err := w.flushWindow(ctx, prevStart, prevEnd); len(msgBatch) > 0 || err != nil { - return msgBatch, aFn, err - } - } - - for { - nextEndChan := w.closedTimerChan - if waitFor := nextEnd.Sub(w.clock()) + w.allowedLateness; waitFor > 0 { - nextEndChan = time.After(waitFor) - } - - select { - case <-nextEndChan: - case <-ctx.Done(): - return nil, nil, ctx.Err() - case <-w.endOfInputChan: - // Nack all pending messages so that we re-consume them on the next - // start up. TODO: Eventually allow users to customize this as they - // may wish to flush partial windows instead. - w.pendingMut.Lock() - for _, pending := range w.pending { - _ = pending.ackFn(ctx, errWindowClosed) - } - w.pending = nil - w.pendingMut.Unlock() - return nil, nil, service.ErrEndOfBuffer - } - if msgBatch, aFn, err := w.flushWindow(ctx, nextStart, nextEnd); len(msgBatch) > 0 || err != nil { - return msgBatch, aFn, err - } - - // Window did not contain any messages, so move onto next. - _, _, nextStart, nextEnd = w.nextSystemWindow() - } -} - -func (w *systemWindowBuffer) EndOfInput() { - w.closeEndOfInputOnce.Do(func() { - close(w.endOfInputChan) - }) -} - -func (w *systemWindowBuffer) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/buffer_system_window_test.go b/internal/impl/pure/buffer_system_window_test.go deleted file mode 100644 index f3b109dd79..0000000000 --- a/internal/impl/pure/buffer_system_window_test.go +++ /dev/null @@ -1,646 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "strconv" - "sync" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestSystemWindowBufferConfigs(t *testing.T) { - tests := []struct { - config string - lintErrContains string - buildErrContains string - }{ - { - config: ` -system_window: - size: 60m -`, - }, - { - config: ` -system_window: {} -`, - lintErrContains: "field size is required", - }, - { - config: ` -system_window: - timestamp_mapping: 'root =' - size: 60m -`, - lintErrContains: "expected whitespace", - }, - { - config: ` -system_window: - size: 60m - slide: 5m - offset: 1m - allowed_lateness: 2m -`, - }, - { - config: ` -system_window: - size: 60m - slide: 120m - offset: 1m - allowed_lateness: 2m -`, - buildErrContains: "invalid window slide", - }, - { - config: ` -system_window: - size: 60m - offset: 60m - allowed_lateness: 2m -`, - buildErrContains: "invalid offset", - }, - { - config: ` -system_window: - size: 60m - slide: 10m - offset: 10m - allowed_lateness: 2m -`, - buildErrContains: "invalid offset", - }, - { - config: ` -system_window: - size: 60m - slide: 10m - allowed_lateness: 200m -`, - buildErrContains: "invalid allowed_lateness", - }, - } - - for i, test := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - env := service.NewStreamBuilder() - require.NoError(t, env.SetLoggerYAML(`level: OFF`)) - err := env.AddConsumerFunc(func(context.Context, *service.Message) error { - return nil - }) - require.NoError(t, err) - _, err = env.AddProducerFunc() - require.NoError(t, err) - - err = env.SetBufferYAML(test.config) - if test.lintErrContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.lintErrContains) - return - } - require.NoError(t, err) - - strm, err := env.Build() - require.NoError(t, err) - - cancelledCtx, done := context.WithCancel(context.Background()) - done() - err = strm.Run(cancelledCtx) - if test.buildErrContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.buildErrContains) - return - } - require.EqualError(t, err, "context canceled") - require.NoError(t, strm.StopWithin(time.Second)) - }) - } -} - -func TestSystemCurrentWindowCalc(t *testing.T) { - tests := []struct { - now string - size, slide, offset time.Duration - prevStart, prevEnd, start, end string - }{ - { - now: `2006-01-02T15:00:00Z`, - size: time.Hour, - start: `2006-01-02T14:00:00.000000001Z`, - end: `2006-01-02T15:00:00Z`, - prevStart: `2006-01-02T13:00:00.000000001Z`, - prevEnd: `2006-01-02T14:00:00Z`, - }, - { - now: `2006-01-02T15:00:00.000000001Z`, - size: time.Hour, - start: `2006-01-02T15:00:00.000000001Z`, - end: `2006-01-02T16:00:00Z`, - prevStart: `2006-01-02T14:00:00.000000001Z`, - prevEnd: `2006-01-02T15:00:00Z`, - }, - { - now: `2006-01-02T15:04:05.123456789Z`, - size: time.Hour, - start: `2006-01-02T15:00:00.000000001Z`, - end: `2006-01-02T16:00:00Z`, - prevStart: `2006-01-02T14:00:00.000000001Z`, - prevEnd: `2006-01-02T15:00:00Z`, - }, - { - now: `2006-01-02T15:34:05.123456789Z`, - size: time.Hour, - start: `2006-01-02T15:00:00.000000001Z`, - end: `2006-01-02T16:00:00Z`, - prevStart: `2006-01-02T14:00:00.000000001Z`, - prevEnd: `2006-01-02T15:00:00Z`, - }, - { - now: `2006-01-02T00:04:05.123456789Z`, - size: time.Hour, - start: `2006-01-02T00:00:00.000000001Z`, - end: `2006-01-02T01:00:00Z`, - prevStart: `2006-01-01T23:00:00.000000001Z`, - prevEnd: `2006-01-02T00:00:00Z`, - }, - { - now: `2006-01-02T15:04:05.123456789Z`, - size: time.Hour, - slide: time.Minute * 10, - start: `2006-01-02T14:10:00.000000001Z`, - end: `2006-01-02T15:10:00Z`, - prevStart: `2006-01-02T14:00:00.000000001Z`, - prevEnd: `2006-01-02T15:00:00Z`, - }, - { - now: `2006-01-02T15:04:05.123456789Z`, - size: time.Hour, - offset: time.Minute * 30, - start: `2006-01-02T14:30:00.000000001Z`, - end: `2006-01-02T15:30:00Z`, - prevStart: `2006-01-02T13:30:00.000000001Z`, - prevEnd: `2006-01-02T14:30:00Z`, - }, - { - now: `2006-01-02T15:04:05.123456789Z`, - size: time.Hour, - slide: time.Minute * 10, - offset: time.Minute * 5, - start: `2006-01-02T14:05:00.000000001Z`, - end: `2006-01-02T15:05:00Z`, - prevStart: `2006-01-02T13:55:00.000000001Z`, - prevEnd: `2006-01-02T14:55:00Z`, - }, - { - now: `2006-01-02T15:09:59.123456789Z`, - size: time.Hour, - slide: time.Minute * 10, - offset: time.Minute * 5, - start: `2006-01-02T14:15:00.000000001Z`, - end: `2006-01-02T15:15:00Z`, - prevStart: `2006-01-02T14:05:00.000000001Z`, - prevEnd: `2006-01-02T15:05:00Z`, - }, - } - - for i, test := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - w, err := newSystemWindowBuffer(nil, func() time.Time { - ts, err := time.Parse(time.RFC3339Nano, test.now) - require.NoError(t, err) - return ts.UTC() - }, test.size, test.slide, test.offset, 0, nil) - require.NoError(t, err) - - prevStart, prevEnd, start, end := w.nextSystemWindow() - - assert.Equal(t, test.start, start.Format(time.RFC3339Nano), "start") - assert.Equal(t, test.end, end.Format(time.RFC3339Nano), "end") - assert.Equal(t, test.prevStart, prevStart.Format(time.RFC3339Nano), "prevStart") - assert.Equal(t, test.prevEnd, prevEnd.Format(time.RFC3339Nano), "prevEnd") - }) - } -} - -func noopAck(context.Context, error) error { - return nil -} - -func TestSystemWindowWritePurge(t *testing.T) { - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 1).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, 0, 0, 0, nil) - require.NoError(t, err) - - err = w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"1","ts":7.999999999}`)), - service.NewMessage([]byte(`{"id":"2","ts":8.5}`)), - service.NewMessage([]byte(`{"id":"3","ts":9.5}`)), - service.NewMessage([]byte(`{"id":"4","ts":10.5}`)), - }, noopAck) - require.NoError(t, err) - assert.Len(t, w.pending, 4) - - err = w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"5","ts":10.6}`)), - service.NewMessage([]byte(`{"id":"6","ts":10.7}`)), - service.NewMessage([]byte(`{"id":"7","ts":10.8}`)), - service.NewMessage([]byte(`{"id":"8","ts":10.9}`)), - }, noopAck) - require.NoError(t, err) - assert.Len(t, w.pending, 6) -} - -func TestSystemWindowCreation(t *testing.T) { - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 1).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, 0, 0, 0, nil) - require.NoError(t, err) - - err = w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"1","ts":7.999999999}`)), - service.NewMessage([]byte(`{"id":"2","ts":8.5}`)), - service.NewMessage([]byte(`{"id":"3","ts":9.5}`)), - service.NewMessage([]byte(`{"id":"4","ts":10.5}`)), - }, noopAck) - require.NoError(t, err) - assert.Len(t, w.pending, 4) - - resBatch, _, err := w.ReadBatch(context.Background()) - require.NoError(t, err) - - require.Len(t, resBatch, 1) - msgBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"3","ts":9.5}`, string(msgBytes)) - - assert.Len(t, w.pending, 1) - assert.Equal(t, "1970-01-01T00:00:10Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano)) - - currentTS = time.Unix(10, 999999100).UTC() - - resBatch, _, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - - require.Len(t, resBatch, 1) - msgBytes, err = resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"4","ts":10.5}`, string(msgBytes)) - - assert.Empty(t, w.pending) - assert.Equal(t, "1970-01-01T00:00:11Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano)) - - currentTS = time.Unix(11, 999999100).UTC() - - smallWaitCtx, done := context.WithTimeout(context.Background(), time.Millisecond*50) - resBatch, _, err = w.ReadBatch(smallWaitCtx) - done() - require.Error(t, err) - assert.Empty(t, resBatch) - assert.Equal(t, "1970-01-01T00:00:12Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano)) - - err = w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"5","ts":8.1}`)), - service.NewMessage([]byte(`{"id":"6","ts":9.999999999}`)), - service.NewMessage([]byte(`{"id":"7","ts":10}`)), - service.NewMessage([]byte(`{"id":"8","ts":11.999999999}`)), - service.NewMessage([]byte(`{"id":"9","ts":12.1}`)), - service.NewMessage([]byte(`{"id":"10","ts":13}`)), - }, noopAck) - require.NoError(t, err) - require.Len(t, w.pending, 2) - - msgBytes, err = w.pending[0].m.AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"9","ts":12.1}`, string(msgBytes)) - - msgBytes, err = w.pending[1].m.AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"10","ts":13}`, string(msgBytes)) -} - -func TestSystemWindowCreationSliding(t *testing.T) { - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 0).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, time.Millisecond*500, 0, 0, nil) - require.NoError(t, err) - w.latestFlushedWindowEnd = time.Unix(9, 500_000_000) - - err = w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"1","ts":9.85}`)), - service.NewMessage([]byte(`{"id":"2","ts":9.9}`)), - service.NewMessage([]byte(`{"id":"3","ts":10.15}`)), - service.NewMessage([]byte(`{"id":"4","ts":10.3}`)), - service.NewMessage([]byte(`{"id":"5","ts":10.5}`)), - service.NewMessage([]byte(`{"id":"6","ts":10.7}`)), - service.NewMessage([]byte(`{"id":"7","ts":10.9}`)), - service.NewMessage([]byte(`{"id":"8","ts":11.1}`)), - service.NewMessage([]byte(`{"id":"9","ts":11.35}`)), - service.NewMessage([]byte(`{"id":"10","ts":11.52}`)), - service.NewMessage([]byte(`{"id":"11","ts":11.8}`)), - }, noopAck) - require.NoError(t, err) - assert.Len(t, w.pending, 11) - - assertBatchIndex := func(i int, batch service.MessageBatch, exp string) { - t.Helper() - require.Greater(t, len(batch), i) - msgBytes, err := batch[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(msgBytes)) - } - - resBatch, _, err := w.ReadBatch(context.Background()) - require.NoError(t, err) - - assert.Len(t, resBatch, 2) - assertBatchIndex(0, resBatch, `{"id":"1","ts":9.85}`) - assertBatchIndex(1, resBatch, `{"id":"2","ts":9.9}`) - - currentTS = time.Unix(10, 500000000).UTC() - resBatch, _, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - - assert.Len(t, resBatch, 5) - assertBatchIndex(0, resBatch, `{"id":"1","ts":9.85}`) - assertBatchIndex(1, resBatch, `{"id":"2","ts":9.9}`) - assertBatchIndex(2, resBatch, `{"id":"3","ts":10.15}`) - assertBatchIndex(3, resBatch, `{"id":"4","ts":10.3}`) - assertBatchIndex(4, resBatch, `{"id":"5","ts":10.5}`) - - currentTS = time.Unix(11, 0).UTC() - resBatch, _, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - - assert.Len(t, resBatch, 5) - assertBatchIndex(0, resBatch, `{"id":"3","ts":10.15}`) - assertBatchIndex(1, resBatch, `{"id":"4","ts":10.3}`) - assertBatchIndex(2, resBatch, `{"id":"5","ts":10.5}`) - assertBatchIndex(3, resBatch, `{"id":"6","ts":10.7}`) - assertBatchIndex(4, resBatch, `{"id":"7","ts":10.9}`) - - currentTS = time.Unix(11, 500_000_000).UTC() - resBatch, _, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - - assert.Len(t, resBatch, 4) - assertBatchIndex(0, resBatch, `{"id":"6","ts":10.7}`) - assertBatchIndex(1, resBatch, `{"id":"7","ts":10.9}`) - assertBatchIndex(2, resBatch, `{"id":"8","ts":11.1}`) - assertBatchIndex(3, resBatch, `{"id":"9","ts":11.35}`) - - currentTS = time.Unix(12, 0).UTC() - resBatch, _, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - - assert.Len(t, resBatch, 4) - assertBatchIndex(0, resBatch, `{"id":"8","ts":11.1}`) - assertBatchIndex(1, resBatch, `{"id":"9","ts":11.35}`) - assertBatchIndex(2, resBatch, `{"id":"10","ts":11.52}`) - assertBatchIndex(3, resBatch, `{"id":"11","ts":11.8}`) -} - -func TestSystemWindowAckOneToMany(t *testing.T) { - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 1).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, 0, 0, 0, nil) - require.NoError(t, err) - - var ackCalled int - var ackErr error - - require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"1","ts":9.5}`)), - service.NewMessage([]byte(`{"id":"2","ts":10.5}`)), - service.NewMessage([]byte(`{"id":"3","ts":11.5}`)), - }, func(ctx context.Context, err error) error { - ackCalled++ - if err != nil { - ackErr = err - } - return nil - })) - - ackFuncs := []service.AckFunc{} - - resBatch, aFn, err := w.ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, resBatch, 1) - msgBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"1","ts":9.5}`, string(msgBytes)) - ackFuncs = append(ackFuncs, aFn) - - currentTS = time.Unix(11, 0).UTC() - - resBatch, aFn, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, resBatch, 1) - msgBytes, err = resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"2","ts":10.5}`, string(msgBytes)) - ackFuncs = append(ackFuncs, aFn) - - currentTS = time.Unix(12, 0).UTC() - - resBatch, aFn, err = w.ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, resBatch, 1) - msgBytes, err = resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"3","ts":11.5}`, string(msgBytes)) - ackFuncs = append(ackFuncs, aFn) - - require.Len(t, ackFuncs, 3) - assert.Equal(t, 0, ackCalled) - assert.NoError(t, ackErr) - - require.NoError(t, ackFuncs[0](context.Background(), nil)) - assert.Equal(t, 0, ackCalled) - assert.NoError(t, ackErr) - - require.NoError(t, ackFuncs[1](context.Background(), errors.New("custom error"))) - assert.Equal(t, 0, ackCalled) - assert.NoError(t, ackErr) - - require.NoError(t, ackFuncs[2](context.Background(), nil)) - assert.Equal(t, 1, ackCalled) - assert.EqualError(t, ackErr, "custom error") -} - -func TestSystemWindowAckManyToOne(t *testing.T) { - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 1).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, 0, 0, 0, nil) - require.NoError(t, err) - - ackCalls := map[int]error{} - - require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"1","ts":9.5}`)), - }, func(ctx context.Context, err error) error { - ackCalls[0] = err - return nil - })) - - require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"2","ts":9.6}`)), - }, func(ctx context.Context, err error) error { - ackCalls[1] = err - return nil - })) - - require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(`{"id":"3","ts":9.7}`)), - }, func(ctx context.Context, err error) error { - ackCalls[2] = err - return nil - })) - - resBatch, aFn, err := w.ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, resBatch, 3) - - msgBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"1","ts":9.5}`, string(msgBytes)) - - msgBytes, err = resBatch[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"2","ts":9.6}`, string(msgBytes)) - - msgBytes, err = resBatch[2].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"id":"3","ts":9.7}`, string(msgBytes)) - - assert.Empty(t, ackCalls) - require.NoError(t, aFn(context.Background(), errors.New("custom error"))) - - assert.Equal(t, map[int]error{ - 0: errors.New("custom error"), - 1: errors.New("custom error"), - 2: errors.New("custom error"), - }, ackCalls) -} - -func TestSystemWindowParallelReadAndWrites(t *testing.T) { - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 500000000).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, 0, 0, 0, nil) - require.NoError(t, err) - - var wg sync.WaitGroup - wg.Add(2) - - startChan := make(chan struct{}) - go func() { - defer wg.Done() - <-startChan - for i := 0; i < 1000; i++ { - msg := fmt.Sprintf(`{"id":"%v","ts":10.5}`, i) - writeErr := w.WriteBatch(context.Background(), service.MessageBatch{ - service.NewMessage([]byte(msg)), - }, func(ctx context.Context, err error) error { - return nil - }) - require.NoError(t, writeErr) - } - }() - go func() { - defer wg.Done() - <-startChan - _, _, readErr := w.ReadBatch(context.Background()) - require.NoError(t, readErr) - }() - - close(startChan) - wg.Wait() -} - -func TestSystemWindowOwnership(t *testing.T) { - ctx := context.Background() - - mapping, err := bloblang.Parse(`root = this.ts`) - require.NoError(t, err) - - currentTS := time.Unix(10, 1).UTC() - w, err := newSystemWindowBuffer(mapping, func() time.Time { - return currentTS - }, time.Second, 0, 0, 0, nil) - require.NoError(t, err) - - inMsg := service.NewMessage(nil) - inMsg.SetStructuredMut(map[string]any{ - "hello": "world", - "ts": 10, - }) - - err = w.WriteBatch(ctx, service.MessageBatch{inMsg}, func(ctx context.Context, _ error) error { - inStruct, err := inMsg.AsStructuredMut() - require.NoError(t, err) - _, err = gabs.Wrap(inStruct).Set("quack", "moo") - require.NoError(t, err) - return nil - }) - require.NoError(t, err) - assert.Len(t, w.pending, 1) - - outBatch, ackFunc, err := w.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, outBatch, 1) - - outStruct, err := outBatch[0].AsStructuredMut() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - "ts": 10, - }, outStruct) - - require.NoError(t, ackFunc(ctx, nil)) - - _, err = gabs.Wrap(outStruct).Set("woof", "meow") - require.NoError(t, err) - - inStruct, err := inMsg.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - "moo": "quack", - "ts": 10, - }, inStruct) -} diff --git a/internal/impl/pure/cache_integration_test.go b/internal/impl/pure/cache_integration_test.go deleted file mode 100644 index 87e084e631..0000000000 --- a/internal/impl/pure/cache_integration_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package pure - -import ( - "testing" - - "github.com/benthosdev/benthos/v4/public/service/integration" -) - -func TestIntegrationMultilevelCache(t *testing.T) { - integration.CheckSkip(t) - - t.Parallel() - - template := ` -cache_resources: - - label: testcache - multilevel: [ first, second ] - - label: first - memory: {} - - label: second - memory: {} -` - suite := integration.CacheTests( - integration.CacheTestOpenClose(), - integration.CacheTestMissingKey(), - integration.CacheTestDoubleAdd(), - integration.CacheTestDelete(), - integration.CacheTestGetAndSet(50), - ) - suite.Run(t, template) -} diff --git a/internal/impl/pure/cache_lru.go b/internal/impl/pure/cache_lru.go deleted file mode 100644 index ca1cb7d041..0000000000 --- a/internal/impl/pure/cache_lru.go +++ /dev/null @@ -1,298 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - lruarcv2 "github.com/hashicorp/golang-lru/arc/v2" - lruv2 "github.com/hashicorp/golang-lru/v2" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - lruCacheFieldCapLabel = "cap" - lruCacheFieldCapDefaultValue = 1000 - lruCacheFieldInitValuesLabel = "init_values" - - // specific to algorithm - lruCacheFieldAlgorithmLabel = "algorithm" - lruCacheFieldAlgorithmValueStandard = "standard" - lruCacheFieldAlgorithmValueARC = "arc" - lruCacheFieldAlgorithmValue2Q = "two_queues" - lruCacheFieldAlgorithmDefaultValue = lruCacheFieldAlgorithmValueStandard - - // specific to algorithm two queues - lruCacheField2QRecentRatioLabel = "two_queues_recent_ratio" - lruCacheField2QGhostRatioLabel = "two_queues_ghost_ratio" - lruCacheField2QRecentRatioDefaultValue = lruv2.Default2QRecentRatio - lruCacheField2QGhostRatioDefaultValue = lruv2.Default2QGhostEntries - - // optimistic - lruCacheFieldOptimisticLabel = "optimistic" - lruCacheFieldOptimisticDefaultValue = false -) - -func lruCacheConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Summary(`Stores key/value pairs in a lru in-memory cache. This cache is therefore reset every time the service restarts.`). - Description(`This provides the lru package which implements a fixed-size thread safe LRU cache. - -It uses the package ` + "https://github.com/hashicorp/golang-lru/v2[`lru`]" + ` - -The field ` + lruCacheFieldInitValuesLabel + ` can be used to pre-populate the memory cache with any number of key/value pairs: - -` + "```yaml" + ` -cache_resources: - - label: foocache - lru: - cap: 1024 - init_values: - foo: bar -` + "```" + ` - -These values can be overridden during execution.`). - Field(service.NewIntField(lruCacheFieldCapLabel). - Description("The cache maximum capacity (number of entries)"). - Default(lruCacheFieldCapDefaultValue)). - Field(service.NewStringMapField(lruCacheFieldInitValuesLabel). - Description("A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables."). - Default(map[string]any{}). - Example(map[string]any{ - "Nickelback": "1995", - "Spice Girls": "1994", - "The Human League": "1977", - })). - Field(service.NewStringAnnotatedEnumField(lruCacheFieldAlgorithmLabel, map[string]string{ - lruCacheFieldAlgorithmValueStandard: "is a simple LRU cache. It is based on the LRU implementation in groupcache", - lruCacheFieldAlgorithmValueARC: "is an adaptive replacement cache. It tracks recent evictions as well as recent usage in both the frequent and recent caches. Its computational overhead is comparable to " + lruCacheFieldAlgorithmValue2Q + ", but the memory overhead is linear with the size of the cache. ARC has been patented by IBM.", - lruCacheFieldAlgorithmValue2Q: "tracks frequently used and recently used entries separately. This avoids a burst of accesses from taking out frequently used entries, at the cost of about 2x computational overhead and some extra bookkeeping.", - }). - Description("the lru cache implementation"). - Default(lruCacheFieldAlgorithmDefaultValue). - Advanced()). - Field(service.NewFloatField("two_queues_recent_ratio"). - Description("is the ratio of the " + lruCacheFieldAlgorithmValue2Q + " cache dedicated to recently added entries that have only been accessed once."). - Default(lruCacheField2QRecentRatioDefaultValue). - Advanced(). - Optional()). - Field(service.NewFloatField("two_queues_ghost_ratio"). - Description("is the default ratio of ghost entries kept to track entries recently evicted on " + lruCacheFieldAlgorithmValue2Q + " cache."). - Default(lruv2.Default2QGhostEntries). - Advanced(). - Optional()). - Field(service.NewBoolField(lruCacheFieldOptimisticLabel). - Description("If true, we do not lock on read/write events. The lru package is thread-safe, however the ADD operation is not atomic."). - Default(lruCacheFieldOptimisticDefaultValue). - Advanced()) - - return spec -} - -func init() { - err := service.RegisterCache( - "lru", lruCacheConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - f, err := lruMemCacheFromConfig(conf) - if err != nil { - return nil, err - } - return f, nil - }) - if err != nil { - panic(err) - } -} - -func lruMemCacheFromConfig(conf *service.ParsedConfig) (*lruCacheAdapter, error) { - capacity, err := conf.FieldInt(lruCacheFieldCapLabel) - if err != nil { - return nil, err - } - - initValues, err := conf.FieldStringMap(lruCacheFieldInitValuesLabel) - if err != nil { - return nil, err - } - - algorithm, err := conf.FieldString(lruCacheFieldAlgorithmLabel) - if err != nil { - return nil, err - } - - var recentRatioPtr, ghostRatioPtr *float64 - - if conf.Contains(lruCacheField2QRecentRatioLabel) || conf.Contains(lruCacheField2QGhostRatioLabel) { - recentRatio, err := conf.FieldFloat(lruCacheField2QRecentRatioLabel) - if err != nil { - return nil, err - } - - ghostRatio, err := conf.FieldFloat(lruCacheField2QGhostRatioLabel) - if err != nil { - return nil, err - } - - recentRatioPtr = &recentRatio - ghostRatioPtr = &ghostRatio - } - - optimistic, err := conf.FieldBool(lruCacheFieldOptimisticLabel) - if err != nil { - return nil, err - } - - return lruMemCache(capacity, algorithm, initValues, recentRatioPtr, ghostRatioPtr, optimistic) -} - -//------------------------------------------------------------------------------ - -var errInvalidLRUCacheCapacityValue = errors.New("invalid lru cache parameter capacity: must be bigger than 0") - -func lruMemCache(capacity int, - algorithm string, - initValues map[string]string, - recentRatio, ghostRatio *float64, - optimistic bool, -) (ca *lruCacheAdapter, err error) { - if capacity <= 0 { - return nil, errInvalidLRUCacheCapacityValue - } - - var inner lruCache - - switch algorithm { - case lruCacheFieldAlgorithmValueStandard: - var c *lruv2.Cache[string, []byte] - c, err = lruv2.New[string, []byte](capacity) - if err != nil { - return - } - - inner = &lruv2SimpleCacheAdaptor[string, []byte]{ - Cache: c, - } - - case lruCacheFieldAlgorithmValueARC: - inner, err = lruarcv2.NewARC[string, []byte](capacity) - if err != nil { - return - } - - case lruCacheFieldAlgorithmValue2Q: - if recentRatio != nil && ghostRatio != nil { - inner, err = lruv2.New2QParams[string, []byte](capacity, *recentRatio, *ghostRatio) - } else { - inner, err = lruv2.New2Q[string, []byte](capacity) - } - - if err != nil { - return - } - default: - return nil, fmt.Errorf("algorithm %q not supported. the supported values are %q, %q and %q", algorithm, - lruCacheFieldAlgorithmValueStandard, lruCacheFieldAlgorithmValueARC, lruCacheFieldAlgorithmValue2Q) - } - - for k, v := range initValues { - inner.Add(k, []byte(v)) - } - - return &lruCacheAdapter{ - inner: inner, - optimistic: optimistic, - }, nil -} - -//------------------------------------------------------------------------------ - -var ( - _ lruCache = (*lruv2SimpleCacheAdaptor[string, []byte])(nil) - _ lruCache = (*lruv2.TwoQueueCache[string, []byte])(nil) - _ lruCache = (*lruarcv2.ARCCache[string, []byte])(nil) -) - -type lruCache interface { - Peek(key string) (value []byte, ok bool) - Get(key string) (value []byte, ok bool) - Add(key string, value []byte) - Remove(key string) -} - -type lruv2SimpleCacheAdaptor[K comparable, V any] struct { - *lruv2.Cache[K, V] -} - -func (ad *lruv2SimpleCacheAdaptor[K, V]) Add(key K, value V) { - _ = ad.Cache.Add(key, value) -} - -func (ad *lruv2SimpleCacheAdaptor[K, V]) Remove(key K) { - _ = ad.Cache.Remove(key) -} - -//------------------------------------------------------------------------------ - -var _ service.Cache = (*lruCacheAdapter)(nil) - -type lruCacheAdapter struct { - inner lruCache - - optimistic bool - - sync.Mutex -} - -func (ca *lruCacheAdapter) Get(_ context.Context, key string) ([]byte, error) { - value, ok := ca.inner.Get(key) - if !ok { - return nil, service.ErrKeyNotFound - } - - return value, nil -} - -func (ca *lruCacheAdapter) Set(_ context.Context, key string, value []byte, _ *time.Duration) error { - ca.inner.Add(key, value) - - return nil -} - -func (ca *lruCacheAdapter) unsafeAdd(key string, value []byte) error { - _, ok := ca.inner.Peek(key) - if ok { - return service.ErrKeyAlreadyExists - } - - ca.inner.Add(key, value) - - return nil -} - -func (ca *lruCacheAdapter) Add(_ context.Context, key string, value []byte, _ *time.Duration) error { - if ca.optimistic { - return ca.unsafeAdd(key, value) - } - - ca.Lock() - - err := ca.unsafeAdd(key, value) - - ca.Unlock() - - return err -} - -func (ca *lruCacheAdapter) Delete(_ context.Context, key string) error { - ca.inner.Remove(key) - - return nil -} - -func (ca *lruCacheAdapter) Close(_ context.Context) error { - return nil -} diff --git a/internal/impl/pure/cache_lru_test.go b/internal/impl/pure/cache_lru_test.go deleted file mode 100644 index 9f76bf0ca4..0000000000 --- a/internal/impl/pure/cache_lru_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestLRUCacheStandard(t *testing.T) { - t.Parallel() - - defConf, err := lruCacheConfig().ParseYAML(``, nil) - require.NoError(t, err) - - c, err := lruMemCacheFromConfig(defConf) - require.NoError(t, err) - - testServiceCache(t, c) -} - -func TestLRUCacheOptimistic(t *testing.T) { - t.Parallel() - - defConf, err := lruCacheConfig().ParseYAML(` -optimistic: true -`, nil) - require.NoError(t, err) - - c, err := lruMemCacheFromConfig(defConf) - require.NoError(t, err) - - testServiceCache(t, c) -} - -func TestLRUCacheInitValues(t *testing.T) { - t.Parallel() - - defConf, err := lruCacheConfig().ParseYAML(` -cap: 1024 -init_values: - foo: bar - foo2: bar2 -`, nil) - require.NoError(t, err) - - c, err := lruMemCacheFromConfig(defConf) - require.NoError(t, err) - - ctx := context.Background() - - exp := "bar" - if act, err := c.Get(ctx, "foo"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - exp = "bar2" - if act, err := c.Get(ctx, "foo2"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } -} - -func TestLRUCacheAlgorithms(t *testing.T) { - t.Parallel() - - algorithms := []string{"standard", "arc", "two_queues"} - - for _, algorithm := range algorithms { - algorithm := algorithm - t.Run(algorithm, func(t *testing.T) { - t.Parallel() - - yamlConf := fmt.Sprintf("algorithm: %q", algorithm) - - defConf, err := lruCacheConfig().ParseYAML(yamlConf, nil) - require.NoError(t, err) - - c, err := lruMemCacheFromConfig(defConf) - require.NoError(t, err) - - testServiceCache(t, c) - }) - } -} - -func BenchmarkLRU(b *testing.B) { - defConf, err := lruCacheConfig().ParseYAML(``, nil) - require.NoError(b, err) - - c, err := lruMemCacheFromConfig(defConf) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - j := i % lruCacheFieldCapDefaultValue - key, value := fmt.Sprintf("key%v", j), []byte(fmt.Sprintf("foo%v", j)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } -} - -func BenchmarkLRUParallel(b *testing.B) { - defConf, err := lruCacheConfig().ParseYAML(``, nil) - require.NoError(b, err) - - c, err := lruMemCacheFromConfig(defConf) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - b.RunParallel(func(p *testing.PB) { - for i := 0; p.Next(); i++ { - j := i % lruCacheFieldCapDefaultValue - key, value := fmt.Sprintf("key%v", j), []byte(fmt.Sprintf("foo%v", j)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } - }) -} - -func testServiceCache(t *testing.T, c service.Cache) { - t.Helper() - - ctx := context.Background() - key := "foo" - - expErr := service.ErrKeyNotFound - - if _, act := c.Get(ctx, key); !errors.Is(act, expErr) { - t.Errorf("wrong error returned on c.Get(ctx, %q): %v != %v", key, act, expErr) - } - - if err := c.Set(ctx, key, []byte("1"), nil); err != nil { - t.Errorf("unexpected error while c.Set(ctx, %q, ): %v", key, err) - } - - exp := "1" - if act, err := c.Get(ctx, key); err != nil { - t.Errorf("unexpected error while c.Get(ctx, %q): %v", key, err) - } else if string(act) != exp { - t.Errorf("Wrong result c.Get(ctx, %q): %v != %v", key, string(act), exp) - } - - key = "bar" - - if err := c.Add(ctx, key, []byte("2"), nil); err != nil { - t.Errorf("unexpected error while c.Add(ctx, %q, ): %v", key, err) - } - - exp = "2" - - if act, err := c.Get(ctx, key); err != nil { - t.Errorf("unexpected error while c.Get(ctx, %q): %v", key, err) - } else if string(act) != exp { - t.Errorf("wrong result c.Get(ctx, %q): %v != %v", key, string(act), exp) - } - - key = "foo" - expErr = service.ErrKeyAlreadyExists - - if act := c.Add(ctx, key, []byte("2"), nil); !errors.Is(act, expErr) { - t.Errorf("unexpected error returned on c.Add(ctx, %q, ): %v != %v", key, act, expErr) - } - - if err := c.Set(ctx, key, []byte("3"), nil); err != nil { - t.Errorf("unexpected error while c.Set(ctx, %q, ): %v", key, err) - } - - exp = "3" - if act, err := c.Get(ctx, key); err != nil { - t.Errorf("unexpected error while c.Get(ctx, %q): %v", key, err) - } else if string(act) != exp { - t.Errorf("wrong result c.Get(ctx, %q): %v != %v", key, string(act), exp) - } - - if err := c.Delete(ctx, key); err != nil { - t.Errorf("unexpected error while c.Delete(ctx, %q, ): %v", key, err) - } - - expErr = service.ErrKeyNotFound - - if _, act := c.Get(ctx, key); !errors.Is(act, expErr) { - t.Errorf("wrong error returned on c.Get(ctx, %q): %v != %v", key, act, expErr) - } -} diff --git a/internal/impl/pure/cache_memory.go b/internal/impl/pure/cache_memory.go deleted file mode 100644 index d7509dabce..0000000000 --- a/internal/impl/pure/cache_memory.go +++ /dev/null @@ -1,244 +0,0 @@ -package pure - -import ( - "context" - "sync" - "time" - - "github.com/OneOfOne/xxhash" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func memCacheConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Summary(`Stores key/value pairs in a map held in memory. This cache is therefore reset every time the service restarts. Each item in the cache has a TTL set from the moment it was last edited, after which it will be removed during the next compaction.`). - Description(`The compaction interval determines how often the cache is cleared of expired items, and this process is only triggered on writes to the cache. Access to the cache is blocked during this process. - -Item expiry can be disabled entirely by setting the ` + "`compaction_interval`" + ` to an empty string. - -The field ` + "`init_values`" + ` can be used to prepopulate the memory cache with any number of key/value pairs which are exempt from TTLs: - -` + "```yaml" + ` -cache_resources: - - label: foocache - memory: - default_ttl: 60s - init_values: - foo: bar -` + "```" + ` - -These values can be overridden during execution, at which point the configured TTL is respected as usual.`). - Field(service.NewDurationField("default_ttl"). - Description("The default TTL of each item. After this period an item will be eligible for removal during the next compaction."). - Default("5m")). - Field(service.NewDurationField("compaction_interval"). - Description("The period of time to wait before each compaction, at which point expired items are removed. This field can be set to an empty string in order to disable compactions/expiry entirely."). - Default("60s")). - Field(service.NewStringMapField("init_values"). - Description("A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables."). - Default(map[string]any{}). - Example(map[string]any{ - "Nickelback": "1995", - "Spice Girls": "1994", - "The Human League": "1977", - })). - Field(service.NewIntField("shards"). - Description("A number of logical shards to spread keys across, increasing the shards can have a performance benefit when processing a large number of keys."). - Default(1). - Advanced()) - return spec -} - -func init() { - err := service.RegisterCache( - "memory", memCacheConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - f, err := newMemCacheFromConfig(conf) - if err != nil { - return nil, err - } - return f, nil - }) - if err != nil { - panic(err) - } -} - -func newMemCacheFromConfig(conf *service.ParsedConfig) (*memoryCache, error) { - ttl, err := conf.FieldDuration("default_ttl") - if err != nil { - return nil, err - } - - var compInterval time.Duration - if test, _ := conf.FieldString("compaction_interval"); test != "" { - if compInterval, err = conf.FieldDuration("compaction_interval"); err != nil { - return nil, err - } - } - - nShards, err := conf.FieldInt("shards") - if err != nil { - return nil, err - } - - initValues, err := conf.FieldStringMap("init_values") - if err != nil { - return nil, err - } - - return newMemCache(ttl, compInterval, nShards, initValues), nil -} - -//------------------------------------------------------------------------------ - -type item struct { - value []byte - expires time.Time -} - -type shard struct { - items map[string]item - - compInterval time.Duration - lastCompaction time.Time - - sync.RWMutex -} - -func (s *shard) isExpired(i item) bool { - if s.compInterval == 0 { - return false - } - if i.expires.IsZero() { - return false - } - return i.expires.Before(time.Now()) -} - -func (s *shard) compaction() { - if s.compInterval == 0 { - return - } - if time.Since(s.lastCompaction) < s.compInterval { - return - } - for k, v := range s.items { - if s.isExpired(v) { - delete(s.items, k) - } - } - s.lastCompaction = time.Now() -} - -//------------------------------------------------------------------------------ - -func newMemCache(ttl, compInterval time.Duration, nShards int, initValues map[string]string) *memoryCache { - m := &memoryCache{ - defaultTTL: ttl, - } - - if nShards <= 1 { - m.shards = []*shard{ - { - items: map[string]item{}, - compInterval: compInterval, - lastCompaction: time.Now(), - }, - } - } else { - for i := 0; i < nShards; i++ { - m.shards = append(m.shards, &shard{ - items: map[string]item{}, - compInterval: compInterval, - lastCompaction: time.Now(), - }) - } - } - - for k, v := range initValues { - m.getShard(k).items[k] = item{ - value: []byte(v), - expires: time.Time{}, - } - } - - return m -} - -type memoryCache struct { - shards []*shard - defaultTTL time.Duration -} - -func (m *memoryCache) getShard(key string) *shard { - if len(m.shards) == 1 { - return m.shards[0] - } - - return m.shards[xxhash.ChecksumString64(key)%uint64(len(m.shards))] -} - -func (m *memoryCache) Get(_ context.Context, key string) ([]byte, error) { - shard := m.getShard(key) - shard.RLock() - k, exists := shard.items[key] - shard.RUnlock() - if !exists { - return nil, service.ErrKeyNotFound - } - // Simulate compaction by returning ErrKeyNotFound if ttl expired. - if shard.isExpired(k) { - return nil, service.ErrKeyNotFound - } - return k.value, nil -} - -func (m *memoryCache) Set(_ context.Context, key string, value []byte, ttl *time.Duration) error { - var expires time.Time - if ttl != nil { - expires = time.Now().Add(*ttl) - } else { - expires = time.Now().Add(m.defaultTTL) - } - shard := m.getShard(key) - shard.Lock() - shard.compaction() - shard.items[key] = item{value: value, expires: expires} - shard.Unlock() - return nil -} - -func (m *memoryCache) Add(_ context.Context, key string, value []byte, ttl *time.Duration) error { - var expires time.Time - if ttl != nil { - expires = time.Now().Add(*ttl) - } else { - expires = time.Now().Add(m.defaultTTL) - } - shard := m.getShard(key) - shard.Lock() - if _, exists := shard.items[key]; exists { - shard.Unlock() - return service.ErrKeyAlreadyExists - } - shard.compaction() - shard.items[key] = item{value: value, expires: expires} - shard.Unlock() - return nil -} - -func (m *memoryCache) Delete(_ context.Context, key string) error { - shard := m.getShard(key) - shard.Lock() - shard.compaction() - delete(shard.items, key) - shard.Unlock() - return nil -} - -func (m *memoryCache) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/cache_memory_test.go b/internal/impl/pure/cache_memory_test.go deleted file mode 100644 index 2b4b63795b..0000000000 --- a/internal/impl/pure/cache_memory_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestMemoryCache(t *testing.T) { - defConf, err := memCacheConfig().ParseYAML(``, nil) - require.NoError(t, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(t, err) - - ctx := context.Background() - - expErr := service.ErrKeyNotFound - if _, act := c.Get(ctx, "foo"); act != expErr { - t.Errorf("Wrong error returned: %v != %v", act, expErr) - } - - if err = c.Set(ctx, "foo", []byte("1"), nil); err != nil { - t.Error(err) - } - - exp := "1" - if act, err := c.Get(ctx, "foo"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - if err = c.Add(ctx, "bar", []byte("2"), nil); err != nil { - t.Error(err) - } - - exp = "2" - if act, err := c.Get(ctx, "bar"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - expErr = service.ErrKeyAlreadyExists - if act := c.Add(ctx, "foo", []byte("2"), nil); expErr != act { - t.Errorf("Wrong error returned: %v != %v", act, expErr) - } - - if err = c.Set(ctx, "foo", []byte("3"), nil); err != nil { - t.Error(err) - } - - exp = "3" - if act, err := c.Get(ctx, "foo"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - if err = c.Delete(ctx, "foo"); err != nil { - t.Error(err) - } - - if _, err = c.Get(ctx, "foo"); err != service.ErrKeyNotFound { - t.Errorf("Wrong error returned: %v != %v", err, service.ErrKeyNotFound) - } -} - -func TestMemoryCacheCompaction(t *testing.T) { - defConf, err := memCacheConfig().ParseYAML(` -default_ttl: 0s -compaction_interval: 1ns -`, nil) - require.NoError(t, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(t, err) - - ctx := context.Background() - - _, err = c.Get(ctx, "foo") - assert.Equal(t, service.ErrKeyNotFound, err) - - err = c.Set(ctx, "foo", []byte("1"), nil) - require.NoError(t, err) - - _, err = c.Get(ctx, "foo") - assert.Equal(t, service.ErrKeyNotFound, err) - - <-time.After(time.Millisecond * 50) - - // This should trigger compaction. - err = c.Add(ctx, "bar", []byte("2"), nil) - require.NoError(t, err) - - _, err = c.Get(ctx, "bar") - assert.Equal(t, service.ErrKeyNotFound, err) -} - -func TestMemoryCacheInitValues(t *testing.T) { - defConf, err := memCacheConfig().ParseYAML(` -default_ttl: 0s -compaction_interval: "" -init_values: - foo: bar - foo2: bar2 -`, nil) - require.NoError(t, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(t, err) - - ctx := context.Background() - - exp := "bar" - if act, err := c.Get(ctx, "foo"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - // This should trigger compaction. - if err = c.Add(ctx, "foo3", []byte("bar3"), nil); err != nil { - t.Error(err) - } - - exp = "bar" - if act, err := c.Get(ctx, "foo"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - exp = "bar2" - if act, err := c.Get(ctx, "foo2"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } -} - -func TestMemoryCacheCompactionOnRead(t *testing.T) { - defConf, err := memCacheConfig().ParseYAML(` -default_ttl: 0s -compaction_interval: 1ns -`, nil) - require.NoError(t, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(t, err) - - ctx := context.Background() - - expErr := service.ErrKeyNotFound - if _, act := c.Get(ctx, "foo"); act != expErr { - t.Errorf("Wrong error returned: %v != %v", act, expErr) - } - - if err = c.Set(ctx, "foo", []byte("1"), nil); err != nil { - t.Error(err) - } - - <-time.After(time.Millisecond * 50) - - // This should trigger compaction. - if _, act := c.Get(ctx, "foo"); act != expErr { - t.Errorf("Wrong error returned: %v != %v", act, expErr) - } -} - -//------------------------------------------------------------------------------ - -func BenchmarkMemoryShards1(b *testing.B) { - defConf, err := memCacheConfig().ParseYAML(` -default_ttl: 0s -compaction_interval: "" -`, nil) - require.NoError(b, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - key, value := fmt.Sprintf("key%v", i), []byte(fmt.Sprintf("foo%v", i)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } -} - -func BenchmarkMemoryShards10(b *testing.B) { - defConf, err := memCacheConfig().ParseYAML(` -default_ttl: 0s -compaction_interval: "" -shards: 10 -`, nil) - require.NoError(b, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - key, value := fmt.Sprintf("key%v", i), []byte(fmt.Sprintf("foo%v", i)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } -} - -func BenchmarkMemoryShards100(b *testing.B) { - defConf, err := memCacheConfig().ParseYAML(` -default_ttl: 0s -compaction_interval: "" -shards: 10 -`, nil) - require.NoError(b, err) - - c, err := newMemCacheFromConfig(defConf) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - key, value := fmt.Sprintf("key%v", i), []byte(fmt.Sprintf("foo%v", i)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } -} diff --git a/internal/impl/pure/cache_multilevel.go b/internal/impl/pure/cache_multilevel.go deleted file mode 100644 index b0152ad379..0000000000 --- a/internal/impl/pure/cache_multilevel.go +++ /dev/null @@ -1,201 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "time" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func multilevelCacheConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Summary(`Combines multiple caches as levels, performing read-through and write-through operations across them.`). - Field(service.NewStringListField("")). - Example( - "Hot and cold cache", - "The multilevel cache is useful for reducing traffic against a remote cache by routing it through a local cache. In the following example requests will only go through to the memcached server if the local memory cache is missing the key.", - ` -pipeline: - processors: - - branch: - processors: - - cache: - resource: leveled - operator: get - key: ${! json("key") } - - catch: - - mapping: 'root = {"err":error()}' - result_map: 'root.result = this' - -cache_resources: - - label: leveled - multilevel: [ hot, cold ] - - - label: hot - memory: - default_ttl: 60s - - - label: cold - memcached: - addresses: [ TODO:11211 ] - default_ttl: 60s -`) - return spec -} - -func init() { - err := service.RegisterCache( - "multilevel", multilevelCacheConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - levels, err := conf.FieldStringList() - if err != nil { - return nil, err - } - return newMultilevelCache(levels, mgr, mgr.Logger()) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type cacheProvider interface { - AccessCache(ctx context.Context, name string, fn func(c service.Cache)) error -} - -type multilevelCache struct { - mgr cacheProvider - log *service.Logger - caches []string -} - -func newMultilevelCache(levels []string, mgr cacheProvider, log *service.Logger) (service.Cache, error) { - if len(levels) < 2 { - return nil, fmt.Errorf("expected at least two cache levels, found %v", len(levels)) - } - // TODO: Probe caches - // for _, name := range levels { - // } - return &multilevelCache{ - mgr: mgr, - log: log, - caches: levels, - }, nil -} - -//------------------------------------------------------------------------------ - -func (l *multilevelCache) setUpToLevelPassive(ctx context.Context, i int, key string, value []byte) { - for j, name := range l.caches { - if j == i { - break - } - var setErr error - if err := l.mgr.AccessCache(ctx, name, func(c service.Cache) { - setErr = c.Set(ctx, key, value, nil) - }); err != nil { - l.log.Errorf("Unable to passively set key '%v' for cache '%v': %v", key, name, err) - } - if setErr != nil { - l.log.Errorf("Unable to passively set key '%v' for cache '%v': %v", key, name, setErr) - } - } -} - -func (l *multilevelCache) Get(ctx context.Context, key string) ([]byte, error) { - for i, name := range l.caches { - var data []byte - var err error - if cerr := l.mgr.AccessCache(ctx, name, func(c service.Cache) { - data, err = c.Get(ctx, key) - }); cerr != nil { - return nil, fmt.Errorf("unable to access cache '%v': %v", name, cerr) - } - if err != nil { - if err != service.ErrKeyNotFound { - return nil, err - } - } else { - l.setUpToLevelPassive(ctx, i, key, data) - return data, nil - } - } - return nil, service.ErrKeyNotFound -} - -func (l *multilevelCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - for _, name := range l.caches { - var err error - if cerr := l.mgr.AccessCache(ctx, name, func(c service.Cache) { - err = c.Set(ctx, key, value, ttl) - }); cerr != nil { - return fmt.Errorf("unable to access cache '%v': %v", name, cerr) - } - if err != nil { - return err - } - } - return nil -} - -func (l *multilevelCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - for i := 0; i < len(l.caches)-1; i++ { - var err error - if cerr := l.mgr.AccessCache(ctx, l.caches[i], func(c service.Cache) { - _, err = c.Get(ctx, key) - }); cerr != nil { - return fmt.Errorf("unable to access cache '%v': %v", l.caches[i], cerr) - } - if err != nil { - if err != service.ErrKeyNotFound { - return err - } - } else { - return service.ErrKeyAlreadyExists - } - } - - var err error - if cerr := l.mgr.AccessCache(ctx, l.caches[len(l.caches)-1], func(c service.Cache) { - err = c.Add(ctx, key, value, ttl) - }); cerr != nil { - return fmt.Errorf("unable to access cache '%v': %v", l.caches[len(l.caches)-1], cerr) - } - if err != nil { - return err - } - - for i := len(l.caches) - 2; i >= 0; i-- { - if cerr := l.mgr.AccessCache(ctx, l.caches[i], func(c service.Cache) { - err = c.Add(ctx, key, value, ttl) - }); cerr != nil { - return fmt.Errorf("unable to access cache '%v': %v", l.caches[i], cerr) - } - if err != nil { - return err - } - } - return nil -} - -func (l *multilevelCache) Delete(ctx context.Context, key string) error { - for _, name := range l.caches { - var err error - if cerr := l.mgr.AccessCache(ctx, name, func(c service.Cache) { - err = c.Delete(ctx, key) - }); cerr != nil { - return fmt.Errorf("unable to access cache '%v': %v", name, cerr) - } - if err != nil && err != service.ErrKeyNotFound { - return err - } - } - return nil -} - -func (l *multilevelCache) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/cache_multilevel_test.go b/internal/impl/pure/cache_multilevel_test.go deleted file mode 100644 index 8ccf77edf4..0000000000 --- a/internal/impl/pure/cache_multilevel_test.go +++ /dev/null @@ -1,363 +0,0 @@ -package pure - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestMultilevelCacheErrors(t *testing.T) { - strmBuilder := service.NewStreamBuilder() - require.NoError(t, strmBuilder.SetYAML(` -input: - generate: - interval: 1ns - count: 1 - mapping: 'root = "hello world"' - -output: - cache: - target: testing - -cache_resources: - - label: testing - multilevel: [] - -logger: - level: NONE -`)) - - _, err := strmBuilder.Build() - require.Error(t, err) - assert.Contains(t, err.Error(), "expected at least two cache levels") - - strmBuilder = service.NewStreamBuilder() - require.NoError(t, strmBuilder.SetYAML(` -input: - generate: - interval: 1ns - count: 1 - mapping: 'root = "hello world"' - -output: - cache: - target: test - -cache_resources: - - label: test - multilevel: - - foo - -logger: - level: NONE -`)) - - _, err = strmBuilder.Build() - require.Error(t, err) - assert.Contains(t, err.Error(), "expected at least two cache levels") - - strmBuilder = service.NewStreamBuilder() - require.NoError(t, strmBuilder.SetYAML(` -input: - generate: - interval: 1ns - count: 1 - mapping: 'root = "hello world"' - -output: - cache: - target: test - -cache_resources: - - label: test - multilevel: [ foo, bar ] - - - label: foo - memory: {} - - - label: bar - memory: {} - -logger: - level: NONE -`)) - - s, err := strmBuilder.Build() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - require.NoError(t, s.Run(tCtx)) -} - -type mockCacheProv struct { - caches map[string]service.Cache -} - -func (m *mockCacheProv) AccessCache(ctx context.Context, name string, fn func(c service.Cache)) error { - c, ok := m.caches[name] - if !ok { - return errors.New("cache not found") - } - fn(c) - return nil -} - -func TestMultilevelCacheGetting(t *testing.T) { - memCache1 := newMemCache(time.Minute, 0, 1, nil) - memCache2 := newMemCache(time.Minute, 0, 1, nil) - p := &mockCacheProv{ - caches: map[string]service.Cache{ - "foo": memCache1, - "bar": memCache2, - }, - } - - c, err := newMultilevelCache([]string{"foo", "bar"}, p, nil) - require.NoError(t, err) - - ctx := context.Background() - - _, err = c.Get(ctx, "not_exist") - assert.Equal(t, err, service.ErrKeyNotFound) - - require.NoError(t, memCache2.Set(ctx, "foo", []byte("test value 1"), nil)) - - val, err := c.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - require.NoError(t, memCache2.Delete(ctx, "foo")) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - _, err = memCache2.Get(ctx, "foo") - assert.Equal(t, err, service.ErrKeyNotFound) -} - -func TestMultilevelCacheSet(t *testing.T) { - memCache1 := newMemCache(time.Minute, 0, 1, nil) - memCache2 := newMemCache(time.Minute, 0, 1, nil) - p := &mockCacheProv{ - caches: map[string]service.Cache{ - "foo": memCache1, - "bar": memCache2, - }, - } - - c, err := newMultilevelCache([]string{"foo", "bar"}, p, nil) - require.NoError(t, err) - - ctx := context.Background() - - require.NoError(t, c.Set(ctx, "foo", []byte("test value 1"), nil)) - - val, err := memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - err = c.Set(ctx, "foo", []byte("test value 2"), nil) - require.NoError(t, err) - require.NoError(t, err) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 2")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 2")) -} - -func TestMultilevelCacheDelete(t *testing.T) { - memCache1 := newMemCache(time.Minute, 0, 1, nil) - memCache2 := newMemCache(time.Minute, 0, 1, nil) - p := &mockCacheProv{ - caches: map[string]service.Cache{ - "foo": memCache1, - "bar": memCache2, - }, - } - - c, err := newMultilevelCache([]string{"foo", "bar"}, p, nil) - require.NoError(t, err) - - ctx := context.Background() - - require.NoError(t, memCache2.Set(ctx, "foo", []byte("test value 1"), nil)) - - require.NoError(t, c.Delete(ctx, "foo")) - - _, err = memCache1.Get(ctx, "foo") - assert.Equal(t, err, service.ErrKeyNotFound) - - _, err = memCache2.Get(ctx, "foo") - assert.Equal(t, err, service.ErrKeyNotFound) - - require.NoError(t, memCache1.Set(ctx, "foo", []byte("test value 1"), nil)) - require.NoError(t, memCache2.Set(ctx, "foo", []byte("test value 2"), nil)) - - err = c.Delete(ctx, "foo") - require.NoError(t, err) - - _, err = memCache1.Get(ctx, "foo") - assert.Equal(t, err, service.ErrKeyNotFound) - - _, err = memCache2.Get(ctx, "foo") - assert.Equal(t, err, service.ErrKeyNotFound) -} - -func TestMultilevelCacheAdd(t *testing.T) { - memCache1 := newMemCache(time.Minute, 0, 1, nil) - memCache2 := newMemCache(time.Minute, 0, 1, nil) - p := &mockCacheProv{ - caches: map[string]service.Cache{ - "foo": memCache1, - "bar": memCache2, - }, - } - - c, err := newMultilevelCache([]string{"foo", "bar"}, p, nil) - require.NoError(t, err) - - ctx := context.Background() - - err = c.Add(ctx, "foo", []byte("test value 1"), nil) - require.NoError(t, err) - - val, err := memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - err = c.Add(ctx, "foo", []byte("test value 2"), nil) - assert.Equal(t, err, service.ErrKeyAlreadyExists) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - err = memCache2.Delete(ctx, "foo") - require.NoError(t, err) - - err = c.Add(ctx, "foo", []byte("test value 3"), nil) - assert.Equal(t, err, service.ErrKeyAlreadyExists) - - err = memCache1.Delete(ctx, "foo") - require.NoError(t, err) - - err = c.Add(ctx, "foo", []byte("test value 4"), nil) - require.NoError(t, err) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 4")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 4")) - - err = memCache1.Delete(ctx, "foo") - require.NoError(t, err) - - err = c.Add(ctx, "foo", []byte("test value 5"), nil) - assert.Equal(t, err, service.ErrKeyAlreadyExists) -} - -func TestMultilevelCacheAddMoreCaches(t *testing.T) { - memCache1 := newMemCache(time.Minute, 0, 1, nil) - memCache2 := newMemCache(time.Minute, 0, 1, nil) - memCache3 := newMemCache(time.Minute, 0, 1, nil) - p := &mockCacheProv{ - caches: map[string]service.Cache{ - "foo": memCache1, - "bar": memCache2, - "baz": memCache3, - }, - } - - c, err := newMultilevelCache([]string{"foo", "bar", "baz"}, p, nil) - require.NoError(t, err) - - ctx := context.Background() - - err = c.Add(ctx, "foo", []byte("test value 1"), nil) - require.NoError(t, err) - - val, err := memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache3.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - err = c.Add(ctx, "foo", []byte("test value 2"), nil) - assert.Equal(t, err, service.ErrKeyAlreadyExists) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - val, err = memCache3.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 1")) - - err = memCache1.Delete(ctx, "foo") - require.NoError(t, err) - - err = memCache2.Delete(ctx, "foo") - require.NoError(t, err) - - err = c.Add(ctx, "foo", []byte("test value 3"), nil) - assert.Equal(t, err, service.ErrKeyAlreadyExists) - - err = memCache3.Delete(ctx, "foo") - require.NoError(t, err) - - err = c.Add(ctx, "foo", []byte("test value 4"), nil) - require.NoError(t, err) - - val, err = memCache1.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 4")) - - val, err = memCache2.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 4")) - - val, err = memCache3.Get(ctx, "foo") - require.NoError(t, err) - assert.Equal(t, val, []byte("test value 4")) -} diff --git a/internal/impl/pure/cache_noop.go b/internal/impl/pure/cache_noop.go deleted file mode 100644 index dff950967c..0000000000 --- a/internal/impl/pure/cache_noop.go +++ /dev/null @@ -1,73 +0,0 @@ -package pure - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/public/service" -) - -var _ service.Cache = (*noopCacheAdapter)(nil) - -func noopCacheConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Version("4.27.0"). - Summary("Noop is a cache that stores nothing, all gets returns not found. Why? Sometimes doing nothing is the braver option."). - Field(service.NewObjectField("").Default(map[string]any{})) - - return spec -} - -func init() { - err := service.RegisterCache( - "noop", noopCacheConfig(), - func(_ *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - f := noopMemCache(mgr.Label(), mgr.Logger()) - - return f, nil - }) - if err != nil { - panic(err) - } -} - -func noopMemCache(label string, logger *service.Logger) *noopCacheAdapter { - return &noopCacheAdapter{ - logger: logger.With("cache", "noop", "label", label), - } -} - -type noopCacheAdapter struct { - logger *service.Logger -} - -func (c *noopCacheAdapter) Add(_ context.Context, key string, _ []byte, _ *time.Duration) error { - c.logger.Tracef("pretend to add key %q", key) - - return nil -} - -func (c *noopCacheAdapter) Set(_ context.Context, key string, _ []byte, _ *time.Duration) error { - c.logger.Tracef("pretend to set key %q", key) - - return nil -} - -func (c *noopCacheAdapter) Delete(_ context.Context, key string) error { - c.logger.Tracef("pretend to delete key %q", key) - - return nil -} - -func (c *noopCacheAdapter) Get(_ context.Context, key string) ([]byte, error) { - c.logger.Tracef("pretend to get key %q", key) - - return nil, service.ErrKeyNotFound -} - -func (c *noopCacheAdapter) Close(context.Context) error { - c.logger.Debug("close cache") - - return nil -} diff --git a/internal/impl/pure/cache_noop_test.go b/internal/impl/pure/cache_noop_test.go deleted file mode 100644 index 0409d64e31..0000000000 --- a/internal/impl/pure/cache_noop_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package pure - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestNoopCacheStandard(t *testing.T) { - t.Parallel() - - resources := service.MockResources() - - c := noopMemCache("TestNoopCacheStandard", resources.Logger()) - - err := c.Set(context.Background(), "foo", []byte("bar"), nil) - require.NoError(t, err) - - value, err := c.Get(context.Background(), "foo") - require.EqualError(t, err, "key does not exist") - - assert.Nil(t, value) -} diff --git a/internal/impl/pure/cache_ttlru.go b/internal/impl/pure/cache_ttlru.go deleted file mode 100644 index 3ed6b87d91..0000000000 --- a/internal/impl/pure/cache_ttlru.go +++ /dev/null @@ -1,221 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/hashicorp/golang-lru/v2/expirable" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - ttlruCacheFieldCapLabel = "cap" - ttlruCacheFieldCapDefaultValue = 1024 - - ttlruCacheFieldDefaultTTLLabel = "default_ttl" - ttlruCacheFieldDefaultTTLDefaultValue = 5 * time.Minute - - ttlruCacheFieldInitValuesLabel = "init_values" - - ttlruCacheFieldOptimisticLabel = "optimistic" - ttlruCacheFieldOptimisticDefaultValue = false - - ttlruCacheFieldDeprecatedTTLLabel = "ttl" - ttlruCacheFieldDeprecatedWithoutResetLabel = "without_reset" -) - -func ttlruCacheConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Summary(`Stores key/value pairs in a ttlru in-memory cache. This cache is therefore reset every time the service restarts.`). - Description(`The cache ttlru provides a simple, goroutine safe, cache with a fixed number of entries. Each entry has a per-cache defined TTL. - -This TTL is reset on both modification and access of the value. As a result, if the cache is full, and no items have expired, when adding a new item, the item with the soonest expiration will be evicted. - -It uses the package ` + "https://github.com/hashicorp/golang-lru/v2/expirable[`expirable`]" + ` - -The field ` + ttlruCacheFieldInitValuesLabel + ` can be used to pre-populate the memory cache with any number of key/value pairs: - -` + "```yaml" + ` -cache_resources: - - label: foocache - ttlru: - default_ttl: '5m' - cap: 1024 - init_values: - foo: bar -` + "```" + ` - -These values can be overridden during execution.`). - Field(service.NewIntField(ttlruCacheFieldCapLabel). - Description("The cache maximum capacity (number of entries)"). - Default(ttlruCacheFieldCapDefaultValue)). - Field(service.NewDurationField(ttlruCacheFieldDefaultTTLLabel). - Description("The cache ttl of each element"). - Default(ttlruCacheFieldDefaultTTLDefaultValue.String()). - Version("4.21.0")). - Field(service.NewDurationField(ttlruCacheFieldDeprecatedTTLLabel). - Description("Deprecated. Please use `" + ttlruCacheFieldDefaultTTLLabel + "` field"). - Optional().Advanced()). - Field(service.NewStringMapField(ttlruCacheFieldInitValuesLabel). - Description("A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables."). - Default(map[string]any{}). - Example(map[string]any{ - "Nickelback": "1995", - "Spice Girls": "1994", - "The Human League": "1977", - })). - Field(service.NewBoolField(ttlruCacheFieldOptimisticLabel). - Description("If true, we do not lock on read/write events. The ttlru package is thread-safe, however the ADD operation is not atomic."). - Default(ttlruCacheFieldOptimisticDefaultValue). - Advanced()) - - return spec -} - -func init() { - err := service.RegisterCache( - "ttlru", ttlruCacheConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - logger := mgr.Logger().With("component", "ttlru") - f, err := ttlruMemCacheFromConfig(conf, logger) - if err != nil { - return nil, err - } - return f, nil - }) - if err != nil { - panic(err) - } -} - -func ttlruMemCacheFromConfig(conf *service.ParsedConfig, logger *service.Logger) (*ttlruCacheAdapter, error) { - capacity, err := conf.FieldInt(ttlruCacheFieldCapLabel) - if err != nil { - return nil, err - } - - var ttl time.Duration - if conf.Contains(ttlruCacheFieldDeprecatedTTLLabel) { - ttl, err = conf.FieldDuration(ttlruCacheFieldDeprecatedTTLLabel) - - logger.Warnf("field %q is deprecated, please use %q", - ttlruCacheFieldDeprecatedTTLLabel, ttlruCacheFieldDefaultTTLLabel) - } else { - ttl, err = conf.FieldDuration(ttlruCacheFieldDefaultTTLLabel) - } - if err != nil { - return nil, err - } - - if withoutReset, _ := conf.FieldBool(ttlruCacheFieldDeprecatedWithoutResetLabel); withoutReset { - logger.Warnf("field %q is deprecated, ignoring", ttlruCacheFieldDeprecatedWithoutResetLabel) - } - - initValues, err := conf.FieldStringMap(ttlruCacheFieldInitValuesLabel) - if err != nil { - return nil, err - } - - optimistic, err := conf.FieldBool(ttlruCacheFieldOptimisticLabel) - if err != nil { - return nil, err - } - - return ttlruMemCache(capacity, ttl, initValues, optimistic) -} - -//------------------------------------------------------------------------------ - -type ttlruCacheAdapter struct { - inner *expirable.LRU[string, []byte] - - optimistic bool - - sync.Mutex -} - -var ( - errInvalidTTLRUCacheCapacityValue = errors.New("invalid ttlru cache parameter capacity: must be bigger than 0") - errInvalidTTLRUCachetTTLValue = errors.New("invalid ttlru cache parameter default_ttl: must be bigger than 0s") -) - -func ttlruMemCache(capacity int, - defaultTTL time.Duration, - initValues map[string]string, - optimistic bool, -) (*ttlruCacheAdapter, error) { - if capacity <= 0 { - return nil, errInvalidTTLRUCacheCapacityValue - } - - if defaultTTL <= 0 { - return nil, errInvalidTTLRUCachetTTLValue - } - - c := expirable.NewLRU[string, []byte](capacity, nil, defaultTTL) - - for k, v := range initValues { - _ = c.Add(k, []byte(v)) - } - - return &ttlruCacheAdapter{ - inner: c, - optimistic: optimistic, - }, nil -} - -var _ service.Cache = (*ttlruCacheAdapter)(nil) - -func (ca *ttlruCacheAdapter) Get(_ context.Context, key string) ([]byte, error) { - value, ok := ca.inner.Get(key) - if !ok { - return nil, service.ErrKeyNotFound - } - - return value, nil -} - -func (ca *ttlruCacheAdapter) Set(_ context.Context, key string, value []byte, _ *time.Duration) error { - _ = ca.inner.Add(key, value) - - return nil -} - -func (ca *ttlruCacheAdapter) unsafeAdd(key string, value []byte) error { - _, ok := ca.inner.Peek(key) - if ok { - return service.ErrKeyAlreadyExists - } - - _ = ca.inner.Add(key, value) - - return nil -} - -func (ca *ttlruCacheAdapter) Add(_ context.Context, key string, value []byte, _ *time.Duration) error { - if ca.optimistic { - return ca.unsafeAdd(key, value) - } - - ca.Lock() - - err := ca.unsafeAdd(key, value) - - ca.Unlock() - - return err -} - -func (ca *ttlruCacheAdapter) Delete(_ context.Context, key string) error { - _ = ca.inner.Remove(key) - - return nil -} - -func (ca *ttlruCacheAdapter) Close(_ context.Context) error { - return nil -} diff --git a/internal/impl/pure/cache_ttlru_test.go b/internal/impl/pure/cache_ttlru_test.go deleted file mode 100644 index 6b01c3a8be..0000000000 --- a/internal/impl/pure/cache_ttlru_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestTTLRUCacheDefault(t *testing.T) { - t.Parallel() - - defConf, err := ttlruCacheConfig().ParseYAML(``, nil) - require.NoError(t, err) - - logger := service.MockResources().Logger() - - c, err := ttlruMemCacheFromConfig(defConf, logger) - require.NoError(t, err) - - testServiceCache(t, c) -} - -func TestTTLRUCacheOptimistic(t *testing.T) { - t.Parallel() - - defConf, err := ttlruCacheConfig().ParseYAML(` -optimistic: true -without_reset: true # must be ignored -`, nil) - require.NoError(t, err) - - logger := service.MockResources().Logger() - - c, err := ttlruMemCacheFromConfig(defConf, logger) - require.NoError(t, err) - - testServiceCache(t, c) -} - -func TestTTLRUCacheInitValues(t *testing.T) { - t.Parallel() - - defConf, err := ttlruCacheConfig().ParseYAML(` -default_ttl: '5m' -cap: 1024 -init_values: - foo: bar - foo2: bar2 -`, nil) - require.NoError(t, err) - - logger := service.MockResources().Logger() - - c, err := ttlruMemCacheFromConfig(defConf, logger) - require.NoError(t, err) - - ctx := context.Background() - - exp := "bar" - if act, err := c.Get(ctx, "foo"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } - - exp = "bar2" - if act, err := c.Get(ctx, "foo2"); err != nil { - t.Error(err) - } else if string(act) != exp { - t.Errorf("Wrong result: %v != %v", string(act), exp) - } -} - -func BenchmarkTTLRU(b *testing.B) { - defConf, err := ttlruCacheConfig().ParseYAML(``, nil) - require.NoError(b, err) - - logger := service.MockResources().Logger() - - c, err := ttlruMemCacheFromConfig(defConf, logger) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - j := i % ttlruCacheFieldCapDefaultValue - key, value := fmt.Sprintf("key%v", j), []byte(fmt.Sprintf("foo%v", j)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } -} - -func BenchmarkTTLRUParallel(b *testing.B) { - defConf, err := ttlruCacheConfig().ParseYAML(``, nil) - require.NoError(b, err) - - logger := service.MockResources().Logger() - - c, err := ttlruMemCacheFromConfig(defConf, logger) - require.NoError(b, err) - - ctx := context.Background() - - b.ResetTimer() - - b.RunParallel(func(p *testing.PB) { - for i := 0; p.Next(); i++ { - j := i % ttlruCacheFieldCapDefaultValue - key, value := fmt.Sprintf("key%v", j), []byte(fmt.Sprintf("foo%v", j)) - - assert.NoError(b, c.Set(ctx, key, value, nil)) - - res, err := c.Get(ctx, key) - require.NoError(b, err) - assert.Equal(b, value, res) - } - }) -} diff --git a/internal/impl/pure/extended/zstd.go b/internal/impl/pure/extended/zstd.go deleted file mode 100644 index 87a89304be..0000000000 --- a/internal/impl/pure/extended/zstd.go +++ /dev/null @@ -1,26 +0,0 @@ -package extended - -import ( - "io" - - "github.com/klauspost/compress/zstd" - - "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -var _ = pure.AddKnownCompressionAlgorithm("zstd", pure.KnownCompressionAlgorithm{ - CompressWriter: func(level int, w io.Writer) (io.Writer, error) { - aw, err := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(level))) - if err != nil { - return nil, err - } - return &pure.CombinedWriteCloser{Primary: aw, Sink: w}, nil - }, - DecompressReader: func(r io.Reader) (io.Reader, error) { - ar, err := zstd.NewReader(r) - if err != nil { - return nil, err - } - return &pure.CombinedReadCloser{Primary: ar, Source: r}, nil - }, -}) diff --git a/internal/impl/pure/extended/zstd_test.go b/internal/impl/pure/extended/zstd_test.go deleted file mode 100644 index 73afdc719f..0000000000 --- a/internal/impl/pure/extended/zstd_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package extended - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestZstdCompressionDecompression(t *testing.T) { - exec, err := bloblang.Parse(`root = this.compress(algorithm: "zstd")`) - require.NoError(t, err) - - input := []byte("hello world this is a really long string") - - compressed, err := exec.Query(input) - require.NoError(t, err) - - assert.NotEqual(t, input, compressed) - assert.Greater(t, len(compressed.([]byte)), 1) - - exec, err = bloblang.Parse(`root = this.decompress(algorithm: "zstd")`) - require.NoError(t, err) - - decompressed, err := exec.Query(compressed) - require.NoError(t, err) - - assert.Equal(t, input, decompressed) -} diff --git a/internal/impl/pure/input_batched.go b/internal/impl/pure/input_batched.go deleted file mode 100644 index 47c02cac32..0000000000 --- a/internal/impl/pure/input_batched.go +++ /dev/null @@ -1,46 +0,0 @@ -package pure - -import ( - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/public/service" -) - -func batchedInputConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary("Consumes data from a child input and applies a batching policy to the stream."). - Description(`Batching at the input level is sometimes useful for processing across micro-batches, and can also sometimes be a useful performance trick. However, most inputs are fine without it so unless you have a specific plan for batching this component is not worth using.`). - Field(service.NewInputField("child").Description("The child input.")). - Field(service.NewBatchPolicyField("policy")). - Version("4.11.0") - return spec -} - -func init() { - err := service.RegisterBatchInput( - "batched", batchedInputConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - child, err := conf.FieldInput("child") - if err != nil { - return nil, err - } - - batcherPol, err := conf.FieldBatchPolicy("policy") - if err != nil { - return nil, err - } - - batcher, err := batcherPol.NewBatcher(mgr) - if err != nil { - return nil, err - } - - child = child.BatchedWith(batcher) - sChild := interop.UnwrapOwnedInput(child) - return interop.NewUnwrapInternalInput(sChild), nil - }) - if err != nil { - panic(err) - } -} diff --git a/internal/impl/pure/input_batched_test.go b/internal/impl/pure/input_batched_test.go deleted file mode 100644 index 4724928c1b..0000000000 --- a/internal/impl/pure/input_batched_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package pure - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func batchEquals(t testing.TB, exp []string, act service.MessageBatch) { - actStrs := make([]string, len(exp)) - for i, msg := range act { - actBytes, err := msg.AsBytes() - require.NoError(t, err) - actStrs[i] = string(actBytes) - } - assert.Equal(t, exp, actStrs) -} - -func TestBatchedInputBasic(t *testing.T) { - builder := service.NewStreamBuilder() - require.NoError(t, builder.AddInputYAML(` -batched: - child: - generate: - mapping: 'root.id = count("TEST_BATCHED_INPUT_BASIC")' - count: 10 - interval: "" - policy: - count: 5 -`)) - - var outBatches []service.MessageBatch - require.NoError(t, builder.AddBatchConsumerFunc(func(ctx context.Context, mb service.MessageBatch) error { - outBatches = append(outBatches, mb.DeepCopy()) - return nil - })) - - strm, err := builder.Build() - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - require.NoError(t, strm.Run(ctx)) - - require.Len(t, outBatches, 2) - require.Len(t, outBatches[0], 5) - require.Len(t, outBatches[1], 5) - - batchEquals(t, []string{ - `{"id":1}`, - `{"id":2}`, - `{"id":3}`, - `{"id":4}`, - `{"id":5}`, - }, outBatches[0]) - - batchEquals(t, []string{ - `{"id":6}`, - `{"id":7}`, - `{"id":8}`, - `{"id":9}`, - `{"id":10}`, - }, outBatches[1]) -} - -func TestBatchedInputProcessors(t *testing.T) { - builder := service.NewStreamBuilder() - require.NoError(t, builder.AddInputYAML(` -batched: - child: - generate: - mapping: 'root.id = count("TEST_BATCHED_INPUT_PROCESSORS")' - count: 10 - interval: "" - processors: - - mapping: 'root = content().uppercase()' - policy: - count: 5 -processors: - - mutation: 'root.x = batch_size()' - - mapping: 'root = content() + " and this"' -`)) - - var outBatches []service.MessageBatch - require.NoError(t, builder.AddBatchConsumerFunc(func(ctx context.Context, mb service.MessageBatch) error { - outBatches = append(outBatches, mb.DeepCopy()) - return nil - })) - - strm, err := builder.Build() - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - require.NoError(t, strm.Run(ctx)) - - require.Len(t, outBatches, 2) - require.Len(t, outBatches[0], 5) - require.Len(t, outBatches[1], 5) - - batchEquals(t, []string{ - `{"ID":1,"x":5} and this`, - `{"ID":2,"x":5} and this`, - `{"ID":3,"x":5} and this`, - `{"ID":4,"x":5} and this`, - `{"ID":5,"x":5} and this`, - }, outBatches[0]) - - batchEquals(t, []string{ - `{"ID":6,"x":5} and this`, - `{"ID":7,"x":5} and this`, - `{"ID":8,"x":5} and this`, - `{"ID":9,"x":5} and this`, - `{"ID":10,"x":5} and this`, - }, outBatches[1]) -} diff --git a/internal/impl/pure/input_broker.go b/internal/impl/pure/input_broker.go deleted file mode 100644 index de19282505..0000000000 --- a/internal/impl/pure/input_broker.go +++ /dev/null @@ -1,144 +0,0 @@ -package pure - -import ( - "errors" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/input/batcher" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/public/service" -) - -// ErrBrokerNoInputs is returned when creating a broker with zero inputs. -var ErrBrokerNoInputs = errors.New("attempting to create broker input type with no inputs") - -const ( - ibFieldCopies = "copies" - ibFieldInputs = "inputs" - ibFieldBatching = "batching" -) - -func brokerInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary("Allows you to combine multiple inputs into a single stream of data, where each input will be read in parallel."). - Description(` -A broker type is configured with its own list of input configurations and a field to specify how many copies of the list of inputs should be created. - -Adding more input types allows you to combine streams from multiple sources into one. For example, reading from both RabbitMQ and Kafka: - -`+"```yaml"+` -input: - broker: - copies: 1 - inputs: - - amqp_0_9: - urls: - - amqp://guest:guest@localhost:5672/ - consumer_tag: benthos-consumer - queue: benthos-queue - - # Optional list of input specific processing steps - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() - - - kafka: - addresses: - - localhost:9092 - client_id: benthos_kafka_input - consumer_group: benthos_consumer_group - topics: [ benthos_stream:0 ] -`+"```"+` - -If the number of copies is greater than zero the list will be copied that number of times. For example, if your inputs were of type foo and bar, with 'copies' set to '2', you would end up with two 'foo' inputs and two 'bar' inputs. - -== Batching - -It's possible to configure a xref:configuration:batching.adoc#batch-policy[batch policy] with a broker using the `+"`batching`"+` fields. When doing this the feeds from all child inputs are combined. Some inputs do not support broker based batching and specify this in their documentation. - -== Processors - -It is possible to configure xref:components:processors/about.adoc[processors] at the broker level, where they will be applied to _all_ child inputs, as well as on the individual child inputs. If you have processors at both the broker level _and_ on child inputs then the broker processors will be applied _after_ the child nodes processors.`). - Fields( - service.NewIntField(ibFieldCopies). - Description("Whatever is specified within `inputs` will be created this many times."). - Advanced(). - Default(1), - service.NewInputListField(ibFieldInputs). - Description("A list of inputs to create."), - service.NewBatchPolicyField("batching"), - ) -} - -func init() { - err := service.RegisterBatchInput("broker", brokerInputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - i, err := newBrokerInputFromParsed(conf, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalInput(i), nil - }) - if err != nil { - panic(err) - } -} - -func newBrokerInputFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (input.Streamed, error) { - copies, err := conf.FieldInt(ibFieldCopies) - if err != nil { - return nil, err - } - - children, err := conf.FieldInputList(ibFieldInputs) - if err != nil { - return nil, err - } - - if len(children) == 0 { - return nil, ErrBrokerNoInputs - } - - var b input.Streamed - if len(children) == 1 && copies == 1 { - b = interop.UnwrapOwnedInput(children[0]) - } else { - var inputs []input.Streamed - for _, v := range children { - inputs = append(inputs, interop.UnwrapOwnedInput(v)) - } - for j := 1; j < copies; j++ { - extraChildren, err := conf.FieldInputList(ibFieldInputs) - if err != nil { - return nil, err - } - for _, v := range extraChildren { - inputs = append(inputs, interop.UnwrapOwnedInput(v)) - } - } - if b, err = newFanInInputBroker(inputs); err != nil { - return nil, err - } - } - - batcherPol, err := conf.FieldBatchPolicy(ibFieldBatching) - if err != nil { - return nil, err - } - - if batcherPol.IsNoop() { - return b, nil - } - - pubBatcher, err := batcherPol.NewBatcher(mgr) - if err != nil { - return nil, err - } - - iBatcher := interop.UnwrapBatcher(pubBatcher) - return batcher.New(iBatcher, b, interop.UnwrapManagement(mgr).Logger()), nil -} diff --git a/internal/impl/pure/input_broker_fan_in.go b/internal/impl/pure/input_broker_fan_in.go deleted file mode 100644 index 0f99afa0ec..0000000000 --- a/internal/impl/pure/input_broker_fan_in.go +++ /dev/null @@ -1,137 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fanInInputBroker struct { - transactions chan message.Transaction - - closables []input.Streamed - inputClosedChan chan int - remainingMap map[int]struct{} - remainingMapMut sync.Mutex - - shutSig *shutdown.Signaller -} - -func newFanInInputBroker(inputs []input.Streamed) (*fanInInputBroker, error) { - if len(inputs) == 0 { - return nil, errors.New("fan in broker requires at least one input") - } - - i := &fanInInputBroker{ - transactions: make(chan message.Transaction), - - inputClosedChan: make(chan int), - remainingMap: make(map[int]struct{}), - - closables: []input.Streamed{}, - shutSig: shutdown.NewSignaller(), - } - - for n, input := range inputs { - i.closables = append(i.closables, input) - - // Keep track of # open inputs - i.remainingMap[n] = struct{}{} - - // Launch goroutine that async writes input into single channel - go func(index int) { - defer func() { - // If the input closes we need to signal to the broker - i.inputClosedChan <- index - }() - for { - var in message.Transaction - var open bool - select { - case in, open = <-inputs[index].TransactionChan(): - if !open { - return - } - case <-i.shutSig.HardStopChan(): - return - } - select { - case i.transactions <- in: - case <-i.shutSig.HardStopChan(): - return - } - } - }(n) - } - - go i.loop() - return i, nil -} - -func (i *fanInInputBroker) TransactionChan() <-chan message.Transaction { - return i.transactions -} - -func (i *fanInInputBroker) Connected() bool { - i.remainingMapMut.Lock() - defer i.remainingMapMut.Unlock() - - if len(i.remainingMap) == 0 { - return false - } - - for index := range i.remainingMap { - if !i.closables[index].Connected() { - return false - } - } - return true -} - -func (i *fanInInputBroker) loop() { - defer func() { - close(i.inputClosedChan) - close(i.transactions) - i.shutSig.TriggerHasStopped() - }() - - for { - index := <-i.inputClosedChan - - i.remainingMapMut.Lock() - delete(i.remainingMap, index) - remaining := len(i.remainingMap) - i.remainingMapMut.Unlock() - - if remaining == 0 { - return - } - } -} - -func (i *fanInInputBroker) TriggerStopConsuming() { - for _, closable := range i.closables { - closable.TriggerStopConsuming() - } -} - -func (i *fanInInputBroker) TriggerCloseNow() { - for _, closable := range i.closables { - closable.TriggerCloseNow() - } - i.shutSig.TriggerHardStop() -} - -func (i *fanInInputBroker) WaitForClose(ctx context.Context) error { - select { - case <-i.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/input_broker_fan_in_test.go b/internal/impl/pure/input_broker_fan_in_test.go deleted file mode 100644 index ef5b3a3689..0000000000 --- a/internal/impl/pure/input_broker_fan_in_test.go +++ /dev/null @@ -1,268 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "errors" - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ input.Streamed = &fanInInputBroker{} - -func TestBasicFanIn(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nInputs, nMsgs := 10, 1000 - - Inputs := []input.Streamed{} - mockInputs := []*mock.Input{} - - resChan := make(chan error) - - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - Inputs = append(Inputs, mockInputs[i]) - } - - fanIn, err := newFanInInputBroker(Inputs) - if err != nil { - t.Error(err) - return - } - - for i := 0; i < nMsgs; i++ { - for j := 0; j < nInputs; j++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case mockInputs[j].TChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second * 5): - t.Errorf("Timed out waiting for broker send: %v, %v", i, j) - return - } - go func() { - var ts message.Transaction - select { - case ts = <-fanIn.TransactionChan(): - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second * 5): - t.Errorf("Timed out waiting for broker propagate: %v, %v", i, j) - } - require.NoError(t, ts.Ack(ctx, nil)) - }() - select { - case <-resChan: - case <-time.After(time.Second * 5): - t.Errorf("Timed out waiting for response to input: %v, %v", i, j) - return - } - } - } - - fanIn.TriggerStopConsuming() - - if err := fanIn.WaitForClose(ctx); err != nil { - t.Error(err) - } -} - -func TestFanInConnected(t *testing.T) { - tInOne, tInTwo := make(chan message.Transaction), make(chan message.Transaction) - Inputs := []input.Streamed{ - &mock.Input{TChan: tInOne}, - &mock.Input{TChan: tInTwo}, - } - - fanIn, err := newFanInInputBroker(Inputs) - require.NoError(t, err) - - assert.True(t, fanIn.Connected()) - - close(tInOne) - time.Sleep(time.Millisecond * 100) - assert.True(t, fanIn.Connected()) - - close(tInTwo) - assert.Eventually(t, func() bool { - return !fanIn.Connected() - }, time.Second, time.Millisecond*10) -} - -func TestFanInShutdown(t *testing.T) { - nInputs := 10 - - Inputs := []input.Streamed{} - mockInputs := []*mock.Input{} - - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - Inputs = append(Inputs, mockInputs[i]) - } - - fanIn, err := newFanInInputBroker(Inputs) - if err != nil { - t.Error(err) - return - } - - for _, mockIn := range mockInputs { - select { - case _, open := <-mockIn.TransactionChan(): - if !open { - t.Error("fan in closed early") - } else { - t.Error("fan in sent unexpected message") - } - default: - } - close(mockIn.TChan) - } - - select { - case <-fanIn.TransactionChan(): - case <-time.After(time.Second * 5): - t.Error("fan in failed to close") - } -} - -func TestFanInAsync(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nInputs, nMsgs := 10, 1000 - - Inputs := []input.Streamed{} - mockInputs := []*mock.Input{} - - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - Inputs = append(Inputs, mockInputs[i]) - } - - fanIn, err := newFanInInputBroker(Inputs) - if err != nil { - t.Error(err) - return - } - - wg := sync.WaitGroup{} - wg.Add(nInputs) - - for j := 0; j < nInputs; j++ { - go func(index int) { - rChan := make(chan error) - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v %v", i, index))} - select { - case mockInputs[index].TChan <- message.NewTransaction(message.QuickBatch(content), rChan): - case <-time.After(time.Second * 5): - t.Errorf("Timed out waiting for broker send: %v, %v", i, index) - return - } - select { - case res := <-rChan: - if expected, actual := string(content[0]), res.Error(); expected != actual { - t.Errorf("Wrong response: %v != %v", expected, actual) - } - case <-time.After(time.Second * 5): - t.Errorf("Timed out waiting for response to input: %v, %v", i, index) - return - } - } - wg.Done() - }(j) - } - - for i := 0; i < nMsgs*nInputs; i++ { - var ts message.Transaction - select { - case ts = <-fanIn.TransactionChan(): - case <-time.After(time.Second * 5): - t.Errorf("Timed out waiting for broker propagate: %v", i) - return - } - require.NoError(t, ts.Ack(ctx, errors.New(string(ts.Payload.Get(0).AsBytes())))) - } - - wg.Wait() -} - -func BenchmarkBasicFanIn(b *testing.B) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nInputs := 10 - - Inputs := []input.Streamed{} - mockInputs := []*mock.Input{} - resChan := make(chan error) - - for i := 0; i < nInputs; i++ { - mockInputs = append(mockInputs, &mock.Input{ - TChan: make(chan message.Transaction), - }) - Inputs = append(Inputs, mockInputs[i]) - } - - fanIn, err := newFanInInputBroker(Inputs) - if err != nil { - b.Error(err) - return - } - - defer func() { - fanIn.TriggerStopConsuming() - fanIn.WaitForClose(ctx) - }() - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - for j := 0; j < nInputs; j++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case mockInputs[j].TChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second * 5): - b.Errorf("Timed out waiting for broker send: %v, %v", i, j) - return - } - var ts message.Transaction - select { - case ts = <-fanIn.TransactionChan(): - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - b.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second * 5): - b.Errorf("Timed out waiting for broker propagate: %v, %v", i, j) - return - } - require.NoError(b, ts.Ack(ctx, nil)) - select { - case <-resChan: - case <-time.After(time.Second * 5): - b.Errorf("Timed out waiting for response to input: %v, %v", i, j) - return - } - } - } - - b.StopTimer() -} diff --git a/internal/impl/pure/input_broker_test.go b/internal/impl/pure/input_broker_test.go deleted file mode 100644 index c26f95a12f..0000000000 --- a/internal/impl/pure/input_broker_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestBrokerConfigs(t *testing.T) { - for _, test := range []struct { - name string - config string - output map[string]int - }{ - { - name: "simple inputs", - config: ` -broker: - inputs: - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 2"' -`, - output: map[string]int{ - "hello world 1": 1, - "hello world 2": 1, - }, - }, - { - name: "inputs with copies", - config: ` -broker: - copies: 2 - inputs: - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 2"' -`, - output: map[string]int{ - "hello world 1": 2, - "hello world 2": 2, - }, - }, - { - name: "input processors", - config: ` -broker: - inputs: - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' - processors: - - bloblang: 'root = content().uppercase()' -processors: - - bloblang: 'root = "meow " + content().string()' -`, - output: map[string]int{ - "meow HELLO WORLD 1": 1, - }, - }, - { - name: "input processors to batcher", - config: ` -broker: - inputs: - - generate: - count: 3 - interval: "" - mapping: 'root = "hello world 1"' - processors: - - bloblang: 'root = content().uppercase()' - batching: - count: 3 - processors: - - archive: - format: lines - - bloblang: 'root = content() + " woof"' -processors: - - bloblang: 'root = "meow " + content().string()' -`, - output: map[string]int{ - "meow HELLO WORLD 1\nHELLO WORLD 1\nHELLO WORLD 1 woof": 1, - }, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - builder := service.NewEnvironment().NewStreamBuilder() - require.NoError(t, builder.AddInputYAML(test.config)) - require.NoError(t, builder.SetLoggerYAML(`level: none`)) - - outputMsgs := map[string]int{} - require.NoError(t, builder.AddConsumerFunc(func(ctx context.Context, msg *service.Message) error { - mBytes, _ := msg.AsBytes() - outputMsgs[string(mBytes)]++ - return nil - })) - - strm, err := builder.Build() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - require.NoError(t, strm.Run(tCtx)) - assert.Equal(t, test.output, outputMsgs) - }) - } -} diff --git a/internal/impl/pure/input_generate.go b/internal/impl/pure/input_generate.go deleted file mode 100644 index 2e39ac0580..0000000000 --- a/internal/impl/pure/input_generate.go +++ /dev/null @@ -1,278 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/robfig/cron/v3" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - giFieldMapping = "mapping" - giFieldInterval = "interval" - giFieldCount = "count" - giFieldBatchSize = "batch_size" -) - -func genInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Version("3.40.0"). - Summary("Generates messages at a given interval using a xref:guides:bloblang/about.adoc[Bloblang] mapping executed without a context. This allows you to generate messages for testing your pipeline configs."). - Fields( - service.NewBloblangField(giFieldMapping). - Description("A xref:guides:bloblang/about.adoc[Bloblang] mapping to use for generating messages."). - Examples( - `root = "hello world"`, - `root = {"test":"message","id":uuid_v4()}`, - ), - service.NewStringField(giFieldInterval). - Description("The time interval at which messages should be generated, expressed either as a duration string or as a cron expression. If set to an empty string messages will be generated as fast as downstream services can process them. Cron expressions can specify a timezone by prefixing the expression with `TZ=`, where the location name corresponds to a file within the IANA Time Zone database."). - Examples( - "5s", "1m", "1h", - "@every 1s", "0,30 */2 * * * *", "TZ=Europe/London 30 3-6,20-23 * * *", - ).Default("1s"), - service.NewIntField(giFieldCount). - Description("An optional number of messages to generate, if set above 0 the specified number of messages is generated and then the input will shut down."). - Default(0), - service.NewIntField(giFieldBatchSize). - Description("The number of generated messages that should be accumulated into each batch flushed at the specified interval."). - Default(1), - service.NewAutoRetryNacksToggleField(), - ). - Example("Cron Scheduled Processing", "A common use case for the generate input is to trigger processors on a schedule so that the processors themselves can behave similarly to an input. The following configuration reads rows from a PostgreSQL table every 5 minutes.", ` -input: - generate: - interval: '@every 5m' - mapping: 'root = {}' - processors: - - sql_select: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - table: foo - columns: [ "*" ] -`). - Example("Generate 100 Rows", "The generate input can be used as a convenient way to generate test data. The following example generates 100 rows of structured data by setting an explicit count. The interval field is set to empty, which means data is generated as fast as the downstream components can consume it.", ` -input: - generate: - count: 100 - interval: "" - mapping: | - root = if random_int() % 2 == 0 { - { - "type": "foo", - "foo": "is yummy" - } - } else { - { - "type": "bar", - "bar": "is gross" - } - } -`) -} - -func init() { - err := service.RegisterBatchInput("generate", genInputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - nm := interop.UnwrapManagement(mgr) - - var b input.Async - var err error - if b, err = newGenerateReaderFromParsed(conf, nm); err != nil { - return nil, err - } - - if autoRetry, _ := conf.FieldBool(service.AutoRetryNacksToggleFieldName); autoRetry { - b = input.NewAsyncPreserver(b) - } - - i, err := input.NewAsyncReader("generate", input.NewAsyncPreserver(b), nm) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalInput(i), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type generateReader struct { - remaining int - batchSize int - limited bool - firstIsFree bool - exec *mapping.Executor - timer *time.Ticker - schedule *cron.Schedule - schedulePrev *time.Time -} - -func newGenerateReaderFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (*generateReader, error) { - var ( - duration time.Duration - timer *time.Ticker - schedule *cron.Schedule - schedulePrev *time.Time - err error - firstIsFree = true - ) - - mappingStr, err := conf.FieldString(giFieldMapping) - if err != nil { - return nil, err - } - - intervalStr, err := conf.FieldString(giFieldInterval) - if err != nil { - return nil, err - } - - if intervalStr != "" { - if duration, err = time.ParseDuration(intervalStr); err != nil { - // interval is not a duration so try to parse as a cron expression - var cerr error - if schedule, cerr = parseCronExpression(intervalStr); cerr != nil { - return nil, fmt.Errorf("failed to parse interval as duration string: %v, or as cron expression: %w", err, cerr) - } - firstIsFree = false - - tNext := (*schedule).Next(time.Now()) - if duration = time.Until(tNext); duration < 1 { - duration = 1 - } - schedulePrev = &tNext - } - if duration > 0 { - timer = time.NewTicker(duration) - } - } - exec, err := mgr.BloblEnvironment().NewMapping(mappingStr) - if err != nil { - if perr, ok := err.(*parser.Error); ok { - return nil, fmt.Errorf("failed to parse mapping: %v", perr.ErrorAtPosition([]rune(mappingStr))) - } - return nil, fmt.Errorf("failed to parse mapping: %v", err) - } - - count, err := conf.FieldInt(giFieldCount) - if err != nil { - return nil, err - } - - batchSize, err := conf.FieldInt(giFieldBatchSize) - if err != nil { - return nil, err - } - - return &generateReader{ - exec: exec, - remaining: count, - batchSize: batchSize, - limited: count > 0, - timer: timer, - schedule: schedule, - schedulePrev: schedulePrev, - firstIsFree: firstIsFree, - }, nil -} - -func parseCronExpression(cronExpression string) (*cron.Schedule, error) { - // If time zone is not included, set default to UTC - if !strings.HasPrefix(cronExpression, "TZ=") { - cronExpression = fmt.Sprintf("TZ=%s %s", "UTC", cronExpression) - } - - parser := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) - cronSchedule, err := parser.Parse(cronExpression) - if err != nil { - return nil, err - } - - return &cronSchedule, nil -} - -// Connect establishes a Bloblang reader. -func (b *generateReader) Connect(ctx context.Context) error { - return nil -} - -// ReadBatch a new bloblang generated message. -func (b *generateReader) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - batchSize := b.batchSize - if b.limited { - if b.remaining <= 0 { - return nil, nil, component.ErrTypeClosed - } - if b.remaining < batchSize { - batchSize = b.remaining - } - } - - if !b.firstIsFree && b.timer != nil { - select { - case t, open := <-b.timer.C: - if !open { - return nil, nil, component.ErrTypeClosed - } - if b.schedule != nil { - if b.schedulePrev != nil { - t = *b.schedulePrev - } - - tNext := (*b.schedule).Next(t) - tNow := time.Now() - duration := tNext.Sub(tNow) - if duration < 1 { - duration = 1 - } - - b.schedulePrev = &tNext - b.timer.Reset(duration) - } - case <-ctx.Done(): - return nil, nil, component.ErrTimeout - } - } - b.firstIsFree = false - - batch := make(message.Batch, 0, batchSize) - for i := 0; i < batchSize; i++ { - p, err := b.exec.MapPart(0, batch) - if err != nil { - return nil, nil, err - } - if p != nil { - if b.limited { - b.remaining-- - } - batch = append(batch, p) - } - } - if len(batch) == 0 { - return nil, nil, component.ErrTimeout - } - return batch, func(context.Context, error) error { return nil }, nil -} - -// CloseAsync shuts down the bloblang reader. -func (b *generateReader) Close(ctx context.Context) (err error) { - if b.timer != nil { - b.timer.Stop() - } - return -} diff --git a/internal/impl/pure/input_generate_test.go b/internal/impl/pure/input_generate_test.go deleted file mode 100644 index 4478729929..0000000000 --- a/internal/impl/pure/input_generate_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -func testGenReader(t testing.TB, confStr string, args ...any) *generateReader { - pConf, err := genInputSpec().ParseYAML(fmt.Sprintf(confStr, args...), nil) - require.NoError(t, err) - - r, err := newGenerateReaderFromParsed(pConf, mock.NewManager()) - require.NoError(t, err) - - return r -} - -func TestBloblangInterval(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*80) - defer done() - - b := testGenReader(t, ` -mapping: 'root = "hello world"' -interval: 50ms -`) - - err := b.Connect(ctx) - require.NoError(t, err) - - // First read is immediate. - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, "hello world", string(m.Get(0).AsBytes())) - - // Second takes 50ms. - m, _, err = b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, "hello world", string(m.Get(0).AsBytes())) - - // Third takes another 50ms and therefore times out. - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "action timed out") - - require.NoError(t, b.Close(context.Background())) -} - -func TestBloblangZeroInterval(t *testing.T) { - _ = testGenReader(t, ` -mapping: 'root = "hello world"' -interval: 0s -`) -} - -func TestBloblangCron(t *testing.T) { - t.Skip() - - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*1100) - defer done() - - b := testGenReader(t, ` -mapping: 'root = "hello world"' -interval: '@every 1s' -`) - - assert.NotNil(t, b.schedule) - - err := b.Connect(ctx) - require.NoError(t, err) - - // First takes 1s so. - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, "hello world", string(m.Get(0).AsBytes())) - - // Second takes another 1s and therefore times out. - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "action timed out") - - require.NoError(t, b.Close(context.Background())) -} - -func TestBloblangMapping(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*100) - defer done() - - b := testGenReader(t, ` -mapping: | - root = { - "id": count("docs") - } -interval: 1ms -`) - - err := b.Connect(ctx) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, fmt.Sprintf(`{"id":%v}`, i+1), string(m.Get(0).AsBytes())) - } -} - -func TestBloblangRemaining(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*100) - defer done() - - b := testGenReader(t, ` -mapping: 'root = "foobar"' -interval: 1ms -count: 10 -`) - - err := b.Connect(ctx) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, "foobar", string(m.Get(0).AsBytes())) - } - - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "type was closed") - - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "type was closed") - - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "type was closed") -} - -func TestBloblangRemainingBatched(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*100) - defer done() - - b := testGenReader(t, ` -mapping: 'root = "foobar"' -interval: 1ms -count: 9 -batch_size: 2 -`) - - err := b.Connect(ctx) - require.NoError(t, err) - - for i := 0; i < 5; i++ { - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - if i == 4 { - require.Equal(t, 1, m.Len()) - } else { - require.Equal(t, 2, m.Len()) - assert.Equal(t, "foobar", string(m[1].AsBytes())) - } - assert.Equal(t, "foobar", string(m[0].AsBytes())) - } - - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "type was closed") - - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "type was closed") - - _, _, err = b.ReadBatch(ctx) - assert.EqualError(t, err, "type was closed") -} - -func TestBloblangUnbounded(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*100) - defer done() - - b := testGenReader(t, ` -mapping: 'root = "foobar"' -interval: 0s -`) - - err := b.Connect(ctx) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, "foobar", string(m.Get(0).AsBytes())) - } - - require.NoError(t, b.Close(context.Background())) -} - -func TestBloblangUnboundedEmpty(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*100) - defer done() - - b := testGenReader(t, ` -mapping: 'root = "foobar"' -interval: "" -`) - - err := b.Connect(ctx) - require.NoError(t, err) - - for i := 0; i < 10; i++ { - m, _, err := b.ReadBatch(ctx) - require.NoError(t, err) - require.Equal(t, 1, m.Len()) - assert.Equal(t, "foobar", string(m.Get(0).AsBytes())) - } - - require.NoError(t, b.Close(context.Background())) -} diff --git a/internal/impl/pure/input_inproc.go b/internal/impl/pure/input_inproc.go deleted file mode 100644 index 60b993c116..0000000000 --- a/internal/impl/pure/input_inproc.go +++ /dev/null @@ -1,130 +0,0 @@ -package pure - -import ( - "context" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func inprocInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Description(` -Directly connect to an output within a Benthos process by referencing it by a chosen ID. This allows you to hook up isolated streams whilst running Benthos in ` + "xref:guides:streams_mode/about.adoc[streams mode]" + `, it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. - -It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs.`). - Field(service.NewStringField("").Default("")) -} - -func init() { - err := service.RegisterBatchInput("inproc", inprocInputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - name, err := conf.FieldString() - if err != nil { - return nil, err - } - nm := interop.UnwrapManagement(mgr) - inprocRdr := &inprocInput{ - pipe: name, - mgr: nm, - log: nm.Logger(), - stats: nm.Metrics(), - transactions: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - go inprocRdr.loop() - return interop.NewUnwrapInternalInput(inprocRdr), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type inprocInput struct { - pipe string - mgr bundle.NewManagement - stats metrics.Type - log log.Modular - - transactions chan message.Transaction - - shutSig *shutdown.Signaller -} - -func (i *inprocInput) loop() { - defer func() { - close(i.transactions) - i.shutSig.TriggerHasStopped() - }() - - var inprocChan <-chan message.Transaction - -messageLoop: - for !i.shutSig.IsSoftStopSignalled() { - if inprocChan == nil { - for { - var err error - if inprocChan, err = i.mgr.GetPipe(i.pipe); err != nil { - i.log.Error("Failed to connect to inproc output '%v': %v\n", i.pipe, err) - select { - case <-time.After(time.Second): - case <-i.shutSig.SoftStopChan(): - return - } - } else { - i.log.Info("Receiving inproc messages from ID: %s\n", i.pipe) - break - } - } - } - select { - case t, open := <-inprocChan: - if !open { - inprocChan = nil - continue messageLoop - } - select { - case i.transactions <- t: - case <-i.shutSig.SoftStopChan(): - return - } - case <-i.shutSig.SoftStopChan(): - return - } - } -} - -func (i *inprocInput) TransactionChan() <-chan message.Transaction { - return i.transactions -} - -func (i *inprocInput) Connected() bool { - return true -} - -func (i *inprocInput) TriggerStopConsuming() { - i.shutSig.TriggerSoftStop() -} - -func (i *inprocInput) TriggerCloseNow() { - i.shutSig.TriggerHardStop() -} - -func (i *inprocInput) WaitForClose(ctx context.Context) error { - select { - case <-i.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/input_inproc_test.go b/internal/impl/pure/input_inproc_test.go deleted file mode 100644 index a4c3b0b3b1..0000000000 --- a/internal/impl/pure/input_inproc_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestInprocDryRun(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - t.Parallel() - - mgr, err := manager.New(manager.NewResourceConfig()) - if err != nil { - t.Fatal(err) - } - - mgr.SetPipe("foo", make(chan message.Transaction)) - - ip := testInput(t, ` -inproc: foo -`) - - <-time.After(time.Millisecond * 100) - - ip.TriggerStopConsuming() - if err = ip.WaitForClose(ctx); err != nil { - t.Error(err) - } -} - -func TestInprocDryRunNoConn(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - t.Parallel() - - ip := testInput(t, ` -inproc: foo -`) - - <-time.After(time.Millisecond * 100) - - ip.TriggerStopConsuming() - require.NoError(t, ip.WaitForClose(ctx)) -} diff --git a/internal/impl/pure/input_read_until.go b/internal/impl/pure/input_read_until.go deleted file mode 100644 index e524540bb8..0000000000 --- a/internal/impl/pure/input_read_until.go +++ /dev/null @@ -1,343 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sync/atomic" - "time" - - "github.com/cenkalti/backoff/v4" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - ruiFieldInput = "input" - ruiFieldRestart = "restart_input" - ruiFieldCheck = "check" - ruiFieldIdleTimeout = "idle_timeout" -) - -func readUntilInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary("Reads messages from a child input until a consumed message passes a xref:guides:bloblang/about.adoc[Bloblang query], at which point the input closes. It is also possible to configure a timeout after which the input is closed if no new messages arrive in that period."). - Description(` -Messages are read continuously while the query check returns false, when the query returns true the message that triggered the check is sent out and the input is closed. Use this to define inputs where the stream should end once a certain message appears. - -If the idle timeout is configured, the input will be closed if no new messages arrive after that period of time. Use this field if you want to empty out and close an input that doesn't have a logical end. - -Sometimes inputs close themselves. For example, when the `+"`file`"+` input type reaches the end of a file it will shut down. By default this type will also shut down. If you wish for the input type to be restarted every time it shuts down until the query check is met then set `+"`restart_input` to `true`."+` - -== Metadata - -A metadata key `+"`benthos_read_until` containing the value `final`"+` is added to the first part of the message that triggers the input to stop.`). - Example( - "Consume N Messages", - "A common reason to use this input is to consume only N messages from an input and then stop. This can easily be done with the xref:guides:bloblang/functions.adoc#count[`count` function]:", - ` -# Only read 100 messages, and then exit. -input: - read_until: - check: count("messages") >= 100 - input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup -`, - ). - Example( - "Read from a kafka and close when empty", - "A common reason to use this input is a job that consumes all messages and exits once its empty:", - ` -# Consumes all messages and exit when the last message was consumed 5s ago. -input: - read_until: - idle_timeout: 5s - input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup -`, - ).Fields( - service.NewInputField(ruiFieldInput). - Description("The child input to consume from."), - service.NewBloblangField(ruiFieldCheck). - Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether the input should now be closed."). - Examples( - `this.type == "foo"`, - `count("messages") >= 100`, - ). - Optional(), - service.NewDurationField(ruiFieldIdleTimeout). - Description("The maximum amount of time without receiving new messages after which the input is closed."). - Example("5s"). - Optional(), - service.NewBoolField(ruiFieldRestart). - Description("Whether the input should be reopened if it closes itself before the condition has resolved to true."). - Default(false), - ) -} - -func init() { - err := service.RegisterBatchInput("read_until", readUntilInputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - i, err := newReadUntilInputFromParsed(conf, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalInput(i), nil - }) - if err != nil { - panic(err) - } -} - -type readUntilInput struct { - restart bool - - wrappedInputLocked *atomic.Pointer[input.Streamed] - check *mapping.Executor - idleTimeout time.Duration - - wrappedCtor func() (input.Streamed, error) - - log log.Modular - - transactions chan message.Transaction - - shutSig *shutdown.Signaller -} - -func newReadUntilInputFromParsed(conf *service.ParsedConfig, res *service.Resources) (input.Streamed, error) { - mgr := interop.UnwrapManagement(res) - - wrappedCtor := func() (input.Streamed, error) { - ownedInput, err := conf.FieldInput(ruiFieldInput) - if err != nil { - return nil, err - } - return interop.UnwrapOwnedInput(ownedInput), nil - } - - wrapped, err := wrappedCtor() - if err != nil { - return nil, err - } - - restart, err := conf.FieldBool(ruiFieldRestart) - if err != nil { - return nil, err - } - - var check *mapping.Executor - if checkStr, _ := conf.FieldString(ruiFieldCheck); checkStr != "" { - if check, err = mgr.BloblEnvironment().NewMapping(checkStr); err != nil { - return nil, fmt.Errorf("failed to parse check query: %w", err) - } - } - - var idleTimeout time.Duration = -1 - if idleTimeoutStr, _ := conf.FieldString(ruiFieldIdleTimeout); idleTimeoutStr != "" { - if idleTimeout, err = time.ParseDuration(idleTimeoutStr); err != nil { - return nil, fmt.Errorf("failed to parse idle_timeout string: %v", err) - } - } - - if check == nil && idleTimeout < 0 { - return nil, errors.New("it is required to set either check or idle_timeout") - } - - wInputLocked := &atomic.Pointer[input.Streamed]{} - wInputLocked.Store(&wrapped) - rdr := &readUntilInput{ - restart: restart, - - wrappedCtor: wrappedCtor, - wrappedInputLocked: wInputLocked, - - log: mgr.Logger(), - check: check, - idleTimeout: idleTimeout, - transactions: make(chan message.Transaction), - - shutSig: shutdown.NewSignaller(), - } - - go rdr.loop() - return rdr, nil -} - -func (r *readUntilInput) loop() { - defer func() { - wrappedP := r.wrappedInputLocked.Load() - if wrappedP != nil { - wrapped := *wrappedP - wrapped.TriggerStopConsuming() - wrapped.TriggerCloseNow() - _ = wrapped.WaitForClose(context.Background()) - } - - close(r.transactions) - r.shutSig.TriggerHasStopped() - }() - - // Prevents busy loop when an input never yields messages. - restartBackoff := backoff.NewExponentialBackOff() - restartBackoff.InitialInterval = time.Millisecond - restartBackoff.MaxInterval = time.Millisecond * 100 - restartBackoff.MaxElapsedTime = 0 - - var open bool - - closeCtx, done := r.shutSig.SoftStopCtx(context.Background()) - defer done() - -runLoop: - for !r.shutSig.IsSoftStopSignalled() { - var wrapped input.Streamed - wrappedP := r.wrappedInputLocked.Load() - if wrappedP == nil { - if r.restart { - select { - case <-time.After(restartBackoff.NextBackOff()): - case <-r.shutSig.SoftStopChan(): - return - } - var err error - if wrapped, err = r.wrappedCtor(); err != nil { - r.log.Error("Failed to create input: %v\n", err) - return - } - r.wrappedInputLocked.Store(&wrapped) - } else { - return - } - } else { - wrapped = *wrappedP - } - - var tran message.Transaction - { - timeoutChan, timeoutDone := resetIdleTimeout(r.idleTimeout) - select { - case tran, open = <-wrapped.TransactionChan(): - timeoutDone() - if !open { - r.wrappedInputLocked.Store(nil) - continue runLoop - } - restartBackoff.Reset() - case <-r.shutSig.SoftStopChan(): - timeoutDone() - return - case <-timeoutChan: - timeoutDone() - r.log.Info("Idle timeout reached") - return - } - } - - var err error - check := false - if r.check != nil { - check, err = r.check.QueryPart(0, tran.Payload) - if err != nil { - check = false - r.log.Error("Failed to execute check query: %v\n", err) - } - } - if !check { - select { - case r.transactions <- tran: - case <-r.shutSig.SoftStopChan(): - return - } - continue - } - - tran.Payload.Get(0).MetaSetMut("benthos_read_until", "final") - - // If this transaction succeeds we shut down. - tmpRes := make(chan error) - select { - case r.transactions <- message.NewTransaction(tran.Payload, tmpRes): - case <-r.shutSig.SoftStopChan(): - return - } - - var res error - select { - case res, open = <-tmpRes: - if !open { - return - } - streamEnds := res == nil - if err := tran.Ack(closeCtx, res); err != nil && r.shutSig.IsSoftStopSignalled() { - return - } - if streamEnds { - return - } - case <-r.shutSig.SoftStopChan(): - return - } - } -} - -// TransactionChan returns a transactions channel for consuming messages from -// this input type. -func (r *readUntilInput) TransactionChan() <-chan message.Transaction { - return r.transactions -} - -// Connected returns a boolean indicating whether this input is currently -// connected to its target. -func (r *readUntilInput) Connected() bool { - wrappedP := r.wrappedInputLocked.Load() - if wrappedP != nil { - i := *wrappedP - return i.Connected() - } - return false -} - -func (r *readUntilInput) TriggerStopConsuming() { - r.shutSig.TriggerSoftStop() -} - -func (r *readUntilInput) TriggerCloseNow() { - r.shutSig.TriggerHardStop() -} - -func (r *readUntilInput) WaitForClose(ctx context.Context) error { - select { - case <-r.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func resetIdleTimeout(d time.Duration) (waitForTimeout <-chan time.Time, done func()) { - done = func() {} - if d > 0 { - timer := time.NewTimer(d) - waitForTimeout = timer.C - done = func() { - _ = timer.Stop() - } - } - return -} diff --git a/internal/impl/pure/input_read_until_test.go b/internal/impl/pure/input_read_until_test.go deleted file mode 100644 index 3218938d02..0000000000 --- a/internal/impl/pure/input_read_until_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package pure_test - -import ( - "context" - "errors" - "fmt" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - bmock "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestReadUntilErrs(t *testing.T) { - conf, err := testutil.InputFromYAML(` -read_until: - input: - stdin: {} -`) - require.NoError(t, err) - - _, err = bmock.NewManager().NewInput(conf) - assert.EqualError(t, err, "failed to init input : it is required to set either check or idle_timeout") -} - -func TestReadUntilInput(t *testing.T) { - content := []byte(`foo -bar -baz`) - - tmpfile, err := os.CreateTemp("", "benthos_read_until_test") - if err != nil { - t.Fatal(err) - } - - defer os.Remove(tmpfile.Name()) - - if _, err := tmpfile.Write(content); err != nil { - t.Fatal(err) - } - if err := tmpfile.Close(); err != nil { - t.Fatal(err) - } - - inConfStr := fmt.Sprintf(` -read_until: - input: - file: - paths: [ "%v" ] -`, tmpfile.Name()) - - t.Run("ReadUntilBasic", func(te *testing.T) { - testReadUntilBasic(inConfStr, te) - }) - t.Run("ReadUntilRestart", func(te *testing.T) { - testReadUntilRestart(inConfStr, te) - }) - t.Run("ReadUntilRetry", func(te *testing.T) { - testReadUntilRetry(inConfStr, te) - }) -} - -func testReadUntilBasic(inConf string, t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - rConf, err := testutil.InputFromYAML(inConf + ` - check: 'content() == "bar"' -`) - require.NoError(t, err) - - in, err := bmock.NewManager().NewInput(rConf) - if err != nil { - t.Fatal(err) - } - - expMsgs := []string{ - "foo", - "bar", - } - - for i, expMsg := range expMsgs { - var tran message.Transaction - var open bool - select { - case tran, open = <-in.TransactionChan(): - if !open { - t.Fatal("transaction chan closed") - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if exp, act := expMsg, string(tran.Payload.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message contents: %v != %v", act, exp) - } - if i == len(expMsgs)-1 { - if exp, act := "final", tran.Payload.Get(0).MetaGetStr("benthos_read_until"); exp != act { - t.Errorf("Metadata missing from final message: %v != %v", act, exp) - } - } else if exp, act := "", tran.Payload.Get(0).MetaGetStr("benthos_read_until"); exp != act { - t.Errorf("Metadata final message metadata added to non-final message: %v", act) - } - require.NoError(t, tran.Ack(ctx, nil)) - } - - // Should close automatically now - select { - case _, open := <-in.TransactionChan(): - if open { - t.Fatal("transaction chan not closed") - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if err = in.WaitForClose(ctx); err != nil { - t.Fatal(err) - } -} - -func testReadUntilRestart(inConf string, t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - rConf, err := testutil.InputFromYAML(inConf + ` - check: 'false' - restart_input: true -`) - require.NoError(t, err) - - in, err := bmock.NewManager().NewInput(rConf) - require.NoError(t, err) - - expMsgs := []string{ - "foo", - "bar", - "baz", - } - - for i := 0; i < 3; i++ { - for _, expMsg := range expMsgs { - var tran message.Transaction - var open bool - select { - case tran, open = <-in.TransactionChan(): - require.True(t, open) - case <-time.After(time.Second): - t.Fatal("timed out") - } - - require.Len(t, tran.Payload, 1) - assert.Equal(t, expMsg, string(tran.Payload[0].AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - } - } - - in.TriggerStopConsuming() - require.NoError(t, in.WaitForClose(ctx)) -} - -func testReadUntilRetry(inConf string, t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - rConf, err := testutil.InputFromYAML(inConf + ` - check: 'content() == "bar"' -`) - require.NoError(t, err) - - in, err := bmock.NewManager().NewInput(rConf) - if err != nil { - t.Fatal(err) - } - - expMsgs := map[string]struct{}{ - "foo": {}, - "bar": {}, - } - - var tran message.Transaction - var open bool - - resFns := []func(context.Context, error) error{} - i := 0 - for len(expMsgs) > 0 && i < 10 { - // First try - select { - case tran, open = <-in.TransactionChan(): - if !open { - t.Fatalf("transaction chan closed at %v", i) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - i++ - act := string(tran.Payload.Get(0).AsBytes()) - if _, exists := expMsgs[act]; !exists { - t.Errorf("Unexpected message contents '%v': %v", i, act) - } else { - delete(expMsgs, act) - } - { - tmpTran := tran - resFns = append(resFns, tmpTran.Ack) - } - } - - select { - case <-in.TransactionChan(): - t.Error("Unexpected transaction") - return - case <-time.After(time.Millisecond * 500): - } - - for _, rFn := range resFns { - require.NoError(t, rFn(ctx, errors.New("failed"))) - } - - expMsgs = map[string]struct{}{ - "foo": {}, - "bar": {}, - "baz": {}, - } - -remainingLoop: - for len(expMsgs) > 0 { - // Second try - select { - case tran, open = <-in.TransactionChan(): - if !open { - break remainingLoop - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - act := string(tran.Payload.Get(0).AsBytes()) - if _, exists := expMsgs[act]; !exists { - t.Errorf("Unexpected message contents '%v': %v", i, act) - } else { - delete(expMsgs, act) - } - require.NoError(t, tran.Ack(ctx, nil)) - } - if len(expMsgs) == 3 { - t.Error("Expected at least one extra message") - } - - // Should close automatically now - select { - case _, open := <-in.TransactionChan(): - if open { - t.Fatal("transaction chan not closed") - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if err = in.WaitForClose(ctx); err != nil { - t.Fatal(err) - } -} - -func TestReadUntilTimeout(t *testing.T) { - conf, err := testutil.InputFromYAML(` -read_until: - idle_timeout: 100ms - input: - generate: - count: 1000 - interval: 1s - mapping: 'root.id = counter()' -`) - require.NoError(t, err) - - strm, err := bmock.NewManager().NewInput(conf) - require.NoError(t, err) - - tran, open := <-strm.TransactionChan() - require.True(t, open) - require.Len(t, tran.Payload, 1) - assert.Equal(t, `{"id":1}`, string(tran.Payload[0].AsBytes())) - require.NoError(t, tran.Ack(context.Background(), nil)) - - _, open = <-strm.TransactionChan() - require.False(t, open) -} diff --git a/internal/impl/pure/input_resource.go b/internal/impl/pure/input_resource.go deleted file mode 100644 index 063308d450..0000000000 --- a/internal/impl/pure/input_resource.go +++ /dev/null @@ -1,171 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func resourceInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Resource is an input type that channels messages from a resource input, identified by its name.`). - Description(`Resources allow you to tidy up deeply nested configs. For example, the config: - -` + "```yaml" + ` -input: - broker: - inputs: - - kafka: - addresses: [ TODO ] - topics: [ foo ] - consumer_group: foogroup - - gcp_pubsub: - project: bar - subscription: baz -` + "```" + ` - -Could also be expressed as: - -` + "```yaml" + ` -input: - broker: - inputs: - - resource: foo - - resource: bar - -input_resources: - - label: foo - kafka: - addresses: [ TODO ] - topics: [ foo ] - consumer_group: foogroup - - - label: bar - gcp_pubsub: - project: bar - subscription: baz -` + "```" + ` - -Resources also allow you to reference a single input in multiple places, such as multiple streams mode configs, or multiple entries in a broker input. However, when a resource is referenced more than once the messages it produces are distributed across those references, so each message will only be directed to a single reference, not all of them. - -You can find out more about resources in xref:configuration:resources.adoc[].`). - Field(service.NewStringField("").Default("")) -} - -func init() { - err := service.RegisterBatchInput("resource", resourceInputSpec(), func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - name, err := conf.FieldString() - if err != nil { - return nil, err - } - if !mgr.HasInput(name) { - return nil, fmt.Errorf("input resource '%v' was not found", name) - } - nm := interop.UnwrapManagement(mgr) - ri := &resourceInput{ - mgr: nm, - name: name, - log: nm.Logger(), - tChan: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - go ri.loop() - return interop.NewUnwrapInternalInput(ri), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type resourceInput struct { - mgr bundle.NewManagement - tChan chan message.Transaction - name string - log log.Modular - shutSig *shutdown.Signaller -} - -func (r *resourceInput) loop() { - defer func() { - close(r.tChan) - r.shutSig.TriggerHasStopped() - }() - - for { - var resourceTChan <-chan message.Transaction - if err := r.mgr.AccessInput(context.Background(), r.name, func(i input.Streamed) { - resourceTChan = i.TransactionChan() - }); err != nil { - r.log.Error("Failed to obtain input resource '%v': %v", r.name, err) - select { - case <-r.shutSig.SoftStopChan(): - return - case <-time.After(time.Second): - } - continue - } - - for { - select { - case <-r.shutSig.SoftStopChan(): - return - case t, open := <-resourceTChan: - if !open { - return - } - select { - case r.tChan <- t: - case <-r.shutSig.HardStopChan(): - go func() { - _ = t.Ack(context.Background(), component.ErrFailedSend) - }() - return - } - } - } - } -} - -func (r *resourceInput) TransactionChan() (tChan <-chan message.Transaction) { - return r.tChan -} - -func (r *resourceInput) Connected() (isConnected bool) { - if err := r.mgr.AccessInput(context.Background(), r.name, func(i input.Streamed) { - isConnected = i.Connected() - }); err != nil { - r.log.Error("Failed to obtain input resource '%v': %v", r.name, err) - } - return -} - -func (r *resourceInput) TriggerStopConsuming() { - r.shutSig.TriggerSoftStop() -} - -func (r *resourceInput) TriggerCloseNow() { - r.shutSig.TriggerHardStop() -} - -func (r *resourceInput) WaitForClose(ctx context.Context) error { - select { - case <-r.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/input_resource_test.go b/internal/impl/pure/input_resource_test.go deleted file mode 100644 index ab43e6b036..0000000000 --- a/internal/impl/pure/input_resource_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestResourceInput(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mgr := mock.NewManager() - mgr.Inputs["foo"] = mock.NewInput([]message.Batch{ - {message.NewPart([]byte("hello world 1"))}, - {message.NewPart([]byte("hello world 2"))}, - {message.NewPart([]byte("hello world 3"))}, - }) - - nConf := input.NewConfig() - nConf.Type = "resource" - nConf.Plugin = "foo" - - p, err := mgr.NewInput(nConf) - require.NoError(t, err) - - tChan := p.TransactionChan() - readTran := func() message.Transaction { - select { - case tran, open := <-tChan: - require.True(t, open) - return tran - case <-ctx.Done(): - t.Fatal("timed out") - } - return message.Transaction{} - } - - tr := readTran() - require.Len(t, tr.Payload, 1) - assert.Equal(t, "hello world 1", string(tr.Payload[0].AsBytes())) - require.NoError(t, tr.Ack(ctx, nil)) - - tr = readTran() - require.Len(t, tr.Payload, 1) - assert.Equal(t, "hello world 2", string(tr.Payload[0].AsBytes())) - require.NoError(t, tr.Ack(ctx, nil)) - - tr = readTran() - require.Len(t, tr.Payload, 1) - assert.Equal(t, "hello world 3", string(tr.Payload[0].AsBytes())) - require.NoError(t, tr.Ack(ctx, nil)) - - select { - case _, open := <-tChan: - assert.False(t, open) - case <-ctx.Done(): - t.Error("timed out") - } - assert.NoError(t, p.WaitForClose(ctx)) -} - -func TestResourceInputEarlyTermination(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mgr := mock.NewManager() - mgr.Inputs["foo"] = mock.NewInput([]message.Batch{ - {message.NewPart([]byte("hello world 1"))}, - {message.NewPart([]byte("hello world 2"))}, - {message.NewPart([]byte("hello world 3"))}, - }) - - nConf := input.NewConfig() - nConf.Type = "resource" - nConf.Plugin = "foo" - - p, err := mgr.NewInput(nConf) - require.NoError(t, err) - - tChan := p.TransactionChan() - readTran := func() message.Transaction { - select { - case tran, open := <-tChan: - require.True(t, open) - return tran - case <-ctx.Done(): - t.Fatal("timed out") - } - return message.Transaction{} - } - - tr := readTran() - require.Len(t, tr.Payload, 1) - assert.Equal(t, "hello world 1", string(tr.Payload[0].AsBytes())) - require.NoError(t, tr.Ack(ctx, nil)) - - p.TriggerStopConsuming() - - assert.Eventually(t, func() bool { - select { - case _, open := <-tChan: - return !open - case <-ctx.Done(): - return false - } - }, time.Second, time.Millisecond) - - assert.NoError(t, p.WaitForClose(ctx)) -} - -func TestResourceInputBadName(t *testing.T) { - conf := input.NewConfig() - conf.Type = "resource" - conf.Plugin = "foo" - - _, err := mock.NewManager().NewInput(conf) - if err == nil { - t.Error("expected error from bad resource") - } -} diff --git a/internal/impl/pure/input_sequence.go b/internal/impl/pure/input_sequence.go deleted file mode 100644 index e757ab116e..0000000000 --- a/internal/impl/pure/input_sequence.go +++ /dev/null @@ -1,627 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/OneOfOne/xxhash" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - siFieldShardedJoinType = "type" - siFieldShardedJoinIDPath = "id_path" - siFieldShardedJoinIterations = "iterations" - siFieldShardedJoinMergeStrategy = "merge_strategy" - siFieldShardedJoin = "sharded_join" - siFieldInputs = "inputs" -) - -func sequenceInputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary("Reads messages from a sequence of child inputs, starting with the first and once that input gracefully terminates starts consuming from the next, and so on."). - Description("This input is useful for consuming from inputs that have an explicit end but must not be consumed in parallel."). - Fields( - service.NewObjectField(siFieldShardedJoin, - // TODO: V5 Remove "full-outter" and "outter" - service.NewStringEnumField(siFieldShardedJoinType, "none", "full-outer", "outer", "full-outter", "outter"). - Description("The type of join to perform. A `full-outer` ensures that all identifiers seen in any of the input sequences are sent, and is performed by consuming all input sequences before flushing the joined results. An `outer` join consumes all input sequences but only writes data joined from the last input in the sequence, similar to a left or right outer join. With an `outer` join if an identifier appears multiple times within the final sequence input it will be flushed each time it appears. `full-outter` and `outter` have been deprecated in favour of `full-outer` and `outer`."). - Default("none"), - service.NewStringField(siFieldShardedJoinIDPath). - Description("A xref:configuration:field_paths.adoc[dot path] that points to a common field within messages of each fragmented data set and can be used to join them. Messages that are not structured or are missing this field will be dropped. This field must be set in order to enable joins."). - Default(""), - service.NewIntField(siFieldShardedJoinIterations). - Description("The total number of iterations (shards), increasing this number will increase the overall time taken to process the data, but reduces the memory used in the process. The real memory usage required is significantly higher than the real size of the data and therefore the number of iterations should be at least an order of magnitude higher than the available memory divided by the overall size of the dataset."). - Default(1), - service.NewStringEnumField(siFieldShardedJoinMergeStrategy, "array", "replace", "keep"). - Description("The chosen strategy to use when a data join would otherwise result in a collision of field values. The strategy `array` means non-array colliding values are placed into an array and colliding arrays are merged. The strategy `replace` replaces old values with new values. The strategy `keep` keeps the old value."). - Default("array"), - ). - Description(`EXPERIMENTAL: Provides a way to perform outer joins of arbitrarily structured and unordered data resulting from the input sequence, even when the overall size of the data surpasses the memory available on the machine. - -When configured the sequence of inputs will be consumed one or more times according to the number of iterations, and when more than one iteration is specified each iteration will process an entirely different set of messages by sharding them by the ID field. Increasing the number of iterations reduces the memory consumption at the cost of needing to fully parse the data each time. - -Each message must be structured (JSON or otherwise processed into a structured form) and the fields will be aggregated with those of other messages sharing the ID. At the end of each iteration the joined messages are flushed downstream before the next iteration begins, hence keeping memory usage limited.`). - Version("3.40.0"). - Advanced(), - service.NewInputListField(siFieldInputs). - Description("An array of inputs to read from sequentially."), - ). - Example( - "End of Stream Message", - "A common use case for sequence might be to generate a message at the end of our main input. With the following config once the records within `./dataset.csv` are exhausted our final payload `{\"status\":\"finished\"}` will be routed through the pipeline.", - ` -input: - sequence: - inputs: - - file: - paths: [ ./dataset.csv ] - scanner: - csv: {} - - generate: - count: 1 - mapping: 'root = {"status":"finished"}' -`, - ). - Example( - "Joining Data (Simple)", - `Benthos can be used to join unordered data from fragmented datasets in memory by specifying a common identifier field and a number of sharded iterations. For example, given two CSV files, the first called "main.csv", which contains rows of user data: - -`+"```csv"+` -uuid,name,age -AAA,Melanie,34 -BBB,Emma,28 -CCC,Geri,45 -`+"```"+` - -And the second called "hobbies.csv" that, for each user, contains zero or more rows of hobbies: - -`+"```csv"+` -uuid,hobby -CCC,pokemon go -AAA,rowing -AAA,golf -`+"```"+` - -We can parse and join this data into a single dataset: - -`+"```json"+` -{"uuid":"AAA","name":"Melanie","age":34,"hobbies":["rowing","golf"]} -{"uuid":"BBB","name":"Emma","age":28} -{"uuid":"CCC","name":"Geri","age":45,"hobbies":["pokemon go"]} -`+"```"+` - -With the following config:`, - ` -input: - sequence: - sharded_join: - type: full-outer - id_path: uuid - merge_strategy: array - inputs: - - file: - paths: - - ./hobbies.csv - - ./main.csv - scanner: - csv: {} -`, - ). - Example( - "Joining Data (Advanced)", - `In this example we are able to join unordered and fragmented data from a combination of CSV files and newline-delimited JSON documents by specifying multiple sequence inputs with their own processors for extracting the structured data. - -The first file "main.csv" contains straight forward CSV data: - -`+"```csv"+` -uuid,name,age -AAA,Melanie,34 -BBB,Emma,28 -CCC,Geri,45 -`+"```"+` - -And the second file called "hobbies.ndjson" contains JSON documents, one per line, that associate an identifier with an array of hobbies. However, these data objects are in a nested format: - -`+"```json"+` -{"document":{"uuid":"CCC","hobbies":[{"type":"pokemon go"}]}} -{"document":{"uuid":"AAA","hobbies":[{"type":"rowing"},{"type":"golf"}]}} -`+"```"+` - -And so we will want to map these into a flattened structure before the join, and then we will end up with a single dataset that looks like this: - -`+"```json"+` -{"uuid":"AAA","name":"Melanie","age":34,"hobbies":["rowing","golf"]} -{"uuid":"BBB","name":"Emma","age":28} -{"uuid":"CCC","name":"Geri","age":45,"hobbies":["pokemon go"]} -`+"```"+` - -With the following config:`, - ` -input: - sequence: - sharded_join: - type: full-outer - id_path: uuid - iterations: 10 - merge_strategy: array - inputs: - - file: - paths: [ ./main.csv ] - scanner: - csv: {} - - file: - paths: [ ./hobbies.ndjson ] - scanner: - lines: {} - processors: - - mapping: | - root.uuid = this.document.uuid - root.hobbies = this.document.hobbies.map_each(this.type) -`, - ) -} - -func init() { - err := service.RegisterBatchInput("sequence", sequenceInputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - i, err := newSequenceInputFromParsed(conf, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalInput(i), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type joinedMessage struct { - metadata map[string]any - fields *gabs.Container -} - -func (j *joinedMessage) ToMsg() message.Batch { - part := message.NewPart(nil) - part.SetStructuredMut(message.CopyJSON(j.fields.Data())) - for k, v := range j.metadata { - part.MetaSetMut(k, v) - } - msg := message.Batch{part} - return msg -} - -type messageJoinerCollisionFn func(dest, source any) any - -func getMessageJoinerCollisionFn(name string) (messageJoinerCollisionFn, error) { - switch name { - case "array": - return func(dest, source any) any { - destArr, destIsArray := dest.([]any) - sourceArr, sourceIsArray := source.([]any) - if destIsArray { - if sourceIsArray { - return append(destArr, sourceArr...) - } - return append(destArr, source) - } - if sourceIsArray { - return append(append([]any{}, dest), sourceArr...) - } - return []any{dest, source} - }, nil - case "replace": - return func(dest, source any) any { - return source - }, nil - case "keep": - return func(dest, source any) any { - return dest - }, nil - } - return nil, fmt.Errorf("merge strategy '%v' was not recognised", name) -} - -type messageJoiner struct { - currentIteration int - totalIterations int - idPath string - messages map[string]*joinedMessage - collisionFn messageJoinerCollisionFn - flushOnLast bool -} - -func (m *messageJoiner) Add(msg message.Batch, lastInSequence bool, fn func(msg message.Batch)) { - if m.messages == nil { - m.messages = map[string]*joinedMessage{} - } - - _ = msg.Iter(func(i int, p *message.Part) error { - var incomingObj map[string]any - if jData, err := p.AsStructuredMut(); err == nil { - incomingObj, _ = jData.(map[string]any) - } - if incomingObj == nil { - // Messages that aren't structured objects are dropped. - // TODO: Propagate errors? - return nil - } - - gIncoming := gabs.Wrap(incomingObj) - id, _ := gIncoming.Path(m.idPath).Data().(string) - if id == "" { - // TODO: Propagate errors? - return nil - } - - // Drop all messages that aren't within our current shard. - if int(xxhash.ChecksumString64(id)%uint64(m.totalIterations)) != m.currentIteration { - return nil - } - - meta := map[string]any{} - _ = p.MetaIterMut(func(k string, v any) error { - meta[k] = v - return nil - }) - - jObj := m.messages[id] - if jObj == nil { - jObj = &joinedMessage{ - fields: gIncoming, - metadata: meta, - } - m.messages[id] = jObj - - if m.flushOnLast && lastInSequence { - fn(jObj.ToMsg()) - } - return nil - } - - _ = gIncoming.Delete(m.idPath) - _ = jObj.fields.MergeFn(gIncoming, m.collisionFn) - - _ = p.MetaIterMut(func(k string, v any) error { - jObj.metadata[k] = v - return nil - }) - - if m.flushOnLast && lastInSequence { - fn(jObj.ToMsg()) - } - return nil - }) -} - -func (m *messageJoiner) GetIteration() (int, bool) { - return m.currentIteration, m.currentIteration == (m.totalIterations - 1) -} - -func (m *messageJoiner) Empty(fn func(message.Batch)) bool { - for k, v := range m.messages { - if !m.flushOnLast { - msg := v.ToMsg() - fn(msg) - } - delete(m.messages, k) - } - m.currentIteration++ - return m.currentIteration >= m.totalIterations -} - -//------------------------------------------------------------------------------ - -type sequenceInput struct { - targetMut sync.Mutex - target input.Streamed - remaining []sequenceTarget - spent []sequenceTarget - - joiner *messageJoiner - - log *service.Logger - - transactions chan message.Transaction - - shutSig *shutdown.Signaller -} - -type sequenceTarget struct { - index int - config *service.ParsedConfig -} - -func newSequenceInputFromParsed(conf *service.ParsedConfig, res *service.Resources) (input.Streamed, error) { - pInputConfs, err := conf.FieldAnyList(siFieldInputs) - if err != nil { - return nil, err - } - - if len(pInputConfs) == 0 { - return nil, errors.New("requires at least one child input") - } - - targets := make([]sequenceTarget, 0, len(pInputConfs)) - for i, c := range pInputConfs { - c := c - targets = append(targets, sequenceTarget{ - index: i, - config: c, - }) - } - - rdr := &sequenceInput{ - remaining: targets, - log: res.Logger(), - transactions: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - - if rdr.joiner, err = shardedConfigFromParsed(conf.Namespace(siFieldShardedJoin)); err != nil { - return nil, fmt.Errorf("invalid sharded join config: %w", err) - } - - if target, _, err := rdr.createNextTarget(); err != nil { - return nil, err - } else if target == nil { - return nil, errors.New("failed to initialize first input") - } - - go rdr.loop() - return rdr, nil -} - -func shardedConfigFromParsed(conf *service.ParsedConfig) (*messageJoiner, error) { - typeStr, err := conf.FieldString(siFieldShardedJoinType) - if err != nil { - return nil, err - } - - var flushOnLast bool - switch typeStr { - case "none": - return nil, nil - case "full-outer", "full-outter": - flushOnLast = false - case "outer", "outter": - flushOnLast = true - default: - return nil, fmt.Errorf("join type '%v' was not recognised", typeStr) - } - - idPath, _ := conf.FieldString(siFieldShardedJoinIDPath) - if idPath == "" { - return nil, errors.New("the id path must not be empty") - } - - iterations, err := conf.FieldInt(siFieldShardedJoinIterations) - if err != nil { - return nil, err - } - if iterations <= 0 { - return nil, fmt.Errorf("invalid number of iterations: %v", iterations) - } - - mergeStrat, err := conf.FieldString(siFieldShardedJoinMergeStrategy) - if err != nil { - return nil, err - } - - collisionFn, err := getMessageJoinerCollisionFn(mergeStrat) - if err != nil { - return nil, err - } - return &messageJoiner{ - totalIterations: iterations, - idPath: idPath, - messages: map[string]*joinedMessage{}, - collisionFn: collisionFn, - flushOnLast: flushOnLast, - }, nil -} - -//------------------------------------------------------------------------------ - -func (r *sequenceInput) getTarget() (input.Streamed, bool) { - r.targetMut.Lock() - target := r.target - final := len(r.remaining) == 0 - r.targetMut.Unlock() - return target, final -} - -func (r *sequenceInput) createNextTarget() (input.Streamed, bool, error) { - var target input.Streamed - var err error - - r.targetMut.Lock() - r.target = nil - if len(r.remaining) > 0 { - next := r.remaining[0] - if iInput, err := next.config.FieldInput(); err == nil { - target = interop.UnwrapOwnedInput(iInput) - r.spent = append(r.spent, next) - r.remaining = r.remaining[1:] - } else { - return nil, false, fmt.Errorf("failed to initialize input index %v: %w", r.remaining[0].index, err) - } - } - if target != nil { - r.log.Debugf("Initialized sequence input %v.", len(r.spent)-1) - r.target = target - } - final := len(r.remaining) == 0 - r.targetMut.Unlock() - - return target, final, err -} - -func (r *sequenceInput) resetTargets() { - r.targetMut.Lock() - r.remaining = r.spent - r.spent = nil - r.targetMut.Unlock() -} - -func (r *sequenceInput) dispatchJoinedMessage(wg *sync.WaitGroup, msg message.Batch) { - resChan := make(chan error) - tran := message.NewTransaction(msg, resChan) - select { - case r.transactions <- tran: - case <-r.shutSig.HardStopChan(): - return - } - wg.Add(1) - go func() { - defer wg.Done() - for { - select { - case res := <-resChan: - if res == nil { - return - } - r.log.Errorf("Failed to send joined message: %v\n", res) - case <-r.shutSig.HardStopChan(): - return - } - select { - case <-time.After(time.Second): - case <-r.shutSig.HardStopChan(): - return - } - select { - case r.transactions <- tran: - case <-r.shutSig.HardStopChan(): - return - } - } - }() -} - -func (r *sequenceInput) loop() { - shutNowCtx, done := r.shutSig.HardStopCtx(context.Background()) - defer done() - - var shardJoinWG sync.WaitGroup - defer func() { - shardJoinWG.Wait() - if t, _ := r.getTarget(); t != nil { - t.TriggerStopConsuming() - _ = t.WaitForClose(shutNowCtx) - t.TriggerCloseNow() - } - close(r.transactions) - r.shutSig.TriggerHasStopped() - }() - - target, finalInSequence := r.getTarget() - -runLoop: - for { - if target == nil { - var err error - if target, finalInSequence, err = r.createNextTarget(); err != nil { - r.log.Errorf("Unable to start next sequence: %v\n", err) - select { - case <-time.After(time.Second): - case <-r.shutSig.SoftStopChan(): - return - } - continue runLoop - } - } - if target == nil { - if r.joiner != nil { - iteration, _ := r.joiner.GetIteration() - r.log.Debugf("Finished sharded iteration %v.", iteration) - - // Wait for pending transactions before adding more. - shardJoinWG.Wait() - - lastIteration := r.joiner.Empty(func(msg message.Batch) { - r.dispatchJoinedMessage(&shardJoinWG, msg) - }) - shardJoinWG.Wait() - if lastIteration { - r.log.Info("Finished all sharded iterations and exhausted all sequence inputs, shutting down.") - return - } - r.resetTargets() - continue runLoop - } - - r.log.Info("Exhausted all sequence inputs, shutting down.") - return - } - - var tran message.Transaction - var open bool - select { - case tran, open = <-target.TransactionChan(): - if !open { - target = nil - continue runLoop - } - case <-r.shutSig.SoftStopChan(): - return - } - - if r.joiner != nil { - r.joiner.Add(tran.Payload, finalInSequence, func(msg message.Batch) { - r.dispatchJoinedMessage(&shardJoinWG, msg) - }) - if err := tran.Ack(shutNowCtx, nil); err != nil && shutNowCtx.Err() != nil { - return - } - } else { - select { - case r.transactions <- tran: - case <-r.shutSig.HardStopChan(): - return - } - } - } -} - -func (r *sequenceInput) TransactionChan() <-chan message.Transaction { - return r.transactions -} - -func (r *sequenceInput) Connected() bool { - if t, _ := r.getTarget(); t != nil { - return t.Connected() - } - return false -} - -func (r *sequenceInput) TriggerStopConsuming() { - r.shutSig.TriggerSoftStop() -} - -func (r *sequenceInput) TriggerCloseNow() { - r.shutSig.TriggerHardStop() -} - -func (r *sequenceInput) WaitForClose(ctx context.Context) error { - select { - case <-r.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/input_sequence_test.go b/internal/impl/pure/input_sequence_test.go deleted file mode 100644 index 65609160b9..0000000000 --- a/internal/impl/pure/input_sequence_test.go +++ /dev/null @@ -1,492 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -func writeFiles(t *testing.T, dir string, nameToContent map[string]string) { - t.Helper() - - for k, v := range nameToContent { - require.NoError(t, os.WriteFile(filepath.Join(dir, k), []byte(v), 0o600)) - } -} - -func testInput(t testing.TB, confPattern string, args ...any) input.Streamed { - iConf, err := testutil.InputFromYAML(fmt.Sprintf(confPattern, args...)) - require.NoError(t, err) - - i, err := mock.NewManager().NewInput(iConf) - require.NoError(t, err) - - return i -} - -func TestSequenceHappy(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - tmpDir := t.TempDir() - - files := map[string]string{ - "f1": "foo\nbar\nbaz", - "f2": "buz\nbev\nbif\n", - "f3": "qux\nquz\nqev", - } - - writeFiles(t, tmpDir, files) - - rdr := testInput(t, ` -sequence: - inputs: - - file: - paths: [ "%v" ] - - file: - paths: [ "%v" ] - - file: - paths: [ "%v" ] -`, - filepath.Join(tmpDir, "f1"), - filepath.Join(tmpDir, "f2"), - filepath.Join(tmpDir, "f3"), - ) - - exp, act := []string{ - "foo", "bar", "baz", "buz", "bev", "bif", "qux", "quz", "qev", - }, []string{} - -consumeLoop: - for { - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - break consumeLoop - } - assert.Equal(t, 1, tran.Payload.Len()) - act = append(act, string(tran.Payload.Get(0).AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Minute): - t.Fatalf("Failed to consume message after: %v", act) - } - } - - assert.Equal(t, exp, act) - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) -} - -func TestSequenceJoins(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - tmpDir := t.TempDir() - - files := map[string]string{ - "csv1": "id,name,age\naaa,A,20\nbbb,B,21\nccc,B,22\n", - "csv2": "id,hobby\nccc,fencing\naaa,running\naaa,gaming\n", - "ndjson1": `{"id":"aaa","stuff":{"first":"foo"}} -{"id":"bbb","stuff":{"first":"bar"}} -{"id":"aaa","stuff":{"second":"baz"}}`, - } - - writeFiles(t, tmpDir, files) - - rdr := testInput(t, ` -sequence: - sharded_join: - type: full-outer - id_path: id - iterations: 1 - merge_strategy: array - inputs: - - csv: - paths: [ "%v", "%v" ] - - file: - paths: [ "%v" ] -`, - filepath.Join(tmpDir, "csv1"), - filepath.Join(tmpDir, "csv2"), - filepath.Join(tmpDir, "ndjson1"), - ) - - exp, act := []string{ - `{"age":"20","hobby":["running","gaming"],"id":"aaa","name":"A","stuff":{"first":"foo","second":"baz"}}`, - `{"age":"21","id":"bbb","name":"B","stuff":{"first":"bar"}}`, - `{"age":"22","hobby":"fencing","id":"ccc","name":"B"}`, - }, []string{} - -consumeLoop: - for { - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - break consumeLoop - } - assert.Equal(t, 1, tran.Payload.Len()) - act = append(act, string(tran.Payload.Get(0).AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Minute): - t.Fatalf("Failed to consume message after: %v", act) - } - } - - sort.Strings(exp) - sort.Strings(act) - assert.Equal(t, exp, act) - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) -} - -func TestSequenceJoinsMergeStrategies(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - testCases := []struct { - name string - flushOnFinal bool - mergeStrat string - files map[string]string - finalFile string - result []string - }{ - { - name: "array from final", - flushOnFinal: true, - mergeStrat: "array", - files: map[string]string{ - "csv1": "id,name,age\naaa,A,20\nbbb,B,21\nccc,B,22\n", - "csv2": "id,hobby\nccc,fencing\naaa,running\naaa,gaming\n", - }, - finalFile: "id,stuff\naaa,first\nccc,second\naaa,third\n", - result: []string{ - `{"age":"20","hobby":["running","gaming"],"id":"aaa","name":"A","stuff":"first"}`, - `{"age":"22","hobby":"fencing","id":"ccc","name":"B","stuff":"second"}`, - `{"age":"20","hobby":["running","gaming"],"id":"aaa","name":"A","stuff":["first","third"]}`, - }, - }, - { - name: "replace from final", - flushOnFinal: true, - mergeStrat: "replace", - files: map[string]string{ - "csv1": "id,name,age\naaa,A,20\nbbb,B,21\nccc,B,22\n", - "csv2": "id,hobby\nccc,fencing\naaa,running\naaa,gaming\n", - }, - finalFile: "id,stuff\naaa,first\nccc,second\naaa,third\n", - result: []string{ - `{"age":"20","hobby":"gaming","id":"aaa","name":"A","stuff":"first"}`, - `{"age":"20","hobby":"gaming","id":"aaa","name":"A","stuff":"third"}`, - `{"age":"22","hobby":"fencing","id":"ccc","name":"B","stuff":"second"}`, - }, - }, - { - name: "keep from final", - flushOnFinal: true, - mergeStrat: "keep", - files: map[string]string{ - "csv1": "id,name,age\naaa,A,20\nbbb,B,21\nccc,B,22\n", - "csv2": "id,hobby\nccc,fencing\naaa,running\naaa,gaming\n", - }, - finalFile: "id,stuff\naaa,first\nccc,second\naaa,third\n", - result: []string{ - `{"age":"20","hobby":"running","id":"aaa","name":"A","stuff":"first"}`, - `{"age":"20","hobby":"running","id":"aaa","name":"A","stuff":"first"}`, - `{"age":"22","hobby":"fencing","id":"ccc","name":"B","stuff":"second"}`, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - tmpDir := t.TempDir() - - writeFiles(t, tmpDir, test.files) - writeFiles(t, tmpDir, map[string]string{ - "final.csv": test.finalFile, - }) - - shardType := "full-outer" - if test.flushOnFinal { - shardType = "outer" - } - - conf := fmt.Sprintf(` -sequence: - sharded_join: - type: %v - id_path: id - iterations: 1 - merge_strategy: %v - inputs: - - csv: - paths: -`, - shardType, - test.mergeStrat, - ) - - for k := range test.files { - conf += fmt.Sprintf(` - - "%v" -`, filepath.Join(tmpDir, k)) - } - - conf += fmt.Sprintf(` - - csv: - paths: [ "%v" ] -`, filepath.Join(tmpDir, "final.csv")) - - t.Log(conf) - - rdr := testInput(t, conf) - - exp, act := test.result, []string{} - - consumeLoop: - for { - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - break consumeLoop - } - assert.Equal(t, 1, tran.Payload.Len()) - m := tran.Payload.Get(0) - payload, err := m.AsStructured() - require.NoError(t, err) - require.IsType(t, map[string]interface{}{}, payload) - act = append(act, string(m.AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Minute): - t.Fatalf("Failed to consume message after: %v", act) - } - } - - sort.Strings(exp) - sort.Strings(act) - assert.Equal(t, exp, act) - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) - }) - } -} - -func TestSequenceJoinsBig(t *testing.T) { - t.Skip() - t.Parallel() - - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - tmpDir := t.TempDir() - - jsonPath := filepath.Join(tmpDir, "one.ndjson") - csvPath := filepath.Join(tmpDir, "two.csv") - - ndjsonFile, err := os.Create(jsonPath) - require.NoError(t, err) - - csvFile, err := os.Create(csvPath) - require.NoError(t, err) - - totalRows := 1000 - - exp, act := []string{}, []string{} - - _, err = csvFile.WriteString("id,bar\n") - require.NoError(t, err) - for i := 0; i < totalRows; i++ { - exp = append(exp, fmt.Sprintf(`{"bar":["bar%v","baz%v"],"foo":"foo%v","id":"%v"}`, i, i, i, i)) - - _, err = fmt.Fprintf(ndjsonFile, "{\"id\":\"%v\",\"foo\":\"foo%v\"}\n", i, i) - require.NoError(t, err) - - _, err = fmt.Fprintf(csvFile, "%v,bar%v\n", i, i) - require.NoError(t, err) - } - for i := 0; i < totalRows; i++ { - _, err = fmt.Fprintf(csvFile, "%v,baz%v\n", i, i) - require.NoError(t, err) - } - require.NoError(t, ndjsonFile.Close()) - require.NoError(t, csvFile.Close()) - - rdr := testInput(t, ` -sequence: - sharded_join: - type: full-outer - id_path: id - iterations: 5 - merge_strategy: array - inputs: - - csv: - paths: [ "%v" ] - - file: - codec: lines - paths: [ "%v" ] -`, csvPath, jsonPath) - -consumeLoop: - for { - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - break consumeLoop - } - assert.Equal(t, 1, tran.Payload.Len()) - act = append(act, string(tran.Payload.Get(0).AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Minute): - t.Fatalf("Failed to consume message after: %v", act) - } - } - - sort.Strings(exp) - sort.Strings(act) - assert.Equal(t, exp, act) - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) -} - -func TestSequenceSad(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - tmpDir := t.TempDir() - - files := map[string]string{ - "f1": "foo\nbar\nbaz", - "f4": "buz\nbev\nbif\n", - } - - writeFiles(t, tmpDir, files) - - conf, err := testutil.InputFromYAML(fmt.Sprintf(` -sequence: - inputs: - - file: - paths: - - "%v/f1" - - file: - paths: - - "%v/f2" - - file: - paths: - - "%v/f3" -`, tmpDir, tmpDir, tmpDir)) - require.NoError(t, err) - - rdr, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - exp := []string{ - "foo", "bar", "baz", - } - - for i, str := range exp { - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - t.Fatal("closed earlier than expected") - } - assert.Equal(t, 1, tran.Payload.Len()) - assert.Equal(t, str, string(tran.Payload.Get(0).AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Minute): - t.Fatalf("Failed to consume message %v", i) - } - } - - select { - case <-rdr.TransactionChan(): - t.Fatal("unexpected transaction") - case <-time.After(100 * time.Millisecond): - } - - exp = []string{ - "buz", "bev", "bif", - } - - require.NoError(t, os.Rename(filepath.Join(tmpDir, "f4"), filepath.Join(tmpDir, "f2"))) - - for i, str := range exp { - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - t.Fatal("closed earlier than expected") - } - assert.Equal(t, 1, tran.Payload.Len()) - assert.Equal(t, str, string(tran.Payload.Get(0).AsBytes())) - require.NoError(t, tran.Ack(ctx, nil)) - case <-time.After(time.Minute): - t.Fatalf("Failed to consume message %v", i) - } - } - - rdr.TriggerStopConsuming() - assert.NoError(t, rdr.WaitForClose(ctx)) -} - -func TestSequenceEarlyTermination(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - t.Parallel() - - tmpDir := t.TempDir() - - writeFiles(t, tmpDir, map[string]string{ - "f1": "foo\nbar\nbaz", - }) - - conf, err := testutil.InputFromYAML(fmt.Sprintf(` -sequence: - inputs: - - file: - paths: - - "%v/f1" -`, tmpDir)) - require.NoError(t, err) - - rdr, err := mock.NewManager().NewInput(conf) - require.NoError(t, err) - - select { - case tran, open := <-rdr.TransactionChan(): - if !open { - t.Fatal("closed earlier than expected") - } - assert.Equal(t, 1, tran.Payload.Len()) - assert.Equal(t, "foo", string(tran.Payload.Get(0).AsBytes())) - case <-time.After(time.Minute): - t.Fatal("timed out") - } - - rdr.TriggerCloseNow() - assert.NoError(t, rdr.WaitForClose(ctx)) -} diff --git a/internal/impl/pure/metrics_logger.go b/internal/impl/pure/metrics_logger.go deleted file mode 100644 index ce11531aef..0000000000 --- a/internal/impl/pure/metrics_logger.go +++ /dev/null @@ -1,157 +0,0 @@ -package pure - -import ( - "context" - "net/http" - "time" - - gmetrics "github.com/rcrowley/go-metrics" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - lmFieldPushInterval = "push_interval" - lmFieldFlushMetrics = "flush_metrics" -) - -func loggerMetricsSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Beta(). - Summary(`Prints aggregated metrics through the logger.`). - Description(` -Prints each metric produced by Benthos as a log event (level `+"`info`"+` by default) during shutdown, and optionally on an interval. - -This metrics type is useful for debugging pipelines when you only have access to the logger output and not the service-wide server. Otherwise it's recommended that you use either the `+"`prometheus` or `json_api`"+`types.`). - Fields( - service.NewStringField(lmFieldPushInterval). - Description("An optional period of time to continuously print all metrics."). - Optional(), - service.NewBoolField(lmFieldFlushMetrics). - Description("Whether counters and timing metrics should be reset to 0 each time metrics are printed."). - Default(false), - ) -} - -func init() { - err := service.RegisterMetricsExporter("logger", loggerMetricsSpec(), - func(conf *service.ParsedConfig, log *service.Logger) (service.MetricsExporter, error) { - return newLoggerFromParsed(conf, log) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type loggerMetrics struct { - local *metrics.Local - log *service.Logger - flush bool - shutSig *shutdown.Signaller -} - -func newLoggerFromParsed(conf *service.ParsedConfig, log *service.Logger) (l *loggerMetrics, err error) { - l = &loggerMetrics{ - local: metrics.NewLocal(), - log: log, - shutSig: shutdown.NewSignaller(), - } - if l.flush, err = conf.FieldBool(lmFieldFlushMetrics); err != nil { - return - } - - if piStr, _ := conf.FieldString(lmFieldPushInterval); piStr != "" { - var interval time.Duration - if interval, err = conf.FieldDuration(lmFieldPushInterval); err != nil { - return - } - go func() { - for { - select { - case <-l.shutSig.SoftStopChan(): - return - case <-time.After(interval): - l.publishMetrics() - } - } - }() - } - return -} - -//------------------------------------------------------------------------------ - -func (s *loggerMetrics) publishMetrics() { - var counters map[string]int64 - var timings map[string]gmetrics.Timer - if s.flush { - counters = s.local.FlushCounters() - timings = s.local.FlushTimings() - } else { - counters = s.local.GetCounters() - timings = s.local.GetTimings() - } - - for k, v := range counters { - name, tagNames, tagValues := metrics.ReverseLabelledPath(k) - e := s.log.With("name", name, "value", v) - if len(tagNames) > 0 { - tagKVs := map[string]string{} - for i := range tagNames { - tagKVs[tagNames[i]] = tagValues[i] - } - e = e.With(tagKVs) - } - e.Info("Counter metric") - } - - for k, v := range timings { - ps := v.Percentiles([]float64{0.5, 0.9, 0.99}) - name, tagNames, tagValues := metrics.ReverseLabelledPath(k) - e := s.log.With("name", name, "p50", ps[0], "p90", ps[1], "p99", ps[2]) - if len(tagNames) > 0 { - tagKVs := map[string]string{} - for i := range tagNames { - tagKVs[tagNames[i]] = tagValues[i] - } - e = e.With(tagKVs) - } - e.Info("Timing metric") - } -} - -func (s *loggerMetrics) NewCounterCtor(path string, n ...string) service.MetricsExporterCounterCtor { - tmp := s.local.GetCounterVec(path, n...) - return func(labelValues ...string) service.MetricsExporterCounter { - return tmp.With(labelValues...) - } -} - -func (s *loggerMetrics) NewTimerCtor(path string, n ...string) service.MetricsExporterTimerCtor { - tmp := s.local.GetTimerVec(path, n...) - return func(labelValues ...string) service.MetricsExporterTimer { - return tmp.With(labelValues...) - } -} - -func (s *loggerMetrics) NewGaugeCtor(path string, n ...string) service.MetricsExporterGaugeCtor { - tmp := s.local.GetGaugeVec(path, n...) - return func(labelValues ...string) service.MetricsExporterGauge { - return tmp.With(labelValues...) - } -} - -func (s *loggerMetrics) HandlerFunc() http.HandlerFunc { - return nil -} - -func (s *loggerMetrics) Close(context.Context) error { - s.shutSig.TriggerHardStop() - s.publishMetrics() - return nil -} diff --git a/internal/impl/pure/metrics_none.go b/internal/impl/pure/metrics_none.go deleted file mode 100644 index 015b7de326..0000000000 --- a/internal/impl/pure/metrics_none.go +++ /dev/null @@ -1,50 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterMetricsExporter("none", service.NewConfigSpec(). - Stable(). - Summary(`Disable metrics entirely.`). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, log *service.Logger) (service.MetricsExporter, error) { - return noopMetrics{}, nil - }) - if err != nil { - panic(err) - } -} - -type noopMetrics struct{} - -func (n noopMetrics) NewCounterCtor(name string, labelKeys ...string) service.MetricsExporterCounterCtor { - return func(labelValues ...string) service.MetricsExporterCounter { - return n - } -} - -func (n noopMetrics) NewTimerCtor(name string, labelKeys ...string) service.MetricsExporterTimerCtor { - return func(labelValues ...string) service.MetricsExporterTimer { - return n - } -} - -func (n noopMetrics) NewGaugeCtor(name string, labelKeys ...string) service.MetricsExporterGaugeCtor { - return func(labelValues ...string) service.MetricsExporterGauge { - return n - } -} - -func (n noopMetrics) Close(ctx context.Context) error { - return nil -} - -func (n noopMetrics) Incr(count int64) {} -func (n noopMetrics) IncrFloat64(count float64) {} -func (n noopMetrics) Timing(delta int64) {} -func (n noopMetrics) Set(value int64) {} -func (n noopMetrics) SetFloat64(value float64) {} diff --git a/internal/impl/pure/output_broker.go b/internal/impl/pure/output_broker.go deleted file mode 100644 index f0bbaa8c12..0000000000 --- a/internal/impl/pure/output_broker.go +++ /dev/null @@ -1,211 +0,0 @@ -package pure - -import ( - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/output/batcher" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - boFieldCopies = "copies" - boFieldPattern = "pattern" - boFieldOutputs = "outputs" - boFieldBatching = "batching" -) - -func brokerOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Allows you to route messages to multiple child outputs using a range of brokering <>.`). - Description(` -xref:components:processors/about.adoc[Processors] can be listed to apply across individual outputs or all outputs: - -`+"```yaml"+` -output: - broker: - pattern: fan_out - outputs: - - resource: foo - - resource: bar - # Processors only applied to messages sent to bar. - processors: - - resource: bar_processor - - # Processors applied to messages sent to all brokered outputs. - processors: - - resource: general_processor -`+"```"+``). - Footnotes(` -== Patterns - -The broker pattern determines the way in which messages are allocated and can be chosen from the following: - -=== `+"`fan_out`"+` - -With the fan out pattern all outputs will be sent every message that passes through Benthos in parallel. - -If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. - -Sometimes it is useful to disable the back pressure or retries of certain fan out outputs and instead drop messages that have failed or were blocked. In this case you can wrap outputs with a `+"xref:components:outputs/drop_on.adoc[`drop_on` output]"+`. - -=== `+"`fan_out_fail_fast`"+` - -The same as the `+"`fan_out`"+` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. - -=== `+"`fan_out_sequential`"+` - -Similar to the fan out pattern except outputs are written to sequentially, meaning an output is only written to once the preceding output has confirmed receipt of the same message. - -If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. - -=== `+"`fan_out_sequential_fail_fast`"+` - -The same as the `+"`fan_out_sequential`"+` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. - -=== `+"`round_robin`"+` - -With the round robin pattern each message will be assigned a single output following their order. If an output applies back pressure it will block all subsequent messages. If an output fails to send a message then the message will be re-attempted with the next input, and so on. - -=== `+"`greedy`"+` - -The greedy pattern results in higher output throughput at the cost of potentially disproportionate message allocations to those outputs. Each message is sent to a single output, which is determined by allowing outputs to claim messages as soon as they are able to process them. This results in certain faster outputs potentially processing more messages at the cost of slower outputs.`). - Fields( - service.NewIntField(boFieldCopies). - Description("The number of copies of each configured output to spawn."). - Advanced(). - Default(1), - service.NewStringEnumField(boFieldPattern, - "fan_out", "fan_out_fail_fast", "fan_out_sequential", "fan_out_sequential_fail_fast", "round_robin", "greedy"). - Description("The brokering pattern to use."). - Default("fan_out"), - service.NewOutputListField(boFieldOutputs). - Description("A list of child outputs to broker."), - service.NewBatchPolicyField(boFieldBatching), - ) -} - -// ErrBrokerNoOutputs is returned when creating a Broker type with zero -// outputs. -var ErrBrokerNoOutputs = errors.New("attempting to create broker output type with no outputs") - -func init() { - err := service.RegisterBatchOutput( - "broker", brokerOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - var bi output.Streamed - if bi, err = brokerOutputFromParsed(conf, mgr); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(bi) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -func brokerOutputFromParsed(conf *service.ParsedConfig, res *service.Resources) (output.Streamed, error) { - mgr := interop.UnwrapManagement(res) - - copies, err := conf.FieldInt(boFieldCopies) - if err != nil { - return nil, err - } - - pattern, err := conf.FieldString(boFieldPattern) - if err != nil { - return nil, err - } - - var batchPol *policy.Batcher - { - batchConf, err := conf.FieldBatchPolicy(boFieldBatching) - if err != nil { - return nil, err - } - if !batchConf.IsNoop() { - iBatcher, err := batchConf.NewBatcher(res) - if err != nil { - return nil, err - } - batchPol = interop.UnwrapBatcher(iBatcher) - } - } - - _, isRetryWrapped := map[string]struct{}{ - "fan_out": {}, - "fan_out_sequential": {}, - }[pattern] - - var outputs []output.Streamed - { - pubOutputs, err := conf.FieldOutputList(boFieldOutputs) - if err != nil { - return nil, err - } - for _, v := range pubOutputs { - tmpOut := interop.UnwrapOwnedOutput(v) - if isRetryWrapped { - if tmpOut, err = RetryOutputIndefinitely(mgr, tmpOut); err != nil { - return nil, err - } - } - outputs = append(outputs, tmpOut) - } - } - - lOutputs := len(outputs) * copies - if lOutputs <= 0 { - return nil, ErrBrokerNoOutputs - } - if lOutputs == 1 { - b := outputs[0] - if batchPol != nil { - b = batcher.New(batchPol, b, mgr) - } - return b, nil - } - - for j := 1; j < copies; j++ { - extraChildren, err := conf.FieldOutputList(boFieldOutputs) - if err != nil { - return nil, err - } - for _, v := range extraChildren { - tmpOut := interop.UnwrapOwnedOutput(v) - if isRetryWrapped { - if tmpOut, err = RetryOutputIndefinitely(mgr, tmpOut); err != nil { - return nil, err - } - } - outputs = append(outputs, tmpOut) - } - } - - var b output.Streamed - switch pattern { - case "fan_out", "fan_out_fail_fast": - b, err = newFanOutOutputBroker(outputs) - case "fan_out_sequential", "fan_out_sequential_fail_fast": - b, err = newFanOutSequentialOutputBroker(outputs) - case "round_robin": - b, err = newRoundRobinOutputBroker(outputs) - case "greedy": - b, err = newGreedyOutputBroker(outputs) - default: - return nil, fmt.Errorf("broker pattern was not recognised: %v", pattern) - } - - if batchPol != nil { - b = batcher.New(batchPol, b, mgr) - } - return b, err -} diff --git a/internal/impl/pure/output_broker_fan_out.go b/internal/impl/pure/output_broker_fan_out.go deleted file mode 100644 index 4a84a819de..0000000000 --- a/internal/impl/pure/output_broker_fan_out.go +++ /dev/null @@ -1,131 +0,0 @@ -package pure - -import ( - "context" - "sync/atomic" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fanOutOutputBroker struct { - transactions <-chan message.Transaction - - outputTSChans []chan message.Transaction - outputs []output.Streamed - - shutSig *shutdown.Signaller -} - -func newFanOutOutputBroker(outputs []output.Streamed) (*fanOutOutputBroker, error) { - o := &fanOutOutputBroker{ - transactions: nil, - outputs: outputs, - shutSig: shutdown.NewSignaller(), - } - - o.outputTSChans = make([]chan message.Transaction, len(o.outputs)) - for i := range o.outputTSChans { - o.outputTSChans[i] = make(chan message.Transaction) - if err := o.outputs[i].Consume(o.outputTSChans[i]); err != nil { - return nil, err - } - } - return o, nil -} - -func (o *fanOutOutputBroker) Consume(transactions <-chan message.Transaction) error { - if o.transactions != nil { - return component.ErrAlreadyStarted - } - o.transactions = transactions - - go o.loop() - return nil -} - -func (o *fanOutOutputBroker) Connected() bool { - for _, out := range o.outputs { - if !out.Connected() { - return false - } - } - return true -} - -func (o *fanOutOutputBroker) loop() { - ackInterruptChan := make(chan struct{}) - var ackPending int64 - - defer func() { - // Wait for pending acks to be resolved, or forceful termination - ackWaitLoop: - for atomic.LoadInt64(&ackPending) > 0 { - select { - case <-ackInterruptChan: - case <-time.After(time.Millisecond * 100): - // Just incase an interrupt doesn't arrive. - case <-o.shutSig.HardStopChan(): - break ackWaitLoop - } - } - for _, c := range o.outputTSChans { - close(c) - } - _ = closeAllOutputs(context.Background(), o.outputs) - o.shutSig.TriggerHasStopped() - }() - - for { - var ts message.Transaction - var open bool - select { - case ts, open = <-o.transactions: - if !open { - return - } - case <-o.shutSig.HardStopChan(): - return - } - - _ = atomic.AddInt64(&ackPending, 1) - pendingResponses := int64(len(o.outputTSChans)) - for target := range o.outputTSChans { - msgCopy, i := ts.Payload.ShallowCopy(), target - select { - case o.outputTSChans[i] <- message.NewTransactionFunc(msgCopy, func(ctx context.Context, err error) error { - if atomic.AddInt64(&pendingResponses, -1) == 0 || err != nil { - atomic.StoreInt64(&pendingResponses, 0) - ackErr := ts.Ack(ctx, err) - _ = atomic.AddInt64(&ackPending, -1) - select { - case ackInterruptChan <- struct{}{}: - default: - } - return ackErr - } - return nil - }): - case <-o.shutSig.HardStopChan(): - return - } - } - } -} - -func (o *fanOutOutputBroker) TriggerCloseNow() { - o.shutSig.TriggerHardStop() -} - -func (o *fanOutOutputBroker) WaitForClose(ctx context.Context) error { - select { - case <-o.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_broker_fan_out_sequential.go b/internal/impl/pure/output_broker_fan_out_sequential.go deleted file mode 100644 index 62bc645853..0000000000 --- a/internal/impl/pure/output_broker_fan_out_sequential.go +++ /dev/null @@ -1,138 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync/atomic" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fanOutSequentialOutputBroker struct { - transactions <-chan message.Transaction - - outputTSChans []chan message.Transaction - outputs []output.Streamed - - shutSig *shutdown.Signaller -} - -func newFanOutSequentialOutputBroker(outputs []output.Streamed) (*fanOutSequentialOutputBroker, error) { - o := &fanOutSequentialOutputBroker{ - transactions: nil, - outputs: outputs, - shutSig: shutdown.NewSignaller(), - } - - o.outputTSChans = make([]chan message.Transaction, len(o.outputs)) - for i := range o.outputTSChans { - o.outputTSChans[i] = make(chan message.Transaction) - if err := o.outputs[i].Consume(o.outputTSChans[i]); err != nil { - return nil, err - } - } - return o, nil -} - -func (o *fanOutSequentialOutputBroker) Consume(transactions <-chan message.Transaction) error { - if o.transactions != nil { - return component.ErrAlreadyStarted - } - o.transactions = transactions - - go o.loop() - return nil -} - -func (o *fanOutSequentialOutputBroker) Connected() bool { - for _, out := range o.outputs { - if !out.Connected() { - return false - } - } - return true -} - -func (o *fanOutSequentialOutputBroker) loop() { - ackInterruptChan := make(chan struct{}) - var ackPending int64 - - defer func() { - // Wait for pending acks to be resolved, or forceful termination - for atomic.LoadInt64(&ackPending) > 0 { - select { - case <-ackInterruptChan: - case <-time.After(time.Millisecond * 100): - // Just incase an interrupt doesn't arrive. - } - } - for _, c := range o.outputTSChans { - close(c) - } - _ = closeAllOutputs(context.Background(), o.outputs) - o.shutSig.TriggerHasStopped() - }() - - for { - var ts message.Transaction - var open bool - - select { - case ts, open = <-o.transactions: - if !open { - return - } - case <-o.shutSig.HardStopChan(): - return - } - - _ = atomic.AddInt64(&ackPending, 1) - - i := 0 - var ackFn func(ctx context.Context, err error) error - ackFn = func(ctx context.Context, err error) error { - i++ - if err != nil || len(o.outputTSChans) <= i { - ackErr := ts.Ack(ctx, err) - _ = atomic.AddInt64(&ackPending, -1) - select { - case ackInterruptChan <- struct{}{}: - default: - } - return ackErr - } - select { - case o.outputTSChans[i] <- message.NewTransactionFunc(ts.Payload, ackFn): - case <-o.shutSig.HardStopChan(): - return errors.New("component is shutting down") - case <-ctx.Done(): - return ctx.Err() - } - return nil - } - - select { - case o.outputTSChans[i] <- message.NewTransactionFunc(ts.Payload, ackFn): - case <-o.shutSig.HardStopChan(): - return - } - } -} - -func (o *fanOutSequentialOutputBroker) TriggerCloseNow() { - o.shutSig.TriggerHardStop() -} - -func (o *fanOutSequentialOutputBroker) WaitForClose(ctx context.Context) error { - select { - case <-o.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_broker_fan_out_sequential_test.go b/internal/impl/pure/output_broker_fan_out_sequential_test.go deleted file mode 100644 index c08bc8a3e6..0000000000 --- a/internal/impl/pure/output_broker_fan_out_sequential_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestBasicFanOutSequential(t *testing.T) { - nOutputs, nMsgs := 10, 1000 - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{} - - for i := 0; i < nOutputs; i++ { - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - outputs = append(outputs, mockOutputs[i]) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newFanOutSequentialOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - assert.True(t, oTM.Connected()) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - for j := 0; j < nOutputs; j++ { - select { - case ts := <-mockOutputs[j].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - go func() { - require.NoError(t, ts.Ack(tCtx, nil)) - }() - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate", j) - } - } - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - oTM.TriggerCloseNow() - assert.NoError(t, oTM.WaitForClose(ctx)) -} - -func TestFanOutSequentialBlock(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - mockOne := mock.OutputChanneled{} - mockTwo := mock.OutputChanneled{} - - outputs := []output.Streamed{&mockOne, &mockTwo} - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newFanOutSequentialOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("hello world")}), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - var ts1, ts2 message.Transaction - select { - case ts1 = <-mockOne.TChan: - case <-time.After(time.Second): - t.Fatal("Timed out waiting for mockOne") - } - go func() { - require.NoError(t, ts1.Ack(tCtx, nil)) - }() - - select { - case ts2 = <-mockTwo.TChan: - case <-time.After(time.Second): - t.Fatal("Timed out waiting for mockOne") - } - go func() { - require.NoError(t, ts2.Ack(tCtx, nil)) - }() - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - - close(readChan) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - require.NoError(t, oTM.WaitForClose(ctx)) -} diff --git a/internal/impl/pure/output_broker_fan_out_test.go b/internal/impl/pure/output_broker_fan_out_test.go deleted file mode 100644 index ff61d2d316..0000000000 --- a/internal/impl/pure/output_broker_fan_out_test.go +++ /dev/null @@ -1,325 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "fmt" - "sync" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ output.Streamed = &fanOutOutputBroker{} - -func TestBasicFanOut(t *testing.T) { - nOutputs, nMsgs := 10, 1000 - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{} - - for i := 0; i < nOutputs; i++ { - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - outputs = append(outputs, mockOutputs[i]) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newFanOutOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - assert.True(t, oTM.Connected()) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - resFnSlice := []func(context.Context, error) error{} - for j := 0; j < nOutputs; j++ { - var ts message.Transaction - select { - case ts = <-mockOutputs[j].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - resFnSlice = append(resFnSlice, ts.Ack) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate") - } - } - - for j := 0; j < nOutputs; j++ { - require.NoError(t, resFnSlice[j](tCtx, err)) - } - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestBasicFanOutMutations(t *testing.T) { - mockOutputA := &mock.OutputChanneled{} - mockOutputB := &mock.OutputChanneled{} - outputs := []output.Streamed{ - mockOutputA, - mockOutputB, - } - - readChan := make(chan message.Transaction) - - oTM, err := newFanOutOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - assert.True(t, oTM.Connected()) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - inMsg := message.NewPart(nil) - inMsg.SetStructuredMut(map[string]any{ - "hello": "world", - }) - - inBatch := message.Batch{inMsg} - select { - case readChan <- message.NewTransactionFunc(inBatch, func(ctx context.Context, _ error) error { - inStruct, err := inMsg.AsStructuredMut() - require.NoError(t, err) - - assert.Equal(t, map[string]any{ - "hello": "world", - }, inStruct) - - _, err = gabs.Wrap(inStruct).Set("quack", "moo") - require.NoError(t, err) - return nil - }): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - testMockOutput := func(mockOutput *mock.OutputChanneled) { - var ts message.Transaction - select { - case ts = <-mockOutput.TChan: - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate") - } - - outStruct, err := ts.Payload.Get(0).AsStructuredMut() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - }, outStruct) - - _, err = gabs.Wrap(outStruct).Set("woof", "meow") - require.NoError(t, err) - require.NoError(t, ts.Ack(tCtx, nil)) - } - - testMockOutput(mockOutputA) - testMockOutput(mockOutputB) - - inStruct, err := inMsg.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - "moo": "quack", - }, inStruct) - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestFanOutBackPressure(t *testing.T) { - mockOne := mock.OutputChanneled{} - mockTwo := mock.OutputChanneled{} - - outputs := []output.Streamed{&mockOne, &mockTwo} - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newFanOutOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - wg := sync.WaitGroup{} - wg.Add(1) - doneChan := make(chan struct{}) - go func() { - defer wg.Done() - // Consume as fast as possible from mock one - for { - select { - case ts := <-mockOne.TChan: - require.NoError(t, ts.Ack(ctx, nil)) - case <-doneChan: - return - } - } - }() - - i := 0 -bpLoop: - for ; i < 1000; i++ { - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("hello world")}), resChan): - case <-time.After(time.Millisecond * 200): - break bpLoop - } - } - if i > 500 { - t.Error("We shouldn't be capable of dumping this many messages into a blocked broker") - } - - close(readChan) - done() - close(doneChan) - wg.Wait() -} - -func TestFanOutShutDownFromReceive(t *testing.T) { - outputs := []output.Streamed{} - mockOutput := &mock.OutputChanneled{} - outputs = append(outputs, mockOutput) - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newFanOutOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - select { - case _, open := <-mockOutput.TChan: - require.True(t, open) - // We do not ack the transaction - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(ctx)) - - select { - case _, open := <-mockOutput.TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } -} - -func TestFanOutShutDownFromSend(t *testing.T) { - outputs := []output.Streamed{} - mockOutput := &mock.OutputChanneled{} - outputs = append(outputs, mockOutput) - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newFanOutOutputBroker(outputs) - require.NoError(t, err) - require.NoError(t, oTM.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch(nil), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(ctx)) - - select { - case _, open := <-mockOutput.TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out waiting for msg rcv") - } -} - -//------------------------------------------------------------------------------ - -func BenchmarkBasicFanOut(b *testing.B) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nOutputs, nMsgs := 3, b.N - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{} - - for i := 0; i < nOutputs; i++ { - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - outputs = append(outputs, mockOutputs[i]) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - oTM, err := newFanOutOutputBroker(outputs) - require.NoError(b, err) - require.NoError(b, oTM.Consume(readChan)) - - content := [][]byte{[]byte("hello world")} - rFnSlice := make([]func(context.Context, error) error, nOutputs) - - b.ReportAllocs() - b.StartTimer() - - for i := 0; i < nMsgs; i++ { - readChan <- message.NewTransaction(message.QuickBatch(content), resChan) - for j := 0; j < nOutputs; j++ { - ts := <-mockOutputs[j].TChan - rFnSlice[j] = ts.Ack - } - for j := 0; j < nOutputs; j++ { - require.NoError(b, rFnSlice[j](tCtx, nil)) - } - res := <-resChan - if res != nil { - b.Errorf("Received unexpected errors from broker: %v", res) - } - } - - b.StopTimer() -} diff --git a/internal/impl/pure/output_broker_greedy.go b/internal/impl/pure/output_broker_greedy.go deleted file mode 100644 index a47e4a4b9c..0000000000 --- a/internal/impl/pure/output_broker_greedy.go +++ /dev/null @@ -1,51 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type greedyOutputBroker struct { - outputs []output.Streamed -} - -func newGreedyOutputBroker(outputs []output.Streamed) (*greedyOutputBroker, error) { - return &greedyOutputBroker{ - outputs: outputs, - }, nil -} - -func (g *greedyOutputBroker) Consume(ts <-chan message.Transaction) error { - for _, out := range g.outputs { - if err := out.Consume(ts); err != nil { - return err - } - } - return nil -} - -func (g *greedyOutputBroker) Connected() bool { - for _, out := range g.outputs { - if !out.Connected() { - return false - } - } - return true -} - -func (g *greedyOutputBroker) TriggerCloseNow() { - for _, out := range g.outputs { - out.TriggerCloseNow() - } -} - -func (g *greedyOutputBroker) WaitForClose(ctx context.Context) error { - for _, out := range g.outputs { - if err := out.WaitForClose(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/output_broker_greedy_test.go b/internal/impl/pure/output_broker_greedy_test.go deleted file mode 100644 index bdb50527ab..0000000000 --- a/internal/impl/pure/output_broker_greedy_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ output.Streamed = &greedyOutputBroker{} - -func TestGreedyDoubleClose(t *testing.T) { - oTM, err := newGreedyOutputBroker([]output.Streamed{}) - if err != nil { - t.Error(err) - return - } - - // This shouldn't cause a panic - oTM.TriggerCloseNow() - oTM.TriggerCloseNow() -} - -//------------------------------------------------------------------------------ - -func TestBasicGreedy(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nMsgs := 1000 - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{ - {}, - {}, - {}, - } - - for _, o := range mockOutputs { - outputs = append(outputs, o) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newGreedyOutputBroker(outputs) - if err != nil { - t.Error(err) - return - } - if err = oTM.Consume(readChan); err != nil { - t.Error(err) - return - } - - // Only read from a single output. - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - go func() { - var ts message.Transaction - select { - case ts = <-mockOutputs[0].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - select { - case res := <-resChan: - if res != nil { - t.Errorf("Received unexpected errors from broker: %v", res) - } - case <-time.After(time.Second): - t.Errorf("Timed out responding to broker") - return - } - } - - oTM.TriggerCloseNow() - if err := oTM.WaitForClose(tCtx); err != nil { - t.Error(err) - } -} diff --git a/internal/impl/pure/output_broker_round_robin.go b/internal/impl/pure/output_broker_round_robin.go deleted file mode 100644 index 772cc8ae05..0000000000 --- a/internal/impl/pure/output_broker_round_robin.go +++ /dev/null @@ -1,102 +0,0 @@ -package pure - -import ( - "context" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type roundRobinOutputBroker struct { - transactions <-chan message.Transaction - - outputTSChans []chan message.Transaction - outputs []output.Streamed - - shutSig *shutdown.Signaller -} - -func newRoundRobinOutputBroker(outputs []output.Streamed) (*roundRobinOutputBroker, error) { - o := &roundRobinOutputBroker{ - transactions: nil, - outputs: outputs, - shutSig: shutdown.NewSignaller(), - } - o.outputTSChans = make([]chan message.Transaction, len(o.outputs)) - for i := range o.outputTSChans { - o.outputTSChans[i] = make(chan message.Transaction) - if err := o.outputs[i].Consume(o.outputTSChans[i]); err != nil { - return nil, err - } - } - return o, nil -} - -func (o *roundRobinOutputBroker) Consume(ts <-chan message.Transaction) error { - if o.transactions != nil { - return component.ErrAlreadyStarted - } - o.transactions = ts - - go o.loop() - return nil -} - -func (o *roundRobinOutputBroker) Connected() bool { - for _, out := range o.outputs { - if !out.Connected() { - return false - } - } - return true -} - -func (o *roundRobinOutputBroker) loop() { - defer func() { - for _, c := range o.outputTSChans { - close(c) - } - _ = closeAllOutputs(context.Background(), o.outputs) - o.shutSig.TriggerHasStopped() - }() - - i := 0 - var open bool - for { - var ts message.Transaction - select { - case ts, open = <-o.transactions: - if !open { - return - } - case <-o.shutSig.HardStopChan(): - return - } - select { - case o.outputTSChans[i] <- ts: - case <-o.shutSig.HardStopChan(): - return - } - - i++ - if i >= len(o.outputTSChans) { - i = 0 - } - } -} - -func (o *roundRobinOutputBroker) TriggerCloseNow() { - o.shutSig.TriggerHardStop() -} - -func (o *roundRobinOutputBroker) WaitForClose(ctx context.Context) error { - select { - case <-o.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_broker_round_robin_test.go b/internal/impl/pure/output_broker_round_robin_test.go deleted file mode 100644 index 63d2a4a407..0000000000 --- a/internal/impl/pure/output_broker_round_robin_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ output.Streamed = &roundRobinOutputBroker{} - -func TestRoundRobinDoubleClose(t *testing.T) { - oTM, err := newRoundRobinOutputBroker([]output.Streamed{}) - if err != nil { - t.Error(err) - return - } - - // This shouldn't cause a panic - oTM.TriggerCloseNow() - oTM.TriggerCloseNow() -} - -//------------------------------------------------------------------------------ - -func TestBasicRoundRobin(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nMsgs := 1000 - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{ - {}, - {}, - {}, - } - - for _, o := range mockOutputs { - outputs = append(outputs, o) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newRoundRobinOutputBroker(outputs) - if err != nil { - t.Error(err) - return - } - if err = oTM.Consume(readChan); err != nil { - t.Error(err) - return - } - - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - go func() { - var ts message.Transaction - select { - case ts = <-mockOutputs[i%3].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-mockOutputs[(i+1)%3].TChan: - t.Errorf("Received message in wrong order: %v != %v", i%3, (i+1)%3) - return - case <-mockOutputs[(i+2)%3].TChan: - t.Errorf("Received message in wrong order: %v != %v", i%3, (i+2)%3) - return - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - select { - case res := <-resChan: - if res != nil { - t.Errorf("Received unexpected errors from broker: %v", res) - } - case <-time.After(time.Second): - t.Errorf("Timed out responding to broker") - return - } - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -//------------------------------------------------------------------------------ - -func BenchmarkBasicRoundRobin(b *testing.B) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - nOutputs, nMsgs := 3, b.N - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{} - - for i := 0; i < nOutputs; i++ { - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - outputs = append(outputs, mockOutputs[i]) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newRoundRobinOutputBroker(outputs) - if err != nil { - b.Error(err) - return - } - if err = oTM.Consume(readChan); err != nil { - b.Error(err) - return - } - - content := [][]byte{[]byte("hello world")} - - b.StartTimer() - - for i := 0; i < nMsgs; i++ { - readChan <- message.NewTransaction(message.QuickBatch(content), resChan) - ts := <-mockOutputs[i%3].TChan - require.NoError(b, ts.Ack(tCtx, nil)) - res := <-resChan - if res != nil { - b.Errorf("Received unexpected errors from broker: %v", res) - } - } - - b.StopTimer() -} diff --git a/internal/impl/pure/output_broker_test.go b/internal/impl/pure/output_broker_test.go deleted file mode 100644 index 3d93b9cf5a..0000000000 --- a/internal/impl/pure/output_broker_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package pure_test - -import ( - "context" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestFanOutBroker(t *testing.T) { - dir := t.TempDir() - - conf, err := testutil.OutputFromYAML(strings.ReplaceAll(` -broker: - pattern: fan_out - outputs: - - file: - path: '$DIR/one/foo-${!count("1s")}.txt' - codec: all-bytes - processors: - - bloblang: 'root = "one-" + content()' - - file: - path: '$DIR/two/bar-${!count("2s")}.txt' - codec: all-bytes - processors: - - bloblang: 'root = "two-" + content()' -`, "$DIR", dir)) - require.NoError(t, err) - - s, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - if err = s.Consume(sendChan); err != nil { - t.Fatal(err) - } - - defer func() { - s.TriggerCloseNow() - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - assert.NoError(t, s.WaitForClose(ctx)) - done() - }() - - inputs := []string{ - "first", "second", "third", - } - expFiles := map[string]string{ - "./one/foo-1.txt": "one-first", - "./one/foo-2.txt": "one-second", - "./one/foo-3.txt": "one-third", - "./two/bar-1.txt": "two-first", - "./two/bar-2.txt": "two-second", - "./two/bar-3.txt": "two-third", - } - - for _, input := range inputs { - testMsg := message.QuickBatch([][]byte{[]byte(input)}) - select { - case sendChan <- message.NewTransaction(testMsg, resChan): - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - select { - case res := <-resChan: - if res != nil { - t.Fatal(res) - } - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } - - for k, exp := range expFiles { - k = filepath.Join(dir, k) - fileBytes, err := os.ReadFile(k) - if err != nil { - t.Errorf("Expected file '%v' could not be read: %v", k, err) - continue - } - if act := string(fileBytes); exp != act { - t.Errorf("Wrong contents for file '%v': %v != %v", k, act, exp) - } - } -} - -func TestRoundRobinBroker(t *testing.T) { - dir := t.TempDir() - - conf, err := testutil.OutputFromYAML(strings.ReplaceAll(` -broker: - pattern: round_robin - outputs: - - file: - path: '$DIR/one/foo-${!count("rrfoo")}.txt' - codec: all-bytes - processors: - - bloblang: 'root = "one-" + content()' - - file: - path: '$DIR/two/bar-${!count("rrbar")}.txt' - codec: all-bytes - processors: - - bloblang: 'root = "two-" + content()' -`, "$DIR", dir)) - require.NoError(t, err) - - s, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - if err = s.Consume(sendChan); err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - s.TriggerCloseNow() - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - assert.NoError(t, s.WaitForClose(ctx)) - done() - }) - - inputs := []string{ - "first", "second", "third", "fourth", - } - expFiles := map[string]string{ - "./one/foo-1.txt": "one-first", - "./one/foo-2.txt": "one-third", - "./two/bar-1.txt": "two-second", - "./two/bar-2.txt": "two-fourth", - } - - for _, input := range inputs { - testMsg := message.QuickBatch([][]byte{[]byte(input)}) - select { - case sendChan <- message.NewTransaction(testMsg, resChan): - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - select { - case res := <-resChan: - if res != nil { - t.Fatal(res) - } - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } - - for k, exp := range expFiles { - k = filepath.Join(dir, k) - fileBytes, err := os.ReadFile(k) - if err != nil { - t.Errorf("Expected file '%v' could not be read: %v", k, err) - continue - } - if act := string(fileBytes); exp != act { - t.Errorf("Wrong contents for file '%v': %v != %v", k, act, exp) - } - } -} - -func TestGreedyBroker(t *testing.T) { - dir := t.TempDir() - - conf, err := testutil.OutputFromYAML(strings.ReplaceAll(` -broker: - pattern: greedy - outputs: - - file: - path: '$DIR/one/foo-${!count("gfoo")}.txt' - codec: all-bytes - processors: - - bloblang: 'root = "one-" + content()' - - sleep: - duration: 50ms - - file: - path: '$DIR/two/bar-${!count("gbar")}.txt' - codec: all-bytes - processors: - - bloblang: 'root = "two-" + content()' - - sleep: - duration: 50ms -`, "$DIR", dir)) - require.NoError(t, err) - - s, err := mock.NewManager().NewOutput(conf) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - if err = s.Consume(sendChan); err != nil { - t.Fatal(err) - } - - defer func() { - close(sendChan) - s.TriggerCloseNow() - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - assert.NoError(t, s.WaitForClose(ctx)) - done() - }() - - inputs := []string{ - "first", "second", "third", "fourth", - } - expFiles := map[string][2]string{ - "./one/foo-1.txt": {"one-first", "one-second"}, - "./one/foo-2.txt": {"one-third", "one-fourth"}, - "./two/bar-1.txt": {"two-first", "two-second"}, - "./two/bar-2.txt": {"two-third", "two-fourth"}, - } - - for _, input := range inputs { - testMsg := message.QuickBatch([][]byte{[]byte(input)}) - select { - case sendChan <- message.NewTransaction(testMsg, resChan): - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - - select { - case res := <-resChan: - if res != nil { - t.Fatal(res) - } - case <-time.After(time.Second): - t.Fatal("Action timed out") - } - } - - for k, exp := range expFiles { - k = filepath.Join(dir, k) - fileBytes, err := os.ReadFile(k) - if err != nil { - t.Errorf("Expected file '%v' could not be read: %v", k, err) - continue - } - if act := string(fileBytes); exp[0] != act && exp[1] != act { - t.Errorf("Wrong contents for file '%v': %v != (%v || %v)", k, act, exp[0], exp[1]) - } - } -} - -type mockOutput struct { - outputs map[string]struct{} - mut sync.Mutex -} - -func (m *mockOutput) Connect(context.Context) error { - return nil -} - -func (m *mockOutput) Write(ctx context.Context, msg *service.Message) error { - m.mut.Lock() - defer m.mut.Unlock() - mBytes, err := msg.AsBytes() - m.outputs[string(mBytes)] = struct{}{} - return err -} - -func (m *mockOutput) Close(context.Context) error { - return nil -} - -func TestOutputBrokerConfigs(t *testing.T) { - for _, test := range []struct { - name string - inputConfig string - outputConfig string - output map[string]struct{} - }{ - { - name: "simple inputs", - inputConfig: ` -generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' -`, - outputConfig: ` -broker: - outputs: - - testmeow: {} - processors: - - bloblang: '"first " + content()' - - testmeow: {} - processors: - - bloblang: '"second " + content()' -`, - output: map[string]struct{}{ - "first hello world 1": {}, - "second hello world 1": {}, - }, - }, - { - name: "single input nested processors", - inputConfig: ` -generate: - count: 1 - interval: "" - mapping: 'root = "hello world 1"' -`, - outputConfig: ` -broker: - outputs: - - testmeow: {} - processors: - - bloblang: 'root = content().uppercase()' -processors: - - bloblang: 'root = "outer: " + content()' -`, - output: map[string]struct{}{ - "OUTER: HELLO WORLD 1": {}, - }, - }, - { - name: "single input nested and batched processors", - inputConfig: ` -generate: - count: 3 - interval: "" - mapping: 'root = "hello world 1"' -`, - outputConfig: ` -processors: - - bloblang: 'root = "outer: " + content()' - -broker: - batching: - count: 3 - processors: - - archive: - format: lines - - outputs: - - testmeow: {} - processors: - - bloblang: 'root = "inner: " + content()' -`, - output: map[string]struct{}{ - "inner: outer: hello world 1\nouter: hello world 1\nouter: hello world 1": {}, - }, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - mOut := &mockOutput{ - outputs: map[string]struct{}{}, - } - - env := service.NewEnvironment() - require.NoError(t, env.RegisterOutput("testmeow", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - maxInFlight = 1 - out = mOut - return - })) - - builder := env.NewStreamBuilder() - require.NoError(t, builder.AddInputYAML(test.inputConfig)) - require.NoError(t, builder.AddOutputYAML(test.outputConfig)) - require.NoError(t, builder.SetLoggerYAML(`level: none`)) - - strm, err := builder.Build() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - require.NoError(t, strm.Run(tCtx)) - assert.Equal(t, test.output, mOut.outputs) - }) - } -} diff --git a/internal/impl/pure/output_cache.go b/internal/impl/pure/output_cache.go deleted file mode 100644 index e18fe8c0bf..0000000000 --- a/internal/impl/pure/output_cache.go +++ /dev/null @@ -1,216 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "time" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - coFieldTarget = "target" - coFieldKey = "key" - coFieldTTL = "ttl" -) - -func CacheOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Services"). - Summary(`Stores each message in a xref:components:caches/about.adoc[cache].`). - Description(`Caches are configured as xref:components:caches/about.adoc[resources], where there's a wide variety to choose from. - -The `+"`target`"+` field must reference a configured cache resource label like follows: - -`+"```yaml"+` -output: - cache: - target: foo - key: ${!json("document.id")} - -cache_resources: - - label: foo - memcached: - addresses: - - localhost:11211 - default_ttl: 60s -`+"```"+` - -In order to create a unique `+"`key`"+` value per item you should use function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries].`+service.OutputPerformanceDocs(true, false)). - Fields( - service.NewStringField(coFieldTarget). - Description("The target cache to store messages in."), - service.NewInterpolatedStringField(coFieldKey). - Description("The key to store messages by, function interpolation should be used in order to derive a unique key for each message."). - Examples( - `${!count("items")}-${!timestamp_unix_nano()}`, - `${!json("doc.id")}`, - `${!meta("kafka_key")}`, - ). - Default(`${!count("items")}-${!timestamp_unix_nano()}`), - service.NewInterpolatedStringField(coFieldTTL). - Description("The TTL of each individual item as a duration string. After this period an item will be eligible for removal during the next compaction. Not all caches support per-key TTLs, and those that do not will fall back to their generally configured TTL setting."). - Examples("60s", "5m", "36h"). - Version("3.33.0"). - Advanced(). - Optional(), - service.NewOutputMaxInFlightField(), - ) -} - -func init() { - err := service.RegisterBatchOutput( - "cache", CacheOutputSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - if maxInFlight, err = conf.FieldMaxInFlight(); err != nil { - return - } - - mgr := interop.UnwrapManagement(res) - - var ca *CacheWriter - if ca, err = NewCacheWriter(conf, mgr); err != nil { - return - } - - var s output.Streamed - if s, err = output.NewAsyncWriter("cache", maxInFlight, ca, mgr); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(s) - return - }) - if err != nil { - panic(err) - } -} - -type CacheWriter struct { - mgr bundle.NewManagement - - target string - key *field.Expression - ttl *field.Expression - - log log.Modular -} - -// NewCacheWriter creates a writer for cache the output plugin. -func NewCacheWriter(conf *service.ParsedConfig, mgr bundle.NewManagement) (*CacheWriter, error) { - target, err := conf.FieldString(coFieldTarget) - if err != nil { - return nil, err - } - - keyStr, err := conf.FieldString(coFieldKey) - if err != nil { - return nil, err - } - key, err := mgr.BloblEnvironment().NewField(keyStr) - if err != nil { - return nil, fmt.Errorf("failed to parse key expression: %v", err) - } - - ttlStr, _ := conf.FieldString(coFieldTTL) - ttl, err := mgr.BloblEnvironment().NewField(ttlStr) - if err != nil { - return nil, fmt.Errorf("failed to parse ttl expression: %v", err) - } - - if !mgr.ProbeCache(target) { - return nil, fmt.Errorf("cache resource '%v' was not found", target) - } - return &CacheWriter{ - mgr: mgr, - target: target, - key: key, - ttl: ttl, - log: mgr.Logger(), - }, nil -} - -// Connect does nothing. -func (c *CacheWriter) Connect(ctx context.Context) error { - return nil -} - -func (c *CacheWriter) writeMulti(ctx context.Context, msg message.Batch) (err error) { - items := map[string]cache.TTLItem{} - if err = msg.Iter(func(i int, p *message.Part) error { - ttls, terr := c.ttl.String(i, msg) - if terr != nil { - return fmt.Errorf("ttl interpolation error: %w", terr) - } - var ttl *time.Duration - if ttls != "" { - t, terr := time.ParseDuration(ttls) - if terr != nil { - c.log.Debug("Invalid duration string for TTL field: %v\n", terr) - return fmt.Errorf("ttl field: %w", terr) - } - ttl = &t - } - keyStr, terr := c.key.String(i, msg) - if terr != nil { - return fmt.Errorf("key interpolation error: %w", terr) - } - items[keyStr] = cache.TTLItem{ - Value: p.AsBytes(), - TTL: ttl, - } - return nil - }); err != nil { - return - } - if cerr := c.mgr.AccessCache(ctx, c.target, func(ac cache.V1) { - err = ac.SetMulti(ctx, items) - }); cerr != nil { - err = cerr - } - return -} - -// WriteBatch attempts to store a message within a cache. -func (c *CacheWriter) WriteBatch(ctx context.Context, msg message.Batch) (err error) { - if msg.Len() > 1 { - return c.writeMulti(ctx, msg) - } - var key, ttls string - if key, err = c.key.String(0, msg); err != nil { - err = fmt.Errorf("key interpolation error: %v", err) - return - } - if ttls, err = c.ttl.String(0, msg); err != nil { - err = fmt.Errorf("ttl interpolation error: %v", err) - return - } - var ttl *time.Duration - if ttls != "" { - t, err := time.ParseDuration(ttls) - if err != nil { - c.log.Debug("Invalid duration string for TTL field: %v", err) - return fmt.Errorf("ttl field: %w", err) - } - ttl = &t - } - - if cerr := c.mgr.AccessCache(ctx, c.target, func(cache cache.V1) { - err = cache.Set(ctx, key, msg.Get(0).AsBytes(), ttl) - }); cerr != nil { - err = cerr - } - return -} - -// Close does nothing. -func (c *CacheWriter) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/output_cache_test.go b/internal/impl/pure/output_cache_test.go deleted file mode 100644 index a49cb66787..0000000000 --- a/internal/impl/pure/output_cache_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/impl/pure" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func testCacheOutput(t testing.TB, res bundle.NewManagement, confPattern string, args ...any) *pure.CacheWriter { - pConf, err := pure.CacheOutputSpec().ParseYAML(fmt.Sprintf(confPattern, args...), nil) - require.NoError(t, err) - - w, err := pure.NewCacheWriter(pConf, res) - require.NoError(t, err) - - return w -} - -func TestCacheSingle(t *testing.T) { - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - w := testCacheOutput(t, mgr, ` -key: ${!json("id")} -target: foocache -`) - - tCtx := context.Background() - - require.NoError(t, w.WriteBatch(tCtx, message.QuickBatch([][]byte{ - []byte(`{"id":"1","value":"first"}`), - }))) - - assert.Equal(t, map[string]mock.CacheItem{ - "1": {Value: `{"id":"1","value":"first"}`}, - }, mgr.Caches["foocache"]) -} - -func TestCacheBatch(t *testing.T) { - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - w := testCacheOutput(t, mgr, ` -key: ${!json("id")} -target: foocache -`) - - tCtx := context.Background() - - require.NoError(t, w.WriteBatch(tCtx, message.QuickBatch([][]byte{ - []byte(`{"id":"1","value":"first"}`), - []byte(`{"id":"2","value":"second"}`), - []byte(`{"id":"3","value":"third"}`), - []byte(`{"id":"4","value":"fourth"}`), - }))) - - assert.Equal(t, map[string]mock.CacheItem{ - "1": {Value: `{"id":"1","value":"first"}`}, - "2": {Value: `{"id":"2","value":"second"}`}, - "3": {Value: `{"id":"3","value":"third"}`}, - "4": {Value: `{"id":"4","value":"fourth"}`}, - }, mgr.Caches["foocache"]) -} - -func TestCacheSingleTTL(t *testing.T) { - c := map[string]mock.CacheItem{} - - mgr := mock.NewManager() - mgr.Caches["foocache"] = c - - w := testCacheOutput(t, mgr, ` -key: ${!json("id")} -target: foocache -ttl: 2s -`) - - tCtx := context.Background() - - require.NoError(t, w.WriteBatch(tCtx, message.QuickBatch([][]byte{ - []byte(`{"id":"1","value":"first"}`), - }))) - - two := time.Second * 2 - assert.Equal(t, map[string]mock.CacheItem{ - "1": {Value: `{"id":"1","value":"first"}`, TTL: &two}, - }, c) -} - -func TestCacheBatchTTL(t *testing.T) { - c := map[string]mock.CacheItem{} - - mgr := mock.NewManager() - mgr.Caches["foocache"] = c - - w := testCacheOutput(t, mgr, ` -key: ${!json("id")} -target: foocache -ttl: 2s -`) - - tCtx := context.Background() - - require.NoError(t, w.WriteBatch(tCtx, message.QuickBatch([][]byte{ - []byte(`{"id":"1","value":"first"}`), - []byte(`{"id":"2","value":"second"}`), - []byte(`{"id":"3","value":"third"}`), - []byte(`{"id":"4","value":"fourth"}`), - }))) - - twosec := time.Second * 2 - - assert.Equal(t, map[string]mock.CacheItem{ - "1": { - Value: `{"id":"1","value":"first"}`, - TTL: &twosec, - }, - "2": { - Value: `{"id":"2","value":"second"}`, - TTL: &twosec, - }, - "3": { - Value: `{"id":"3","value":"third"}`, - TTL: &twosec, - }, - "4": { - Value: `{"id":"4","value":"fourth"}`, - TTL: &twosec, - }, - }, c) -} - -//------------------------------------------------------------------------------ - -func TestCacheBasic(t *testing.T) { - mgrConf, err := testutil.ManagerFromYAML(` -cache_resources: - - label: foo - memory: {} -`) - require.NoError(t, err) - - mgr, err := manager.New(mgrConf) - require.NoError(t, err) - - c := testCacheOutput(t, mgr, ` -key: ${!json("key")} -target: foo -`) - - tCtx := context.Background() - - exp := map[string]string{} - for i := 0; i < 100; i++ { - key := fmt.Sprintf("key%v", i) - value := fmt.Sprintf(`{"key":"%v","test":"hello world"}`, key) - exp[key] = value - if err := c.WriteBatch(tCtx, message.QuickBatch([][]byte{[]byte(value)})); err != nil { - t.Fatal(err) - } - } - - var memCache cache.V1 - require.NoError(t, mgr.AccessCache(context.Background(), "foo", func(v cache.V1) { - memCache = v - })) - - for k, v := range exp { - res, err := memCache.Get(context.Background(), k) - if err != nil { - t.Errorf("Missing key '%v': %v", k, err) - } - if exp, act := v, string(res); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - } -} - -func TestCacheBatches(t *testing.T) { - mgrConf, err := testutil.ManagerFromYAML(` -cache_resources: - - label: foo - memory: {} -`) - require.NoError(t, err) - - mgr, err := manager.New(mgrConf) - require.NoError(t, err) - - c := testCacheOutput(t, mgr, ` -key: ${!json("key")} -target: foo -`) - - tCtx := context.Background() - - exp := map[string]string{} - for i := 0; i < 10; i++ { - msg := message.QuickBatch(nil) - for j := 0; j < 10; j++ { - key := fmt.Sprintf("key%v", i*10+j) - value := fmt.Sprintf(`{"key":"%v","test":"hello world"}`, key) - exp[key] = value - msg = append(msg, message.NewPart([]byte(value))) - } - if err := c.WriteBatch(tCtx, msg); err != nil { - t.Fatal(err) - } - } - - var memCache cache.V1 - require.NoError(t, mgr.AccessCache(context.Background(), "foo", func(v cache.V1) { - memCache = v - })) - - for k, v := range exp { - res, err := memCache.Get(context.Background(), k) - if err != nil { - t.Errorf("Missing key '%v': %v", k, err) - } - if exp, act := v, string(res); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - } -} diff --git a/internal/impl/pure/output_drop.go b/internal/impl/pure/output_drop.go deleted file mode 100644 index 89a16ea263..0000000000 --- a/internal/impl/pure/output_drop.go +++ /dev/null @@ -1,52 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "drop", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Drops all messages.`). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, res *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - nm := interop.UnwrapManagement(res) - var o output.Streamed - if o, err = output.NewAsyncWriter("drop", 1, newDropWriter(nm.Logger()), nm); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(o) - return - }) - if err != nil { - panic(err) - } -} - -type dropWriter struct { - log log.Modular -} - -func newDropWriter(log log.Modular) *dropWriter { - return &dropWriter{log: log} -} - -func (d *dropWriter) Connect(ctx context.Context) error { - return nil -} - -func (d *dropWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - return nil -} - -func (d *dropWriter) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/output_drop_on.go b/internal/impl/pure/output_drop_on.go deleted file mode 100644 index cc2103e8c8..0000000000 --- a/internal/impl/pure/output_drop_on.go +++ /dev/null @@ -1,303 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "regexp" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - dooFieldError = "error" - dooFieldErrorPatterns = "error_patterns" - dooFieldBackPressure = "back_pressure" - dooFieldOutput = "output" -) - -func dropOnOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Attempts to write messages to a child output and if the write fails for one of a list of configurable reasons the message is dropped (acked) instead of being reattempted (or nacked).`). - Description(`Regular Benthos outputs will apply back pressure when downstream services aren't accessible, and Benthos retries (or nacks) all messages that fail to be delivered. However, in some circumstances, or for certain output types, we instead might want to relax these mechanisms, which is when this output becomes useful.`). - Example( - "Dropping failed HTTP requests", - "In this example we have a fan_out broker, where we guarantee delivery to our Kafka output, but drop messages if they fail our secondary HTTP client output.", - ` -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ foobar:6379 ] - topic: foo - - drop_on: - error: true - output: - http_client: - url: http://example.com/foo/messages - verb: POST -`, - ). - Example( - "Dropping from outputs that cannot connect", - "Most outputs that attempt to establish and long-lived connection will apply back-pressure when the connection is lost. The following example has a websocket output where if it takes longer than 10 seconds to establish a connection, or recover a lost one, pending messages are dropped.", - ` -output: - drop_on: - back_pressure: 10s - output: - websocket: - url: ws://example.com/foo/messages -`, - ). - Fields( - service.NewBoolField(dooFieldError). - Description("Whether messages should be dropped when the child output returns an error of any type. For example, this could be when an `http_client` output gets a 4XX response code. In order to instead drop only on specific error patterns use the `error_matches` field instead."). - Default(false), - service.NewStringListField(dooFieldErrorPatterns). - Description("A list of regular expressions (re2) where if the child output returns an error that matches any part of any of these patterns the message will be dropped."). - Optional(). - Version("4.27.0"). - Examples([]any{ - "and that was really bad$", - }, []any{ - "roughly [0-9]+ issues occurred", - }), - service.NewDurationField(dooFieldBackPressure). - Description("An optional duration string that determines the maximum length of time to wait for a given message to be accepted by the child output before the message should be dropped instead. The most common reason for an output to block is when waiting for a lost connection to be re-established. Once a message has been dropped due to back pressure all subsequent messages are dropped immediately until the output is ready to process them again. Note that if `error` is set to `false` and this field is specified then messages dropped due to back pressure will return an error response (are nacked or reattempted)."). - Examples("30s", "1m"). - Optional(), - service.NewOutputField(dooFieldOutput). - Description("A child output to wrap with this drop mechanism."), - ) -} - -func init() { - err := service.RegisterBatchOutput( - "drop_on", dropOnOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - maxInFlight = 1 - var s output.Streamed - if s, err = newDropOnWriter(conf, interop.UnwrapManagement(mgr).Logger()); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(s) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type dropOnWriter struct { - log log.Modular - - onError bool - onErrorMatches []*regexp.Regexp - onBackpressure time.Duration - wrapped output.Streamed - - transactionsIn <-chan message.Transaction - transactionsOut chan message.Transaction - - shutSig *shutdown.Signaller -} - -func newDropOnWriter(conf *service.ParsedConfig, log log.Modular) (*dropOnWriter, error) { - onError, err := conf.FieldBool(dooFieldError) - if err != nil { - return nil, err - } - - var onErrMatchesPatterns []*regexp.Regexp - if onErrMatches, _ := conf.FieldStringList(dooFieldErrorPatterns); len(onErrMatches) > 0 { - if onError { - return nil, fmt.Errorf("field '%v' is ineffective when '%v' is set to `true`", dooFieldErrorPatterns, dooFieldError) - } - for i, str := range onErrMatches { - tmp, err := regexp.Compile(str) - if err != nil { - return nil, fmt.Errorf("error pattern %v failed to compile: %w", i, err) - } - onErrMatchesPatterns = append(onErrMatchesPatterns, tmp) - } - } - - var backPressure time.Duration - if bpStr, _ := conf.FieldString(dooFieldBackPressure); bpStr != "" { - var err error - if backPressure, err = time.ParseDuration(bpStr); err != nil { - return nil, fmt.Errorf("failed to parse back_pressure duration: %w", err) - } - } - - pOut, err := conf.FieldOutput(dooFieldOutput) - if err != nil { - return nil, err - } - - return &dropOnWriter{ - log: log, - wrapped: interop.UnwrapOwnedOutput(pOut), - transactionsOut: make(chan message.Transaction), - - onError: onError, - onErrorMatches: onErrMatchesPatterns, - onBackpressure: backPressure, - - shutSig: shutdown.NewSignaller(), - }, nil -} - -func (d *dropOnWriter) loop() { - cnCtx, cnDone := d.shutSig.HardStopCtx(context.Background()) - defer func() { - close(d.transactionsOut) - - d.wrapped.TriggerCloseNow() - _ = d.wrapped.WaitForClose(context.Background()) - - d.shutSig.TriggerHasStopped() - cnDone() - }() - - resChan := make(chan error) - - var gotBackPressure bool - for { - var ts message.Transaction - var open bool - select { - case ts, open = <-d.transactionsIn: - if !open { - return - } - case <-d.shutSig.HardStopChan(): - return - } - - var res error - if d.onBackpressure > 0 { - if !func() bool { - // Use a ticker here and call Stop explicitly. - ticker := time.NewTicker(d.onBackpressure) - defer ticker.Stop() - - if gotBackPressure { - select { - case d.transactionsOut <- message.NewTransaction(ts.Payload, resChan): - gotBackPressure = false - default: - } - } else { - select { - case d.transactionsOut <- message.NewTransaction(ts.Payload, resChan): - case <-ticker.C: - gotBackPressure = true - case <-d.shutSig.HardStopChan(): - return false - } - } - if !gotBackPressure { - select { - case res = <-resChan: - case <-ticker.C: - gotBackPressure = true - go func() { - // We must pull the response that we're due, since - // the component isn't being shut down. - <-resChan - }() - case <-d.shutSig.HardStopChan(): - return false - } - } - if gotBackPressure { - d.log.Warn("Message dropped due to back pressure.") - if d.onError { - res = nil - } else { - res = fmt.Errorf("experienced back pressure beyond: %v", d.onBackpressure) - } - } - return true - }() { - return - } - } else { - // Push data as usual, if the output blocks due to a disconnect then - // we wait as long as it takes. - select { - case d.transactionsOut <- message.NewTransaction(ts.Payload, resChan): - case <-d.shutSig.HardStopChan(): - return - } - select { - case res = <-resChan: - case <-d.shutSig.HardStopChan(): - return - } - } - - if res != nil && d.onError { - d.log.Warn("Message dropped due to: %v", res) - res = nil - } - - if res != nil && len(d.onErrorMatches) > 0 { - errStr := res.Error() - for i, m := range d.onErrorMatches { - if m.MatchString(errStr) { - d.log.Warn("Message dropped due to error matching pattern %v: %v", i, res) - res = nil - break - } - } - } - - if err := ts.Ack(cnCtx, res); err != nil && cnCtx.Err() != nil { - return - } - } -} - -func (d *dropOnWriter) Consume(ts <-chan message.Transaction) error { - if d.transactionsIn != nil { - return component.ErrAlreadyStarted - } - if err := d.wrapped.Consume(d.transactionsOut); err != nil { - return err - } - d.transactionsIn = ts - go d.loop() - return nil -} - -func (d *dropOnWriter) Connected() bool { - return d.wrapped.Connected() -} - -func (d *dropOnWriter) TriggerCloseNow() { - d.shutSig.TriggerHardStop() -} - -func (d *dropOnWriter) WaitForClose(ctx context.Context) error { - select { - case <-d.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_drop_on_test.go b/internal/impl/pure/output_drop_on_test.go deleted file mode 100644 index 95341afa7f..0000000000 --- a/internal/impl/pure/output_drop_on_test.go +++ /dev/null @@ -1,374 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - bmock "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func parseYAMLOutputConf(t testing.TB, formatStr string, args ...any) output.Config { - t.Helper() - conf, err := testutil.OutputFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return conf -} - -func TestDropOnNothing(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "test error", http.StatusForbidden) - })) - t.Cleanup(func() { - ts.Close() - }) - - dropConf := parseYAMLOutputConf(t, ` -drop_on: - error: false - output: - http_client: - url: %v - drop_on: [ %v ] -`, ts.URL, http.StatusForbidden) - - d, err := bmock.NewManager().NewOutput(dropConf) - require.NoError(t, err) - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - d.TriggerCloseNow() - assert.NoError(t, d.WaitForClose(ctx)) - done() - }) - - tChan := make(chan message.Transaction) - rChan := make(chan error) - - require.NoError(t, d.Consume(tChan)) - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foobar")}), rChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var res error - select { - case res = <-rChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - - assert.EqualError(t, res, fmt.Sprintf("%s: HTTP request returned unexpected response code (403): 403 Forbidden, Error: test error", ts.URL)) -} - -func TestDropOnError(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "test error", http.StatusForbidden) - })) - t.Cleanup(func() { - ts.Close() - }) - - dropConf := parseYAMLOutputConf(t, ` -drop_on: - error: true - output: - http_client: - url: %v - drop_on: [ %v ] -`, ts.URL, http.StatusForbidden) - - d, err := bmock.NewManager().NewOutput(dropConf) - require.NoError(t, err) - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - d.TriggerCloseNow() - assert.NoError(t, d.WaitForClose(ctx)) - done() - }) - - tChan := make(chan message.Transaction) - rChan := make(chan error) - - require.NoError(t, d.Consume(tChan)) - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foobar")}), rChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var res error - select { - case res = <-rChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - - assert.NoError(t, res) -} - -func TestDropOnBackpressureWithErrors(t *testing.T) { - // Skip this test in most runs as it relies on awkward timers. - t.Skip() - - var wsMut sync.Mutex - var wsReceived []string - var wsAllow bool - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wsMut.Lock() - allow := wsAllow - wsMut.Unlock() - if !allow { - http.Error(w, "nope", http.StatusForbidden) - return - } - - upgrader := websocket.Upgrader{} - - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - defer ws.Close() - - for { - _, actBytes, err := ws.ReadMessage() - if err != nil { - return - } - wsMut.Lock() - wsReceived = append(wsReceived, string(actBytes)) - wsMut.Unlock() - } - })) - t.Cleanup(func() { - ts.Close() - }) - - dropConf := parseYAMLOutputConf(t, ` -drop_on: - back_pressure: 100ms - output: - websocket: - url: %v -`, "ws://"+strings.TrimPrefix(ts.URL, "http://")) - - d, err := bmock.NewManager().NewOutput(dropConf) - require.NoError(t, err) - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - d.TriggerCloseNow() - assert.NoError(t, d.WaitForClose(ctx)) - done() - }) - - tChan := make(chan message.Transaction) - rChan := make(chan error) - - require.NoError(t, d.Consume(tChan)) - - sendAndGet := func(msg, expErr string) { - t.Helper() - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(msg)}), rChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var res error - select { - case res = <-rChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if expErr == "" { - assert.NoError(t, res) - } else { - assert.EqualError(t, res, expErr) - } - } - - sendAndGet("first", "experienced back pressure beyond: 100ms") - sendAndGet("second", "experienced back pressure beyond: 100ms") - wsMut.Lock() - wsAllow = true - wsMut.Unlock() - <-time.After(time.Second) - - sendAndGet("third", "") - sendAndGet("fourth", "") - - <-time.After(time.Second) - wsMut.Lock() - assert.Equal(t, []string{"third", "fourth"}, wsReceived) - wsMut.Unlock() -} - -func TestDropOnDisconnectBackpressureNoErrors(t *testing.T) { - // Skip this test in most runs as it relies on awkward timers. - t.Skip() - - var wsReceived []string - var ws *websocket.Conn - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - - var err error - if ws, err = upgrader.Upgrade(w, r, nil); err != nil { - return - } - defer ws.Close() - - for { - _, actBytes, err := ws.ReadMessage() - if err != nil { - return - } - wsReceived = append(wsReceived, string(actBytes)) - } - })) - t.Cleanup(func() { - ts.Close() - }) - - dropConf := parseYAMLOutputConf(t, ` -drop_on: - back_pressure: 100ms - error: true - output: - websocket: - url: %v -`, "ws://"+strings.TrimPrefix(ts.URL, "http://")) - - d, err := bmock.NewManager().NewOutput(dropConf) - require.NoError(t, err) - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - d.TriggerCloseNow() - assert.NoError(t, d.WaitForClose(ctx)) - done() - }) - - tChan := make(chan message.Transaction) - rChan := make(chan error) - - require.NoError(t, d.Consume(tChan)) - - sendAndGet := func(msg, expErr string) { - t.Helper() - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(msg)}), rChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var res error - select { - case res = <-rChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - - if expErr == "" { - assert.NoError(t, res) - } else { - assert.EqualError(t, res, expErr) - } - } - - sendAndGet("first", "") - sendAndGet("second", "") - - ts.Close() - ws.Close() - <-time.After(time.Second) - - sendAndGet("third", "") - sendAndGet("fourth", "") - - <-time.After(time.Second) - - assert.Equal(t, []string{"first", "second"}, wsReceived) -} - -func TestDropOnErrorMatches(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - msgBody, _ := io.ReadAll(r.Body) - http.Error(w, string(msgBody), http.StatusForbidden) - })) - t.Cleanup(func() { - ts.Close() - }) - - dropConf := parseYAMLOutputConf(t, ` -drop_on: - error_patterns: - - foobar - output: - http_client: - url: %v - drop_on: [ %v ] -`, ts.URL, http.StatusForbidden) - - d, err := bmock.NewManager().NewOutput(dropConf) - require.NoError(t, err) - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - d.TriggerCloseNow() - assert.NoError(t, d.WaitForClose(ctx)) - done() - }) - - tChan := make(chan message.Transaction) - rChan := make(chan error) - - require.NoError(t, d.Consume(tChan)) - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("error doesnt match")}), rChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var res error - select { - case res = <-rChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - require.Error(t, res) - assert.Contains(t, res.Error(), "error doesnt match") - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foobar")}), rChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - select { - case res = <-rChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - assert.NoError(t, res) -} diff --git a/internal/impl/pure/output_fallback.go b/internal/impl/pure/output_fallback.go deleted file mode 100644 index f1900828d8..0000000000 --- a/internal/impl/pure/output_fallback.go +++ /dev/null @@ -1,273 +0,0 @@ -package pure - -import ( - "context" - "errors" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "fallback", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Version("3.58.0"). - Summary("Attempts to send each message to a child output, starting from the first output on the list. If an output attempt fails then the next output in the list is attempted, and so on."). - Description(` -This pattern is useful for triggering events in the case where certain output targets have broken. For example, if you had an output type `+"`http_client`"+` but wished to reroute messages whenever the endpoint becomes unreachable you could use this pattern: - -`+"```yaml"+` -output: - fallback: - - http_client: - url: http://foo:4195/post/might/become/unreachable - retries: 3 - retry_period: 1s - - http_client: - url: http://bar:4196/somewhere/else - retries: 3 - retry_period: 1s - processors: - - mapping: 'root = "failed to send this message to foo: " + content()' - - file: - path: /usr/local/benthos/everything_failed.jsonl -`+"```"+` - -== Metadata - -When a given output fails the message routed to the following output will have a metadata value named `+"`fallback_error`"+` containing a string error message outlining the cause of the failure. The content of this string will depend on the particular output and can be used to enrich the message or provide information used to broker the data to an appropriate output using something like a `+"`switch`"+` output. - -== Batching - -When an output within a fallback sequence uses batching, like so: - -`+"```yaml"+` -output: - fallback: - - aws_dynamodb: - table: foo - string_columns: - id: ${!json("id")} - content: ${!content()} - batching: - count: 10 - period: 1s - - file: - path: /usr/local/benthos/failed_stuff.jsonl -`+"```"+` - -Benthos makes a best attempt at inferring which specific messages of the batch failed, and only propagates those individual messages to the next fallback tier. - -However, depending on the output and the error returned it is sometimes not possible to determine the individual messages that failed, in which case the whole batch is passed to the next tier in order to preserve at-least-once delivery guarantees.`). - Field(service.NewOutputListField("").Default([]any{})), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - var w *fallbackBroker - if w, err = newFallbackFromParsed(conf); err != nil { - return - } - - out = interop.NewUnwrapInternalOutput(w) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -func newFallbackFromParsed(conf *service.ParsedConfig) (*fallbackBroker, error) { - pOutputs, err := conf.FieldOutputList() - if err != nil { - return nil, err - } - if len(pOutputs) == 0 { - return nil, ErrBrokerNoOutputs - } - - outputs := make([]output.Streamed, len(pOutputs)) - for i, po := range pOutputs { - outputs[i] = interop.UnwrapOwnedOutput(po) - } - - var t *fallbackBroker - if t, err = newFallbackBroker(outputs); err != nil { - return nil, err - } - return t, nil -} - -type fallbackBroker struct { - transactions <-chan message.Transaction - - outputTSChans []chan message.Transaction - outputs []output.Streamed - - shutSig *shutdown.Signaller -} - -func newFallbackBroker(outputs []output.Streamed) (*fallbackBroker, error) { - t := &fallbackBroker{ - transactions: nil, - outputs: outputs, - shutSig: shutdown.NewSignaller(), - } - if len(outputs) == 0 { - return nil, errors.New("missing outputs") - } - t.outputTSChans = make([]chan message.Transaction, len(t.outputs)) - for i := range t.outputTSChans { - t.outputTSChans[i] = make(chan message.Transaction) - if err := t.outputs[i].Consume(t.outputTSChans[i]); err != nil { - return nil, err - } - } - return t, nil -} - -//------------------------------------------------------------------------------ - -// Consume assigns a new messages channel for the broker to read. -func (t *fallbackBroker) Consume(ts <-chan message.Transaction) error { - if t.transactions != nil { - return component.ErrAlreadyStarted - } - t.transactions = ts - - go t.loop() - return nil -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (t *fallbackBroker) Connected() bool { - for _, out := range t.outputs { - if !out.Connected() { - return false - } - } - return true -} - -//------------------------------------------------------------------------------ - -// loop is an internal loop that brokers incoming messages to many outputs. -func (t *fallbackBroker) loop() { - defer func() { - for _, c := range t.outputTSChans { - close(c) - } - _ = closeAllOutputs(context.Background(), t.outputs) - t.shutSig.TriggerHasStopped() - }() - - for { - var open bool - var tran message.Transaction - - select { - case tran, open = <-t.transactions: - if !open { - return - } - case <-t.shutSig.SoftStopChan(): - return - } - - outSorter, outBatch := message.NewSortGroup(tran.Payload) - nextBatchFromErr := func(err error) message.Batch { - var bErr *batch.Error - if len(outBatch) <= 1 || !errors.As(err, &bErr) { - tmpBatch := outBatch.ShallowCopy() - for _, m := range tmpBatch { - m.MetaSetMut("fallback_error", err.Error()) - } - return tmpBatch - } - - var onlyErrs message.Batch - seenIndexes := map[int]struct{}{} - bErr.WalkPartsBySource(outSorter, outBatch, func(i int, p *message.Part, err error) bool { - if err != nil && p != nil { - if _, exists := seenIndexes[i]; exists { - return true - } - seenIndexes[i] = struct{}{} - tmp := p.ShallowCopy() - tmp.MetaSetMut("fallback_error", err.Error()) - onlyErrs = append(onlyErrs, tmp) - } - return true - }) - - // This is an edge case that means the only failed messages aren't - // capable of being associated with our origin batch. To be safe we - // fall everything through. - if len(onlyErrs) == 0 { - tmpBatch := outBatch.ShallowCopy() - for _, m := range tmpBatch { - m.MetaSetMut("fallback_error", err.Error()) - } - return tmpBatch - } - - outSorter, outBatch = message.NewSortGroup(onlyErrs) - return outBatch - } - - i := 0 - var ackFn func(ctx context.Context, err error) error - ackFn = func(ctx context.Context, err error) error { - i++ - if err == nil || len(t.outputTSChans) <= i { - return tran.Ack(ctx, err) - } - - select { - case t.outputTSChans[i] <- message.NewTransactionFunc(nextBatchFromErr(err), ackFn): - case <-ctx.Done(): - return ctx.Err() - } - return nil - } - - select { - case t.outputTSChans[i] <- message.NewTransactionFunc(outBatch.ShallowCopy(), ackFn): - case <-t.shutSig.SoftStopChan(): - return - } - } -} - -func (t *fallbackBroker) TriggerCloseNow() { - t.shutSig.TriggerHardStop() -} - -func (t *fallbackBroker) WaitForClose(ctx context.Context) error { - select { - case <-t.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func closeAllOutputs(ctx context.Context, outputs []output.Streamed) error { - for _, o := range outputs { - o.TriggerCloseNow() - } - for _, o := range outputs { - if err := o.WaitForClose(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/output_fallback_test.go b/internal/impl/pure/output_fallback_test.go deleted file mode 100644 index 438fee67da..0000000000 --- a/internal/impl/pure/output_fallback_test.go +++ /dev/null @@ -1,440 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "errors" - "fmt" - "os" - "path/filepath" - "strconv" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ output.Streamed = &fallbackBroker{} - -func parseYAMLOutputConf(t testing.TB, formatStr string, args ...any) (conf output.Config) { - t.Helper() - var err error - conf, err = testutil.OutputFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return -} - -func TestFallbackOutputBasic(t *testing.T) { - dir := t.TempDir() - - conf := parseYAMLOutputConf(t, ` -fallback: - - http_client: - url: http://localhost:11111111/badurl - retries: 1 - retry_period: "1ms" - processors: - - mapping: 'root = "this-should-never-appear %%v".format(count("fallbacktofoo")) + content()' - - file: - path: '%v' - codec: all-bytes - processors: - - mapping: 'root = "two-" + content()' - - file: - path: /dev/null - processors: - - mapping: 'root = "this-should-never-appear %%v".format(count("fallbacktobar")) + content()' -`, filepath.Join(dir, "two", `bar-${!count("fallbacktofoo")}-${!count("fallbacktobar")}.txt`)) - - s, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(sendChan)) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - done() - }) - - inputs := []string{ - "first", "second", "third", "fourth", - } - expFiles := map[string]string{ - "./two/bar-2-1.txt": "two-first", - "./two/bar-4-2.txt": "two-second", - "./two/bar-6-3.txt": "two-third", - "./two/bar-8-4.txt": "two-fourth", - } - - for _, input := range inputs { - testMsg := message.QuickBatch([][]byte{[]byte(input)}) - select { - case sendChan <- message.NewTransaction(testMsg, resChan): - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - - select { - case res := <-resChan: - if res != nil { - t.Fatal(res) - } - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - } - - for k, exp := range expFiles { - k = filepath.Join(dir, k) - fileBytes, err := os.ReadFile(k) - if err != nil { - t.Errorf("Expected file '%v' could not be read: %v", k, err) - continue - } - if act := string(fileBytes); exp != act { - t.Errorf("Wrong contents for file '%v': %v != %v", k, act, exp) - } - } -} - -func TestFallbackDoubleClose(t *testing.T) { - oTM, err := newFallbackBroker([]output.Streamed{&mock.OutputChanneled{}}) - if err != nil { - t.Fatal(err) - } - - // This shouldn't cause a panic - oTM.TriggerCloseNow() - oTM.TriggerCloseNow() -} - -//------------------------------------------------------------------------------ - -func TestFallbackHappyPath(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{ - {}, - {}, - {}, - } - - for _, o := range mockOutputs { - outputs = append(outputs, o) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newFallbackBroker(outputs) - if err != nil { - t.Error(err) - return - } - if err = oTM.Consume(readChan); err != nil { - t.Error(err) - return - } - - for i := 0; i < 10; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - go func() { - var ts message.Transaction - select { - case ts = <-mockOutputs[0].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-mockOutputs[1].TChan: - t.Error("Received message in wrong order") - return - case <-mockOutputs[2].TChan: - t.Error("Received message in wrong order") - return - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - select { - case res := <-resChan: - if res != nil { - t.Error(res) - } - case <-time.After(time.Second): - t.Errorf("Timed out responding to broker") - return - } - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestFallbackHappyishPath(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{ - {}, - {}, - {}, - } - - for _, o := range mockOutputs { - outputs = append(outputs, o) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newFallbackBroker(outputs) - require.NoError(t, err) - - require.NoError(t, oTM.Consume(readChan)) - - for i := 0; i < 10; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - go func() { - var ts message.Transaction - select { - case ts = <-mockOutputs[0].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-mockOutputs[1].TChan: - t.Error("Received message in wrong order") - return - case <-mockOutputs[2].TChan: - t.Error("Received message in wrong order") - return - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - go func() { - require.NoError(t, ts.Ack(tCtx, errors.New("test err"))) - }() - - select { - case ts = <-mockOutputs[1].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - assert.Equal(t, "test err", ts.Payload.Get(0).MetaGetStr("fallback_error")) - case <-mockOutputs[0].TChan: - t.Error("Received message in wrong order") - return - case <-mockOutputs[2].TChan: - t.Error("Received message in wrong order") - return - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - require.NoError(t, ts.Ack(tCtx, nil)) - }() - - select { - case res := <-resChan: - if res != nil { - t.Error(res) - } - case <-time.After(time.Second): - t.Errorf("Timed out responding to broker") - return - } - } - - close(readChan) - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestFallbackAllFail(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{ - {}, - {}, - {}, - } - - for _, o := range mockOutputs { - outputs = append(outputs, o) - } - - readChan := make(chan message.Transaction) - resChan := make(chan error) - - oTM, err := newFallbackBroker(outputs) - if err != nil { - t.Fatal(err) - } - if err = oTM.Consume(readChan); err != nil { - t.Fatal(err) - } - - for i := 0; i < 10; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for broker send") - } - - testErr := errors.New("test error") - go func() { - for j := 0; j < 3; j++ { - var ts message.Transaction - select { - case ts = <-mockOutputs[j%3].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - case <-mockOutputs[(j+1)%3].TChan: - t.Errorf("Received message in wrong order: %v != %v", j%3, (j+1)%3) - return - case <-mockOutputs[(j+2)%3].TChan: - t.Errorf("Received message in wrong order: %v != %v", j%3, (j+2)%3) - return - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - go func() { - require.NoError(t, ts.Ack(tCtx, testErr)) - }() - } - }() - - select { - case res := <-resChan: - if exp, act := testErr, res; exp != act { - t.Errorf("Wrong error returned: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - oTM.TriggerCloseNow() - require.NoError(t, oTM.WaitForClose(tCtx)) -} - -func TestFallbackAllFailParallel(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - outputs := []output.Streamed{} - mockOutputs := []*mock.OutputChanneled{ - {}, - {}, - {}, - } - - for _, o := range mockOutputs { - outputs = append(outputs, o) - } - - readChan := make(chan message.Transaction) - - oTM, err := newFallbackBroker(outputs) - if err != nil { - t.Fatal(err) - } - if err = oTM.Consume(readChan); err != nil { - t.Fatal(err) - } - - resChans := make([]chan error, 10) - for i := range resChans { - resChans[i] = make(chan error, 1) - } - - tallies := [3]int32{} - - wg := sync.WaitGroup{} - wg.Add(len(mockOutputs)) - testErr := errors.New("test error") - - for i, o := range mockOutputs { - i := i - o := o - go func() { - defer wg.Done() - for range resChans { - select { - case ts := <-o.TChan: - go require.NoError(t, ts.Ack(tCtx, testErr)) - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker propagate") - return - } - atomic.AddInt32(&tallies[i], 1) - } - }() - } - - for i, resChan := range resChans { - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo")}), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send", strconv.Itoa(i)) - } - } - - for _, resChan := range resChans { - select { - case res := <-resChan: - if exp, act := testErr, res; exp != act { - t.Errorf("Wrong error returned: %v != %v", act, exp) - } - case <-time.After(time.Second): - t.Error("Timed out responding to broker") - } - } - - wg.Wait() - for _, tally := range tallies { - if int(tally) != len(resChans) { - t.Errorf("Wrong count of propagated messages: %v", tally) - } - } - - close(readChan) - require.NoError(t, oTM.WaitForClose(tCtx)) -} diff --git a/internal/impl/pure/output_inproc.go b/internal/impl/pure/output_inproc.go deleted file mode 100644 index 8124c6fa66..0000000000 --- a/internal/impl/pure/output_inproc.go +++ /dev/null @@ -1,123 +0,0 @@ -package pure - -import ( - "context" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "inproc", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Description(` -Sends data directly to Benthos inputs by connecting to a unique ID. This allows you to hook up isolated streams whilst running Benthos in `+"xref:guides:streams_mode/about.adoc[streams mode]"+`, it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. - -It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs.`). - Field(service.NewStringField("").Default("")), - func(conf *service.ParsedConfig, res *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - nm := interop.UnwrapManagement(res) - - var id string - if id, err = conf.FieldString(); err != nil { - return - } - - var o output.Streamed - if o, err = newInprocOutput(id, nm); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(o) - return - }) - if err != nil { - panic(err) - } -} - -type inprocOutput struct { - pipe string - mgr bundle.NewManagement - log log.Modular - - transactionsOut chan message.Transaction - transactionsIn <-chan message.Transaction - - shutSig *shutdown.Signaller -} - -func newInprocOutput(id string, mgr bundle.NewManagement) (output.Streamed, error) { - i := &inprocOutput{ - pipe: id, - mgr: mgr, - log: mgr.Logger(), - transactionsOut: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - mgr.SetPipe(i.pipe, i.transactionsOut) - return i, nil -} - -func (i *inprocOutput) loop() { - defer func() { - i.mgr.UnsetPipe(i.pipe, i.transactionsOut) - close(i.transactionsOut) - i.shutSig.TriggerHasStopped() - }() - - i.log.Info("Sending inproc messages to ID: %s\n", i.pipe) - - var open bool - for { - var ts message.Transaction - select { - case ts, open = <-i.transactionsIn: - if !open { - return - } - case <-i.shutSig.HardStopChan(): - return - } - - select { - case i.transactionsOut <- ts: - case <-i.shutSig.HardStopChan(): - return - } - } -} - -func (i *inprocOutput) Consume(ts <-chan message.Transaction) error { - if i.transactionsIn != nil { - return component.ErrAlreadyStarted - } - i.transactionsIn = ts - go i.loop() - return nil -} - -func (i *inprocOutput) Connected() bool { - return true -} - -func (i *inprocOutput) TriggerCloseNow() { - i.shutSig.TriggerHardStop() -} - -func (i *inprocOutput) WaitForClose(ctx context.Context) error { - select { - case <-i.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_inproc_test.go b/internal/impl/pure/output_inproc_test.go deleted file mode 100644 index 9c5bcb3520..0000000000 --- a/internal/impl/pure/output_inproc_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestInproc(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.GetPipe("foo") - assert.Equal(t, err, component.ErrPipeNotFound) - - conf := output.NewConfig() - conf.Type = "inproc" - conf.Plugin = "foo" - - ip, err := mgr.NewOutput(conf) - require.NoError(t, err) - - tinchan := make(chan message.Transaction) - require.NoError(t, ip.Consume(tinchan)) - - select { - case tinchan <- message.NewTransaction(nil, nil): - case <-time.After(time.Second): - t.Error("Timed out") - } - - var toutchan <-chan message.Transaction - if toutchan, err = mgr.GetPipe("foo"); err != nil { - t.Error(err) - } - - select { - case <-toutchan: - case <-time.After(time.Second): - t.Error("Timed out") - } - - ip.TriggerCloseNow() - require.NoError(t, ip.WaitForClose(tCtx)) - - select { - case _, open := <-toutchan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out") - } - _, err = mgr.GetPipe("foo") - assert.Equal(t, err, component.ErrPipeNotFound) -} diff --git a/internal/impl/pure/output_reject.go b/internal/impl/pure/output_reject.go deleted file mode 100644 index 5316de94a9..0000000000 --- a/internal/impl/pure/output_reject.go +++ /dev/null @@ -1,104 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "reject", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Rejects all messages, treating them as though the output destination failed to publish them.`). - Description(` -The routing of messages after this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch. - -To learn when this output could be useful, see [the <>.`). - Example( - "Rejecting Failed Messages", - ` -This input is particularly useful for routing messages that have failed during processing, where instead of routing them to some sort of dead letter queue we wish to push the error upstream. We can do this with a switch broker:`, - ` -output: - switch: - retry_until_success: false - cases: - - check: '!errored()' - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/the_foos - - - output: - reject: "processing failed due to: ${! error() }" -`, - ). - Field(service.NewStringField("").Default("")), - func(conf *service.ParsedConfig, res *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - var rejMsg string - if rejMsg, err = conf.FieldString(); err != nil { - return - } - - mgr := interop.UnwrapManagement(res) - - var w *rejectWriter - if w, err = newRejectWriter(mgr, rejMsg); err != nil { - return - } - - var s output.Streamed - if s, err = output.NewAsyncWriter("reject", 1, w, mgr); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(s) - return - }) - if err != nil { - panic(err) - } -} - -type rejectWriter struct { - errExpr *field.Expression - log log.Modular -} - -func newRejectWriter(mgr bundle.NewManagement, errorString string) (*rejectWriter, error) { - if errorString == "" { - return nil, errors.New("an error message must be provided in order to provide context for the rejection") - } - errExpr, err := mgr.BloblEnvironment().NewField(errorString) - if err != nil { - return nil, fmt.Errorf("failed to parse error expression: %w", err) - } - return &rejectWriter{errExpr: errExpr, log: mgr.Logger()}, nil -} - -func (w *rejectWriter) Connect(ctx context.Context) error { - return nil -} - -func (w *rejectWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - errStr, err := w.errExpr.String(0, msg) - if err != nil { - // Wow this would be awkward - w.log.Error("Reject message interpolation error: %v", err) - return fmt.Errorf("reject message interpolation error: %w", err) - } - return errors.New(errStr) -} - -func (w *rejectWriter) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/output_reject_errored.go b/internal/impl/pure/output_reject_errored.go deleted file mode 100644 index 8c34a8c3f1..0000000000 --- a/internal/impl/pure/output_reject_errored.go +++ /dev/null @@ -1,280 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "reject_errored", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Rejects messages that have failed their processing steps, resulting in nack behavior at the input level, otherwise sends them to a child output.`). - Description(` -The routing of messages rejected by this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch.`). - Example( - "Rejecting Failed Messages", - ` -The most straight forward use case for this output type is to nack messages that have failed their processing steps. In this example our mapping might fail, in which case the messages that failed are rejected and will be nacked by our input:`, - ` -input: - nats_jetstream: - urls: [ nats://127.0.0.1:4222 ] - subject: foos.pending - -pipeline: - processors: - - mutation: 'root.age = this.fuzzy.age.int64()' - -output: - reject_errored: - nats_jetstream: - urls: [ nats://127.0.0.1:4222 ] - subject: foos.processed -`, - ). - Example( - "DLQing Failed Messages", - ` -Another use case for this output is to send failed messages straight into a dead-letter queue. You use it within a xref:components:outputs/fallback.adoc[fallback output] that allows you to specify where these failed messages should go to next.`, - ` -pipeline: - processors: - - mutation: 'root.age = this.fuzzy.age.int64()' - -output: - fallback: - - reject_errored: - http_client: - url: http://foo:4195/post/might/become/unreachable - retries: 3 - retry_period: 1s - - http_client: - url: http://bar:4196/somewhere/else - retries: 3 - retry_period: 1s -`, - ). - Field(service.NewOutputField("")), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - var w *rejectErroredBroker - if w, err = newRejectErroredFromParsed(conf, mgr); err != nil { - return - } - - out = interop.NewUnwrapInternalOutput(w) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -func newRejectErroredFromParsed(conf *service.ParsedConfig, res *service.Resources) (*rejectErroredBroker, error) { - pOutput, err := conf.FieldOutput() - if err != nil { - return nil, err - } - - output := interop.UnwrapOwnedOutput(pOutput) - - var t *rejectErroredBroker - if t, err = newRejectErroredBroker(output, res); err != nil { - return nil, err - } - return t, nil -} - -type rejectErroredBroker struct { - log *service.Logger - - transactions <-chan message.Transaction - - outputTSChan chan message.Transaction - output output.Streamed - - shutSig *shutdown.Signaller -} - -func newRejectErroredBroker(output output.Streamed, res *service.Resources) (*rejectErroredBroker, error) { - t := &rejectErroredBroker{ - log: res.Logger(), - transactions: nil, - output: output, - shutSig: shutdown.NewSignaller(), - } - t.outputTSChan = make(chan message.Transaction) - if err := t.output.Consume(t.outputTSChan); err != nil { - return nil, err - } - return t, nil -} - -//------------------------------------------------------------------------------ - -// Consume assigns a new messages channel for the broker to read. -func (t *rejectErroredBroker) Consume(ts <-chan message.Transaction) error { - if t.transactions != nil { - return component.ErrAlreadyStarted - } - t.transactions = ts - - go t.loop() - return nil -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (t *rejectErroredBroker) Connected() bool { - return t.output.Connected() -} - -//------------------------------------------------------------------------------ - -// loop is an internal loop that brokers incoming messages to many outputs. -func (t *rejectErroredBroker) loop() { - defer func() { - close(t.outputTSChan) - t.output.TriggerCloseNow() - _ = t.output.WaitForClose(context.Background()) - t.shutSig.TriggerHasStopped() - }() - - closeNowCtx, done := t.shutSig.HardStopCtx(context.Background()) - defer done() - - for { - var open bool - var tran message.Transaction - - select { - case tran, open = <-t.transactions: - if !open { - return - } - case <-t.shutSig.SoftStopChan(): - return - } - - if len(tran.Payload) == 1 { - // No need for pretentious batch fluffery when there's only one - // message. - if err := tran.Payload[0].ErrorGet(); err != nil { - if aerr := tran.Ack(closeNowCtx, fmt.Errorf("rejecting due to failed processing: %w", err)); aerr != nil { - t.log.With("error", aerr).Warn("Failed to nack rejected message") - } - } else { - select { - case t.outputTSChan <- tran: - case <-t.shutSig.HardStopChan(): - return - } - } - continue - } - - // Check for any failed messages in the batch. - var batchErr *batch.Error - for i, m := range tran.Payload { - err := m.ErrorGet() - if err == nil { - continue - } - err = fmt.Errorf("rejecting due to failed processing: %w", err) - if batchErr == nil { - batchErr = batch.NewError(tran.Payload, err) - } - batchErr.Failed(i, err) - } - - // If no messages failed we can pass the batch through unchanged. - if batchErr == nil { - select { - case t.outputTSChan <- tran: - case <-t.shutSig.HardStopChan(): - return - } - continue - } - - // If all messages failed then we can nack the entire batch immediately. - if batchErr.IndexedErrors() == len(tran.Payload) { - _ = tran.Ack(closeNowCtx, batchErr) - continue - } - - // If we get here it means that we have a batch of messages that mixes - // rejected and non-rejected. This is an awkward place to be because if - // a nack were to come back from the transaction we need to merge it - // into our existing batch error. For this we need a sort group. - sortGroup, sortedBatch := message.NewSortGroup(tran.Payload) - - // Reduce batch down into only those we aren't rejecting. - forwardBatch := make(message.Batch, 0, len(tran.Payload)-batchErr.IndexedErrors()) - batchErr.WalkPartsNaively(func(i int, _ *message.Part, err error) bool { - if err == nil { - forwardBatch = append(forwardBatch, sortedBatch[i]) - } - return true - }) - - select { - case t.outputTSChan <- message.NewTransactionFunc(forwardBatch, func(ctx context.Context, err error) error { - if err == nil { - // An ack is simpler, we return the batch error containing our - // rejections and then move on. - return tran.Ack(ctx, batchErr) - } - - var tmpBatchErr *batch.Error - if errors.As(err, &tmpBatchErr) { - tmpBatchErr.WalkPartsBySource(sortGroup, sortedBatch, func(i int, p *message.Part, err error) bool { - if err != nil { - batchErr.Failed(i, err) - } - return true - }) - return tran.Ack(ctx, batchErr) - } - - // If the nack returned isn't a batch error then it's batch-wide, - // this means all messages were either rejected or failed to be - // delivered. - for _, p := range forwardBatch { - if i := sortGroup.GetIndex(p); i >= 0 { - batchErr.Failed(i, err) - } - } - return tran.Ack(ctx, batchErr) - }): - case <-t.shutSig.HardStopChan(): - return - } - } -} - -func (t *rejectErroredBroker) TriggerCloseNow() { - t.shutSig.TriggerHardStop() -} - -func (t *rejectErroredBroker) WaitForClose(ctx context.Context) error { - select { - case <-t.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_reject_errored_test.go b/internal/impl/pure/output_reject_errored_test.go deleted file mode 100644 index 0e5754339e..0000000000 --- a/internal/impl/pure/output_reject_errored_test.go +++ /dev/null @@ -1,468 +0,0 @@ -package pure_test - -import ( - "context" - "io" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestRejectErroredHappy(t *testing.T) { - var resMut sync.Mutex - results := map[string][]string{} // Maps seen paths to seen data - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - resMut.Lock() - defer resMut.Unlock() - - results[r.URL.Path] = append(results[r.URL.Path], string(body)) - })) - - conf := parseYAMLOutputConf(t, strings.ReplaceAll(` -processors: - - mapping: 'root = content().uppercase()' -fallback: - - reject_errored: - http_client: - url: $URL/a - retries: 1 - retry_period: "1ms" - - http_client: - url: $URL/dlq - retries: 1 - retry_period: "1ms" -`, "$URL", server.URL)) - - s, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(sendChan)) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - done() - }) - - for _, testBatch := range [][]string{ - { - "test a", - }, - { - "test b", - "test c", - "test d", - "test e", - }, - } { - var b message.Batch - for _, m := range testBatch { - b = append(b, message.NewPart([]byte(m))) - } - - select { - case sendChan <- message.NewTransaction(b, resChan): - case <-time.After(time.Second * 30): - t.Fatal("Action timed out") - } - - select { - case err := <-resChan: - require.NoError(t, err) - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - } - - resMut.Lock() - assert.Equal(t, map[string][]string{ - "/a": { - "TEST A", - "TEST B", - "TEST C", - "TEST D", - "TEST E", - }, - }, results) - resMut.Unlock() -} - -func TestRejectErroredSad(t *testing.T) { - var resMut sync.Mutex - results := map[string][]string{} // Maps seen paths to seen data - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - resMut.Lock() - defer resMut.Unlock() - - results[r.URL.Path] = append(results[r.URL.Path], string(body)) - })) - - conf := parseYAMLOutputConf(t, strings.ReplaceAll(` -processors: - - mapping: 'root = if content().contains("nope") { throw("no way") }' -fallback: - - reject_errored: - http_client: - url: $URL/a - retries: 1 - retry_period: "1ms" - - http_client: - url: $URL/dlq - retries: 1 - retry_period: "1ms" -`, "$URL", server.URL)) - - s, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(sendChan)) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - done() - }) - - for _, testBatch := range [][]string{ - { - "test nope a", - }, - { - "test b", - "test nope c", - "test d", - "test nope e", - }, - } { - var b message.Batch - for _, m := range testBatch { - b = append(b, message.NewPart([]byte(m))) - } - - select { - case sendChan <- message.NewTransaction(b, resChan): - case <-time.After(time.Second * 30): - t.Fatal("Action timed out") - } - - select { - case err := <-resChan: - require.NoError(t, err) - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - } - - resMut.Lock() - assert.Equal(t, map[string][]string{ - "/a": { - "test b", - "test d", - }, - "/dlq": { - "test nope a", - "test nope c", - "test nope e", - }, - }, results) - resMut.Unlock() -} - -func TestRejectErroredSadWholeBatch(t *testing.T) { - var resMut sync.Mutex - results := map[string][]string{} // Maps seen paths to seen data - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - resMut.Lock() - defer resMut.Unlock() - - results[r.URL.Path] = append(results[r.URL.Path], string(body)) - })) - - conf := parseYAMLOutputConf(t, strings.ReplaceAll(` -processors: - - mapping: 'root = if content().contains("nope") { throw("no way") }' -fallback: - - reject_errored: - http_client: - url: $URL/a - retries: 1 - retry_period: "1ms" - - http_client: - url: $URL/dlq - retries: 1 - retry_period: "1ms" -`, "$URL", server.URL)) - - s, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(sendChan)) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - done() - }) - - for _, testBatch := range [][]string{ - { - "test nope a", - "test nope b", - "test nope c", - "test nope d", - }, - } { - var b message.Batch - for _, m := range testBatch { - b = append(b, message.NewPart([]byte(m))) - } - - select { - case sendChan <- message.NewTransaction(b, resChan): - case <-time.After(time.Second * 30): - t.Fatal("Action timed out") - } - - select { - case err := <-resChan: - require.NoError(t, err) - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - } - - resMut.Lock() - assert.Equal(t, map[string][]string{ - "/dlq": { - "test nope a", - "test nope b", - "test nope c", - "test nope d", - }, - }, results) - resMut.Unlock() -} - -func TestRejectErroredNestedBatchErrors(t *testing.T) { - var resMut sync.Mutex - results := map[string][]string{} // Maps seen paths to seen data - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - resMut.Lock() - defer resMut.Unlock() - - results[r.URL.Path] = append(results[r.URL.Path], string(body)) - })) - - conf := parseYAMLOutputConf(t, strings.ReplaceAll(` -processors: - - mapping: 'root = if content().contains("nope") { throw("no way") }' -fallback: - - reject_errored: - reject_errored: - http_client: - url: $URL/a - retries: 1 - retry_period: "1ms" - processors: - - mapping: 'root = if content().contains("nah") { throw("nuh uh") }' - - http_client: - url: $URL/dlq - retries: 1 - retry_period: "1ms" -`, "$URL", server.URL)) - - s, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(sendChan)) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - done() - }) - - for _, testBatch := range [][]string{ - { - "test nope a", - "test b", - "test nah c", - "test nope d", - }, - { - "test nah e", - }, - { - "test nah f", - "test nah g", - }, - } { - var b message.Batch - for _, m := range testBatch { - b = append(b, message.NewPart([]byte(m))) - } - - select { - case sendChan <- message.NewTransaction(b, resChan): - case <-time.After(time.Second * 30): - t.Fatal("Action timed out") - } - - select { - case err := <-resChan: - require.NoError(t, err) - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - } - - resMut.Lock() - assert.Equal(t, map[string][]string{ - "/a": { - "test b", - }, - "/dlq": { - "test nope a", - "test nah c", - "test nope d", - "test nah e", - "test nah f", - "test nah g", - }, - }, results) - resMut.Unlock() -} - -func TestRejectErroredNestedTotalErrors(t *testing.T) { - var resMut sync.Mutex - results := map[string][]string{} // Maps seen paths to seen data - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - resMut.Lock() - defer resMut.Unlock() - - results[r.URL.Path] = append(results[r.URL.Path], string(body)) - })) - - conf := parseYAMLOutputConf(t, strings.ReplaceAll(` -processors: - - mapping: 'root = if content().contains("nope") { throw("no way") }' -fallback: - - reject_errored: - reject: "everything" - - http_client: - url: $URL/dlq - retries: 1 - retry_period: "1ms" -`, "$URL", server.URL)) - - s, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - sendChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(sendChan)) - - t.Cleanup(func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - done() - }) - - for _, testBatch := range [][]string{ - { - "test nope a", - "test b", - "test c", - "test nope d", - }, - { - "test nope e", - }, - { - "test f", - }, - { - "test g", - "test h", - }, - } { - var b message.Batch - for _, m := range testBatch { - b = append(b, message.NewPart([]byte(m))) - } - - select { - case sendChan <- message.NewTransaction(b, resChan): - case <-time.After(time.Second * 30): - t.Fatal("Action timed out") - } - - select { - case err := <-resChan: - require.NoError(t, err) - case <-time.After(time.Second * 2): - t.Fatal("Action timed out") - } - } - - resMut.Lock() - assert.Equal(t, map[string][]string{ - "/dlq": { - "test nope a", - "test b", - "test c", - "test nope d", - "test nope e", - "test f", - "test g", - "test h", - }, - }, results) - resMut.Unlock() -} diff --git a/internal/impl/pure/output_resource.go b/internal/impl/pure/output_resource.go deleted file mode 100644 index 7bcc494e5e..0000000000 --- a/internal/impl/pure/output_resource.go +++ /dev/null @@ -1,169 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "resource", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary(`Resource is an output type that channels messages to a resource output, identified by its name.`). - Description(`Resources allow you to tidy up deeply nested configs. For example, the config: - -`+"```yaml"+` -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ TODO ] - topic: foo - - gcp_pubsub: - project: bar - topic: baz -`+"```"+` - -Could also be expressed as: - -`+"```yaml"+` -output: - broker: - pattern: fan_out - outputs: - - resource: foo - - resource: bar - -output_resources: - - label: foo - kafka: - addresses: [ TODO ] - topic: foo - - - label: bar - gcp_pubsub: - project: bar - topic: baz -`+"```"+` - -You can find out more about resources in xref:configuration:resources.adoc[]`). - Field(service.NewStringField("").Default("")), - func(conf *service.ParsedConfig, res *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - var resName string - if resName, err = conf.FieldString(); err != nil { - return - } - if !res.HasOutput(resName) { - err = fmt.Errorf("output resource '%v' was not found", resName) - return - } - - mgr := interop.UnwrapManagement(res) - out = interop.NewUnwrapInternalOutput(&resourceOutput{ - mgr: mgr, - name: resName, - log: mgr.Logger(), - shutSig: shutdown.NewSignaller(), - }) - return - }) - if err != nil { - panic(err) - } -} - -type resourceOutput struct { - mgr bundle.NewManagement - name string - log log.Modular - - transactions <-chan message.Transaction - - shutSig *shutdown.Signaller -} - -func (r *resourceOutput) loop() { - cnCtx, cnDone := r.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - defer func() { - r.shutSig.TriggerHasStopped() - }() - - var ts *message.Transaction - for { - if ts == nil { - select { - case t, open := <-r.transactions: - if !open { - return - } - ts = &t - case <-r.shutSig.HardStopChan(): - return - } - } - - var err error - if oerr := r.mgr.AccessOutput(cnCtx, r.name, func(o output.Sync) { - err = o.WriteTransaction(cnCtx, *ts) - }); oerr != nil { - err = oerr - } - if err != nil { - r.log.Error("Failed to obtain output resource '%v': %v", r.name, err) - select { - case <-time.After(time.Second): - case <-r.shutSig.HardStopChan(): - return - } - } else { - ts = nil - } - } -} - -func (r *resourceOutput) Consume(ts <-chan message.Transaction) error { - if r.transactions != nil { - return component.ErrAlreadyStarted - } - r.transactions = ts - go r.loop() - return nil -} - -func (r *resourceOutput) Connected() (isConnected bool) { - var err error - if err = r.mgr.AccessOutput(context.Background(), r.name, func(o output.Sync) { - isConnected = o.Connected() - }); err != nil { - r.log.Error("Failed to obtain output resource '%v': %v", r.name, err) - } - return -} - -func (r *resourceOutput) TriggerCloseNow() { - r.shutSig.TriggerHardStop() -} - -func (r *resourceOutput) WaitForClose(ctx context.Context) error { - select { - case <-r.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_resource_test.go b/internal/impl/pure/output_resource_test.go deleted file mode 100644 index b8da3e935e..0000000000 --- a/internal/impl/pure/output_resource_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestResourceOutput(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*5) - defer done() - - var outLock sync.Mutex - var outTS []message.Transaction - - mgr := mock.NewManager() - mgr.Outputs["foo"] = func(c context.Context, t message.Transaction) error { - outLock.Lock() - defer outLock.Unlock() - outTS = append(outTS, t) - return nil - } - - nConf := output.NewConfig() - nConf.Type = "resource" - nConf.Plugin = "foo" - - p, err := mgr.NewOutput(nConf) - require.NoError(t, err) - - assert.True(t, p.Connected()) - - tChan := make(chan message.Transaction) - assert.NoError(t, p.Consume(tChan)) - - for i := 0; i < 10; i++ { - msg := fmt.Sprintf("foo:%v", i) - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(msg)}), nil): - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - require.Eventually(t, func() bool { - outLock.Lock() - ok := len(outTS) == 10 - outLock.Unlock() - return ok - }, time.Second*5, time.Millisecond*100) - - outLock.Lock() - for i := 0; i < 10; i++ { - exp := fmt.Sprintf("foo:%v", i) - require.NotNil(t, outTS[i]) - require.NotNil(t, outTS[i].Payload) - assert.Equal(t, 1, outTS[i].Payload.Len()) - assert.Equal(t, exp, string(outTS[i].Payload.Get(0).AsBytes())) - } - outLock.Unlock() - - p.TriggerCloseNow() - assert.NoError(t, p.WaitForClose(tCtx)) -} - -func TestOutputResourceBadName(t *testing.T) { - mgr := mock.NewManager() - - conf := output.NewConfig() - conf.Type = "resource" - conf.Plugin = "foo" - - _, err := mgr.NewOutput(conf) - if err == nil { - t.Error("expected error from bad resource") - } -} diff --git a/internal/impl/pure/output_retry.go b/internal/impl/pure/output_retry.go deleted file mode 100644 index 1faaa77d9c..0000000000 --- a/internal/impl/pure/output_retry.go +++ /dev/null @@ -1,270 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "time" - - "github.com/cenkalti/backoff/v4" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/retries" - "github.com/benthosdev/benthos/v4/public/service" -) - -const roFieldOutput = "output" - -func retryOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary("Attempts to write messages to a child output and if the write fails for any reason the message is retried either until success or, if the retries or max elapsed time fields are non-zero, either is reached."). - Description(` -All messages in Benthos are always retried on an output error, but this would usually involve propagating the error back to the source of the message, whereby it would be reprocessed before reaching the output layer once again. - -This output type is useful whenever we wish to avoid reprocessing a message on the event of a failed send. We might, for example, have a deduplication processor that we want to avoid reapplying to the same message more than once in the pipeline. - -Rather than retrying the same output you may wish to retry the send using a different output target (a dead letter queue). In which case you should instead use the ` + "xref:components:outputs/fallback.adoc[`fallback`]" + ` output type.`). - Fields(retries.CommonRetryBackOffFields(0, "500ms", "3s", "0s")...). - Fields( - service.NewOutputField(roFieldOutput). - Description("A child output."), - ) -} - -func init() { - err := service.RegisterBatchOutput( - "retry", retryOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - maxInFlight = 1 - - var s output.Streamed - if s, err = retryOutputFromConfig(conf, interop.UnwrapManagement(mgr)); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(s) - return - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -// RetryOutputIndefinitely returns a wrapped variant of the provided output -// where send errors downstream are automatically caught and retried rather than -// propagated upstream as nacks. -func RetryOutputIndefinitely(mgr bundle.NewManagement, wrapped output.Streamed) (output.Streamed, error) { - return newIndefiniteRetry(mgr, nil, wrapped) -} - -func retryOutputFromConfig(conf *service.ParsedConfig, mgr bundle.NewManagement) (output.Streamed, error) { - pOut, err := conf.FieldOutput(dooFieldOutput) - if err != nil { - return nil, err - } - - var boffCtor func() backoff.BackOff - if boffCtor, err = retries.CommonRetryBackOffCtorFromParsed(conf); err != nil { - return nil, err - } - - return newIndefiniteRetry(mgr, boffCtor, interop.UnwrapOwnedOutput(pOut)) -} - -func newIndefiniteRetry(mgr bundle.NewManagement, backoffCtor func() backoff.BackOff, wrapped output.Streamed) (*indefiniteRetry, error) { - if backoffCtor == nil { - backoffCtor = func() backoff.BackOff { - boff := backoff.NewExponentialBackOff() - boff.InitialInterval = time.Millisecond * 500 - boff.MaxInterval = time.Second * 3 - boff.MaxElapsedTime = 0 - return boff - } - } - - return &indefiniteRetry{ - log: mgr.Logger(), - wrapped: wrapped, - backoffCtor: backoffCtor, - transactionsOut: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - }, nil -} - -// indefiniteRetry is an output type that continuously writes a message to a -// child output until the send is successful. -type indefiniteRetry struct { - wrapped output.Streamed - backoffCtor func() backoff.BackOff - - log log.Modular - - transactionsIn <-chan message.Transaction - transactionsOut chan message.Transaction - - shutSig *shutdown.Signaller -} - -func (r *indefiniteRetry) loop() { - wg := sync.WaitGroup{} - - defer func() { - wg.Wait() - close(r.transactionsOut) - r.wrapped.TriggerCloseNow() - _ = r.wrapped.WaitForClose(context.Background()) - r.shutSig.TriggerHasStopped() - }() - - cnCtx, cnDone := r.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - errInterruptChan := make(chan struct{}) - var errLooped int64 - - for !r.shutSig.IsSoftStopSignalled() { - // Do not consume another message while pending messages are being - // reattempted. - for atomic.LoadInt64(&errLooped) > 0 { - select { - case <-errInterruptChan: - case <-time.After(time.Millisecond * 100): - // Just incase an interrupt doesn't arrive. - case <-r.shutSig.HardStopChan(): - return - } - } - - var tran message.Transaction - var open bool - select { - case tran, open = <-r.transactionsIn: - if !open { - return - } - case <-r.shutSig.HardStopChan(): - return - } - - rChan := make(chan error) - select { - case r.transactionsOut <- message.NewTransaction(tran.Payload.ShallowCopy(), rChan): - case <-r.shutSig.HardStopChan(): - return - } - - wg.Add(1) - go func(ts message.Transaction, resChan chan error) { - var backOff backoff.BackOff - var resOut error - var inErrLoop bool - - defer func() { - wg.Done() - if inErrLoop { - atomic.AddInt64(&errLooped, -1) - - // We're exiting our error loop, so (attempt to) interrupt the - // consumer. - select { - case errInterruptChan <- struct{}{}: - default: - } - } - }() - - for !r.shutSig.IsHardStopSignalled() { - var res error - select { - case res = <-resChan: - case <-r.shutSig.HardStopChan(): - return - } - - if res != nil { - if !inErrLoop { - inErrLoop = true - atomic.AddInt64(&errLooped, 1) - } - - if backOff == nil { - backOff = r.backoffCtor() - } - - nextBackoff := backOff.NextBackOff() - if nextBackoff == backoff.Stop { - r.log.Error("Failed to send message: %v\n", res) - resOut = errors.New("message failed to reach a target destination") - break - } - - r.log.Warn("Failed to send message: %v\n", res) - - select { - case <-time.After(nextBackoff): - case <-r.shutSig.HardStopChan(): - return - } - - select { - case r.transactionsOut <- message.NewTransaction(ts.Payload.ShallowCopy(), resChan): - case <-r.shutSig.HardStopChan(): - return - } - } else { - resOut = nil - break - } - } - - if err := ts.Ack(cnCtx, resOut); err != nil && cnCtx.Err() != nil { - return - } - }(tran, rChan) - } -} - -// Consume assigns a messages channel for the output to read. -func (r *indefiniteRetry) Consume(ts <-chan message.Transaction) error { - if r.transactionsIn != nil { - return component.ErrAlreadyStarted - } - if err := r.wrapped.Consume(r.transactionsOut); err != nil { - return err - } - r.transactionsIn = ts - go r.loop() - return nil -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (r *indefiniteRetry) Connected() bool { - return r.wrapped.Connected() -} - -// CloseAsync shuts down the Retry input and stops processing requests. -func (r *indefiniteRetry) TriggerCloseNow() { - r.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the Retry input has closed down. -func (r *indefiniteRetry) WaitForClose(ctx context.Context) error { - select { - case <-r.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_retry_test.go b/internal/impl/pure/output_retry_test.go deleted file mode 100644 index 5b42c42daf..0000000000 --- a/internal/impl/pure/output_retry_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package pure - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestRetryConfigErrs(t *testing.T) { - conf := parseYAMLOutputConf(t, ` -retry: {} -`) - - if _, err := bundle.AllOutputs.Init(conf, mock.NewManager()); err == nil { - t.Error("Expected error from bad retry output") - } - - conf = parseYAMLOutputConf(t, ` -retry: - output: - drop: {} - backoff: - initial_interval: not a time period -`) - - if _, err := bundle.AllOutputs.Init(conf, mock.NewManager()); err == nil { - t.Error("Expected error from bad initial period") - } -} - -func assertEqualMsg(t testing.TB, left, right message.Batch) { - t.Helper() - - require.Equal(t, left.Len(), right.Len()) - for i := 0; i < left.Len(); i++ { - pLeft, pRight := left.Get(i), right.Get(i) - - leftBytes, rightBytes := pLeft.AsBytes(), pRight.AsBytes() - assert.Equal(t, string(leftBytes), string(rightBytes)) - } -} - -func TestRetryBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - conf := parseYAMLOutputConf(t, ` -retry: - output: - drop: {} -`) - - output, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - ret, ok := output.(*indefiniteRetry) - if !ok { - t.Fatalf("Failed to cast: %T", output) - } - - mOut := &mock.OutputChanneled{} - ret.wrapped = mOut - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = ret.Consume(tChan); err != nil { - t.Fatal(err) - } - - testMsg := message.QuickBatch(nil) - go func() { - select { - case tChan <- message.NewTransaction(testMsg, resChan): - case <-time.After(time.Second): - t.Error("timed out") - } - }() - - var tran message.Transaction - select { - case tran = <-mOut.TChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - - assertEqualMsg(t, tran.Payload, testMsg) - require.NoError(t, tran.Ack(ctx, nil)) - - select { - case res := <-resChan: - if err = res; err != nil { - t.Error(err) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - output.TriggerCloseNow() - require.NoError(t, output.WaitForClose(ctx)) -} - -func TestRetrySadPath(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - conf := parseYAMLOutputConf(t, ` -retry: - output: - drop: {} - backoff: - initial_interval: 10us - max_interval: 10us -`) - - output, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - ret, ok := output.(*indefiniteRetry) - if !ok { - t.Fatal("Failed to cast") - } - - mOut := &mock.OutputChanneled{} - ret.wrapped = mOut - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - if err = ret.Consume(tChan); err != nil { - t.Fatal(err) - } - - testMsg := message.QuickBatch(nil) - tran := message.NewTransaction(testMsg, resChan) - - go func() { - select { - case tChan <- tran: - case <-time.After(time.Second): - t.Error("timed out") - } - }() - - for i := 0; i < 100; i++ { - select { - case tran = <-mOut.TChan: - case <-resChan: - t.Fatal("Received response not retry") - case <-time.After(time.Second): - t.Fatal("timed out") - } - - assertEqualMsg(t, tran.Payload, testMsg) - require.NoError(t, tran.Ack(ctx, component.ErrFailedSend)) - } - - select { - case tran = <-mOut.TChan: - case <-resChan: - t.Fatal("Received response not retry") - case <-time.After(time.Second): - t.Fatal("timed out") - } - - assertEqualMsg(t, tran.Payload, testMsg) - require.NoError(t, tran.Ack(ctx, nil)) - - select { - case res := <-resChan: - if err = res; err != nil { - t.Error(err) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } - - output.TriggerCloseNow() - require.NoError(t, output.WaitForClose(ctx)) -} - -func expectFromRetry( - resReturn error, - tChan <-chan message.Transaction, - t *testing.T, - responsesSlice ...string, -) { - t.Helper() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - responses := map[string]struct{}{} - for _, k := range responsesSlice { - responses[k] = struct{}{} - } - - resFns := []func(context.Context, error) error{} - - for len(responses) > 0 { - select { - case tran := <-tChan: - act := string(tran.Payload.Get(0).AsBytes()) - if _, exists := responses[act]; exists { - delete(responses, act) - } else { - t.Errorf("Wrong result: %v", act) - } - resFns = append(resFns, tran.Ack) - case <-time.After(time.Second): - t.Fatal("timed out") - } - } - - for _, resFn := range resFns { - require.NoError(t, resFn(ctx, resReturn)) - } -} - -func sendForRetry( - value string, - tChan chan message.Transaction, - resChan chan error, - t *testing.T, -) { - t.Helper() - - select { - case tChan <- message.NewTransaction(message.QuickBatch( - [][]byte{[]byte(value)}, - ), resChan): - case <-time.After(time.Second): - t.Fatal("timed out") - } -} - -func ackForRetry( - exp error, - resChan <-chan error, - t *testing.T, -) { - t.Helper() - - select { - case res := <-resChan: - if res != exp { - t.Errorf("Unexpected response error: %v != %v", res, exp) - } - case <-time.After(time.Second): - t.Fatal("timed out") - } -} - -func TestRetryParallel(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - conf := parseYAMLOutputConf(t, ` -retry: - output: - drop: {} - backoff: - initial_interval: 10us - max_interval: 10us -`) - - output, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - ret, ok := output.(*indefiniteRetry) - if !ok { - t.Fatal("Failed to cast") - } - - mOut := &mock.OutputChanneled{} - ret.wrapped = mOut - - tChan := make(chan message.Transaction) - if err = ret.Consume(tChan); err != nil { - t.Fatal(err) - } - - resChan1, resChan2 := make(chan error), make(chan error) - sendForRetry("first", tChan, resChan1, t) - expectFromRetry(component.ErrFailedSend, mOut.TChan, t, "first") - - sendForRetry("second", tChan, resChan2, t) - expectFromRetry(component.ErrFailedSend, mOut.TChan, t, "first", "second") - - select { - case tChan <- message.NewTransaction(nil, nil): - t.Fatal("Accepted transaction during retry loop") - default: - } - expectFromRetry(nil, mOut.TChan, t, "first", "second") - ackForRetry(nil, resChan1, t) - ackForRetry(nil, resChan2, t) - - sendForRetry("third", tChan, resChan1, t) - expectFromRetry(nil, mOut.TChan, t, "third") - ackForRetry(nil, resChan1, t) - - sendForRetry("fourth", tChan, resChan2, t) - expectFromRetry(component.ErrFailedSend, mOut.TChan, t, "fourth") - - expectFromRetry(nil, mOut.TChan, t, "fourth") - ackForRetry(nil, resChan2, t) - - output.TriggerCloseNow() - require.NoError(t, output.WaitForClose(ctx)) -} - -func TestRetryMutations(t *testing.T) { - mockOutput := &mock.OutputChanneled{} - - conf := parseYAMLOutputConf(t, ` -retry: - output: - drop: {} - backoff: - initial_interval: 10us - max_interval: 10us -`) - - output, err := bundle.AllOutputs.Init(conf, mock.NewManager()) - require.NoError(t, err) - - ret, ok := output.(*indefiniteRetry) - require.True(t, ok) - - ret.wrapped = mockOutput - - readChan := make(chan message.Transaction) - require.NoError(t, ret.Consume(readChan)) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - inMsg := message.NewPart(nil) - inMsg.SetStructuredMut(map[string]any{ - "hello": "world", - }) - - inBatch := message.Batch{inMsg} - select { - case readChan <- message.NewTransactionFunc(inBatch, func(ctx context.Context, _ error) error { - inStruct, err := inMsg.AsStructuredMut() - require.NoError(t, err) - - assert.Equal(t, map[string]any{ - "hello": "world", - }, inStruct) - - _, err = gabs.Wrap(inStruct).Set("quack", "moo") - require.NoError(t, err) - return nil - }): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for broker send") - return - } - - testMockOutput := func(mockOutput *mock.OutputChanneled, ackErr error) { - var ts message.Transaction - select { - case ts = <-mockOutput.TChan: - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate") - } - - outStruct, err := ts.Payload.Get(0).AsStructuredMut() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - }, outStruct) - - _, err = gabs.Wrap(outStruct).Set("woof", "meow") - require.NoError(t, err) - require.NoError(t, ts.Ack(tCtx, ackErr)) - } - - testMockOutput(mockOutput, errors.New("test err")) - testMockOutput(mockOutput, nil) - - output.TriggerCloseNow() - require.NoError(t, output.WaitForClose(tCtx)) - - inStruct, err := inMsg.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "hello": "world", - "moo": "quack", - }, inStruct) -} diff --git a/internal/impl/pure/output_switch.go b/internal/impl/pure/output_switch.go deleted file mode 100644 index caa4e58425..0000000000 --- a/internal/impl/pure/output_switch.go +++ /dev/null @@ -1,483 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "strconv" - "sync" - "sync/atomic" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - soFieldRetryUntilSuccess = "retry_until_success" - soFieldStrictMode = "strict_mode" - soFieldCases = "cases" - soFieldCasesCheck = "check" - soFieldCasesContinue = "continue" - soFieldCasesOutput = "output" -) - -func switchOutputSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`The switch output type allows you to route messages to different outputs based on their contents.`). - Description(`Messages that do not pass the check of a single output case are effectively dropped. In order to prevent this outcome set the field `+"<> to `true`"+`, in which case messages that do not pass at least one case are considered failed and will be nacked and/or reprocessed depending on your input.`). - Example( - "Basic Multiplexing", - ` -The most common use for a switch output is to multiplex messages across a range of output destinations. The following config checks the contents of the field `+"`type` of messages and sends `foo` type messages to an `amqp_1` output, `bar` type messages to a `gcp_pubsub` output, and everything else to a `redis_streams` output"+`. - -Outputs can have their own processors associated with them, and in this example the `+"`redis_streams`"+` output has a processor that enforces the presence of a type field before sending it.`, - ` -output: - switch: - cases: - - check: this.type == "foo" - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/the_foos - - - check: this.type == "bar" - output: - gcp_pubsub: - project: dealing_with_mike - topic: mikes_bars - - - output: - redis_streams: - url: tcp://localhost:6379 - stream: everything_else - processors: - - mapping: | - root = this - root.type = this.type | "unknown" -`, - ). - Example( - "Control Flow", - ` -The `+"`continue`"+` field allows messages that have passed a case to be tested against the next one also. This can be useful when combining non-mutually-exclusive case checks. - -In the following example a message that passes both the check of the first case as well as the second will be routed to both.`, - ` -output: - switch: - cases: - - check: 'this.user.interests.contains("walks").catch(false)' - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/people_what_think_good - continue: true - - - check: 'this.user.dislikes.contains("videogames").catch(false)' - output: - gcp_pubsub: - project: people - topic: that_i_dont_want_to_hang_with -`, - ). - LintRule(`if this.exists("retry_until_success") && this.retry_until_success { - if this.cases.or([]).any(oconf -> oconf.output.type.or("") == "reject" || oconf.output.reject.type() == "string" ) { - "a 'switch' output with a 'reject' case output must have the field 'switch.retry_until_success' set to 'false', otherwise the 'reject' child output will result in infinite retries" - } -}`). - Fields( - service.NewBoolField(soFieldRetryUntilSuccess). - Description(` -If a selected output fails to send a message this field determines whether it is reattempted indefinitely. If set to false the error is instead propagated back to the input level. - -If a message can be routed to >1 outputs it is usually best to set this to true in order to avoid duplicate messages being routed to an output.`). - Default(false), - service.NewBoolField(soFieldStrictMode). - Description(`This field determines whether an error should be reported if no condition is met. If set to true, an error is propagated back to the input level. The default behavior is false, which will drop the message.`). - Advanced(). - Default(false), - service.NewObjectListField(soFieldCases, - service.NewBloblangField(soFieldCasesCheck). - Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should be routed to the case output. If left empty the case always passes."). - Examples( - `this.type == "foo"`, - `this.contents.urls.contains("https://benthos.dev/")`, - ). - Default(""), - service.NewOutputField(soFieldCasesOutput). - Description("An xref:components:outputs/about.adoc[output] for messages that pass the check to be routed to."), - service.NewBoolField(soFieldCasesContinue). - Description("Indicates whether, if this case passes for a message, the next case should also be tested."). - Default(false). - Advanced(), - ). - Description("A list of switch cases, outlining outputs that can be routed to."). - Example([]any{ - map[string]any{ - "check": `this.urls.contains("http://benthos.dev")`, - "output": map[string]any{ - "cache": map[string]any{ - "target": "foo", - "key": "${!json(\"id\")}", - }, - }, - "continue": true, - }, - map[string]any{ - "output": map[string]any{ - "s3": map[string]any{ - "bucket": "bar", - "path": "${!json(\"id\")}", - }, - }, - }, - }), - ) -} - -var ( - // ErrSwitchNoConditionMet is returned when a message does not match any - // output conditions. - ErrSwitchNoConditionMet = errors.New("no switch output conditions were met by message") - // ErrSwitchNoCasesMatched is returned when a message does not match any - // output cases. - ErrSwitchNoCasesMatched = errors.New("no switch cases were matched by message") - // ErrSwitchNoOutputs is returned when creating a switchOutput type with less than - // 2 outputs. - ErrSwitchNoOutputs = errors.New("attempting to create switch with fewer than 2 cases") -) - -func init() { - err := service.RegisterBatchOutput( - "switch", switchOutputSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - maxInFlight = 1 - - var s output.Streamed - if s, err = switchOutputFromParsed(conf, interop.UnwrapManagement(mgr)); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(s) - return - }) - if err != nil { - panic(err) - } -} - -type switchOutput struct { - logger log.Modular - - transactions <-chan message.Transaction - - strictMode bool - outputTSChans []chan message.Transaction - outputs []output.Streamed - checks []*mapping.Executor - continues []bool - fallthroughs []bool - - shutSig *shutdown.Signaller -} - -func switchOutputFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (*switchOutput, error) { - strictMode, err := conf.FieldBool(soFieldStrictMode) - if err != nil { - return nil, err - } - - retryUntilSuccess, err := conf.FieldBool(soFieldRetryUntilSuccess) - if err != nil { - return nil, err - } - - cases, err := conf.FieldObjectList(soFieldCases) - if err != nil { - return nil, err - } - - o := &switchOutput{ - logger: mgr.Logger(), - transactions: nil, - strictMode: strictMode, - shutSig: shutdown.NewSignaller(), - } - - lCases := len(cases) - if lCases < 2 { - return nil, ErrSwitchNoOutputs - } - if lCases > 0 { - o.outputs = make([]output.Streamed, lCases) - o.checks = make([]*mapping.Executor, lCases) - o.continues = make([]bool, lCases) - o.fallthroughs = make([]bool, lCases) - } - - for i, cConf := range cases { - w, err := cConf.FieldOutput(soFieldCasesOutput) - if err != nil { - return nil, err - } - o.outputs[i] = interop.UnwrapOwnedOutput(w) - - oMgr := mgr.IntoPath("switch", strconv.Itoa(i), "output") - if retryUntilSuccess { - if o.outputs[i], err = RetryOutputIndefinitely(oMgr, o.outputs[i]); err != nil { - return nil, fmt.Errorf("failed to create case '%v' output: %v", i, err) - } - } - - if checkStr, _ := cConf.FieldString(soFieldCasesCheck); checkStr != "" { - if o.checks[i], err = mgr.BloblEnvironment().NewMapping(checkStr); err != nil { - return nil, fmt.Errorf("failed to parse case '%v' check mapping: %v", i, err) - } - } - if o.continues[i], err = cConf.FieldBool(soFieldCasesContinue); err != nil { - return nil, err - } - } - - o.outputTSChans = make([]chan message.Transaction, len(o.outputs)) - for i := range o.outputTSChans { - o.outputTSChans[i] = make(chan message.Transaction) - if err := o.outputs[i].Consume(o.outputTSChans[i]); err != nil { - return nil, err - } - } - return o, nil -} - -func (o *switchOutput) Consume(transactions <-chan message.Transaction) error { - if o.transactions != nil { - return component.ErrAlreadyStarted - } - o.transactions = transactions - - go o.loop() - return nil -} - -func (o *switchOutput) Connected() bool { - for _, out := range o.outputs { - if !out.Connected() { - return false - } - } - return true -} - -func (o *switchOutput) dispatchToTargets( - group *message.SortGroup, - sourceMessage message.Batch, - outputTargets [][]*message.Part, - ackFn func(context.Context, error) error, -) { - var setErr func(error) - var setErrForPart func(*message.Part, error) - var getErr func() error - { - var generalErr error - var batchErr *batch.Error - var errLock sync.Mutex - - setErr = func(err error) { - if err == nil { - return - } - errLock.Lock() - generalErr = err - errLock.Unlock() - } - setErrForPart = func(part *message.Part, err error) { - if err == nil { - return - } - errLock.Lock() - defer errLock.Unlock() - - index := group.GetIndex(part) - if index == -1 { - generalErr = err - return - } - - if batchErr == nil { - batchErr = batch.NewError(sourceMessage, err) - } - batchErr.Failed(index, err) - } - getErr = func() error { - if batchErr != nil { - return batchErr - } - return generalErr - } - } - - var pendingResponses int64 - for _, parts := range outputTargets { - if len(parts) == 0 { - continue - } - pendingResponses++ - } - if pendingResponses == 0 { - ctx, done := o.shutSig.HardStopCtx(context.Background()) - defer done() - _ = ackFn(ctx, nil) - } - - for target, parts := range outputTargets { - if len(parts) == 0 { - continue - } - - i := target - parts := parts - - select { - case o.outputTSChans[i] <- message.NewTransactionFunc(parts, func(ctx context.Context, err error) error { - if err != nil { - var bErr *batch.Error - if errors.As(err, &bErr) { - bErr.WalkPartsBySource(group, sourceMessage, func(i int, p *message.Part, e error) bool { - if e != nil { - setErrForPart(p, e) - } - return true - }) - } else { - for _, p := range parts { - setErrForPart(p, err) - } - } - } - if atomic.AddInt64(&pendingResponses, -1) <= 0 { - return ackFn(ctx, getErr()) - } - return nil - }): - case <-o.shutSig.HardStopChan(): - setErr(component.ErrTypeClosed) - return - } - } -} - -func (o *switchOutput) loop() { - ackInterruptChan := make(chan struct{}) - var ackPending int64 - - defer func() { - // Wait for pending acks to be resolved, or forceful termination - ackWaitLoop: - for atomic.LoadInt64(&ackPending) > 0 { - select { - case <-ackInterruptChan: - case <-time.After(time.Millisecond * 100): - // Just incase an interrupt doesn't arrive. - case <-o.shutSig.HardStopChan(): - break ackWaitLoop - } - } - for _, tChan := range o.outputTSChans { - close(tChan) - } - for _, output := range o.outputs { - output.TriggerCloseNow() - } - for _, output := range o.outputs { - _ = output.WaitForClose(context.Background()) - } - o.shutSig.TriggerHasStopped() - }() - - shutCtx, done := o.shutSig.HardStopCtx(context.Background()) - defer done() - - for !o.shutSig.IsHardStopSignalled() { - var ts message.Transaction - var open bool - - select { - case ts, open = <-o.transactions: - if !open { - return - } - case <-o.shutSig.HardStopChan(): - return - } - - group, trackedMsg := message.NewSortGroup(ts.Payload) - - outputTargets := make([][]*message.Part, len(o.checks)) - if checksErr := trackedMsg.Iter(func(i int, p *message.Part) error { - routedAtLeastOnce := false - for j, exe := range o.checks { - test := true - if exe != nil { - var err error - if test, err = exe.QueryPart(i, trackedMsg); err != nil { - test = false - o.logger.Error("Failed to test case %v: %v\n", j, err) - } - } - if test { - routedAtLeastOnce = true - outputTargets[j] = append(outputTargets[j], p.ShallowCopy()) - if !o.continues[j] { - return nil - } - } - } - if !routedAtLeastOnce && o.strictMode { - o.logger.Error("Message failed to match against at least one output check with strict mode enabled, it will be nacked and/or re-processed") - return ErrSwitchNoConditionMet - } - return nil - }); checksErr != nil { - if err := ts.Ack(shutCtx, checksErr); err != nil && shutCtx.Err() != nil { - return - } - continue - } - - _ = atomic.AddInt64(&ackPending, 1) - o.dispatchToTargets(group, trackedMsg, outputTargets, func(ctx context.Context, err error) error { - ackErr := ts.Ack(ctx, err) - _ = atomic.AddInt64(&ackPending, -1) - select { - case ackInterruptChan <- struct{}{}: - default: - } - return ackErr - }) - } -} - -func (o *switchOutput) TriggerCloseNow() { - o.shutSig.TriggerHardStop() -} - -func (o *switchOutput) WaitForClose(ctx context.Context) error { - select { - case <-o.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/impl/pure/output_switch_test.go b/internal/impl/pure/output_switch_test.go deleted file mode 100644 index 1fa9016ca3..0000000000 --- a/internal/impl/pure/output_switch_test.go +++ /dev/null @@ -1,1016 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "errors" - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func newSwitch(t testing.TB, mockOutputs []*mock.OutputChanneled, confStr string, args ...any) *switchOutput { - t.Helper() - - mgr := mock.NewManager() - - pConf, err := switchOutputSpec().ParseYAML(fmt.Sprintf(confStr, args...), nil) - require.NoError(t, err) - - s, err := switchOutputFromParsed(pConf, mgr) - require.NoError(t, err) - - for i := 0; i < len(mockOutputs); i++ { - close(s.outputTSChans[i]) - s.outputs[i] = mockOutputs[i] - s.outputTSChans[i] = make(chan message.Transaction) - _ = mockOutputs[i].Consume(s.outputTSChans[i]) - } - return s -} - -func TestSwitchNoConditions(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nOutputs, nMsgs := 10, 1000 - - confStr := ` -cases:` - - mockOutputs := []*mock.OutputChanneled{} - for i := 0; i < nOutputs; i++ { - confStr += ` - - output: - drop: {} - continue: true -` - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - } - - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - resFnSlice := []func(context.Context, error) error{} - for j := 0; j < nOutputs; j++ { - select { - case ts := <-mockOutputs[j].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - resFnSlice = append(resFnSlice, ts.Ack) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate") - } - } - for j := 0; j < nOutputs; j++ { - require.NoError(t, resFnSlice[j](ctx, nil)) - } - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchNoRetries(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nOutputs, nMsgs := 10, 1000 - - confStr := ` -retry_until_success: false -cases:` - - mockOutputs := []*mock.OutputChanneled{} - for i := 0; i < nOutputs; i++ { - confStr += ` - - output: - drop: {} - continue: true -` - mockOutputs = append(mockOutputs, &mock.OutputChanneled{}) - } - - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - for i := 0; i < nMsgs; i++ { - content := [][]byte{[]byte(fmt.Sprintf("hello world %v", i))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - resFnSlice := []func(context.Context, error) error{} - for j := 0; j < nOutputs; j++ { - select { - case ts := <-mockOutputs[j].TChan: - if !bytes.Equal(ts.Payload.Get(0).AsBytes(), content[0]) { - t.Errorf("Wrong content returned %s != %s", ts.Payload.Get(0).AsBytes(), content[0]) - } - resFnSlice = append(resFnSlice, ts.Ack) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate") - } - } - for j := 0; j < nOutputs; j++ { - var res error - if j == 1 { - res = errors.New("test") - } else { - res = nil - } - require.NoError(t, resFnSlice[j](ctx, res)) - } - select { - case res := <-resChan: - assert.EqualError(t, res, "test") - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchBatchNoRetries(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - confStr := ` -retry_until_success: false -cases: - - check: 'root = this.id %% 2 == 0' - output: - drop: {} - - check: 'root = true' - output: - reject: "meow" -` - - s := newSwitch(t, nil, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error) - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{ - []byte(`{"content":"hello world","id":0}`), - []byte(`{"content":"hello world","id":1}`), - []byte(`{"content":"hello world","id":2}`), - []byte(`{"content":"hello world","id":3}`), - []byte(`{"content":"hello world","id":4}`), - }) - sortGroup, msg := message.NewSortGroup(msg) - - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - - var res error - select { - case res = <-resChan: - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - - err := res - require.Error(t, err) - - var bOut *batch.Error - require.ErrorAsf(t, err, &bOut, "should be batch error, got: %v", err) - - assert.Equal(t, 2, bOut.IndexedErrors()) - - errContents := []string{} - bOut.WalkPartsBySource(sortGroup, msg, func(i int, p *message.Part, e error) bool { - if e != nil { - errContents = append(errContents, string(p.AsBytes())) - assert.EqualError(t, e, "meow") - } - return true - }) - assert.Equal(t, []string{ - `{"content":"hello world","id":1}`, - `{"content":"hello world","id":3}`, - }, errContents) - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchBatchNoRetriesBatchErr(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - confStr := ` -retry_until_success: false -cases: - - output: - drop: {} - continue: true - - output: - drop: {} - continue: true -` - mockOutputs := []*mock.OutputChanneled{ - {}, {}, - } - - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{ - []byte("hello world 0"), - []byte("hello world 1"), - []byte("hello world 2"), - []byte("hello world 3"), - []byte("hello world 4"), - }) - sortGroup, msg := message.NewSortGroup(msg) - - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker send") - } - - transactions := []message.Transaction{} - for j := 0; j < 2; j++ { - select { - case ts := <-mockOutputs[j].TChan: - transactions = append(transactions, ts) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for broker propagate") - } - } - for j := 0; j < 2; j++ { - var res error - if j == 0 { - batchErr := batch.NewError(transactions[j].Payload, errors.New("not this")) - batchErr.Failed(1, errors.New("err 1")) - batchErr.Failed(3, errors.New("err 3")) - res = batchErr - } else { - res = nil - } - require.NoError(t, transactions[j].Ack(ctx, res)) - } - - select { - case res := <-resChan: - err := res - require.Error(t, err) - - var bOut *batch.Error - require.ErrorAsf(t, err, &bOut, "should be batch error but got %T", err) - - assert.Equal(t, 2, bOut.IndexedErrors()) - - errContents := []string{} - bOut.WalkPartsBySource(sortGroup, msg, func(i int, p *message.Part, e error) bool { - if e != nil { - errContents = append(errContents, string(p.AsBytes())) - assert.EqualError(t, e, fmt.Sprintf("err %v", i)) - } - return true - }) - assert.Equal(t, []string{ - "hello world 1", - "hello world 3", - }, errContents) - case <-time.After(time.Second): - t.Fatal("Timed out responding to broker") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchWithConditions(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nMsgs := 100 - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -cases: - - output: - drop: {} - check: 'this.foo == "bar"' - - output: - drop: {} - check: 'this.foo == "baz"' - - output: - drop: {} -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - closed := 0 - bar := `{"foo":"bar"}` - baz := `{"foo":"baz"}` - - outputLoop: - for closed < len(mockOutputs) { - var ts message.Transaction - var ok bool - - select { - case ts, ok = <-mockOutputs[0].TChan: - if !ok { - closed++ - continue outputLoop - } - if act := string(ts.Payload.Get(0).AsBytes()); act != bar { - t.Errorf("Expected output 0 msgs to equal %s, got %s", bar, act) - } - case ts, ok = <-mockOutputs[1].TChan: - if !ok { - closed++ - continue outputLoop - } - if act := string(ts.Payload.Get(0).AsBytes()); act != baz { - t.Errorf("Expected output 1 msgs to equal %s, got %s", baz, act) - } - case ts, ok = <-mockOutputs[2].TChan: - if !ok { - closed++ - continue outputLoop - } - if act := string(ts.Payload.Get(0).AsBytes()); act == bar || act == baz { - t.Errorf("Expected output 2 msgs to not equal %s or %s, got %s", bar, baz, act) - } - case <-time.After(time.Second): - t.Error("Timed out waiting for output to propagate") - break outputLoop - } - - if !assert.NoError(t, ts.Ack(ctx, nil)) { - break outputLoop - } - } - }() - - for i := 0; i < nMsgs; i++ { - foo := "bar" - if i%3 == 0 { - foo = "qux" - } else if i%2 == 0 { - foo = "baz" - } - content := [][]byte{[]byte(fmt.Sprintf("{\"foo\":%q}", foo))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Errorf("Timed out waiting for output send") - return - } - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to output") - } - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - wg.Wait() -} - -func TestSwitchError(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -cases: - - output: - drop: {} - check: 'this.foo == "bar"' - - output: - drop: {} - check: 'this.foo.not_null() == "baz"' - - output: - drop: {} - check: 'this.foo == "buz"' -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{ - []byte(`{"foo":"bar"}`), - []byte(`{"not_foo":"baz"}`), - []byte(`{"foo":"baz"}`), - []byte(`{"foo":"buz"}`), - []byte(`{"foo":"nope"}`), - }) - - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("timed out waiting to send") - } - - var ts message.Transaction - - for i := 0; i < len(mockOutputs); i++ { - select { - case ts = <-mockOutputs[0].TChan: - assert.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, `{"foo":"bar"}`, string(ts.Payload.Get(0).AsBytes())) - case ts = <-mockOutputs[1].TChan: - assert.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, `{"foo":"baz"}`, string(ts.Payload.Get(0).AsBytes())) - case ts = <-mockOutputs[2].TChan: - assert.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, `{"foo":"buz"}`, string(ts.Payload.Get(0).AsBytes())) - case <-time.After(time.Second): - t.Error("Timed out waiting for output to propagate") - } - require.NoError(t, ts.Ack(ctx, nil)) - } - - select { - case res := <-resChan: - if res != nil { - t.Errorf("Received unexpected errors from output: %v", res) - } - case <-time.After(time.Second): - t.Error("Timed out responding to output") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchBatchSplit(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -cases: - - output: - drop: {} - check: 'this.foo == "bar"' - - output: - drop: {} - check: 'this.foo == "baz"' - - output: - drop: {} - check: 'this.foo == "buz"' -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{ - []byte(`{"foo":"bar"}`), - []byte(`{"foo":"baz"}`), - []byte(`{"foo":"buz"}`), - []byte(`{"foo":"nope"}`), - }) - - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("timed out waiting to send") - } - - var ts message.Transaction - - for i := 0; i < len(mockOutputs); i++ { - select { - case ts = <-mockOutputs[0].TChan: - assert.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, `{"foo":"bar"}`, string(ts.Payload.Get(0).AsBytes())) - case ts = <-mockOutputs[1].TChan: - assert.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, `{"foo":"baz"}`, string(ts.Payload.Get(0).AsBytes())) - case ts = <-mockOutputs[2].TChan: - assert.Equal(t, 1, ts.Payload.Len()) - assert.Equal(t, `{"foo":"buz"}`, string(ts.Payload.Get(0).AsBytes())) - case <-time.After(time.Second): - t.Error("Timed out waiting for output to propagate") - } - require.NoError(t, ts.Ack(ctx, nil)) - } - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Error("Timed out responding to output") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchBatchGroup(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -cases: - - output: - drop: {} - check: 'json().foo.from(0) == "bar"' - - output: - drop: {} - check: 'json().foo.from(0) == "baz"' - - output: - drop: {} - check: 'json().foo.from(0) == "buz"' -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{ - []byte(`{"foo":"baz"}`), - []byte(`{"foo":"bar"}`), - []byte(`{"foo":"buz"}`), - []byte(`{"foo":"nope"}`), - }) - - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("timed out waiting to send") - } - - var ts message.Transaction - - select { - case ts = <-mockOutputs[0].TChan: - t.Error("did not expect message route to 0") - case ts = <-mockOutputs[1].TChan: - assert.Equal(t, 4, ts.Payload.Len()) - assert.Equal(t, `{"foo":"baz"}`, string(ts.Payload.Get(0).AsBytes())) - assert.Equal(t, `{"foo":"bar"}`, string(ts.Payload.Get(1).AsBytes())) - assert.Equal(t, `{"foo":"buz"}`, string(ts.Payload.Get(2).AsBytes())) - assert.Equal(t, `{"foo":"nope"}`, string(ts.Payload.Get(3).AsBytes())) - case ts = <-mockOutputs[2].TChan: - t.Error("did not expect message route to 2") - case <-time.After(time.Second): - t.Error("Timed out waiting for output to propagate") - } - require.NoError(t, ts.Ack(ctx, nil)) - - select { - case <-mockOutputs[0].TChan: - t.Error("did not expect message route to 0") - case <-mockOutputs[2].TChan: - t.Error("did not expect message route to 2") - case res := <-resChan: - if res != nil { - t.Errorf("Received unexpected errors from output: %v", res) - } - case <-time.After(time.Second): - t.Error("Timed out responding to output") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchNoMatch(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -cases: - - output: - drop: {} - check: 'this.foo == "bar"' - - output: - drop: {} - check: 'this.foo == "baz"' - - output: - drop: {} - check: 'false' -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{[]byte(`{"foo":"qux"}`)}) - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for output send") - } - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to output") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchNoMatchStrict(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -strict_mode: true -cases: - - output: - drop: {} - check: 'this.foo == "bar"' - - output: - drop: {} - check: 'this.foo == "baz"' - - output: - drop: {} - check: 'false' -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - msg := message.QuickBatch([][]byte{[]byte(`{"foo":"qux"}`)}) - select { - case readChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for output send") - } - - select { - case res := <-resChan: - require.Error(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to output") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) -} - -func TestSwitchWithConditionsNoFallthrough(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - nMsgs := 100 - - mockOutputs := []*mock.OutputChanneled{{}, {}, {}} - confStr := ` -strict_mode: true -cases: - - output: - drop: {} - check: 'this.foo == "bar"' - - output: - drop: {} - check: 'this.foo == "baz"' - - output: - drop: {} -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - - closed := 0 - bar := `{"foo":"bar"}` - baz := `{"foo":"baz"}` - - outputLoop: - for closed < len(mockOutputs) { - resFns := []func(context.Context, error) error{} - for len(resFns) < 1 { - select { - case ts, ok := <-mockOutputs[0].TChan: - if !ok { - closed++ - continue outputLoop - } - if act := string(ts.Payload.Get(0).AsBytes()); act != bar { - t.Errorf("Expected output 0 msgs to equal %s, got %s", bar, act) - } - resFns = append(resFns, ts.Ack) - case ts, ok := <-mockOutputs[1].TChan: - if !ok { - closed++ - continue outputLoop - } - if act := string(ts.Payload.Get(0).AsBytes()); act != baz { - t.Errorf("Expected output 1 msgs to equal %s, got %s", baz, act) - } - resFns = append(resFns, ts.Ack) - case _, ok := <-mockOutputs[2].TChan: - if !ok { - closed++ - continue outputLoop - } - t.Error("Unexpected msg received by output 3") - case <-time.After(time.Second): - t.Error("Timed out waiting for output to propagate") - break outputLoop - } - } - - for i := 0; i < len(resFns); i++ { - require.NoError(t, resFns[i](ctx, nil)) - } - } - }() - - for i := 0; i < nMsgs; i++ { - foo := "bar" - if i%2 == 0 { - foo = "baz" - } - content := [][]byte{[]byte(fmt.Sprintf("{\"foo\":%q}", foo))} - select { - case readChan <- message.NewTransaction(message.QuickBatch(content), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for output send") - } - - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatal("Timed out responding to output") - } - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - - wg.Wait() -} - -func TestSwitchShutDownFromErrorResponse(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}} - confStr := ` -cases: - - output: - drop: {} - continue: true - - output: - drop: {} - continue: true -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo")}), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - var ts message.Transaction - var open bool - select { - case ts, open = <-mockOutputs[0].TChan: - require.True(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - select { - case _, open = <-mockOutputs[1].TChan: - require.True(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - require.NoError(t, ts.Ack(ctx, errors.New("test"))) - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - - select { - case _, open := <-mockOutputs[0].TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out waiting for msg rcv") - } -} - -func TestSwitchShutDownFromReceive(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}} - confStr := ` -cases: - - output: - drop: {} - continue: true - - output: - drop: {} - continue: true -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo")}), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - select { - case _, open := <-mockOutputs[0].TChan: - require.True(t, open) - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg rcv") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - - select { - case _, open := <-mockOutputs[0].TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out waiting for msg rcv") - } -} - -func TestSwitchShutDownFromSend(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockOutputs := []*mock.OutputChanneled{{}, {}} - confStr := ` -cases: - - output: - drop: {} - continue: true - - output: - drop: {} - continue: true -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("foo")}), resChan): - case <-time.After(time.Second): - t.Fatal("Timed out waiting for msg send") - } - - s.TriggerCloseNow() - require.NoError(t, s.WaitForClose(ctx)) - - select { - case _, open := <-mockOutputs[0].TChan: - assert.False(t, open) - case <-time.After(time.Second): - t.Error("Timed out waiting for msg rcv") - } -} - -func TestSwitchBackPressure(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - t.Parallel() - - mockOutputs := []*mock.OutputChanneled{{}, {}} - confStr := ` -cases: - - output: - drop: {} - continue: true - - output: - drop: {} - continue: true -` - s := newSwitch(t, mockOutputs, confStr) - - readChan := make(chan message.Transaction) - resChan := make(chan error, 1) - - require.NoError(t, s.Consume(readChan)) - - wg := sync.WaitGroup{} - wg.Add(1) - doneChan := make(chan struct{}) - go func() { - defer wg.Done() - // Consume as fast as possible from mock one - for { - select { - case ts := <-mockOutputs[0].TChan: - require.NoError(t, ts.Ack(ctx, nil)) - case <-doneChan: - return - } - } - }() - - i := 0 -bpLoop: - for ; i < 1000; i++ { - select { - case readChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte("hello world")}), resChan): - case <-time.After(time.Millisecond * 200): - break bpLoop - } - } - if i > 500 { - t.Error("We shouldn't be capable of dumping this many messages into a blocked broker") - } - - close(readChan) - close(doneChan) - wg.Wait() -} diff --git a/internal/impl/pure/output_sync_response.go b/internal/impl/pure/output_sync_response.go deleted file mode 100644 index 2e2b1a83e1..0000000000 --- a/internal/impl/pure/output_sync_response.go +++ /dev/null @@ -1,77 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchOutput( - "sync_response", service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`Returns the final message payload back to the input origin of the message, where it is dealt with according to that specific input type.`). - Description(` -For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this output even when combining input types that might not have support for sync responses. An example of an input able to utilize this is the `+"`http_server`"+`. - -It is safe to combine this output with others using broker types. For example, with the `+"`http_server`"+` input we could send the payload to a Kafka topic and also send a modified payload back with: - -`+"```yaml"+` -input: - http_server: - path: /post -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ TODO:9092 ] - topic: foo_topic - - sync_response: {} - processors: - - mapping: 'root = content().uppercase()' -`+"```"+` - -Using the above example and posting the message 'hello world' to the endpoint `+"`/post`"+` Benthos would send it unchanged to the topic `+"`foo_topic`"+` and also respond with 'HELLO WORLD'. - -For more information please read xref:guides:sync_responses.adoc[synchronous responses].`). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error) { - var s output.Streamed - if s, err = output.NewAsyncWriter("sync_response", 1, SyncResponseWriter{}, interop.UnwrapManagement(mgr)); err != nil { - return - } - out = interop.NewUnwrapInternalOutput(s) - return - }) - if err != nil { - panic(err) - } -} - -// SyncResponseWriter is a writer implementation that adds messages to a -// ResultStore located in the context of the first message part of each batch. -// This is essentially a mechanism that returns the result of a pipeline -// directly back to the origin of the message. -type SyncResponseWriter struct{} - -// Connect is a noop. -func (s SyncResponseWriter) Connect(ctx context.Context) error { - return nil -} - -// WriteBatch writes a message batch to a ResultStore located in the first -// message of the batch. -func (s SyncResponseWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - return transaction.SetAsResponse(msg) -} - -// Close is a noop. -func (s SyncResponseWriter) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/output_sync_response_test.go b/internal/impl/pure/output_sync_response_test.go deleted file mode 100644 index ec36d856eb..0000000000 --- a/internal/impl/pure/output_sync_response_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package pure - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" -) - -func TestSyncResponseWriter(t *testing.T) { - wctx := context.Background() - - impl := transaction.NewResultStore() - w := SyncResponseWriter{} - if err := w.Connect(wctx); err != nil { - t.Fatal(err) - } - - ctx := context.WithValue(context.Background(), transaction.ResultStoreKey, impl) - - msg := message.QuickBatch(nil) - p := message.NewPart([]byte("foo")) - p = message.WithContext(ctx, p) - msg = append(msg, p, message.NewPart([]byte("bar"))) - - if err := w.WriteBatch(wctx, msg); err != nil { - t.Fatal(err) - } - - impl.Get() - results := impl.Get() - if len(results) != 1 { - t.Fatalf("Wrong count of result batches: %v", len(results)) - } - if results[0].Len() != 2 { - t.Fatalf("Wrong count of messages: %v", results[0].Len()) - } - if exp, act := "foo", string(results[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message contents: %v != %v", act, exp) - } - if exp, act := "bar", string(results[0].Get(1).AsBytes()); exp != act { - t.Errorf("Wrong message contents: %v != %v", act, exp) - } - if store := message.GetContext(results[0].Get(0)).Value(transaction.ResultStoreKey); store != nil { - t.Error("Unexpected nested result store") - } - - require.NoError(t, w.Close(ctx)) -} diff --git a/internal/impl/pure/package.go b/internal/impl/pure/package.go deleted file mode 100644 index c47082a900..0000000000 --- a/internal/impl/pure/package.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package pure contains all component implementations that are pure, in that -// they do not interact with external systems. This includes all base component -// types such as brokers and is likely necessary as a base for all builds. -package pure diff --git a/internal/impl/pure/processor_archive.go b/internal/impl/pure/processor_archive.go deleted file mode 100644 index 8390e74157..0000000000 --- a/internal/impl/pure/processor_archive.go +++ /dev/null @@ -1,300 +0,0 @@ -package pure - -import ( - "archive/tar" - "archive/zip" - "bytes" - "context" - "fmt" - "os" - "time" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func archiveProcConfig() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Parsing", "Utility"). - Summary("Archives all the messages of a batch into a single message according to the selected archive format."). - Description(` -Some archive formats (such as tar, zip) treat each archive item (message part) as a file with a path. Since message parts only contain raw data a unique path must be generated for each part. This can be done by using function interpolations on the 'path' field as described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. For types that aren't file based (such as binary) the file field is ignored. - -The resulting archived message adopts the metadata of the _first_ message part of the batch. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). - Field(service.NewStringAnnotatedEnumField("format", map[string]string{ - `concatenate`: `Join the raw contents of each message into a single binary message.`, - `tar`: `Archive messages to a unix standard tape archive.`, - `zip`: `Archive messages to a zip file.`, - `binary`: `Archive messages to a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format].`, - `lines`: `Join the raw contents of each message and insert a line break between each one.`, - `json_array`: `Attempt to parse each message as a JSON document and append the result to an array, which becomes the contents of the resulting message.`, - }).Description("The archiving format to apply.")). - Field(service.NewInterpolatedStringField("path"). - Description("The path to set for each message in the archive (when applicable)."). - Example("${!count(\"files\")}-${!timestamp_unix_nano()}.txt"). - Example("${!meta(\"kafka_key\")}-${!json(\"id\")}.json"). - Default("")). - Example("Tar Archive", ` -If we had JSON messages in a batch each of the form: - -`+"```json"+` -{"doc":{"id":"foo","body":"hello world 1"}} -`+"```"+` - -And we wished to tar archive them, setting their filenames to their respective unique IDs (with the extension `+"`.json`"+`), our config might look like -this:`, ` -pipeline: - processors: - - archive: - format: tar - path: ${!json("doc.id")}.json -`) -} - -func init() { - err := service.RegisterBatchProcessor( - "archive", archiveProcConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - return newArchiveFromParsed(conf, mgr) - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type archiveFunc func(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) - -type headerFunc func(index int, body *service.Message) os.FileInfo - -func tarArchive(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) { - buf := &bytes.Buffer{} - tw := tar.NewWriter(buf) - - for i, part := range msg { - hdr, err := tar.FileInfoHeader(hFunc(i, part), "") - if err != nil { - return nil, err - } - if err := tw.WriteHeader(hdr); err != nil { - return nil, err - } - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - if _, err := tw.Write(pBytes); err != nil { - return nil, err - } - } - - tw.Close() - msg[0].SetBytes(buf.Bytes()) - return msg[0], nil -} - -func zipArchive(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) { - buf := &bytes.Buffer{} - zw := zip.NewWriter(buf) - - for i, part := range msg { - h, err := zip.FileInfoHeader(hFunc(i, part)) - if err != nil { - return nil, err - } - h.Method = zip.Deflate - - w, err := zw.CreateHeader(h) - if err != nil { - return nil, err - } - - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - if _, err = w.Write(pBytes); err != nil { - return nil, err - } - } - zw.Close() - - msg[0].SetBytes(buf.Bytes()) - return msg[0], nil -} - -func binaryArchive(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) { - parts := make([][]byte, 0, len(msg)) - for _, p := range msg { - pBytes, err := p.AsBytes() - if err != nil { - return nil, err - } - parts = append(parts, pBytes) - } - - msg[0].SetBytes(message.SerializeBytes(parts)) - return msg[0], nil -} - -func linesArchive(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) { - tmpParts := make([][]byte, len(msg)) - for i, part := range msg { - var err error - if tmpParts[i], err = part.AsBytes(); err != nil { - return nil, err - } - } - msg[0].SetBytes(bytes.Join(tmpParts, []byte("\n"))) - return msg[0], nil -} - -func concatenateArchive(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) { - var buf bytes.Buffer - for _, part := range msg { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - _, _ = buf.Write(pBytes) - } - msg[0].SetBytes(buf.Bytes()) - return msg[0], nil -} - -func jsonArrayArchive(hFunc headerFunc, msg service.MessageBatch) (*service.Message, error) { - var array []any - - for _, part := range msg { - doc, jerr := part.AsStructuredMut() - if jerr != nil { - return nil, fmt.Errorf("failed to parse message as JSON: %v", jerr) - } - array = append(array, doc) - } - msg[0].SetStructuredMut(array) - return msg[0], nil -} - -func strToArchiver(str string) (archiveFunc, error) { - switch str { - case "tar": - return tarArchive, nil - case "zip": - return zipArchive, nil - case "binary": - return binaryArchive, nil - case "lines": - return linesArchive, nil - case "json_array": - return jsonArrayArchive, nil - case "concatenate": - return concatenateArchive, nil - } - return nil, fmt.Errorf("archive format not recognised: %v", str) -} - -//------------------------------------------------------------------------------ - -type archive struct { - archive archiveFunc - path *service.InterpolatedString - log *service.Logger -} - -func newArchiveFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (*archive, error) { - formatStr, err := conf.FieldString("format") - if err != nil { - return nil, err - } - pathStr, err := conf.FieldInterpolatedString("path") - if err != nil { - return nil, err - } - return newArchive(mgr, formatStr, pathStr) -} - -func newArchive(nm *service.Resources, format string, path *service.InterpolatedString) (*archive, error) { - archiver, err := strToArchiver(format) - if err != nil { - return nil, err - } - return &archive{ - archive: archiver, - path: path, - log: nm.Logger(), - }, nil -} - -//------------------------------------------------------------------------------ - -type fakeInfo struct { - name string - size int64 - mode os.FileMode -} - -func (f fakeInfo) Name() string { - return f.name -} - -func (f fakeInfo) Size() int64 { - return f.size -} - -func (f fakeInfo) Mode() os.FileMode { - return f.mode -} - -func (f fakeInfo) ModTime() time.Time { - return time.Now() -} - -func (f fakeInfo) IsDir() bool { - return false -} - -func (f fakeInfo) Sys() any { - return nil -} - -func (d *archive) createHeaderFunc(msg service.MessageBatch) func(int, *service.Message) os.FileInfo { - return func(index int, body *service.Message) os.FileInfo { - bBytes, _ := body.AsBytes() - name, err := msg.TryInterpolatedString(index, d.path) - if err != nil { - d.log.Errorf("Name interpolation error: %w", err) - } - return fakeInfo{ - name: name, - size: int64(len(bBytes)), - mode: 0o666, - } - } -} - -//------------------------------------------------------------------------------ - -func (d *archive) ProcessBatch(ctx context.Context, msg service.MessageBatch) ([]service.MessageBatch, error) { - if len(msg) == 0 { - return nil, nil - } - - newPart, err := d.archive(d.createHeaderFunc(msg), msg) - if err != nil { - d.log.Errorf("Failed to create archive: %v\n", err) - return nil, err - } - - newPart = newPart.WithContext(batch.CtxWithCollapsedCount(newPart.Context(), len(msg))) - return []service.MessageBatch{{newPart}}, nil -} - -func (d *archive) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_archive_test.go b/internal/impl/pure/processor_archive_test.go deleted file mode 100644 index 2ca3074461..0000000000 --- a/internal/impl/pure/processor_archive_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package pure - -import ( - "archive/tar" - "archive/zip" - "bytes" - "context" - "fmt" - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestArchiveBadAlgo(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: does not exist -`, nil) - require.NoError(t, err) - - _, err = newArchiveFromParsed(conf, service.MockResources()) - assert.EqualError(t, err, "archive format not recognised: does not exist") -} - -func TestArchiveTar(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: tar -path: 'foo-${!meta("path")}' -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - proc, err := newArchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - var msg service.MessageBatch - for i, e := range exp { - p := service.NewMessage(e) - p.MetaSet("path", fmt.Sprintf("bar%v", i)) - msg = append(msg, p) - } - - batches, err := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, err) - require.Len(t, batches, 1) - require.Len(t, batches[0], 1) - - bBytes, err := batches[0][0].AsBytes() - require.NoError(t, err) - - buf := bytes.NewBuffer(bBytes) - tr := tar.NewReader(buf) - i := 0 - for { - var hdr *tar.Header - hdr, err = tr.Next() - if err == io.EOF { - // end of tar archive - break - } - require.NoError(t, err) - - newPartBuf := bytes.Buffer{} - _, err = newPartBuf.ReadFrom(tr) - require.NoError(t, err) - - assert.Equal(t, string(exp[i]), newPartBuf.String()) - assert.Equal(t, fmt.Sprintf("foo-bar%v", i), hdr.FileInfo().Name()) - i++ - } - - assert.Equal(t, len(exp), i) -} - -func TestArchiveZip(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: zip -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - proc, err := newArchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - var msg service.MessageBatch - for i, e := range exp { - p := service.NewMessage(e) - p.MetaSet("path", fmt.Sprintf("bar%v", i)) - msg = append(msg, p) - } - - batches, err := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, err) - require.Len(t, batches, 1) - require.Len(t, batches[0], 1) - - bBytes, err := batches[0][0].AsBytes() - require.NoError(t, err) - - buf := bytes.NewReader(bBytes) - zr, err := zip.NewReader(buf, int64(buf.Len())) - require.NoError(t, err) - - i := 0 - for _, f := range zr.File { - fr, err := f.Open() - require.NoError(t, err) - - newPartBuf := bytes.Buffer{} - _, err = newPartBuf.ReadFrom(fr) - require.NoError(t, err) - - assert.Equal(t, string(exp[i]), newPartBuf.String()) - i++ - } - - assert.Equal(t, len(exp), i) -} - -func TestArchiveLines(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: lines -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - proc, err := newArchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - var msg service.MessageBatch - for i, e := range exp { - p := service.NewMessage(e) - p.MetaSet("path", fmt.Sprintf("bar%v", i)) - msg = append(msg, p) - } - - batches, err := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, err) - require.Len(t, batches, 1) - require.Len(t, batches[0], 1) - - require.Equal(t, 5, batch.CtxCollapsedCount(batches[0][0].Context())) - bBytes, err := batches[0][0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, `hello world first part -hello world second part -third part -fourth -5`, string(bBytes)) -} - -func TestArchiveConcatenate(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: concatenate -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - proc, err := newArchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - var msg service.MessageBatch - for i, e := range exp { - p := service.NewMessage(e) - p.MetaSet("path", fmt.Sprintf("bar%v", i)) - msg = append(msg, p) - } - - batches, err := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, err) - require.Len(t, batches, 1) - require.Len(t, batches[0], 1) - - require.Equal(t, 5, batch.CtxCollapsedCount(batches[0][0].Context())) - bBytes, err := batches[0][0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, `hello world first parthello world second partthird partfourth5`, string(bBytes)) -} - -func TestArchiveJSONArray(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: json_array -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte(`{"foo":"bar"}`), - []byte(`5`), - []byte(`"testing 123"`), - []byte(`["nested","array"]`), - []byte(`true`), - } - - proc, err := newArchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - var msg service.MessageBatch - for i, e := range exp { - p := service.NewMessage(e) - p.MetaSet("path", fmt.Sprintf("bar%v", i)) - msg = append(msg, p) - } - - batches, err := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, err) - require.Len(t, batches, 1) - require.Len(t, batches[0], 1) - - require.Equal(t, 5, batch.CtxCollapsedCount(batches[0][0].Context())) - bBytes, err := batches[0][0].AsBytes() - require.NoError(t, err) - - assert.Equal(t, `[{"foo":"bar"},5,"testing 123",["nested","array"],true]`, string(bBytes)) -} - -func TestArchiveEmpty(t *testing.T) { - conf, err := archiveProcConfig().ParseYAML(` -format: json_array -`, nil) - require.NoError(t, err) - - proc, err := newArchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - var msg service.MessageBatch - - batches, err := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, err) - require.Empty(t, batches) -} diff --git a/internal/impl/pure/processor_bloblang.go b/internal/impl/pure/processor_bloblang.go deleted file mode 100644 index e522519116..0000000000 --- a/internal/impl/pure/processor_bloblang.go +++ /dev/null @@ -1,166 +0,0 @@ -package pure - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("bloblang", service.NewConfigSpec(). - Stable(). - Categories("Mapping", "Parsing"). - Summary("Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping on messages."). - Description(` -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information see xref:guides:bloblang/about.adoc[]. - -If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `+"`from \"\"`"+`, where the path must be absolute, or relative from the location that Benthos is executed from. - -== Component rename - -This processor was recently renamed to the `+"xref:components:processors/mapping.adoc[`mapping` processor]"+` in order to make the purpose of the processor more prominent. It is still valid to use the existing `+"`bloblang`"+` name but eventually it will be deprecated and replaced by the new name in example configs.`). - Footnotes(` -== Error handling - -Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use -xref:configuration:error_handling.adoc[standard processor error handling patterns]. - -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behavior, which you can read about in xref:guides:bloblang/about#error-handling.adoc[Error handling].`). - Example("Mapping", ` -Given JSON documents containing an array of fans: - -`+"```json"+` -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"grace","obsession":0.21}, - {"name":"ali","obsession":0.89}, - {"name":"vic","obsession":0.43} - ] -} -`+"```"+` - -We can reduce the fans to only those with an obsession score above 0.5, giving us: - -`+"```json"+` -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"ali","obsession":0.89} - ] -} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - bloblang: | - root = this - root.fans = this.fans.filter(fan -> fan.obsession > 0.5) -`, - ). - Example("More Mapping", ` -When receiving JSON documents of the form: - -`+"```json"+` -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -`+"```"+` - -We could collapse the location names from the state of Washington into a field `+"`Cities`"+`: - -`+"```json"+` -{"Cities": "Bellevue, Olympia, Seattle"} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - bloblang: | - root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -`). - Field(service.NewBloblangField("").Default("")), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - m, err := conf.FieldString() - if err != nil { - return nil, err - } - mgr := interop.UnwrapManagement(res) - p, err := newBloblang(m, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor( - processor.NewAutoObservedBatchedProcessor("bloblang", p, mgr), - ), nil - }) - if err != nil { - panic(err) - } -} - -type bloblangProc struct { - exec *mapping.Executor - log log.Modular -} - -func newBloblang(conf string, mgr bundle.NewManagement) (processor.AutoObservedBatched, error) { - exec, err := mgr.BloblEnvironment().NewMapping(conf) - if err != nil { - if perr, ok := err.(*parser.Error); ok { - return nil, fmt.Errorf("%v", perr.ErrorAtPosition([]rune(conf))) - } - return nil, err - } - return &bloblangProc{ - exec: exec, - log: mgr.Logger(), - }, nil -} - -func (b *bloblangProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - newParts := make([]*message.Part, 0, msg.Len()) - _ = msg.Iter(func(i int, part *message.Part) error { - p, err := b.exec.MapPart(i, msg) - if err != nil { - ctx.OnError(err, i, part) - b.log.Error("%v", err) - p = part - } - if p != nil { - newParts = append(newParts, p) - } - return nil - }) - if len(newParts) == 0 { - return nil, nil - } - return []message.Batch{newParts}, nil -} - -func (b *bloblangProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_bloblang_test.go b/internal/impl/pure/processor_bloblang_test.go deleted file mode 100644 index 57a25c619d..0000000000 --- a/internal/impl/pure/processor_bloblang_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestBloblangCrossfire(t *testing.T) { - msg := message.QuickBatch(nil) - - part := message.NewPart([]byte(`{"foo":{"bar":{"baz":"original value","qux":"dont change"}}}`)) - part.MetaSetMut("foo", "orig1") - part.MetaSetMut("bar", "orig2") - msg = append(msg, part, message.NewPart([]byte(`{}`))) - if err := msg.Iter(func(i int, p *message.Part) error { - _, err := p.AsStructuredMut() - return err - }); err != nil { - t.Fatal(err) - } - - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = ` - foo = json("foo").from(0) - foo.bar_new = "this is swapped now" - foo.bar.baz = "and this changed" - meta foo = meta("foo").from(0) - meta bar = meta("bar").from(0) - meta baz = "new meta" -` - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - outMsgs, res := proc.ProcessBatch(context.Background(), msg.ShallowCopy()) - require.NoError(t, res) - require.Len(t, outMsgs, 1) - - inputPartOne := msg.Get(0) - inputPartTwo := msg.Get(1) - resPartOne := outMsgs[0].Get(0) - resPartTwo := outMsgs[0].Get(1) - - assert.Equal(t, `{"foo":{"bar":{"baz":"original value","qux":"dont change"}}}`, string(inputPartOne.AsBytes())) - assert.Equal(t, "orig1", inputPartOne.MetaGetStr("foo")) - assert.Equal(t, "orig2", inputPartOne.MetaGetStr("bar")) - assert.Equal(t, "", inputPartOne.MetaGetStr("baz")) - - assert.Equal(t, `{}`, string(inputPartTwo.AsBytes())) - assert.Equal(t, "", inputPartTwo.MetaGetStr("foo")) - assert.Equal(t, "", inputPartTwo.MetaGetStr("bar")) - assert.Equal(t, "", inputPartTwo.MetaGetStr("baz")) - - assert.Equal(t, `{"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(resPartOne.AsBytes())) - assert.Equal(t, "orig1", resPartOne.MetaGetStr("foo")) - assert.Equal(t, "orig2", resPartOne.MetaGetStr("bar")) - assert.Equal(t, "new meta", resPartOne.MetaGetStr("baz")) - - assert.Equal(t, `{"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(resPartTwo.AsBytes())) - assert.Equal(t, "orig1", resPartTwo.MetaGetStr("foo")) - assert.Equal(t, "orig2", resPartTwo.MetaGetStr("bar")) - assert.Equal(t, "new meta", resPartTwo.MetaGetStr("baz")) -} - -type testKeyType int - -const testFooKey testKeyType = iota - -func TestBloblangContext(t *testing.T) { - msg := message.QuickBatch(nil) - - part := message.NewPart([]byte(`{"foo":{"bar":{"baz":"original value"}}}`)) - part.MetaSetMut("foo", "orig1") - part.MetaSetMut("bar", "orig2") - - key, val := testFooKey, "bar" - msg = append(msg, message.WithContext(context.WithValue(context.Background(), key, val), part)) - - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = `result = foo.bar.baz.uppercase()` - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - outMsgs, res := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, res) - require.Len(t, outMsgs, 1) - - resPart := outMsgs[0].Get(0) - - assert.Equal(t, `{"result":"ORIGINAL VALUE"}`, string(resPart.AsBytes())) - assert.Equal(t, "orig1", resPart.MetaGetStr("foo")) - assert.Equal(t, "orig2", resPart.MetaGetStr("bar")) - assert.Equal(t, val, message.GetContext(resPart).Value(key)) -} - -func TestBloblangCustomObject(t *testing.T) { - msg := message.QuickBatch(nil) - - part := message.NewPart(nil) - - gObj := gabs.New() - _, _ = gObj.ArrayOfSize(3, "foos") - - gObjEle := gabs.New() - _, _ = gObjEle.Set("FROM NEW OBJECT", "foo") - - _, _ = gObj.S("foos").SetIndex(gObjEle.Data(), 0) - _, _ = gObj.S("foos").SetIndex(5, 1) - - part.SetStructured(gObj.Data()) - msg = append(msg, part) - - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = `root.foos = this.foos` - proc, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - outMsgs, res := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, res) - require.Len(t, outMsgs, 1) - - resPart := outMsgs[0].Get(0) - - assert.Equal(t, `{"foos":[{"foo":"FROM NEW OBJECT"},5,null]}`, string(resPart.AsBytes())) -} - -func TestBloblangFiltering(t *testing.T) { - msg := message.QuickBatch([][]byte{ - []byte(`{"foo":{"delete":true}}`), - []byte(`{"foo":{"dont":"delete me"}}`), - []byte(`{"bar":{"delete":true}}`), - []byte(`{"bar":{"dont":"delete me"}}`), - }) - - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = ` - root = match { - (foo | bar).delete.or(false) => deleted(), - } - ` - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - outMsgs, res := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, res) - require.Len(t, outMsgs, 1) - require.Equal(t, 2, outMsgs[0].Len()) - assert.NoError(t, outMsgs[0].Get(0).ErrorGet()) - assert.NoError(t, outMsgs[0].Get(1).ErrorGet()) - assert.Equal(t, `{"foo":{"dont":"delete me"}}`, string(outMsgs[0].Get(0).AsBytes())) - assert.Equal(t, `{"bar":{"dont":"delete me"}}`, string(outMsgs[0].Get(1).AsBytes())) -} - -func TestBloblangFilterAll(t *testing.T) { - msg := message.QuickBatch([][]byte{ - []byte(`{"foo":{"delete":true}}`), - []byte(`{"foo":{"dont":"delete me"}}`), - []byte(`{"bar":{"delete":true}}`), - []byte(`{"bar":{"dont":"delete me"}}`), - }) - - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = `root = deleted()` - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - outMsgs, res := proc.ProcessBatch(context.Background(), msg) - assert.Empty(t, outMsgs) - assert.NoError(t, res) -} - -func TestBloblangJSONError(t *testing.T) { - msg := message.QuickBatch([][]byte{ - []byte(`this is not valid json`), - }) - - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = ` - foo = json().bar -` - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - outMsgs, res := proc.ProcessBatch(context.Background(), msg) - require.NoError(t, res) - require.Len(t, outMsgs, 1) - - resPart := outMsgs[0].Get(0) - - assert.Equal(t, `this is not valid json`, string(resPart.AsBytes())) - require.Error(t, resPart.ErrorGet()) - assert.Equal(t, `failed assignment (line 2): invalid character 'h' in literal true (expecting 'r')`, resPart.ErrorGet().Error()) -} diff --git a/internal/impl/pure/processor_bounds_check.go b/internal/impl/pure/processor_bounds_check.go deleted file mode 100644 index 5d5219c095..0000000000 --- a/internal/impl/pure/processor_bounds_check.go +++ /dev/null @@ -1,142 +0,0 @@ -package pure - -import ( - "context" - "errors" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - bcpFieldMaxParts = "max_parts" - bcpFieldMinParts = "min_parts" - bcpFieldMaxPartSize = "max_part_size" - bcpFieldMinPartSize = "min_part_size" -) - -func bcProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary("Removes messages (and batches) that do not fit within certain size boundaries."). - Fields( - service.NewIntField(bcpFieldMaxPartSize). - Description("The maximum size of a message to allow (in bytes)"). - Default(1*1024*1024*1024), - service.NewIntField(bcpFieldMinPartSize). - Description("The minimum size of a message to allow (in bytes)"). - Default(1), - service.NewIntField(bcpFieldMaxParts). - Description("The maximum size of message batches to allow (in message count)"). - Advanced(). - Default(100), - service.NewIntField(bcpFieldMinParts). - Description("The minimum size of message batches to allow (in message count)"). - Advanced(). - Default(1), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "bounds_check", bcProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - maxParts, err := conf.FieldInt(bcpFieldMaxParts) - if err != nil { - return nil, err - } - - minParts, err := conf.FieldInt(bcpFieldMinParts) - if err != nil { - return nil, err - } - - maxPartSize, err := conf.FieldInt(bcpFieldMaxPartSize) - if err != nil { - return nil, err - } - - minPartSize, err := conf.FieldInt(bcpFieldMinPartSize) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newBoundsCheck(maxParts, minParts, maxPartSize, minPartSize, mgr) - if err != nil { - return nil, err - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("bounds_check", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type boundsCheck struct { - maxParts int - minParts int - maxPartSize int - minPartSize int - log log.Modular -} - -// newBoundsCheck returns a BoundsCheck processor. -func newBoundsCheck(maxParts, minParts, maxPartSize, minPartSize int, mgr bundle.NewManagement) (processor.AutoObservedBatched, error) { - return &boundsCheck{ - maxParts: maxParts, - minParts: minParts, - maxPartSize: maxPartSize, - minPartSize: minPartSize, - log: mgr.Logger(), - }, nil -} - -func (m *boundsCheck) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - lParts := msg.Len() - if lParts < m.minParts { - m.log.Debug( - "Rejecting message due to message parts below minimum (%v): %v\n", - m.minParts, lParts, - ) - return nil, nil - } else if lParts > m.maxParts { - m.log.Debug( - "Rejecting message due to message parts exceeding limit (%v): %v\n", - m.maxParts, lParts, - ) - return nil, nil - } - - var reject bool - _ = msg.Iter(func(i int, p *message.Part) error { - if size := len(p.AsBytes()); size > m.maxPartSize || - size < m.minPartSize { - m.log.Debug( - "Rejecting message due to message part size (%v -> %v): %v\n", - m.minPartSize, - m.maxPartSize, - size, - ) - reject = true - return errors.New("exit") - } - return nil - }) - if reject { - return nil, nil - } - - msgs := [1]message.Batch{msg} - return msgs[:], nil -} - -func (m *boundsCheck) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_bounds_check_test.go b/internal/impl/pure/processor_bounds_check_test.go deleted file mode 100644 index 21d32c74e1..0000000000 --- a/internal/impl/pure/processor_bounds_check_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestBoundsCheck(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -bounds_check: - min_parts: 2 - max_parts: 3 - max_part_size: 10 - min_part_size: 1 -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - goodParts := [][][]byte{ - { - []byte("hello"), - []byte("world"), - }, - { - []byte("helloworld"), - []byte("helloworld"), - }, - { - []byte("hello"), - []byte("world"), - []byte("!"), - }, - { - []byte("helloworld"), - []byte("helloworld"), - []byte("helloworld"), - }, - } - - badParts := [][][]byte{ - { - []byte("hello world"), - }, - { - []byte("hello world"), - []byte("hello world this exceeds max part size"), - }, - { - []byte("hello"), - []byte("world"), - []byte("this"), - []byte("exceeds"), - []byte("max"), - []byte("num"), - []byte("parts"), - }, - { - []byte("hello"), - []byte(""), - }, - } - - for _, parts := range goodParts { - msg := message.QuickBatch(parts) - msgs, _ := proc.ProcessBatch(context.Background(), msg) - require.Len(t, msgs, 1) - require.Equal(t, len(parts), msgs[0].Len()) - for i, p := range parts { - assert.Equal(t, string(p), string(msgs[0].Get(i).AsBytes()), i) - } - } - - for _, parts := range badParts { - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - assert.Empty(t, msgs) - assert.NoError(t, res) - } -} diff --git a/internal/impl/pure/processor_branch.go b/internal/impl/pure/processor_branch.go deleted file mode 100644 index 78b60d3de7..0000000000 --- a/internal/impl/pure/processor_branch.go +++ /dev/null @@ -1,521 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sort" - "time" - - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - branchProcFieldReqMap = "request_map" - branchProcFieldProcs = "processors" - branchProcFieldResMap = "result_map" -) - -func branchProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`The `+"`branch`"+` processor allows you to create a new request message via a xref:guides:bloblang/about.adoc[Bloblang mapping], execute a list of processors on the request messages, and, finally, map the result back into the source message using another mapping.`). - Description(` -This is useful for preserving the original message contents when using processors that would otherwise replace the entire contents. - -== Metadata - -Metadata fields that are added to messages during branch processing will not be automatically copied into the resulting message. In order to do this you should explicitly declare in your `+"`result_map`"+` either a wholesale copy with `+"`meta = metadata()`"+`, or selective copies with `+"`meta foo = metadata(\"bar\")`"+` and so on. It is also possible to reference the metadata of the origin message in the `+"`result_map`"+` using the xref:guides:bloblang/about.adoc#metadata[`+"`@`"+` operator]. - -== Error handling - -If the `+"`request_map`"+` fails the child processors will not be executed. If the child processors themselves result in an (uncaught) error then the `+"`result_map`"+` will not be executed. If the `+"`result_map`"+` fails the message will remain unchanged. Under any of these conditions standard xref:configuration:error_handling.adoc[error handling methods] can be used in order to filter, DLQ or recover the failed messages. - -== Conditional branching - -If the root of your request map is set to `+"`deleted()`"+` then the branch processors are skipped for the given message, this allows you to conditionally branch messages.`). - Example("HTTP Request", ` -This example strips the request message into an empty body, grabs an HTTP payload, and places the result back into the original message at the path `+"`image.pull_count`"+`:`, ` -pipeline: - processors: - - branch: - request_map: 'root = ""' - processors: - - http: - url: https://hub.docker.com/v2/repositories/jeffail/benthos - verb: GET - headers: - Content-Type: application/json - result_map: root.image.pull_count = this.pull_count - -# Example input: {"id":"foo","some":"pre-existing data"} -# Example output: {"id":"foo","some":"pre-existing data","image":{"pull_count":1234}} -`). - Example("Non Structured Results", ` -When the result of your branch processors is unstructured and you wish to simply set a resulting field to the raw output use the content function to obtain the raw bytes of the resulting message and then coerce it into your value type of choice:`, ` -pipeline: - processors: - - branch: - request_map: 'root = this.document.id' - processors: - - cache: - resource: descriptions_cache - key: ${! content() } - operator: get - result_map: root.document.description = content().string() - -# Example input: {"document":{"id":"foo","content":"hello world"}} -# Example output: {"document":{"id":"foo","content":"hello world","description":"this is a cool doc"}} -`). - Example("Lambda Function", ` -This example maps a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged.`, ` -pipeline: - processors: - - branch: - request_map: '{"id":this.doc.id,"username":this.user.name}' - processors: - - aws_lambda: - function: trigger_user_update - -# Example input: {"doc":{"id":"foo","body":"hello world"},"user":{"name":"fooey"}} -# Output matches the input, which is unchanged -`). - Example("Conditional Caching", ` -This example caches a document by a message ID only when the type of the document is a foo:`, ` -pipeline: - processors: - - branch: - request_map: | - meta id = this.id - root = if this.type == "foo" { - this.document - } else { - deleted() - } - processors: - - cache: - resource: TODO - operator: set - key: ${! @id } - value: ${! content() } -`). - Fields(branchSpecFields()...) -} - -func branchSpecFields() []*service.ConfigField { - return []*service.ConfigField{ - service.NewBloblangField(branchProcFieldReqMap). - Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata)."). - Examples(`root = { - "id": this.doc.id, - "content": this.doc.body.text -}`, - `root = if this.type == "foo" { - this.foo.request -} else { - deleted() -}`). - Default(""), - service.NewProcessorListField(branchProcFieldProcs). - Description("A list of processors to apply to mapped requests. When processing message batches the resulting batch must match the size and ordering of the input batch, therefore filtering, grouping should not be performed within these processors."), - service.NewBloblangField(branchProcFieldResMap). - Description("A xref:guides:bloblang/about.adoc[Bloblang mapping] that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata)."). - Examples(`meta foo_code = metadata("code") -root.foo_result = this`, - `meta = metadata() -root.bar.body = this.body -root.bar.id = this.user.id`, - `root.raw_result = content().string()`, - `root.enrichments.foo = if metadata("request_failed") != null { - throw(metadata("request_failed")) -} else { - this -}`, `# Retain only the updated metadata fields which were present in the origin message -meta = metadata().filter(v -> @.get(v.key) != null)`). - Default(""), - } -} - -func init() { - err := service.RegisterBatchProcessor( - "branch", branchProcSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - b, err := newBranchFromParsed(conf, interop.UnwrapManagement(mgr)) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(b), nil - }) - if err != nil { - panic(err) - } -} - -// Branch contains conditions and maps for transforming a batch of messages into -// a subset of request messages, and mapping results from those requests back -// into the original message batch. -type Branch struct { - log log.Modular - tracer trace.TracerProvider - - requestMap *mapping.Executor - resultMap *mapping.Executor - children []processor.V1 - - // Metrics - mReceived metrics.StatCounter - mBatchReceived metrics.StatCounter - mSent metrics.StatCounter - mBatchSent metrics.StatCounter - mError metrics.StatCounter - mLatency metrics.StatTimer -} - -func newBranchFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (b *Branch, err error) { - stats := mgr.Metrics() - b = &Branch{ - log: mgr.Logger(), - tracer: mgr.Tracer(), - - mReceived: stats.GetCounter("processor_received"), - mBatchReceived: stats.GetCounter("processor_batch_received"), - mSent: stats.GetCounter("processor_sent"), - mBatchSent: stats.GetCounter("processor_batch_sent"), - mError: stats.GetCounter("processor_error"), - mLatency: stats.GetTimer("processor_latency_ns"), - } - - var pChildren []*service.OwnedProcessor - if pChildren, err = conf.FieldProcessorList(branchProcFieldProcs); err != nil { - return - } - if len(pChildren) == 0 { - return nil, errors.New("the branch processor requires at least one child processor") - } - b.children = make([]processor.V1, len(pChildren)) - for i, c := range pChildren { - b.children[i] = interop.UnwrapOwnedProcessor(c) - } - - if reqMapStr, _ := conf.FieldString(branchProcFieldReqMap); reqMapStr != "" { - if b.requestMap, err = mgr.BloblEnvironment().NewMapping(reqMapStr); err != nil { - return nil, fmt.Errorf("failed to parse request mapping: %w", err) - } - } - if resMapStr, _ := conf.FieldString(branchProcFieldResMap); resMapStr != "" { - if b.resultMap, err = mgr.BloblEnvironment().NewMapping(resMapStr); err != nil { - return nil, fmt.Errorf("failed to parse result mapping: %w", err) - } - } - - return b, nil -} - -//------------------------------------------------------------------------------ - -// TargetsUsed returns a list of paths that this branch depends on. Each path is -// prefixed by a namespace `metadata` or `path` indicating the source. -func (b *Branch) targetsUsed() [][]string { - if b.requestMap == nil { - return nil - } - - var paths [][]string - _, queryTargets := b.requestMap.QueryTargets(query.TargetsContext{}) - -pathLoop: - for _, p := range queryTargets { - path := make([]string, 0, len(p.Path)+1) - switch p.Type { - case query.TargetValue: - path = append(path, "path") - case query.TargetMetadata: - path = append(path, "metadata") - default: - continue pathLoop - } - paths = append(paths, append(path, p.Path...)) - } - - return paths -} - -// TargetsProvided returns a list of paths that this branch provides. -func (b *Branch) targetsProvided() [][]string { - if b.resultMap == nil { - return nil - } - - var paths [][]string - -pathLoop: - for _, p := range b.resultMap.AssignmentTargets() { - path := make([]string, 0, len(p.Path)+1) - switch p.Type { - case mapping.TargetValue: - path = append(path, "path") - case mapping.TargetMetadata: - path = append(path, "metadata") - default: - continue pathLoop - } - paths = append(paths, append(path, p.Path...)) - } - - return paths -} - -//------------------------------------------------------------------------------ - -// ProcessBatch applies the processor to a message, either creating >0 -// resulting messages or a response to be sent back to the message source. -func (b *Branch) ProcessBatch(ctx context.Context, batch message.Batch) ([]message.Batch, error) { - b.mReceived.Incr(int64(batch.Len())) - b.mBatchReceived.Incr(1) - startedAt := time.Now() - - branchMsg, propSpans := tracing.WithChildSpans(b.tracer, "branch", batch.ShallowCopy()) - defer func() { - for _, s := range propSpans { - s.Finish() - } - }() - - parts := make([]*message.Part, 0, branchMsg.Len()) - _ = branchMsg.Iter(func(i int, p *message.Part) error { - // Remove errors so that they aren't propagated into the branch. - p.ErrorSet(nil) - parts = append(parts, p) - return nil - }) - - resultParts, mapErrs, err := b.createResult(ctx, parts, batch) - if err != nil { - // Add general error to all messages. - _ = batch.Iter(func(i int, p *message.Part) error { - p.ErrorSet(err) - return nil - }) - // And override with mapping specific errors where appropriate. - for _, e := range mapErrs { - batch.Get(e.index).ErrorSet(e.err) - } - msgs := [1]message.Batch{batch} - return msgs[:], nil - } - - for _, e := range mapErrs { - batch.Get(e.index).ErrorSet(e.err) - b.log.Error("Branch error: %v", e.err) - } - - if mapErrs, err = b.overlayResult(batch, resultParts); err != nil { - _ = batch.Iter(func(i int, p *message.Part) error { - p.ErrorSet(err) - return nil - }) - return []message.Batch{batch}, nil - } - for _, e := range mapErrs { - batch.Get(e.index).ErrorSet(e.err) - b.log.Error("Branch error: %v", e.err) - } - - b.mLatency.Timing(time.Since(startedAt).Nanoseconds()) - return []message.Batch{batch}, nil -} - -//------------------------------------------------------------------------------ - -type branchMapError struct { - index int - err error -} - -func newBranchMapError(index int, err error) branchMapError { - return branchMapError{index: index, err: err} -} - -//------------------------------------------------------------------------------ - -// createResult performs reduction and child processors to a payload. The size -// of the payload will remain unchanged, where reduced indexes are nil. This -// result can be overlayed onto the original message in order to complete the -// map. -func (b *Branch) createResult(ctx context.Context, parts []*message.Part, referenceMsg message.Batch) ([]*message.Part, []branchMapError, error) { - originalLen := len(parts) - - // Create request payloads - var skipped, failed []int - var mapErrs []branchMapError - - newParts := make([]*message.Part, 0, len(parts)) - for i := 0; i < len(parts); i++ { - if parts[i] == nil { - // Skip if the message part is nil. - skipped = append(skipped, i) - continue - } - if b.requestMap != nil { - _ = parts[i].SetBytes(nil) - newPart, err := b.requestMap.MapOnto(parts[i], i, referenceMsg) - if err != nil { - b.mError.Incr(1) - b.log.Debug("Failed to map request '%v': %v\n", i, err) - - // Skip if message part fails mapping. - failed = append(failed, i) - mapErrs = append(mapErrs, newBranchMapError(i, fmt.Errorf("request mapping failed: %w", err))) - } else if newPart == nil { - // Skip if the message part is deleted. - skipped = append(skipped, i) - } else { - newParts = append(newParts, newPart) - } - } else { - newParts = append(newParts, parts[i]) - } - } - parts = newParts - - // Execute child processors - var procResults []message.Batch - var err error - if len(parts) > 0 { - var res error - if procResults, res = processor.ExecuteAll(ctx, b.children, parts); res != nil { - err = fmt.Errorf("child processors failed: %v", res) - } - if len(procResults) == 0 { - err = errors.New("child processors resulted in zero messages") - } - if err != nil { - b.mError.Incr(1) - b.log.Error("Child processors failed: %v\n", err) - return nil, mapErrs, err - } - } - - // Re-align processor results with original message indexes - var alignedResult []*message.Part - if alignedResult, err = alignBranchResult(originalLen, skipped, failed, procResults); err != nil { - b.mError.Incr(1) - b.log.Error("Failed to align branch result: %v. Avoid using filters or archive/unarchive processors within your branch, or anything that increases or reduces the number of messages. These processors should instead be applied before or after the branch processor.\n", err) - return nil, mapErrs, err - } - - for i, p := range alignedResult { - if p == nil { - continue - } - if fail := p.ErrorGet(); fail != nil { - alignedResult[i] = nil - mapErrs = append(mapErrs, newBranchMapError(i, fmt.Errorf("processors failed: %w", fail))) - } - } - - return alignedResult, mapErrs, nil -} - -// overlayResult attempts to merge the result of a process_map with the original -// payload as per the map specified in the postmap and postmap_optional fields. -func (b *Branch) overlayResult(payload message.Batch, results []*message.Part) ([]branchMapError, error) { - if exp, act := payload.Len(), len(results); exp != act { - b.mError.Incr(1) - return nil, fmt.Errorf( - "message count returned from branch has diverged from the request, started with %v messages, finished with %v", - act, exp, - ) - } - - var failed []branchMapError - - if b.resultMap != nil { - for i, result := range results { - if result == nil { - continue - } - - newPart, err := b.resultMap.MapOnto(payload.Get(i), i, message.Batch(results)) - if err != nil { - b.mError.Incr(1) - b.log.Debug("Failed to map result '%v': %v\n", i, err) - - failed = append(failed, newBranchMapError(i, fmt.Errorf("result mapping failed: %w", err))) - continue - } - - // TODO: Allow filtering here? - if newPart != nil { - payload[i] = newPart - } - } - } - - b.mBatchSent.Incr(1) - b.mSent.Incr(int64(payload.Len())) - return failed, nil -} - -func alignBranchResult(length int, skipped, failed []int, result []message.Batch) ([]*message.Part, error) { - resMsgParts := []*message.Part{} - for _, m := range result { - _ = m.Iter(func(i int, p *message.Part) error { - resMsgParts = append(resMsgParts, p) - return nil - }) - } - - skippedOrFailed := make([]int, len(skipped)+len(failed)) - i := copy(skippedOrFailed, skipped) - copy(skippedOrFailed[i:], failed) - - sort.Ints(skippedOrFailed) - - // Check that size of response is aligned with payload. - if rLen, pLen := len(resMsgParts)+len(skippedOrFailed), length; rLen != pLen { - return nil, fmt.Errorf( - "message count from branch processors does not match request, started with %v messages, finished with %v", - rLen, pLen, - ) - } - - var resultParts []*message.Part - if len(skippedOrFailed) == 0 { - resultParts = resMsgParts - } else { - // Remember to insert nil for each skipped part at the correct index. - resultParts = make([]*message.Part, length) - sIndex := 0 - for i = 0; i < len(resMsgParts); i++ { - for sIndex < len(skippedOrFailed) && skippedOrFailed[sIndex] == (i+sIndex) { - sIndex++ - } - resultParts[i+sIndex] = resMsgParts[i] - } - } - - return resultParts, nil -} - -// Close blocks until the processor has closed down or the context is cancelled. -func (b *Branch) Close(ctx context.Context) error { - for _, child := range b.children { - if err := child.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_branch_test.go b/internal/impl/pure/processor_branch_test.go deleted file mode 100644 index 6267032054..0000000000 --- a/internal/impl/pure/processor_branch_test.go +++ /dev/null @@ -1,275 +0,0 @@ -package pure_test - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestBranchBasic(t *testing.T) { - msg := func(content string, meta ...string) mockMsg { - t.Helper() - m := mockMsg{ - content: content, - meta: map[string]string{}, - } - for i, v := range meta { - if i%2 == 1 { - m.meta[meta[i-1]] = v - } - } - return m - } - - tests := map[string]struct { - requestMap string - processorMap string - resultMap string - input []mockMsg - output []mockMsg - }{ - "empty request mapping": { - requestMap: "", - processorMap: "root.nested = this", - resultMap: "root.result = this.nested", - input: []mockMsg{ - msg(`{"value":"foobar"}`), - }, - output: []mockMsg{ - msg(`{"result":{"value":"foobar"},"value":"foobar"}`), - }, - }, - "empty result mapping": { - requestMap: "root.nested = this", - processorMap: "root = this", - resultMap: "", - input: []mockMsg{ - msg(`{"value":"foobar"}`), - }, - output: []mockMsg{ - msg(`{"value":"foobar"}`), - }, - }, - "copy metadata over only": { - requestMap: `meta foo = meta("foo")`, - processorMap: `meta foo = meta("foo") + " and this"`, - resultMap: `meta new_foo = meta("foo")`, - input: []mockMsg{ - msg( - `{"value":"foobar"}`, - "foo", "bar", - ), - }, - output: []mockMsg{ - msg( - `{"value":"foobar"}`, - "foo", "bar", - "new_foo", "bar and this", - ), - }, - }, - "do not carry error into branch": { - requestMap: `root = this`, - processorMap: `root = this - root.name_upper = this.name.uppercase()`, - resultMap: `root.result = if this.failme.bool(false) { - throw("this is a branch error") } else { this.name_upper - }`, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`).withErr(errors.New("this is a pre-existing failure")), - msg(`{"failme":true,"id":1,"name":"second"}`), - msg(`{"failme":true,"id":2,"name":"third"}`).withErr(errors.New("this is a pre-existing failure")), - }, - output: []mockMsg{ - msg(`{"id":0,"name":"first","result":"FIRST"}`).withErr(errors.New("this is a pre-existing failure")), - msg(`{"failme":true,"id":1,"name":"second"}`).withErr(errors.New("result mapping failed: failed assignment (line 1): this is a branch error")), - msg(`{"failme":true,"id":2,"name":"third"}`).withErr(errors.New("result mapping failed: failed assignment (line 1): this is a branch error")), - }, - }, - "map error into branch": { - requestMap: `root.err = error()`, - processorMap: `root.err = this.err.string().uppercase()`, - resultMap: `root.result_err = this.err`, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`).withErr(errors.New("this is a pre-existing failure")), - msg(`{"id":1,"name":"second"}`), - }, - output: []mockMsg{ - msg(`{"id":0,"name":"first","result_err":"THIS IS A PRE-EXISTING FAILURE"}`).withErr(errors.New("this is a pre-existing failure")), - msg(`{"id":1,"name":"second","result_err":"NULL"}`), - }, - }, - "filtered and failed mappings": { - requestMap: `root = match { - this.id == 0 => throw("i dont like zero"), - this.id == 3 => deleted(), - _ => {"name":this.name,"id":this.id} - }`, - processorMap: `root = this - root.name_upper = this.name.uppercase()`, - resultMap: `root.result = match { - this.id == 2 => throw("i dont like two either"), - _ => this.name_upper - }`, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`), - msg(`{"id":1,"name":"second"}`), - msg(`{"id":2,"name":"third"}`), - msg(`{"id":3,"name":"fourth"}`), - msg(`{"id":4,"name":"fifth"}`), - }, - output: []mockMsg{ - msg(`{"id":0,"name":"first"}`).withErr(errors.New("request mapping failed: failed assignment (line 1): i dont like zero")), - msg(`{"id":1,"name":"second","result":"SECOND"}`), - msg(`{"id":2,"name":"third"}`).withErr(errors.New("result mapping failed: failed assignment (line 1): i dont like two either")), - msg(`{"id":3,"name":"fourth"}`), - msg(`{"id":4,"name":"fifth","result":"FIFTH"}`), - }, - }, - "filter all requests": { - requestMap: `root = deleted()`, - processorMap: `root = this`, - resultMap: `root.result = this`, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`), - msg(`{"id":1,"name":"second"}`), - msg(`{"id":2,"name":"third"}`), - msg(`{"id":3,"name":"fourth"}`), - msg(`{"id":4,"name":"fifth"}`), - }, - output: []mockMsg{ - msg(`{"id":0,"name":"first"}`), - msg(`{"id":1,"name":"second"}`), - msg(`{"id":2,"name":"third"}`), - msg(`{"id":3,"name":"fourth"}`), - msg(`{"id":4,"name":"fifth"}`), - }, - }, - "filter during processing": { - requestMap: `root = if this.id == 3 { throw("foo") } else { this }`, - processorMap: `root = deleted()`, - resultMap: `root.result = this`, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`), - msg(`{"id":1,"name":"second"}`), - msg(`{"id":2,"name":"third"}`), - msg(`{"id":3,"name":"fourth"}`), - msg(`{"id":4,"name":"fifth"}`), - }, - output: []mockMsg{ - msg(`{"id":0,"name":"first"}`).withErr(errors.New("child processors resulted in zero messages")), - msg(`{"id":1,"name":"second"}`).withErr(errors.New("child processors resulted in zero messages")), - msg(`{"id":2,"name":"third"}`).withErr(errors.New("child processors resulted in zero messages")), - msg(`{"id":3,"name":"fourth"}`).withErr(errors.New("request mapping failed: failed assignment (line 1): foo")), - msg(`{"id":4,"name":"fifth"}`).withErr(errors.New("child processors resulted in zero messages")), - }, - }, - "filter some during processing": { - requestMap: `root = if this.id == 3 { throw("foo") } else { this }`, - processorMap: `root = if this.id == 2 { deleted() }`, - resultMap: `root.result = this`, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`), - msg(`{"id":1,"name":"second"}`), - msg(`{"id":2,"name":"third"}`), - msg(`{"id":3,"name":"fourth"}`), - msg(`{"id":4,"name":"fifth"}`), - }, - output: []mockMsg{ - msg(`{"id":0,"name":"first"}`).withErr(errors.New("message count from branch processors does not match request, started with 4 messages, finished with 5")), - msg(`{"id":1,"name":"second"}`).withErr(errors.New("message count from branch processors does not match request, started with 4 messages, finished with 5")), - msg(`{"id":2,"name":"third"}`).withErr(errors.New("message count from branch processors does not match request, started with 4 messages, finished with 5")), - msg(`{"id":3,"name":"fourth"}`).withErr(errors.New("request mapping failed: failed assignment (line 1): foo")), - msg(`{"id":4,"name":"fifth"}`).withErr(errors.New("message count from branch processors does not match request, started with 4 messages, finished with 5")), - }, - }, - } - - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - t.Parallel() - - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -branch: - request_map: | - %v - processors: - - bloblang: | - %v - result_map: | - %v -`, test.requestMap, test.processorMap, test.resultMap)) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - msg := message.QuickBatch(nil) - for _, m := range test.input { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - if m.err != nil { - part.ErrorSet(m.err) - } - msg = append(msg, part) - } - - outMsgs, res := proc.ProcessBatch(context.Background(), msg.ShallowCopy()) - - require.NoError(t, res) - require.Len(t, outMsgs, 1) - - assert.Equal(t, len(test.output), outMsgs[0].Len()) - for i, out := range test.output { - comparePart := mockMsg{ - content: string(outMsgs[0].Get(i).AsBytes()), - meta: map[string]string{}, - } - - _ = outMsgs[0].Get(i).MetaIterStr(func(k, v string) error { - comparePart.meta[k] = v - return nil - }) - - if out.err != nil { - assert.EqualError(t, outMsgs[0].Get(i).ErrorGet(), out.err.Error()) - } else { - assert.NoError(t, outMsgs[0].Get(i).ErrorGet()) - } - outMsgs[0].Get(i).ErrorSet(nil) - out.err = nil - - assert.Equal(t, out, comparePart) - } - - // Ensure nothing changed - for i, m := range test.input { - doc, err := msg.Get(i).AsStructuredMut() - if err == nil { - msg.Get(i).SetStructured(doc) - } - assert.Equal(t, m.content, string(msg.Get(i).AsBytes())) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, proc.Close(ctx)) - }) - } -} diff --git a/internal/impl/pure/processor_cache.go b/internal/impl/pure/processor_cache.go deleted file mode 100644 index f9f0b9395c..0000000000 --- a/internal/impl/pure/processor_cache.go +++ /dev/null @@ -1,343 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - cachePFieldResource = "resource" - cachePFieldOperator = "operator" - cachePFieldKey = "key" - cachePFieldValue = "value" - cachePFieldTTL = "ttl" -) - -func cacheProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Integration"). - Stable(). - Summary("Performs operations against a xref:components:caches/about.adoc[cache resource] for each message, allowing you to store or retrieve data within message payloads."). - Description(` -For use cases where you wish to cache the result of processors consider using the `+"xref:components:processors/cached.adoc[`cached` processor]"+` instead. - -This processor will interpolate functions within the `+"`key` and `value`"+` fields individually for each message. This allows you to specify dynamic keys and values based on the contents of the message payloads and metadata. You can find a list of functions in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries].`). - Footnotes(` -== Operators - -=== `+"`set`"+` - -Set a key in the cache to a value. If the key already exists the contents are -overridden. - -=== `+"`add`"+` - -Set a key in the cache to a value. If the key already exists the action fails -with a 'key already exists' error, which can be detected with -xref:configuration:error_handling.adoc[processor error handling]. - -=== `+"`get`"+` - -Retrieve the contents of a cached key and replace the original message payload -with the result. If the key does not exist the action fails with an error, which -can be detected with xref:configuration:error_handling.adoc[processor error handling]. - -=== `+"`delete`"+` - -Delete a key and its contents from the cache. If the key does not exist the -action is a no-op and will not fail with an error.`). - Example("Deduplication", ` -Deduplication can be done using the add operator with a key extracted from the message payload, since it fails when a key already exists we can remove the duplicates using a xref:components:processors/mapping.adoc[`+"`mapping` processor"+`]:`, - ` -pipeline: - processors: - - cache: - resource: foocache - operator: add - key: '${! json("message.id") }' - value: "storeme" - - mapping: root = if errored() { deleted() } - -cache_resources: - - label: foocache - redis: - url: tcp://TODO:6379 -`). - Example("Deduplication Batch-Wide", ` -Sometimes it's necessary to deduplicate a batch of messages (also known as a window) by a single identifying value. This can be done by introducing a `+"xref:components:processors/branch.adoc[`branch` processor]"+`, which executes the cache only once on behalf of the batch, in this case with a value make from a field extracted from the first and last messages of the batch:`, - ` -pipeline: - processors: - # Try and add one message to a cache that identifies the whole batch - - branch: - request_map: | - root = if batch_index() == 0 { - json("id").from(0) + json("meta.tail_id").from(-1) - } else { deleted() } - processors: - - cache: - resource: foocache - operator: add - key: ${! content() } - value: t - # Delete all messages if we failed - - mapping: | - root = if errored().from(0) { - deleted() - } -`). - Example("Hydration", ` -It's possible to enrich payloads with content previously stored in a cache by using the xref:components:processors/branch.adoc[`+"`branch`"+`] processor:`, - ` -pipeline: - processors: - - branch: - processors: - - cache: - resource: foocache - operator: get - key: '${! json("message.document_id") }' - result_map: 'root.message.document = this' - - # NOTE: If the data stored in the cache is not valid JSON then use - # something like this instead: - # result_map: 'root.message.document = content().string()' - -cache_resources: - - label: foocache - memcached: - addresses: [ "TODO:11211" ] -`). - Fields( - service.NewStringField(cachePFieldResource). - Description("The xref:components:caches/about.adoc[`cache` resource] to target with this processor."), - service.NewStringEnumField(cachePFieldOperator, "set", "add", "get", "delete"). - Description("The <> to perform with the cache."), - service.NewInterpolatedStringField(cachePFieldKey). - Description("A key to use with the cache."), - service.NewInterpolatedStringField(cachePFieldValue). - Description("A value to use with the cache (when applicable)."). - Optional(), - service.NewInterpolatedStringField(cachePFieldTTL). - Description("The TTL of each individual item as a duration string. After this period an item will be eligible for removal during the next compaction. Not all caches support per-key TTLs, those that do will have a configuration field `default_ttl`, and those that do not will fall back to their generally configured TTL setting."). - Examples("60s", "5m", "36h"). - Version("3.33.0"). - Advanced(). - Optional(), - ) -} - -type cacheProcConfig struct { - Resource string - Operator string - Key string - Value string - TTL string -} - -func init() { - err := service.RegisterBatchProcessor( - "cache", cacheProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - var cConf cacheProcConfig - var err error - - if cConf.Resource, err = conf.FieldString(cachePFieldResource); err != nil { - return nil, err - } - if cConf.Operator, err = conf.FieldString(cachePFieldOperator); err != nil { - return nil, err - } - if cConf.Key, err = conf.FieldString(cachePFieldKey); err != nil { - return nil, err - } - cConf.Value, _ = conf.FieldString(cachePFieldValue) - cConf.TTL, _ = conf.FieldString(cachePFieldTTL) - - mgr := interop.UnwrapManagement(res) - p, err := newCache(cConf, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("cache", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type cacheProc struct { - key *field.Expression - value *field.Expression - ttl *field.Expression - - mgr bundle.NewManagement - cacheName string - operator cacheOperator -} - -func newCache(conf cacheProcConfig, mgr bundle.NewManagement) (*cacheProc, error) { - cacheName := conf.Resource - if cacheName == "" { - return nil, errors.New("cache name must be specified") - } - - op, err := cacheOperatorFromString(conf.Operator) - if err != nil { - return nil, err - } - - key, err := mgr.BloblEnvironment().NewField(conf.Key) - if err != nil { - return nil, fmt.Errorf("failed to parse key expression: %v", err) - } - - value, err := mgr.BloblEnvironment().NewField(conf.Value) - if err != nil { - return nil, fmt.Errorf("failed to parse value expression: %v", err) - } - - ttl, err := mgr.BloblEnvironment().NewField(conf.TTL) - if err != nil { - return nil, fmt.Errorf("failed to parse ttl expression: %v", err) - } - - if !mgr.ProbeCache(cacheName) { - return nil, fmt.Errorf("cache resource '%v' was not found", cacheName) - } - - return &cacheProc{ - key: key, - value: value, - ttl: ttl, - - mgr: mgr, - cacheName: cacheName, - operator: op, - }, nil -} - -//------------------------------------------------------------------------------ - -type cacheOperator func(ctx context.Context, cache cache.V1, key string, value []byte, ttl *time.Duration) ([]byte, bool, error) - -func newCacheSetOperator() cacheOperator { - return func(ctx context.Context, cache cache.V1, key string, value []byte, ttl *time.Duration) ([]byte, bool, error) { - err := cache.Set(ctx, key, value, ttl) - return nil, false, err - } -} - -func newCacheAddOperator() cacheOperator { - return func(ctx context.Context, cache cache.V1, key string, value []byte, ttl *time.Duration) ([]byte, bool, error) { - err := cache.Add(ctx, key, value, ttl) - return nil, false, err - } -} - -func newCacheGetOperator() cacheOperator { - return func(ctx context.Context, cache cache.V1, key string, _ []byte, _ *time.Duration) ([]byte, bool, error) { - result, err := cache.Get(ctx, key) - return result, true, err - } -} - -func newCacheDeleteOperator() cacheOperator { - return func(ctx context.Context, cache cache.V1, key string, _ []byte, ttl *time.Duration) ([]byte, bool, error) { - err := cache.Delete(ctx, key) - return nil, false, err - } -} - -func cacheOperatorFromString(operator string) (cacheOperator, error) { - switch operator { - case "set": - return newCacheSetOperator(), nil - case "add": - return newCacheAddOperator(), nil - case "get": - return newCacheGetOperator(), nil - case "delete": - return newCacheDeleteOperator(), nil - } - return nil, fmt.Errorf("operator not recognised: %v", operator) -} - -//------------------------------------------------------------------------------ - -func (c *cacheProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - _ = msg.Iter(func(index int, part *message.Part) error { - key, err := c.key.String(index, msg) - if err != nil { - err = fmt.Errorf("key interpolation error: %w", err) - ctx.OnError(err, index, nil) - return nil - } - - value, err := c.value.Bytes(index, msg) - if err != nil { - err = fmt.Errorf("value interpolation error: %w", err) - ctx.OnError(err, index, nil) - return nil - } - - var ttl *time.Duration - ttls, err := c.ttl.String(index, msg) - if err != nil { - err = fmt.Errorf("ttl interpolation error: %w", err) - ctx.OnError(err, index, nil) - return nil - } - - if ttls != "" { - td, err := time.ParseDuration(ttls) - if err != nil { - err = fmt.Errorf("ttl must be a duration: %w", err) - ctx.OnError(err, index, nil) - return nil - } - ttl = &td - } - - var result []byte - var useResult bool - if cerr := c.mgr.AccessCache(context.Background(), c.cacheName, func(cache cache.V1) { - result, useResult, err = c.operator(context.Background(), cache, key, value, ttl) - }); cerr != nil { - err = cerr - } - if err != nil { - if err != component.ErrKeyAlreadyExists { - err = fmt.Errorf("operator failed for key '%s': %v", key, err) - } else { - err = fmt.Errorf("key already exists: %v", key) - } - ctx.OnError(err, index, nil) - return nil - } - - if useResult { - part.SetBytes(result) - } - return nil - }) - - return []message.Batch{msg}, nil -} - -func (c *cacheProc) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_cache_test.go b/internal/impl/pure/processor_cache_test.go deleted file mode 100644 index 017a989990..0000000000 --- a/internal/impl/pure/processor_cache_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package pure_test - -import ( - "context" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestCacheSet(t *testing.T) { - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - conf, err := testutil.ProcessorFromYAML(` -cache: - operator: set - key: ${!json("key")} - value: ${!json("value")} - resource: foocache -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1","value":"foo 1"}`), - []byte(`{"key":"2","value":"foo 2"}`), - []byte(`{"key":"1","value":"foo 3"}`), - }) - - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - - if len(output) != 1 { - t.Fatalf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := message.GetAllBytes(input), message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - - actV, ok := mgr.Caches["foocache"]["1"] - require.True(t, ok) - assert.Equal(t, "foo 3", actV.Value) - - actV, ok = mgr.Caches["foocache"]["2"] - require.True(t, ok) - assert.Equal(t, "foo 2", actV.Value) -} - -func TestCacheAdd(t *testing.T) { - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - conf, err := testutil.ProcessorFromYAML(` -cache: - key: ${!json("key")} - value: ${!json("value")} - resource: foocache - operator: add -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1","value":"foo 1"}`), - []byte(`{"key":"2","value":"foo 2"}`), - []byte(`{"key":"1","value":"foo 3"}`), - }) - - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - - if len(output) != 1 { - t.Fatalf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := message.GetAllBytes(input), message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - - assert.NoError(t, output[0].Get(0).ErrorGet()) - assert.NoError(t, output[0].Get(1).ErrorGet()) - assert.Error(t, output[0].Get(2).ErrorGet()) - - actV, ok := mgr.Caches["foocache"]["1"] - require.True(t, ok) - assert.Equal(t, "foo 1", actV.Value) - - actV, ok = mgr.Caches["foocache"]["2"] - require.True(t, ok) - assert.Equal(t, "foo 2", actV.Value) -} - -func TestCacheGet(t *testing.T) { - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{ - "1": {Value: "foo 1"}, - "2": {Value: "foo 2"}, - } - - conf, err := testutil.ProcessorFromYAML(` -cache: - operator: get - key: ${!json("key")} - resource: foocache -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1"}`), - []byte(`{"key":"2"}`), - []byte(`{"key":"3"}`), - }) - expParts := [][]byte{ - []byte(`foo 1`), - []byte(`foo 2`), - []byte(`{"key":"3"}`), - } - - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - - if len(output) != 1 { - t.Fatalf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := expParts, message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - - assert.NoError(t, output[0].Get(0).ErrorGet()) - assert.NoError(t, output[0].Get(1).ErrorGet()) - assert.Error(t, output[0].Get(2).ErrorGet()) -} - -func TestCacheDelete(t *testing.T) { - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{ - "1": {Value: "foo 1"}, - "2": {Value: "foo 2"}, - "3": {Value: "foo 3"}, - } - - conf, err := testutil.ProcessorFromYAML(` -cache: - operator: delete - key: ${!json("key")} - resource: foocache -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1"}`), - []byte(`{"key":"3"}`), - []byte(`{"key":"4"}`), - }) - - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - - if len(output) != 1 { - t.Fatalf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := message.GetAllBytes(input), message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - - assert.NoError(t, output[0].Get(0).ErrorGet()) - assert.NoError(t, output[0].Get(1).ErrorGet()) - assert.NoError(t, output[0].Get(2).ErrorGet()) - - _, ok := mgr.Caches["foocache"]["1"] - require.False(t, ok) - - actV, ok := mgr.Caches["foocache"]["2"] - require.True(t, ok) - assert.Equal(t, "foo 2", actV.Value) - - _, ok = mgr.Caches["foocache"]["3"] - require.False(t, ok) -} diff --git a/internal/impl/pure/processor_cached.go b/internal/impl/pure/processor_cached.go deleted file mode 100644 index d5970f8e81..0000000000 --- a/internal/impl/pure/processor_cached.go +++ /dev/null @@ -1,364 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "encoding/binary" - "errors" - "fmt" - "time" - - "golang.org/x/sync/errgroup" - - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func newCachedProcessorConfigSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Version("4.3.0"). - Categories("Utility"). - Summary("Cache the result of applying one or more processors to messages identified by a key. If the key already exists within the cache the contents of the message will be replaced with the cached result instead of applying the processors. This component is therefore useful in situations where an expensive set of processors need only be executed periodically."). - Description("The format of the data when stored within the cache is a custom and versioned schema chosen to balance performance and storage space. It is therefore not possible to point this processor to a cache that is pre-populated with data that this processor has not created itself."). - Field(service.NewStringField("cache").Description("The cache resource to read and write processor results from.")). - Field(service.NewBloblangField("skip_on"). - Description("A condition that can be used to skip caching the results from the processors."). - Example("errored()"). - Optional()). - Field(service.NewInterpolatedStringField("key"). - Description("A key to be resolved for each message, if the key already exists in the cache then the cached result is used, otherwise the processors are applied and the result is cached under this key. The key could be static and therefore apply generally to all messages or it could be an interpolated expression that is potentially unique for each message."). - Example("my_foo_result"). - Example(`${! this.document.id }`). - Example(`${! meta("kafka_key") }`). - Example(`${! meta("kafka_topic") }`)). - Field(service.NewInterpolatedStringField("ttl"). - Description("An optional expiry period to set for each cache entry. Some caches only have a general TTL and will therefore ignore this setting."). - Optional()). - Field(service.NewProcessorListField("processors").Description("The list of processors whose result will be cached.")). - Example( - "Cached Enrichment", - "In the following example we want to we enrich messages consumed from Kafka with data specific to the origin topic partition, we do this by placing an `http` processor within a `branch`, where the HTTP URL contains interpolation functions with the topic and partition in the path.\n\nHowever, it would be inefficient to make this HTTP request for every single message as the result is consistent for all data of a given topic partition. We can solve this by placing our enrichment call within a `cached` processor where the key contains the topic and partition, resulting in messages that originate from the same topic/partition combination using the cached result of the prior.", - ` -pipeline: - processors: - - branch: - processors: - - cached: - key: '${! meta("kafka_topic") }-${! meta("kafka_partition") }' - cache: foo_cache - processors: - - mapping: 'root = ""' - - http: - url: http://example.com/enrichment/${! meta("kafka_topic") }/${! meta("kafka_partition") } - verb: GET - result_map: 'root.enrichment = this' - -cache_resources: - - label: foo_cache - memory: - # Disable compaction so that cached items never expire - compaction_interval: "" -`, - ). - Example( - "Periodic Global Enrichment", - "In the following example we enrich all messages with the same data obtained from a static URL with an `http` processor within a `branch`. However, we expect the data from this URL to change roughly every 10 minutes, so we configure a `cached` processor with a static key (since this request is consistent for all messages) and a TTL of `10m`.", - ` -pipeline: - processors: - - branch: - request_map: 'root = ""' - processors: - - cached: - key: static_foo - cache: foo_cache - ttl: 10m - processors: - - http: - url: http://example.com/get/foo.json - verb: GET - result_map: 'root.foo = this' - -cache_resources: - - label: foo_cache - memory: {} -`, - ) -} - -func init() { - err := service.RegisterProcessor( - "cached", newCachedProcessorConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return newCachedProcessorFromParsedConf(mgr, conf) - }) - if err != nil { - panic(err) - } -} - -type cachedProcessor struct { - manager *service.Resources - - cacheName string - key *service.InterpolatedString - ttl *service.InterpolatedString - processors []*service.OwnedProcessor - skipOn *bloblang.Executor -} - -func newCachedProcessorFromParsedConf(manager *service.Resources, conf *service.ParsedConfig) (proc *cachedProcessor, err error) { - proc = &cachedProcessor{ - manager: manager, - } - - if proc.cacheName, err = conf.FieldString("cache"); err != nil { - return nil, err - } - if !manager.HasCache(proc.cacheName) { - return nil, fmt.Errorf("cache named %v not found", proc.cacheName) - } - - if proc.key, err = conf.FieldInterpolatedString("key"); err != nil { - return nil, err - } - - if conf.Contains("ttl") { - proc.ttl, err = conf.FieldInterpolatedString("ttl") - if err != nil { - return nil, err - } - } - - if proc.processors, err = conf.FieldProcessorList("processors"); err != nil { - return - } - - if conf.Contains("skip_on") { - var skipOn *bloblang.Executor - if skipOn, err = conf.FieldBloblang("skip_on"); err != nil { - return nil, err - } - proc.skipOn = skipOn - } - - return -} - -func (proc *cachedProcessor) Process(ctx context.Context, msg *service.Message) (service.MessageBatch, error) { - cacheKey, err := proc.key.TryString(msg) - if err != nil { - return nil, fmt.Errorf("failed to interpolate key expression: %w", err) - } - - var ttl *time.Duration - - if proc.ttl != nil { - duration, err := proc.ttl.TryString(msg) - if err != nil { - return nil, fmt.Errorf("failed to interpolate ttl expression: %w", err) - } - - tempTTL, err := time.ParseDuration(duration) - if err != nil { - return nil, fmt.Errorf("failed to parse ttl expression: %w", err) - } - - ttl = &tempTTL - } - - var cachedBytes []byte - if cerr := proc.manager.AccessCache(ctx, proc.cacheName, func(cache service.Cache) { - cachedBytes, err = cache.Get(ctx, cacheKey) - }); cerr != nil { - return nil, cerr - } - - // Return early if we have a cached result - if err == nil { - batch, err := cachedProcResultToBatch(msg, cachedBytes) - if err != nil { - err = fmt.Errorf("failed to parse cached result, this indicates the data was not set by this processor: %w", err) - } - return batch, err - } - - // Or if an error occurred that wasn't ErrKeyNotFound - if !errors.Is(err, service.ErrKeyNotFound) { - return nil, err - } - - // Result is not cached, so execute processors and cache the result - resultBatch, err := service.ExecuteProcessors(ctx, proc.processors, service.MessageBatch{msg}) - if err != nil { - return nil, err - } - - shouldCache := true - var collapsedBatch service.MessageBatch - for _, b := range resultBatch { - skip, err := shouldSkip(b, proc.skipOn) - if err != nil { - proc.manager.Logger().Errorf("skip_on check failed: %w, caching will be skipped as a precaution", err) - skip = true - } - shouldCache = shouldCache && !skip - collapsedBatch = append(collapsedBatch, b...) - } - - if !shouldCache { - return collapsedBatch, nil - } - - // Any errors in creating a serialized batch or caching are non-fatal and - // should be logged but otherwise regarded as insignificant to the flowing - // messages. - result, err := cachedProcSerialiseBatch(collapsedBatch) - if err != nil { - proc.manager.Logger().Errorf("failed to serialize resulting batch for caching: %w", err) - return collapsedBatch, nil - } - - var setErr error - cerr := proc.manager.AccessCache(ctx, proc.cacheName, func(cache service.Cache) { - setErr = cache.Set(ctx, cacheKey, result, ttl) - }) - if cerr != nil { - proc.manager.Logger().Errorf("failed to access cache for result: %w", err) - } - if setErr != nil { - proc.manager.Logger().Errorf("failed to write result to cache: %w", err) - } - return collapsedBatch, nil -} - -func (proc *cachedProcessor) Close(ctx context.Context) error { - var group errgroup.Group - for _, ownedProc := range proc.processors { - op := ownedProc - group.Go(func() error { - return op.Close(ctx) - }) - } - - return group.Wait() -} - -func shouldSkip(batch service.MessageBatch, predicate *bloblang.Executor) (bool, error) { - if predicate == nil { - return false, nil - } - - predResult, err := batch.BloblangQuery(0, predicate) - if err != nil { - return false, fmt.Errorf("failed to execute skip_on mapping: %w", err) - } - - raw, err := predResult.AsStructured() - if err != nil { - return false, fmt.Errorf("skip_on mapping did not return structured result: %w", err) - } - - skip, ok := raw.(bool) - if !ok { - return false, fmt.Errorf("skip_on did return boolean result: %v", raw) - } - - return skip, nil -} - -//------------------------------------------------------------------------------ - -// We use versioning for the bytes we write to the cache, this allows us to -// update and modify our serialiser in future in a backwards compatible way. - -func cachedProcExtractUint32(b []byte) (n uint32, remaining []byte, err error) { - if len(b) < 4 { - err = errors.New("message is too small to extract number") - return - } - n = binary.BigEndian.Uint32(b[:4]) - remaining = b[4:] - return -} - -func cachedProcUint32ToBytes(n uint32) []byte { - newBytes := make([]byte, 4) - binary.BigEndian.PutUint32(newBytes, n) - return newBytes -} - -func cachedProcSerialiseBatch(batch service.MessageBatch) ([]byte, error) { - var buf bytes.Buffer - - // Insert schema version - // TODO: Increment this on any schema change - if _, err := buf.Write(cachedProcUint32ToBytes(1)); err != nil { - return nil, err - } - - // Insert number of batch messages - if _, err := buf.Write(cachedProcUint32ToBytes(uint32(len(batch)))); err != nil { - return nil, err - } - - // For each message - for i, msg := range batch { - mBytes, err := msg.AsBytes() - if err != nil { - return nil, fmt.Errorf("unable to extract bytes from message %v: %w", i, err) - } - - // Insert size of message - if _, err := buf.Write(cachedProcUint32ToBytes(uint32(len(mBytes)))); err != nil { - return nil, err - } - - // Write data - if _, err := buf.Write(mBytes); err != nil { - return nil, err - } - } - - return buf.Bytes(), nil -} - -func cachedProcV1DeserialiseBatch(msg *service.Message, data []byte) (resBatch service.MessageBatch, err error) { - // Extract number of batch messages - var nBatches uint32 - if nBatches, data, err = cachedProcExtractUint32(data); err != nil { - return nil, fmt.Errorf("failed to extract batch size: %w", err) - } - - for i := 0; i < int(nBatches); i++ { - // Extract message length - var msgSize uint32 - if msgSize, data, err = cachedProcExtractUint32(data); err != nil { - return nil, fmt.Errorf("failed to extract message %v size: %w", i, err) - } - - if len(data) < int(msgSize) { - return nil, fmt.Errorf("failed to extract message %v size: input data ended unexpectedly", i) - } - - // Copy message with bytes - msgCopy := msg.Copy() - msgCopy.SetBytes(data[:msgSize]) - resBatch = append(resBatch, msgCopy) - - data = data[msgSize:] - } - - return -} - -func cachedProcResultToBatch(msg *service.Message, cachedResult []byte) (service.MessageBatch, error) { - verID, remaining, err := cachedProcExtractUint32(cachedResult) - if err != nil { - return nil, fmt.Errorf("failed to extract serialization format version: %w", err) - } - if verID == 1 { - return cachedProcV1DeserialiseBatch(msg, remaining) - } - return nil, fmt.Errorf("invalid format version: %v", verID) -} diff --git a/internal/impl/pure/processor_cached_test.go b/internal/impl/pure/processor_cached_test.go deleted file mode 100644 index 1cd49e64f4..0000000000 --- a/internal/impl/pure/processor_cached_test.go +++ /dev/null @@ -1,231 +0,0 @@ -package pure - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestCachedHappy(t *testing.T) { - conf, err := newCachedProcessorConfigSpec().ParseYAML(` -key: ${! content() } -ttl: 3660s -cache: foo -skip_on: errored() -processors: - - bloblang: 'root = content().string() + " FOO " + uuid_v4()' - - bloblang: 'root = content().string() + " BAR " + uuid_v4()' -`, nil) - require.NoError(t, err) - - mRes := service.MockResources( - service.MockResourcesOptAddCache("foo"), - service.MockResourcesOptAddCache("bar"), - ) - - proc, err := newCachedProcessorFromParsedConf(mRes, conf) - require.NoError(t, err) - - tCtx := context.Background() - - resBatchA1, err := proc.Process(tCtx, service.NewMessage([]byte("keya"))) - require.NoError(t, err) - require.Len(t, resBatchA1, 1) - - resBatchB1, err := proc.Process(tCtx, service.NewMessage([]byte("keyb"))) - require.NoError(t, err) - require.Len(t, resBatchB1, 1) - - resBatchA2, err := proc.Process(tCtx, service.NewMessage([]byte("keya"))) - require.NoError(t, err) - require.Len(t, resBatchA2, 1) - - resBatchB2, err := proc.Process(tCtx, service.NewMessage([]byte("keyb"))) - require.NoError(t, err) - require.Len(t, resBatchB2, 1) - - resBytesA1, err := resBatchA1[0].AsBytes() - require.NoError(t, err) - assert.Contains(t, string(resBytesA1), "keya FOO ") - assert.Contains(t, string(resBytesA1), " BAR ") - - resBytesB1, err := resBatchB1[0].AsBytes() - require.NoError(t, err) - assert.Contains(t, string(resBytesB1), "keyb FOO ") - assert.Contains(t, string(resBytesB1), " BAR ") - - resBytesA2, err := resBatchA2[0].AsBytes() - require.NoError(t, err) - assert.Contains(t, string(resBytesA2), "keya FOO ") - assert.Contains(t, string(resBytesA2), " BAR ") - - resBytesB2, err := resBatchB2[0].AsBytes() - require.NoError(t, err) - assert.Contains(t, string(resBytesB2), "keyb FOO ") - assert.Contains(t, string(resBytesB2), " BAR ") - - assert.Equal(t, string(resBytesA1), string(resBytesA2)) - assert.Equal(t, string(resBytesB1), string(resBytesB2)) - - require.NoError(t, mRes.AccessCache(tCtx, "foo", func(c service.Cache) { - _, err = c.Get(tCtx, "keya") - assert.NoError(t, err) - - _, err = c.Get(tCtx, "keyb") - assert.NoError(t, err) - })) - - require.NoError(t, mRes.AccessCache(tCtx, "bar", func(c service.Cache) { - _, err = c.Get(tCtx, "keya") - assert.Error(t, err) - - _, err = c.Get(tCtx, "keyb") - assert.Error(t, err) - })) - - assert.NoError(t, proc.Close(tCtx)) -} - -func TestCachedHappyBatched(t *testing.T) { - conf, err := newCachedProcessorConfigSpec().ParseYAML(` -key: ${! meta("key") } -ttl: ${! meta("ttl").or("60s")} -cache: foo -processors: - - bloblang: 'root = this.map_each(ele -> ele + " FOO")' - - unarchive: - format: json_array -`, nil) - require.NoError(t, err) - - mRes := service.MockResources(service.MockResourcesOptAddCache("foo")) - - proc, err := newCachedProcessorFromParsedConf(mRes, conf) - require.NoError(t, err) - - tCtx := context.Background() - - msg := service.NewMessage([]byte(``)) - msg.MetaSet("key", "keya") - msg.MetaSet("ttl", "3660s") - - resBatchA1, err := proc.Process(tCtx, service.NewMessage([]byte(`["valuea","valueb","valuec"]`))) - require.NoError(t, err) - require.Len(t, resBatchA1, 3) - - msg = service.NewMessage([]byte(``)) - msg.MetaSet("key", "keya") - - resBatchA2, err := proc.Process(tCtx, service.NewMessage([]byte(`["valued","valuee"]`))) - require.NoError(t, err) - require.Len(t, resBatchA2, 3) - - resBytes, err := resBatchA1[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `"valuea FOO"`, string(resBytes)) - - resBytes, err = resBatchA1[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, `"valueb FOO"`, string(resBytes)) - - resBytes, err = resBatchA1[2].AsBytes() - require.NoError(t, err) - assert.Equal(t, `"valuec FOO"`, string(resBytes)) - - resBytes, err = resBatchA2[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `"valuea FOO"`, string(resBytes)) - - resBytes, err = resBatchA2[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, `"valueb FOO"`, string(resBytes)) - - resBytes, err = resBatchA2[2].AsBytes() - require.NoError(t, err) - assert.Equal(t, `"valuec FOO"`, string(resBytes)) - - assert.NoError(t, proc.Close(tCtx)) -} - -func TestCachedSkip(t *testing.T) { - conf, err := newCachedProcessorConfigSpec().ParseYAML(` -key: ${! content() } -cache: foo -skip_on: "errored()" -processors: - - bloblang: | - let body = content().string() - root = "%s FOO %d".format($body, count($body)) - - bloblang: root = throw("simulated error") -`, nil) - require.NoError(t, err) - - mRes := service.MockResources( - service.MockResourcesOptAddCache("foo"), - service.MockResourcesOptAddCache("bar"), - ) - - proc, err := newCachedProcessorFromParsedConf(mRes, conf) - require.NoError(t, err) - - tCtx := context.Background() - - resBatchA1, err := proc.Process(tCtx, service.NewMessage([]byte("keya"))) - require.NoError(t, err) - require.Len(t, resBatchA1, 1) - - resBatchA2, err := proc.Process(tCtx, service.NewMessage([]byte("keya"))) - require.NoError(t, err) - require.Len(t, resBatchA2, 1) - - resBytesA1, err := resBatchA1[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "keya FOO 1", string(resBytesA1)) - - resBytesA2, err := resBatchA2[0].AsBytes() - require.NoError(t, err) - // Since caching is skipped, the second message runs through processors - assert.Equal(t, "keya FOO 2", string(resBytesA2)) - - require.NoError(t, mRes.AccessCache(tCtx, "foo", func(c service.Cache) { - val, err := c.Get(tCtx, "keya") - assert.ErrorIs(t, err, service.ErrKeyNotFound) - assert.Empty(t, val) - })) - - assert.NoError(t, proc.Close(tCtx)) -} - -func TestCachedBadTTL(t *testing.T) { - conf, err := newCachedProcessorConfigSpec().ParseYAML(` -key: ${! content() } -ttl: ${! 2+2 } -cache: foo -skip_on: "errored()" -processors: - - bloblang: | - let body = content().string() - root = "%s FOO %d".format($body, count($body)) - - bloblang: root = throw("simulated error") -`, nil) - require.NoError(t, err) - - mRes := service.MockResources( - service.MockResourcesOptAddCache("foo"), - ) - - proc, err := newCachedProcessorFromParsedConf(mRes, conf) - require.NoError(t, err) - - tCtx := context.Background() - - resBatchA1, err := proc.Process(tCtx, service.NewMessage([]byte("keya"))) - assert.EqualError(t, err, `failed to parse ttl expression: time: missing unit in duration "4"`) - assert.Empty(t, resBatchA1) - - assert.NoError(t, proc.Close(tCtx)) -} diff --git a/internal/impl/pure/processor_catch.go b/internal/impl/pure/processor_catch.go deleted file mode 100644 index 026a5fd769..0000000000 --- a/internal/impl/pure/processor_catch.go +++ /dev/null @@ -1,116 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("catch", service.NewConfigSpec(). - Stable(). - Categories("Composition"). - Summary("Applies a list of child processors _only_ when a previous processing step has failed."). - Description(` -Behaves similarly to the `+"xref:components:processors/for_each.adoc[`for_each`]"+` processor, where a list of child processors are applied to individual messages of a batch. However, processors are only applied to messages that failed a processing step prior to the catch. - -For example, with the following config: - -`+"```yaml"+` -pipeline: - processors: - - resource: foo - - catch: - - resource: bar - - resource: baz -`+"```"+` - -If the processor `+"`foo`"+` fails for a particular message, that message will be fed into the processors `+"`bar` and `baz`"+`. Messages that do not fail for the processor `+"`foo`"+` will skip these processors. - -When messages leave the catch block their fail flags are cleared. This processor is useful for when it's possible to recover failed messages, or when special actions (such as logging/metrics) are required before dropping them. - -More information about error handling can be found in xref:configuration:error_handling.adoc[].`). - LintRule(`if this.or([]).any(pconf -> pconf.type.or("") == "try" || pconf.try.type() == "array" ) { - "'catch' block contains a 'try' block which will never execute due to errors only being cleared at the end of the 'catch', for more information about nesting 'try' within 'catch' read: https://www.docs.redpanda.com/redpanda-connect/components/processors/try#nesting-within-a-catch-block" -}`). - Field(service.NewProcessorListField("").Default([]any{})), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - mgr := interop.UnwrapManagement(res) - childPubProcs, err := conf.FieldProcessorList() - if err != nil { - return nil, err - } - - childProcs := make([]processor.V1, len(childPubProcs)) - for i, p := range childPubProcs { - childProcs[i] = interop.UnwrapOwnedProcessor(p) - } - - tp, err := newCatch(childProcs) - if err != nil { - return nil, err - } - - p := processor.NewAutoObservedBatchedProcessor("catch", tp, mgr) - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -type catchProc struct { - children []processor.V1 -} - -func newCatch(children []processor.V1) (*catchProc, error) { - return &catchProc{ - children: children, - }, nil -} - -func (p *catchProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - resultMsgs := make([]message.Batch, msg.Len()) - _ = msg.Iter(func(i int, p *message.Part) error { - resultMsgs[i] = message.Batch{p} - return nil - }) - - var err error - if resultMsgs, err = processor.ExecuteCatchAll(ctx.Context(), p.children, resultMsgs...); err != nil || len(resultMsgs) == 0 { - return nil, err - } - - resMsg := message.QuickBatch(nil) - for _, m := range resultMsgs { - _ = m.Iter(func(i int, p *message.Part) error { - resMsg = append(resMsg, p) - return nil - }) - } - if resMsg.Len() == 0 { - return nil, nil - } - - _ = resMsg.Iter(func(i int, p *message.Part) error { - p.ErrorSet(nil) - return nil - }) - - resMsgs := [1]message.Batch{resMsg} - return resMsgs[:], nil -} - -func (p *catchProc) Close(ctx context.Context) error { - for _, child := range p.children { - if err := child.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_catch_test.go b/internal/impl/pure/processor_catch_test.go deleted file mode 100644 index f9f05cfbc4..0000000000 --- a/internal/impl/pure/processor_catch_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package pure_test - -import ( - "context" - "errors" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestCatchEmpty(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -catch: [] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][]byte{ - []byte("foo bar baz"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(exp)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - _ = msgs[0].Iter(func(i int, p *message.Part) error { - if p.ErrorGet() != nil { - t.Errorf("Unexpected part %v failed flag", i) - } - return nil - }) -} - -func TestCatchBasic(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -catch: - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("Zm9vIGJhciBiYXo="), - []byte("MSAyIDMgNA=="), - []byte("aGVsbG8gZm9vIHdvcmxk"), - } - - msg := message.QuickBatch(parts) - _ = msg.Iter(func(i int, p *message.Part) error { - p.ErrorSet(errors.New("foo")) - return nil - }) - msgs, res := proc.ProcessBatch(context.Background(), msg) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - _ = msgs[0].Iter(func(i int, p *message.Part) error { - if p.ErrorGet() != nil { - t.Errorf("Unexpected part %v failed flag", i) - } - return nil - }) -} - -func TestCatchFilterSome(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -catch: - - bloblang: 'root = if !content().contains("foo") { deleted() }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("foo bar baz"), - []byte("hello foo world"), - } - msg := message.QuickBatch(parts) - _ = msg.Iter(func(i int, p *message.Part) error { - p.ErrorSet(errors.New("foo")) - return nil - }) - msgs, res := proc.ProcessBatch(context.Background(), msg) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - _ = msgs[0].Iter(func(i int, p *message.Part) error { - if p.ErrorGet() != nil { - t.Errorf("Unexpected part %v failed flag", i) - } - return nil - }) -} - -func TestCatchMultiProcs(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -catch: - - bloblang: 'root = if !content().contains("foo") { deleted() }' - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("Zm9vIGJhciBiYXo="), - []byte("aGVsbG8gZm9vIHdvcmxk"), - } - msg := message.QuickBatch(parts) - _ = msg.Iter(func(i int, p *message.Part) error { - p.ErrorSet(errors.New("foo")) - return nil - }) - msgs, res := proc.ProcessBatch(context.Background(), msg) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - _ = msgs[0].Iter(func(i int, p *message.Part) error { - if p.ErrorGet() != nil { - t.Errorf("Unexpected part %v failed flag", i) - } - return nil - }) -} - -func TestCatchNotFails(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -catch: - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte(`FAILED ENCODE ME PLEASE`), - []byte("NOT FAILED, DO NOT ENCODE"), - []byte(`FAILED ENCODE ME PLEASE 2`), - } - exp := [][]byte{ - []byte("RkFJTEVEIEVOQ09ERSBNRSBQTEVBU0U="), - []byte("NOT FAILED, DO NOT ENCODE"), - []byte("RkFJTEVEIEVOQ09ERSBNRSBQTEVBU0UgMg=="), - } - msg := message.QuickBatch(parts) - msg.Get(0).ErrorSet(errors.New("foo")) - msg.Get(2).ErrorSet(errors.New("foo")) - msgs, res := proc.ProcessBatch(context.Background(), msg) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - _ = msgs[0].Iter(func(i int, p *message.Part) error { - if p.ErrorGet() != nil { - t.Errorf("Unexpected part %v failed flag", i) - } - return nil - }) -} - -func TestCatchFilterAll(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -catch: - - bloblang: 'root = if !content().contains("foo") { deleted() }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("bar baz"), - []byte("1 2 3 4"), - []byte("hello world"), - } - msg := message.QuickBatch(parts) - _ = msg.Iter(func(i int, p *message.Part) error { - p.ErrorSet(errors.New("foo")) - return nil - }) - msgs, res := proc.ProcessBatch(context.Background(), msg) - assert.NoError(t, res) - if len(msgs) != 0 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } -} diff --git a/internal/impl/pure/processor_compress.go b/internal/impl/pure/processor_compress.go deleted file mode 100644 index 36d50963f7..0000000000 --- a/internal/impl/pure/processor_compress.go +++ /dev/null @@ -1,89 +0,0 @@ -package pure - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - compressPFieldAlgorithm = "algorithm" - compressPFieldLevel = "level" -) - -func init() { - compAlgs := CompressionAlgsList() - err := service.RegisterBatchProcessor( - "compress", service.NewConfigSpec(). - Categories("Parsing"). - Stable(). - Summary(fmt.Sprintf("Compresses messages according to the selected algorithm. Supported compression algorithms are: %v", compAlgs)). - Description(`The 'level' field might not apply to all algorithms.`). - Fields( - service.NewStringEnumField(compressPFieldAlgorithm, compAlgs...). - Description("The compression algorithm to use."). - LintRule(``), - service.NewIntField(compressPFieldLevel). - Description("The level of compression to use. May not be applicable to all algorithms."). - Default(-1), - ), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - algStr, err := conf.FieldString(compressPFieldAlgorithm) - if err != nil { - return nil, err - } - - level, err := conf.FieldInt(compressPFieldLevel) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newCompress(algStr, level, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("compress", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type compressProc struct { - level int - comp CompressFunc - log log.Modular -} - -func newCompress(algStr string, level int, mgr bundle.NewManagement) (*compressProc, error) { - cor, err := strToCompressFunc(algStr) - if err != nil { - return nil, err - } - return &compressProc{ - level: level, - comp: cor, - log: mgr.Logger(), - }, nil -} - -func (c *compressProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - newBytes, err := c.comp(c.level, msg.AsBytes()) - if err != nil { - c.log.Error("Failed to compress message: %v\n", err) - return nil, err - } - msg.SetBytes(newBytes) - return []*message.Part{msg}, nil -} - -func (c *compressProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_compress_test.go b/internal/impl/pure/processor_compress_test.go deleted file mode 100644 index 71a4fddc1b..0000000000 --- a/internal/impl/pure/processor_compress_test.go +++ /dev/null @@ -1,316 +0,0 @@ -package pure_test - -import ( - "bytes" - "context" - "reflect" - "testing" - - "github.com/klauspost/compress/flate" - "github.com/klauspost/compress/gzip" - "github.com/klauspost/compress/snappy" - "github.com/klauspost/compress/zlib" - "github.com/klauspost/pgzip" - "github.com/pierrec/lz4/v4" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestCompressBadAlgo(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: does not exist -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - if err == nil { - t.Error("Expected error from bad algo") - } -} - -func TestCompressGZIP(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: gzip -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - var buf bytes.Buffer - - zw := gzip.NewWriter(&buf) - _, _ = zw.Write(input[i]) - zw.Close() - - exp = append(exp, buf.Bytes()) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Compress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestCompressPGZIP(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: pgzip -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - var buf bytes.Buffer - - zw := pgzip.NewWriter(&buf) - _, _ = zw.Write(input[i]) - zw.Close() - - exp = append(exp, buf.Bytes()) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Compress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestCompressZLIB(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: zlib -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - var buf bytes.Buffer - - zw := zlib.NewWriter(&buf) - _, _ = zw.Write(input[i]) - zw.Close() - - exp = append(exp, buf.Bytes()) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Compress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestCompressFlate(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: flate -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - var buf bytes.Buffer - - zw, err := flate.NewWriter(&buf, -1) - if err != nil { - t.Fatal(err) - } - _, _ = zw.Write(input[i]) - zw.Close() - - exp = append(exp, buf.Bytes()) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Compress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestCompressSnappy(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: snappy -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - output := snappy.Encode(nil, input[i]) - exp = append(exp, output) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Compress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestCompressLZ4(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -compress: - algorithm: lz4 -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - var buf bytes.Buffer - - w := lz4.NewWriter(&buf) - if _, err := w.Write(input[i]); err != nil { - w.Close() - t.Fatalf("Failed to compress input: %s", err) - } - w.Close() - - exp = append(exp, buf.Bytes()) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Compress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} diff --git a/internal/impl/pure/processor_decompress.go b/internal/impl/pure/processor_decompress.go deleted file mode 100644 index ca28d6bf50..0000000000 --- a/internal/impl/pure/processor_decompress.go +++ /dev/null @@ -1,78 +0,0 @@ -package pure - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - decompressPFieldAlgorithm = "algorithm" -) - -func init() { - compAlgs := DecompressionAlgsList() - err := service.RegisterBatchProcessor( - "decompress", service.NewConfigSpec(). - Categories("Parsing"). - Stable(). - Summary(fmt.Sprintf("Decompresses messages according to the selected algorithm. Supported decompression algorithms are: %v", compAlgs)). - Fields( - service.NewStringEnumField(decompressPFieldAlgorithm, compAlgs...). - Description("The decompression algorithm to use."). - LintRule(``), - ), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - algStr, err := conf.FieldString(compressPFieldAlgorithm) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newDecompress(algStr, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("decompress", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type decompressProc struct { - decomp DecompressFunc - log log.Modular -} - -func newDecompress(algStr string, mgr bundle.NewManagement) (*decompressProc, error) { - dcor, err := strToDecompressFunc(algStr) - if err != nil { - return nil, err - } - return &decompressProc{ - decomp: dcor, - log: mgr.Logger(), - }, nil -} - -func (d *decompressProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - newBytes, err := d.decomp(msg.AsBytes()) - if err != nil { - d.log.Error("Failed to decompress message part: %v\n", err) - return nil, err - } - - msg.SetBytes(newBytes) - return []*message.Part{msg}, nil -} - -func (d *decompressProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_decompress_test.go b/internal/impl/pure/processor_decompress_test.go deleted file mode 100644 index 91efff7b16..0000000000 --- a/internal/impl/pure/processor_decompress_test.go +++ /dev/null @@ -1,325 +0,0 @@ -package pure_test - -import ( - "bytes" - "context" - "reflect" - "testing" - - "github.com/klauspost/compress/flate" - "github.com/klauspost/compress/gzip" - "github.com/klauspost/compress/snappy" - "github.com/klauspost/compress/zlib" - "github.com/klauspost/pgzip" - "github.com/pierrec/lz4/v4" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestDecompressBadAlgo(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: does not exist -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - if err == nil { - t.Error("Expected error from bad algo") - } -} - -func TestDecompressGZIP(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: gzip -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - exp = append(exp, input[i]) - - var buf bytes.Buffer - - zw := gzip.NewWriter(&buf) - _, _ = zw.Write(input[i]) - zw.Close() - - input[i] = buf.Bytes() - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Decompress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestDecompressPGZIP(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: pgzip -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - exp = append(exp, input[i]) - - var buf bytes.Buffer - - zw := pgzip.NewWriter(&buf) - _, _ = zw.Write(input[i]) - zw.Close() - - input[i] = buf.Bytes() - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Decompress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestDecompressSnappy(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: snappy -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - exp = append(exp, input[i]) - input[i] = snappy.Encode(nil, input[i]) - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Decompress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestDecompressZLIB(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: zlib -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - exp = append(exp, input[i]) - - var buf bytes.Buffer - - zw := zlib.NewWriter(&buf) - _, _ = zw.Write(input[i]) - zw.Close() - - input[i] = buf.Bytes() - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Decompress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestDecompressFlate(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: flate -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - exp = append(exp, input[i]) - - var buf bytes.Buffer - - zw, err := flate.NewWriter(&buf, 0) - if err != nil { - t.Fatal(err) - } - _, _ = zw.Write(input[i]) - zw.Close() - - input[i] = buf.Bytes() - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Decompress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} - -func TestDecompressLZ4(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -decompress: - algorithm: lz4 -`) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - - for i := range input { - exp = append(exp, input[i]) - - buf := bytes.Buffer{} - w := lz4.NewWriter(&buf) - if _, err := w.Write(input[i]); err != nil { - w.Close() - t.Fatalf("Failed to compress input: %s", err) - } - w.Close() - - input[i] = buf.Bytes() - } - - if reflect.DeepEqual(input, exp) { - t.Fatal("Input and exp output are the same") - } - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Error("Decompress failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } -} diff --git a/internal/impl/pure/processor_dedupe.go b/internal/impl/pure/processor_dedupe.go deleted file mode 100644 index c868513b48..0000000000 --- a/internal/impl/pure/processor_dedupe.go +++ /dev/null @@ -1,176 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - dedupFieldCache = "cache" - dedupFieldKey = "key" - dedupFieldDropOnCacheErr = "drop_on_err" -) - -func dedupeProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`Deduplicates messages by storing a key value in a cache using the `+"`add`"+` operator. If the key already exists within the cache it is dropped.`). - Description(` -Caches must be configured as resources, for more information check out the xref:components:caches/about.adoc[cache documentation]. - -When using this processor with an output target that might fail you should always wrap the output within an indefinite `+"xref:components:outputs/retry.adoc[`retry`]"+` block. This ensures that during outages your messages aren't reprocessed after failures, which would result in messages being dropped. - -== Batch deduplication - -This processor enacts on individual messages only, in order to perform a deduplication on behalf of a batch (or window) of messages instead use the `+"xref:components:processors/cache.adoc#examples[`cache` processor]"+`. - -== Delivery guarantees - -Performing deduplication on a stream using a distributed cache voids any at-least-once guarantees that it previously had. This is because the cache will preserve message signatures even if the message fails to leave the Benthos pipeline, which would cause message loss in the event of an outage at the output sink followed by a restart of the Benthos instance (or a server crash, etc). - -This problem can be mitigated by using an in-memory cache and distributing messages to horizontally scaled Benthos pipelines partitioned by the deduplication key. However, in situations where at-least-once delivery guarantees are important it is worth avoiding deduplication in favour of implement idempotent behavior at the edge of your stream pipelines.`). - Example( - "Deduplicate based on Kafka key", - "The following configuration demonstrates a pipeline that deduplicates messages based on the Kafka key.", - ` -pipeline: - processors: - - dedupe: - cache: keycache - key: ${! meta("kafka_key") } - -cache_resources: - - label: keycache - memory: - default_ttl: 60s -`, - ). - Fields( - service.NewStringField(dedupFieldCache). - Description("The xref:components:caches/about.adoc[`cache` resource] to target with this processor."), - service.NewInterpolatedStringField(dedupFieldKey). - Description("An interpolated string yielding the key to deduplicate by for each message."). - Examples(`${! meta("kafka_key") }`, `${! content().hash("xxhash64") }`), - service.NewBoolField(dedupFieldDropOnCacheErr). - Description("Whether messages should be dropped when the cache returns a general error such as a network issue."). - Default(true), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "dedupe", dedupeProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - cache, err := conf.FieldString(dedupFieldCache) - if err != nil { - return nil, err - } - - keyStr, err := conf.FieldString(dedupFieldKey) - if err != nil { - return nil, err - } - - dropOnErr, err := conf.FieldBool(dedupFieldDropOnCacheErr) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newDedupe(cache, keyStr, dropOnErr, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("dedupe", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type dedupeProc struct { - log log.Modular - - dropOnErr bool - key *field.Expression - mgr bundle.NewManagement - cacheName string -} - -func newDedupe(cache, keyStr string, dropOnErr bool, mgr bundle.NewManagement) (*dedupeProc, error) { - if keyStr == "" { - return nil, errors.New("dedupe key must not be empty") - } - key, err := mgr.BloblEnvironment().NewField(keyStr) - if err != nil { - return nil, fmt.Errorf("failed to parse key expression: %v", err) - } - - if !mgr.ProbeCache(cache) { - return nil, fmt.Errorf("cache resource '%v' was not found", cache) - } - - return &dedupeProc{ - log: mgr.Logger(), - dropOnErr: dropOnErr, - key: key, - mgr: mgr, - cacheName: cache, - }, nil -} - -func (d *dedupeProc) ProcessBatch(ctx *processor.BatchProcContext, batch message.Batch) ([]message.Batch, error) { - newBatch := message.QuickBatch(nil) - _ = batch.Iter(func(i int, p *message.Part) error { - key, err := d.key.String(i, batch) - if err != nil { - err = fmt.Errorf("key interpolation error: %w", err) - ctx.OnError(err, i, nil) - return nil - } - - if cerr := d.mgr.AccessCache(context.Background(), d.cacheName, func(cache cache.V1) { - err = cache.Add(context.Background(), key, []byte{'t'}, nil) - }); cerr != nil { - err = cerr - } - if err != nil { - if errors.Is(err, component.ErrKeyAlreadyExists) { - ctx.Span(i).LogKV("event", "dropped", "type", "deduplicated") - return nil - } - - d.log.Error("Cache error: %v\n", err) - if d.dropOnErr { - ctx.Span(i).LogKV("event", "dropped", "type", "deduplicated") - return nil - } - - ctx.OnError(err, i, p) - } - - newBatch = append(newBatch, p) - return nil - }) - - if newBatch.Len() == 0 { - return nil, nil - } - return []message.Batch{newBatch}, nil -} - -func (d *dedupeProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_dedupe_test.go b/internal/impl/pure/processor_dedupe_test.go deleted file mode 100644 index 428346f554..0000000000 --- a/internal/impl/pure/processor_dedupe_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestDedupe(t *testing.T) { - doc1 := []byte("hello world") - doc2 := []byte("hello world") - doc3 := []byte("hello world 2") - - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - conf, err := testutil.ProcessorFromYAML(` -dedupe: - cache: foocache - key: ${! content() } -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - msgIn := message.QuickBatch([][]byte{doc1}) - msgOut, err := proc.ProcessBatch(context.Background(), msgIn) - require.NoError(t, err) - require.Len(t, msgOut, 1) - - msgIn = message.QuickBatch([][]byte{doc2}) - msgOut, err = proc.ProcessBatch(context.Background(), msgIn) - require.NoError(t, err) - require.Empty(t, msgOut) - - msgIn = message.QuickBatch([][]byte{doc3}) - msgOut, err = proc.ProcessBatch(context.Background(), msgIn) - require.NoError(t, err) - require.Len(t, msgOut, 1) - - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - proc, err = mgr.NewProcessor(conf) - require.NoError(t, err) - - msgIn = message.QuickBatch([][]byte{doc1, doc2, doc3}) - msgOut, err = proc.ProcessBatch(context.Background(), msgIn) - require.NoError(t, err) - require.Len(t, msgOut, 1) - assert.Equal(t, 2, msgOut[0].Len()) -} - -func TestDedupeBadCache(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -dedupe: - cache: foocache -`) - require.NoError(t, err) - - mgr := mock.NewManager() - _, err = mgr.NewProcessor(conf) - require.Error(t, err) -} - -func TestDedupeCacheErrors(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -dedupe: - cache: foocache - key: ${! content() } -`) - require.NoError(t, err) - - mgr := mock.NewManager() - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - proc, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - delete(mgr.Caches, "foocache") - - msgs, err := proc.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo"), []byte("bar")})) - require.NoError(t, err) - assert.Empty(t, msgs) - - conf, err = testutil.ProcessorFromYAML(` -dedupe: - cache: foocache - key: ${! content() } - drop_on_err: false -`) - require.NoError(t, err) - mgr.Caches["foocache"] = map[string]mock.CacheItem{} - - proc, err = mgr.NewProcessor(conf) - require.NoError(t, err) - - delete(mgr.Caches, "foocache") - - msgs, err = proc.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo"), []byte("bar")})) - require.NoError(t, err) - assert.Len(t, msgs, 1) -} diff --git a/internal/impl/pure/processor_for_each.go b/internal/impl/pure/processor_for_each.go deleted file mode 100644 index a704b0d268..0000000000 --- a/internal/impl/pure/processor_for_each.go +++ /dev/null @@ -1,90 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("for_each", service.NewConfigSpec(). - Stable(). - Categories("Composition"). - Summary("A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message."). - Description(` -This is useful for forcing batch wide processors such as `+"xref:components:processors/dedupe.adoc[`dedupe`]"+` or interpolations such as the `+"`value`"+` field of the `+"`metadata`"+` processor to execute on individual message parts of a batch instead. - -Please note that most processors already process per message of a batch, and this processor is not needed in those cases.`). - Field(service.NewProcessorListField("").Default([]any{})), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - mgr := interop.UnwrapManagement(res) - childPubProcs, err := conf.FieldProcessorList() - if err != nil { - return nil, err - } - - childProcs := make([]processor.V1, len(childPubProcs)) - for i, p := range childPubProcs { - childProcs[i] = interop.UnwrapOwnedProcessor(p) - } - - tp, err := newForEach(childProcs, mgr) - if err != nil { - return nil, err - } - - p := processor.NewAutoObservedBatchedProcessor("for_each", tp, mgr) - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -type forEachProc struct { - children []processor.V1 -} - -func newForEach(children []processor.V1, mgr bundle.NewManagement) (*forEachProc, error) { - return &forEachProc{children: children}, nil -} - -func (p *forEachProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - individualMsgs := make([]message.Batch, msg.Len()) - _ = msg.Iter(func(i int, p *message.Part) error { - individualMsgs[i] = message.Batch{p} - return nil - }) - - resMsg := message.QuickBatch(nil) - for _, tmpMsg := range individualMsgs { - resultMsgs, err := processor.ExecuteAll(ctx.Context(), p.children, tmpMsg) - if err != nil { - return nil, err - } - for _, m := range resultMsgs { - _ = m.Iter(func(i int, p *message.Part) error { - resMsg = append(resMsg, p) - return nil - }) - } - } - - if resMsg.Len() == 0 { - return nil, nil - } - return []message.Batch{resMsg}, nil -} - -func (p *forEachProc) Close(ctx context.Context) error { - for _, c := range p.children { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_for_each_test.go b/internal/impl/pure/processor_for_each_test.go deleted file mode 100644 index 76ac7d409a..0000000000 --- a/internal/impl/pure/processor_for_each_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package pure_test - -import ( - "context" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -//------------------------------------------------------------------------------ - -func TestForEachEmpty(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -for_each: [] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][]byte{ - []byte("foo bar baz"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(exp)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestForEachBasic(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -for_each: - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("Zm9vIGJhciBiYXo="), - []byte("MSAyIDMgNA=="), - []byte("aGVsbG8gZm9vIHdvcmxk"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestForEachFilterSome(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -for_each: - - bloblang: 'root = if !content().contains("foo") { deleted() }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("foo bar baz"), - []byte("hello foo world"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestForEachMultiProcs(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -for_each: - - bloblang: 'root = if !content().contains("foo") { deleted() }' - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("Zm9vIGJhciBiYXo="), - []byte("aGVsbG8gZm9vIHdvcmxk"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestForEachFilterAll(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -for_each: - - bloblang: 'root = if !content().contains("foo") { deleted() }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("bar baz"), - []byte("1 2 3 4"), - []byte("hello world"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - assert.NoError(t, res) - if len(msgs) != 0 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } -} diff --git a/internal/impl/pure/processor_grok.go b/internal/impl/pure/processor_grok.go deleted file mode 100644 index 4c64f2c90b..0000000000 --- a/internal/impl/pure/processor_grok.go +++ /dev/null @@ -1,246 +0,0 @@ -package pure - -import ( - "bufio" - "context" - "errors" - "fmt" - "strings" - - "github.com/Jeffail/gabs/v2" - "github.com/Jeffail/grok" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - gpFieldExpressions = "expressions" - gpFieldRemoveEmpty = "remove_empty_values" - gpFieldNamedOnly = "named_captures_only" - gpFieldUseDefaults = "use_default_patterns" - gpFieldPatternPaths = "pattern_paths" - gpFieldPatternDefinitions = "pattern_definitions" -) - -func grokProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Parsing"). - Stable(). - Summary("Parses messages into a structured format by attempting to apply a list of Grok expressions, the first expression to result in at least one value replaces the original message with a JSON object containing the values."). - Description(` -Type hints within patterns are respected, therefore with the pattern `+"`%\\{WORD:first},%{INT:second:int}`"+` and a payload of `+"`foo,1`"+` the resulting payload would be `+"`\\{\"first\":\"foo\",\"second\":1}`"+`. - -== Performance - -This processor currently uses the https://golang.org/s/re2syntax[Go RE2] regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information, see https://swtch.com/~rsc/regexp/regexp1.html.`). - Footnotes(` -== Default patterns - -For summary of the default patterns on offer, see https://github.com/Jeffail/grok/blob/master/patterns.go#L5.`). - Example("VPC Flow Logs", ` -Grok can be used to parse unstructured logs such as VPC flow logs that look like this: - -`+"```text"+` -2 123456789010 eni-1235b8ca123456789 172.31.16.139 172.31.16.21 20641 22 6 20 4249 1418530010 1418530070 ACCEPT OK -`+"```"+` - -Into structured objects that look like this: - -`+"```json"+` -{"accountid":"123456789010","action":"ACCEPT","bytes":4249,"dstaddr":"172.31.16.21","dstport":22,"end":1418530070,"interfaceid":"eni-1235b8ca123456789","logstatus":"OK","packets":20,"protocol":6,"srcaddr":"172.31.16.139","srcport":20641,"start":1418530010,"version":2} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - grok: - expressions: - - '%{VPCFLOWLOG}' - pattern_definitions: - VPCFLOWLOG: '%{NUMBER:version:int} %{NUMBER:accountid} %{NOTSPACE:interfaceid} %{NOTSPACE:srcaddr} %{NOTSPACE:dstaddr} %{NOTSPACE:srcport:int} %{NOTSPACE:dstport:int} %{NOTSPACE:protocol:int} %{NOTSPACE:packets:int} %{NOTSPACE:bytes:int} %{NUMBER:start:int} %{NUMBER:end:int} %{NOTSPACE:action} %{NOTSPACE:logstatus}' -`, - ). - Fields( - service.NewStringListField(gpFieldExpressions). - Description("One or more Grok expressions to attempt against incoming messages. The first expression to match at least one value will be used to form a result."), - service.NewStringMapField(gpFieldPatternDefinitions). - Description("A map of pattern definitions that can be referenced within `patterns`."). - Default(map[string]any{}), - service.NewStringListField(gpFieldPatternPaths). - Description("A list of paths to load Grok patterns from. This field supports wildcards, including super globs (double star)."). - Default([]any{}), - service.NewBoolField(gpFieldNamedOnly). - Description("Whether to only capture values from named patterns."). - Advanced(). - Default(true), - service.NewBoolField(gpFieldUseDefaults). - Description("Whether to use a <>."). - Advanced(). - Default(true), - service.NewBoolField(gpFieldRemoveEmpty). - Description("Whether to remove values that are empty from the resulting structure."). - Advanced(). - Default(true), - ) -} - -type grokProcConfig struct { - Expressions []string - RemoveEmpty bool - NamedOnly bool - UseDefaults bool - PatternPaths []string - PatternDefinitions map[string]string -} - -func init() { - err := service.RegisterBatchProcessor( - "grok", grokProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - var g grokProcConfig - var err error - - if g.Expressions, err = conf.FieldStringList(gpFieldExpressions); err != nil { - return nil, err - } - if g.PatternDefinitions, err = conf.FieldStringMap(gpFieldPatternDefinitions); err != nil { - return nil, err - } - if g.PatternPaths, err = conf.FieldStringList(gpFieldPatternPaths); err != nil { - return nil, err - } - - if g.RemoveEmpty, err = conf.FieldBool(gpFieldRemoveEmpty); err != nil { - return nil, err - } - if g.NamedOnly, err = conf.FieldBool(gpFieldNamedOnly); err != nil { - return nil, err - } - if g.UseDefaults, err = conf.FieldBool(gpFieldUseDefaults); err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newGrok(g, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("grok", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type grokProc struct { - gparsers []*grok.CompiledGrok - log log.Modular -} - -func newGrok(conf grokProcConfig, mgr bundle.NewManagement) (processor.AutoObserved, error) { - grokConf := grok.Config{ - RemoveEmptyValues: conf.RemoveEmpty, - NamedCapturesOnly: conf.NamedOnly, - SkipDefaultPatterns: !conf.UseDefaults, - Patterns: conf.PatternDefinitions, - } - - for _, path := range conf.PatternPaths { - if err := addGrokPatternsFromPath(mgr.FS(), path, grokConf.Patterns); err != nil { - return nil, fmt.Errorf("failed to parse patterns from path '%v': %v", path, err) - } - } - - gcompiler, err := grok.New(grokConf) - if err != nil { - return nil, fmt.Errorf("failed to create grok compiler: %v", err) - } - - var compiled []*grok.CompiledGrok - for _, pattern := range conf.Expressions { - var gcompiled *grok.CompiledGrok - if gcompiled, err = gcompiler.Compile(pattern); err != nil { - return nil, fmt.Errorf("failed to compile Grok pattern '%v': %v", pattern, err) - } - compiled = append(compiled, gcompiled) - } - - g := &grokProc{ - gparsers: compiled, - log: mgr.Logger(), - } - return g, nil -} - -func addGrokPatternsFromPath(fs ifs.FS, path string, patterns map[string]string) error { - if s, err := fs.Stat(path); err != nil { - return err - } else if s.IsDir() { - path += "/*" - } - - files, err := service.Globs(fs, path) - if err != nil { - return err - } - - for _, f := range files { - file, err := fs.Open(f) - if err != nil { - return err - } - - scanner := bufio.NewScanner(file) - - for scanner.Scan() { - l := scanner.Text() - if l != "" && l[0] != '#' { - names := strings.SplitN(l, " ", 2) - patterns[names[0]] = names[1] - } - } - - file.Close() - } - - return nil -} - -func (g *grokProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - body := msg.AsBytes() - - var values map[string]any - for _, compiler := range g.gparsers { - var err error - if values, err = compiler.ParseTyped(body); err != nil { - g.log.Debug("Failed to parse body: %v\n", err) - continue - } - if len(values) > 0 { - break - } - } - if len(values) == 0 { - g.log.Debug("No matches found for payload: %s\n", body) - return nil, errors.New("no pattern matches found") - } - - gObj := gabs.New() - for k, v := range values { - _, _ = gObj.SetP(v, k) - } - - msg.SetStructuredMut(gObj.Data()) - return []*message.Part{msg}, nil -} - -func (g *grokProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_grok_test.go b/internal/impl/pure/processor_grok_test.go deleted file mode 100644 index 80ef1c91f3..0000000000 --- a/internal/impl/pure/processor_grok_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package pure_test - -import ( - "context" - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestGrokAllParts(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -grok: - expressions: - - "%%{WORD:first},%%{INT:second:int}" -`) - require.NoError(t, err) - - gSet, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgIn := message.QuickBatch([][]byte{ - []byte(`foo,0`), - []byte(`foo,1`), - []byte(`foo,2`), - }) - msgs, res := gSet.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatal("Wrong count of messages") - } - if res != nil { - t.Fatal("Non-nil result") - } - - exp := [][]byte{ - []byte(`{"first":"foo","second":0}`), - []byte(`{"first":"foo","second":1}`), - []byte(`{"first":"foo","second":2}`), - } - act := message.GetAllBytes(msgs[0]) - if !reflect.DeepEqual(act, exp) { - t.Errorf("Wrong output from grok: %s != %s", act, exp) - } -} - -func TestGrok(t *testing.T) { - type gTest struct { - name string - pattern string - input string - output string - definitions map[string]any - } - - tests := []gTest{ - { - name: "Common apache parsing", - pattern: "%{COMMONAPACHELOG}", - input: `127.0.0.1 - - [23/Apr/2014:22:58:32 +0200] "GET /index.php HTTP/1.1" 404 207`, - output: `{"auth":"-","bytes":"207","clientip":"127.0.0.1","httpversion":"1.1","ident":"-","request":"/index.php","response":"404","timestamp":"23/Apr/2014:22:58:32 +0200","verb":"GET"}`, - }, - { - name: "Test pattern definitions", - definitions: map[string]any{ - "ACTION": "(pass|deny)", - }, - input: `pass connection from 127.0.0.1`, - pattern: "%{ACTION:action} connection from %{IPV4:ipv4}", - output: `{"action":"pass","ipv4":"127.0.0.1"}`, - }, - { - name: "Test dot path in name definition", - input: `foo 5 bazes from 192.0.1.11`, - pattern: "%{WORD:nested.name} %{INT:nested.value:int} bazes from %{IPV4:nested.ipv4}", - output: `{"nested":{"ipv4":"192.0.1.11","name":"foo","value":5}}`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.definitions == nil { - test.definitions = map[string]any{} - } - conf, err := testutil.ProcessorFromYAML(` -grok: - expressions: - - '%v' - pattern_definitions: %v -`, test.pattern, gabs.Wrap(test.definitions).String()) - require.NoError(t, err) - - gSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - inMsg := message.QuickBatch([][]byte{[]byte(test.input)}) - msgs, _ := gSet.ProcessBatch(context.Background(), inMsg) - require.Len(t, msgs, 1) - - assert.Equal(t, test.output, string(msgs[0].Get(0).AsBytes())) - }) - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.definitions == nil { - test.definitions = map[string]any{} - } - conf, err := testutil.ProcessorFromYAML(` -grok: - expressions: - - '%v' - pattern_definitions: %v -`, test.pattern, gabs.Wrap(test.definitions).String()) - require.NoError(t, err) - - gSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - inMsg := message.QuickBatch([][]byte{[]byte(test.input)}) - msgs, _ := gSet.ProcessBatch(context.Background(), inMsg) - require.Len(t, msgs, 1) - - assert.Equal(t, test.output, string(msgs[0].Get(0).AsBytes())) - }) - } -} - -func TestGrokFileImports(t *testing.T) { - tmpDir := t.TempDir() - - err := os.WriteFile(filepath.Join(tmpDir, "foos"), []byte(` -FOOFLAT %{WORD:first} %{WORD:second} %{WORD:third} -FOONESTED %{INT:nested.first:int} %{WORD:nested.second} %{WORD:nested.third} -`), 0o777) - require.NoError(t, err) - - conf, err := testutil.ProcessorFromYAML(` -grok: - expressions: - - "%%{FOONESTED}" - - "%%{FOOFLAT}" - pattern_paths: [ %v ] -`, tmpDir) - require.NoError(t, err) - - gSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - inMsg := message.QuickBatch([][]byte{[]byte(`hello foo bar`)}) - msgs, _ := gSet.ProcessBatch(context.Background(), inMsg) - require.Len(t, msgs, 1) - assert.Equal(t, `{"first":"hello","second":"foo","third":"bar"}`, string(msgs[0].Get(0).AsBytes())) - - inMsg = message.QuickBatch([][]byte{[]byte(`10 foo bar`)}) - msgs, _ = gSet.ProcessBatch(context.Background(), inMsg) - require.Len(t, msgs, 1) - assert.Equal(t, `{"nested":{"first":10,"second":"foo","third":"bar"}}`, string(msgs[0].Get(0).AsBytes())) -} diff --git a/internal/impl/pure/processor_group_by.go b/internal/impl/pure/processor_group_by.go deleted file mode 100644 index 704aa631d8..0000000000 --- a/internal/impl/pure/processor_group_by.go +++ /dev/null @@ -1,196 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "strconv" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - gbpFieldCheck = "check" - gbpFieldProcessors = "processors" -) - -func groupByProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`Splits a xref:configuration:batching.adoc[batch of messages] into N batches, where each resulting batch contains a group of messages determined by a xref:guides:bloblang/about.adoc[Bloblang query].`). - Description(` -Once the groups are established a list of processors are applied to their respective grouped batch, which can be used to label the batch as per their grouping. Messages that do not pass the check of any specified group are placed in their own group. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). - Example( - "Grouped Processing", - "Imagine we have a batch of messages that we wish to split into a group of foos and everything else, which should be sent to different output destinations based on those groupings. We also need to send the foos as a tar gzip archive. For this purpose we can use the `group_by` processor with a xref:components:outputs/switch.adoc[`switch`] output:", - ` -pipeline: - processors: - - group_by: - - check: content().contains("this is a foo") - processors: - - archive: - format: tar - - compress: - algorithm: gzip - - mapping: 'meta grouping = "foo"' - -output: - switch: - cases: - - check: meta("grouping") == "foo" - output: - gcp_pubsub: - project: foo_prod - topic: only_the_foos - - output: - gcp_pubsub: - project: somewhere_else - topic: no_foos_here -`, - ). - Field(service.NewObjectListField("", - service.NewBloblangField(gbpFieldCheck). - Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message belongs to a given group."). - Examples( - `this.type == "foo"`, - `this.contents.urls.contains("https://benthos.dev/")`, - `true`, - ), - service.NewProcessorListField(gbpFieldProcessors). - Description("A list of xref:components:processors/about.adoc[processors] to execute on the newly formed group."). - Default([]any{}), - )) -} - -func init() { - err := service.RegisterBatchProcessor( - "group_by", groupByProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - groupConfs, err := conf.FieldObjectList() - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p := &groupByProc{log: mgr.Logger()} - p.groups = make([]group, len(groupConfs)) - for i, c := range groupConfs { - if p.groups[i], err = groupFromParsed(c, mgr); err != nil { - return nil, fmt.Errorf("group '%v' parse error: %w", i, err) - } - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("group_by", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type group struct { - Check *mapping.Executor - Processors []processor.V1 -} - -func groupFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (g group, err error) { - var checkStr string - if checkStr, err = conf.FieldString(gbpFieldCheck); err != nil { - return - } - if g.Check, err = mgr.BloblEnvironment().NewMapping(checkStr); err != nil { - return - } - - var iProcs []*service.OwnedProcessor - if iProcs, err = conf.FieldProcessorList(gbpFieldProcessors); err != nil { - return - } - - g.Processors = make([]processor.V1, len(iProcs)) - for i, c := range iProcs { - g.Processors[i] = interop.UnwrapOwnedProcessor(c) - } - return -} - -type groupByProc struct { - log log.Modular - groups []group -} - -func (g *groupByProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - if msg.Len() == 0 { - return nil, nil - } - - groups := make([]message.Batch, len(g.groups)) - for i := range groups { - groups[i] = message.QuickBatch(nil) - } - groupless := message.QuickBatch(nil) - - _ = msg.Iter(func(i int, p *message.Part) error { - for j, group := range g.groups { - res, err := group.Check.QueryPart(i, msg) - if err != nil { - res = false - g.log.Error("Failed to test group %v: %v\n", j, err) - } - if res { - groupStr := strconv.Itoa(j) - ctx.Span(i).LogKV("event", "grouped", "type", groupStr) - ctx.Span(i).SetTag("group", groupStr) - groups[j] = append(groups[j], p) - return nil - } - } - - ctx.Span(i).LogKV("event", "grouped", "type", "default") - ctx.Span(i).SetTag("group", "default") - groupless = append(groupless, p) - return nil - }) - - msgs := []message.Batch{} - for i, gmsg := range groups { - if gmsg.Len() == 0 { - continue - } - - resultMsgs, err := processor.ExecuteAll(ctx.Context(), g.groups[i].Processors, gmsg) - if err != nil { - return nil, err - } - if len(resultMsgs) > 0 { - msgs = append(msgs, resultMsgs...) - } - } - if groupless.Len() > 0 { - msgs = append(msgs, groupless) - } - - if len(msgs) == 0 { - return nil, nil - } - return msgs, nil -} - -func (g *groupByProc) Close(ctx context.Context) error { - for _, group := range g.groups { - for _, p := range group.Processors { - if err := p.Close(ctx); err != nil { - return err - } - } - } - return nil -} diff --git a/internal/impl/pure/processor_group_by_test.go b/internal/impl/pure/processor_group_by_test.go deleted file mode 100644 index 9a98faacab..0000000000 --- a/internal/impl/pure/processor_group_by_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestGroupBy(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -group_by: - - check: 'content().contains("foo")' - processors: - - archive: - format: lines - - check: 'content().contains("bar")' - processors: - - bloblang: 'root = content().uppercase()' - - bloblang: 'root = content().trim()' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - exp := [][][]byte{ - { - []byte(` hello foo world 1 - hello foo bar world 1 - hello foo world 2 - hello foo bar world 2 `), - }, - { - []byte(`HELLO BAR WORLD 1`), - []byte(`HELLO BAR WORLD 2`), - }, - { - []byte(` hello world 1 `), - []byte(` hello world 2 `), - }, - } - act := [][][]byte{} - - input := message.QuickBatch([][]byte{ - []byte(` hello foo world 1 `), - []byte(` hello world 1 `), - []byte(` hello foo bar world 1 `), - []byte(` hello bar world 1 `), - []byte(` hello foo world 2 `), - []byte(` hello world 2 `), - []byte(` hello foo bar world 2 `), - []byte(` hello bar world 2 `), - }) - msgs, res := proc.ProcessBatch(context.Background(), input) - require.NoError(t, res) - - for _, msg := range msgs { - act = append(act, message.GetAllBytes(msg)) - } - assert.Equal(t, exp, act) -} - -func TestGroupByErrs(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -group_by: - - processors: - - archive: - format: lines -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - require.Error(t, err) - require.Contains(t, err.Error(), "field 'check' is required") -} diff --git a/internal/impl/pure/processor_group_by_value.go b/internal/impl/pure/processor_group_by_value.go deleted file mode 100644 index 6041bdae62..0000000000 --- a/internal/impl/pure/processor_group_by_value.go +++ /dev/null @@ -1,126 +0,0 @@ -package pure - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - gbvpFieldValue = "value" -) - -func init() { - err := service.RegisterBatchProcessor( - "group_by_value", service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`Splits a batch of messages into N batches, where each resulting batch contains a group of messages determined by a xref:configuration:interpolation.adoc#bloblang-queries[function interpolated string] evaluated per message.`). - Description(` -This allows you to group messages using arbitrary fields within their content or metadata, process them individually, and send them to unique locations as per their group. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). - Footnotes(` -== Examples - -If we were consuming Kafka messages and needed to group them by their key, archive the groups, and send them to S3 with the key as part of the path we could achieve that with the following: - -`+"```yaml"+` -pipeline: - processors: - - group_by_value: - value: ${! meta("kafka_key") } - - archive: - format: tar - - compress: - algorithm: gzip -output: - aws_s3: - bucket: TODO - path: docs/${! meta("kafka_key") }/${! count("files") }-${! timestamp_unix_nano() }.tar.gz -`+"```"+``). - Field(service.NewInterpolatedStringField(gbvpFieldValue). - Description("The interpolated string to group based on."). - Examples("${! meta(\"kafka_key\") }", "${! json(\"foo.bar\") }-${! meta(\"baz\") }")), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - valueStr, err := conf.FieldString(gbvpFieldValue) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newGroupByValue(valueStr, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("group_by_value", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type groupByValueProc struct { - log log.Modular - value *field.Expression -} - -func newGroupByValue(valueStr string, mgr bundle.NewManagement) (processor.AutoObservedBatched, error) { - value, err := mgr.BloblEnvironment().NewField(valueStr) - if err != nil { - return nil, fmt.Errorf("failed to parse value expression: %v", err) - } - return &groupByValueProc{ - log: mgr.Logger(), - value: value, - }, nil -} - -func (g *groupByValueProc) ProcessBatch(ctx *processor.BatchProcContext, batch message.Batch) ([]message.Batch, error) { - if batch.Len() == 0 { - return nil, nil - } - - groupKeys := []string{} - groupMap := map[string]message.Batch{} - - _ = batch.Iter(func(i int, p *message.Part) error { - v, err := g.value.String(i, batch) - if err != nil { - g.log.Error("Group value interpolation error: %v", err) - err = fmt.Errorf("group value interpolation error: %w", err) - ctx.OnError(err, i, p) - } - - ctx.Span(i).LogKV("event", "grouped", "type", v) - ctx.Span(i).SetTag("group", v) - if group, exists := groupMap[v]; exists { - groupMap[v] = append(group, p) - } else { - g.log.Trace("New group formed: %v\n", v) - groupKeys = append(groupKeys, v) - groupMap[v] = message.Batch{p} - } - return nil - }) - - msgs := []message.Batch{} - for _, key := range groupKeys { - msgs = append(msgs, groupMap[key]) - } - if len(msgs) == 0 { - return nil, nil - } - return msgs, nil -} - -func (g *groupByValueProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_group_by_value_test.go b/internal/impl/pure/processor_group_by_value_test.go deleted file mode 100644 index 5fdc158c3e..0000000000 --- a/internal/impl/pure/processor_group_by_value_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package pure_test - -import ( - "context" - "reflect" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestGroupByValueBasic(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -group_by_value: - value: ${!json("foo")} -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][][]byte{ - { - []byte(`{"foo":0,"bar":0}`), - []byte(`{"foo":0,"bar":7}`), - }, - { - []byte(`{"foo":3,"bar":1}`), - }, - { - []byte(`{"bar":2}`), - }, - { - []byte(`{"foo":2,"bar":3}`), - }, - { - []byte(`{"foo":4,"bar":4}`), - }, - { - []byte(`{"foo":1,"bar":5}`), - []byte(`{"foo":1,"bar":6}`), - []byte(`{"foo":1,"bar":8}`), - }, - } - act := [][][]byte{} - - input := message.QuickBatch([][]byte{ - []byte(`{"foo":0,"bar":0}`), - []byte(`{"foo":3,"bar":1}`), - []byte(`{"bar":2}`), - []byte(`{"foo":2,"bar":3}`), - []byte(`{"foo":4,"bar":4}`), - []byte(`{"foo":1,"bar":5}`), - []byte(`{"foo":1,"bar":6}`), - []byte(`{"foo":0,"bar":7}`), - []byte(`{"foo":1,"bar":8}`), - }) - msgs, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - - for _, msg := range msgs { - act = append(act, message.GetAllBytes(msg)) - } - if !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } -} diff --git a/internal/impl/pure/processor_insert_part.go b/internal/impl/pure/processor_insert_part.go deleted file mode 100644 index 61b9d6bc23..0000000000 --- a/internal/impl/pure/processor_insert_part.go +++ /dev/null @@ -1,123 +0,0 @@ -package pure - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - ippFieldIndex = "index" - ippFieldContent = "content" -) - -func insertPartSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary("Insert a new message into a batch at an index. If the specified index is greater than the length of the existing batch it will be appended to the end."). - Description(` -The index can be negative, and if so the message will be inserted from the end counting backwards starting from -1. E.g. if index = -1 then the new message will become the last of the batch, if index = -2 then the new message will be inserted before the last message, and so on. If the negative index is greater than the length of the existing batch it will be inserted at the beginning. - -The new message will have metadata copied from the first pre-existing message of the batch. - -This processor will interpolate functions within the 'content' field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here].`). - Fields( - service.NewIntField(ippFieldIndex). - Description("The index within the batch to insert the message at."). - Default(-1), - service.NewInterpolatedStringField(ippFieldContent). - Description("The content of the message being inserted."). - Default(""), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "insert_part", insertPartSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - index, err := conf.FieldInt(ippFieldIndex) - if err != nil { - return nil, err - } - - contentStr, err := conf.FieldString(ippFieldContent) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newInsertPart(index, contentStr, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("insert_part", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type insertPart struct { - index int - part *field.Expression - log log.Modular -} - -func newInsertPart(index int, contentStr string, mgr bundle.NewManagement) (processor.AutoObservedBatched, error) { - part, err := mgr.BloblEnvironment().NewField(contentStr) - if err != nil { - return nil, fmt.Errorf("failed to parse content expression: %v", err) - } - return &insertPart{ - part: part, - index: index, - log: mgr.Logger(), - }, nil -} - -func (p *insertPart) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - newPartBytes, err := p.part.Bytes(0, msg) - if err != nil { - p.log.Error("Content interpolation error: %v", err) - return []message.Batch{msg}, nil - } - - index := p.index - msgLen := msg.Len() - if index < 0 { - index = msgLen + index + 1 - if index < 0 { - index = 0 - } - } else if index > msgLen { - index = msgLen - } - - newMsg := message.QuickBatch(nil) - newPart := msg.Get(0).ShallowCopy() - newPart.SetBytes(newPartBytes) - _ = msg.Iter(func(i int, p *message.Part) error { - if i == index { - newMsg = append(newMsg, newPart) - } - newMsg = append(newMsg, p.ShallowCopy()) - return nil - }) - if index == msg.Len() { - newMsg = append(newMsg, newPart) - } - - return []message.Batch{newMsg}, nil -} - -func (p *insertPart) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_insert_part_test.go b/internal/impl/pure/processor_insert_part_test.go deleted file mode 100644 index 71a158e60d..0000000000 --- a/internal/impl/pure/processor_insert_part_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "os" - "reflect" - "strconv" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestInsertBoundaries(t *testing.T) { - for i := 0; i < 10; i++ { - for j := -5; j <= 5; j++ { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -insert_part: - content: hello world - index: %v -`, j)) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - var parts [][]byte - for k := 0; k < i; k++ { - parts = append(parts, []byte("foo")) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if len(msgs) != 1 { - t.Error("Insert Part failed") - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if exp, act := i+1, len(message.GetAllBytes(msgs[0])); exp != act { - t.Errorf("Wrong count of result parts: %v != %v", act, exp) - } - } - } -} - -func TestInsertPart(t *testing.T) { - type test struct { - index int - in [][]byte - out [][]byte - } - - tests := []test{ - { - index: 0, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("hello world"), - []byte("0"), - []byte("1"), - }, - }, - { - index: 0, - in: [][]byte{}, - out: [][]byte{ - []byte("hello world"), - }, - }, - { - index: 1, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("0"), - []byte("hello world"), - []byte("1"), - }, - }, - { - index: 2, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("0"), - []byte("1"), - []byte("hello world"), - }, - }, - { - index: 3, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("0"), - []byte("1"), - []byte("hello world"), - }, - }, - { - index: -1, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("0"), - []byte("1"), - []byte("hello world"), - }, - }, - { - index: -2, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("0"), - []byte("hello world"), - []byte("1"), - }, - }, - { - index: -3, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("hello world"), - []byte("0"), - []byte("1"), - }, - }, - { - index: -4, - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("hello world"), - []byte("0"), - []byte("1"), - }, - }, - } - - for _, test := range tests { - conf, err := testutil.ProcessorFromYAML(` -insert_part: - content: hello world - index: ` + strconv.Itoa(test.index) + ` -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(test.in)) - if len(msgs) != 1 { - t.Errorf("Insert Part failed on: %s", test.in) - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if exp, act := test.out, message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output for %s at index %v: %s != %s", test.in, test.index, act, exp) - } - } -} - -func TestInsertPartInterpolation(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -insert_part: - content: 'hello ${!hostname()} world' -`) - require.NoError(t, err) - - hostname, _ := os.Hostname() - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - type test struct { - in [][]byte - out [][]byte - } - - tests := []test{ - { - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("0"), - []byte("1"), - []byte(fmt.Sprintf("hello %v world", hostname)), - }, - }, - { - in: [][]byte{}, - out: [][]byte{ - []byte(fmt.Sprintf("hello %v world", hostname)), - }, - }, - } - - for _, test := range tests { - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(test.in)) - if len(msgs) != 1 { - t.Errorf("Insert Part failed on: %s", test.in) - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if exp, act := test.out, message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output for %s: %s != %s", test.in, act, exp) - } - } -} diff --git a/internal/impl/pure/processor_jmespath.go b/internal/impl/pure/processor_jmespath.go deleted file mode 100644 index 5e54916a2e..0000000000 --- a/internal/impl/pure/processor_jmespath.go +++ /dev/null @@ -1,163 +0,0 @@ -package pure - -import ( - "context" - "encoding/json" - "fmt" - - jmespath "github.com/jmespath/go-jmespath" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - jmpFieldQuery = "query" -) - -func jmpProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Mapping"). - Stable(). - Summary("Executes a http://jmespath.org/[JMESPath query] on JSON documents and replaces the message with the resulting document."). - Description(` -[TIP] -.Try out Bloblang -==== -For better performance and improved capabilities try native Benthos mapping with the xref:components:processors/mapping.adoc[`+"`mapping`"+` processor]. -==== -`). - Example("Mapping", ` -When receiving JSON documents of the form: - -`+"```json"+` -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -`+"```"+` - -We could collapse the location names from the state of Washington into a field `+"`Cities`"+`: - -`+"```json"+` -{"Cities": "Bellevue, Olympia, Seattle"} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - jmespath: - query: "locations[?state == 'WA'].name | sort(@) | {Cities: join(', ', @)}" -`, - ). - Field(service.NewStringField(jmpFieldQuery). - Description("The JMESPath query to apply to messages.")) -} - -func init() { - err := service.RegisterBatchProcessor( - "jmespath", jmpProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - queryStr, err := conf.FieldString(jmpFieldQuery) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newJMESPath(queryStr, mgr) - if err != nil { - return nil, err - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("jmespath", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type jmespathProc struct { - query *jmespath.JMESPath - log log.Modular -} - -func newJMESPath(queryStr string, mgr bundle.NewManagement) (processor.AutoObserved, error) { - query, err := jmespath.Compile(queryStr) - if err != nil { - return nil, fmt.Errorf("failed to compile JMESPath query: %v", err) - } - j := &jmespathProc{ - query: query, - log: mgr.Logger(), - } - return j, nil -} - -func safeSearch(part any, j *jmespath.JMESPath) (res any, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("jmespath panic: %v", r) - } - }() - return j.Search(part) -} - -// JMESPath doesn't like json.Number so we walk the tree and replace them. -func clearNumbers(v any) (any, bool) { - switch t := v.(type) { - case map[string]any: - for k, v := range t { - if nv, ok := clearNumbers(v); ok { - t[k] = nv - } - } - case []any: - for i, v := range t { - if nv, ok := clearNumbers(v); ok { - t[i] = nv - } - } - case json.Number: - f, err := t.Float64() - if err != nil { - if i, err := t.Int64(); err == nil { - return i, true - } - } - return f, true - } - return nil, false -} - -func (p *jmespathProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - jsonPart, err := msg.AsStructuredMut() - if err != nil { - p.log.Debug("Failed to parse part into json: %v\n", err) - return nil, err - } - if v, replace := clearNumbers(jsonPart); replace { - jsonPart = v - } - - var result any - if result, err = safeSearch(jsonPart, p.query); err != nil { - p.log.Debug("Failed to search json: %v\n", err) - return nil, err - } - - msg.SetStructuredMut(result) - return []*message.Part{msg}, nil -} - -func (p *jmespathProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_jmespath_test.go b/internal/impl/pure/processor_jmespath_test.go deleted file mode 100644 index a05e747247..0000000000 --- a/internal/impl/pure/processor_jmespath_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package pure_test - -import ( - "context" - "strconv" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestJMESPathAllParts(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jmespath: - query: foo.bar -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgIn := message.QuickBatch([][]byte{ - []byte(`{"foo":{"bar":0}}`), - []byte(`{"foo":{"bar":1}}`), - []byte(`{"foo":{"bar":2}}`), - }) - msgs, res := jSet.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatal("Wrong count of messages") - } - if res != nil { - t.Fatal("Non-nil result") - } - for i, part := range message.GetAllBytes(msgs[0]) { - if exp, act := strconv.Itoa(i), string(part); exp != act { - t.Errorf("Wrong output from json: %v != %v", act, exp) - } - } -} - -func TestJMESPathValidation(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jmespath: - query: foo.bar -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgIn := message.QuickBatch([][]byte{[]byte("this is bad json")}) - msgs, res := jSet.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatal("No passthrough for bad input data") - } - if res != nil { - t.Fatal("Non-nil result") - } - if exp, act := "this is bad json", string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Wrong output from bad json: %v != %v", act, exp) - } -} - -func TestJMESPathMutation(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jmespath: - query: "{foo: merge(foo, {bar:'baz'})}" -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - ogObj := gabs.New() - _, _ = ogObj.Set("is this", "foo", "original", "content") - ogExp := ogObj.String() - - msgIn := message.QuickBatch(make([][]byte, 1)) - msgIn.Get(0).SetStructured(ogObj.Data()) - msgs, res := jSet.ProcessBatch(context.Background(), msgIn) - if len(msgs) != 1 { - t.Fatal("No passthrough for bad input data") - } - if res != nil { - t.Fatal("Non-nil result") - } - if exp, act := `{"foo":{"bar":"baz","original":{"content":"is this"}}}`, string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Wrong output: %v != %v", act, exp) - } - - if exp, act := ogExp, ogObj.String(); exp != act { - t.Errorf("Original contents were mutated: %v != %v", act, exp) - } -} - -func TestJMESPath(t *testing.T) { - type jTest struct { - name string - path string - input string - output string - } - - tests := []jTest{ - { - name: "select obj", - path: "foo.bar", - input: `{"foo":{"bar":{"baz":1}}}`, - output: `{"baz":1}`, - }, - { - name: "select array", - path: "foo.bar", - input: `{"foo":{"bar":["baz","qux"]}}`, - output: `["baz","qux"]`, - }, - { - name: "select obj as str", - path: "foo.bar", - input: `{"foo":{"bar":"{\"baz\":1}"}}`, - output: `"{\"baz\":1}"`, - }, - { - name: "select str", - path: "foo.bar", - input: `{"foo":{"bar":"hello world"}}`, - output: `"hello world"`, - }, - { - name: "select float", - path: "foo.bar", - input: `{"foo":{"bar":0.123}}`, - output: `0.123`, - }, - { - name: "select int", - path: "foo.bar", - input: `{"foo":{"bar":123}}`, - output: `123`, - }, - { - name: "select bool", - path: "foo.bar", - input: `{"foo":{"bar":true}}`, - output: `true`, - }, - { - name: "addition int", - path: "sum([foo.bar, `6`])", - input: `{"foo":{"bar":123}}`, - output: `129`, - }, - } - - for _, test := range tests { - conf, err := testutil.ProcessorFromYAML(` -jmespath: - query: "` + test.path + `" -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatalf("Error for test '%v': %v", test.name, err) - } - - inMsg := message.QuickBatch( - [][]byte{ - []byte(test.input), - }, - ) - msgs, _ := jSet.ProcessBatch(context.Background(), inMsg) - if len(msgs) != 1 { - t.Fatalf("Test '%v' did not succeed", test.name) - } - - if exp, act := test.output, string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Wrong result '%v': %v != %v", test.name, act, exp) - } - } -} diff --git a/internal/impl/pure/processor_jq.go b/internal/impl/pure/processor_jq.go deleted file mode 100644 index 6467676422..0000000000 --- a/internal/impl/pure/processor_jq.go +++ /dev/null @@ -1,264 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - - "github.com/itchyny/gojq" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - jqpFieldQuery = "query" - jqpFieldRaw = "raw" - jqpFieldOutputRaw = "output_raw" -) - -func jqProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Mapping"). - Stable(). - Summary("Transforms and filters messages using jq queries."). - Description(` -[TIP] -.Try out Bloblang -==== -For better performance and improved capabilities try out native Benthos mapping with the xref:components:processors/mapping.adoc[`+"`mapping`"+` processor]. -==== - -The provided query is executed on each message, targeting either the contents as a structured JSON value or as a raw string using the field `+"`raw`"+`, and the message is replaced with the query result. - -Message metadata is also accessible within the query from the variable `+"`$metadata`"+`. - -This processor uses the https://github.com/itchyny/gojq[gojq library], and therefore does not require jq to be installed as a dependency. However, this also means there are some https://github.com/itchyny/gojq#difference-to-jq[differences in how these queries are executed] versus the jq cli. - -If the query does not emit any value then the message is filtered, if the query returns multiple values then the resulting message will be an array containing all values. - -The full query syntax is described in https://stedolan.github.io/jq/manual/[jq's documentation]. - -== Error handling - -Queries can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns].`). - Example("Mapping", ` -When receiving JSON documents of the form: - -`+"```json"+` -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -`+"```"+` - -We could collapse the location names from the state of Washington into a field `+"`Cities`"+`: - -`+"```json"+` -{"Cities": "Bellevue, Olympia, Seattle"} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - jq: - query: '{Cities: .locations | map(select(.state == "WA").name) | sort | join(", ") }' -`, - ). - Fields( - service.NewStringField(jqpFieldQuery). - Description("The jq query to filter and transform messages with."), - service.NewBoolField(jqpFieldRaw). - Description("Whether to process the input as a raw string instead of as JSON."). - Advanced(). - Default(false), - service.NewBoolField(jqpFieldOutputRaw). - Description("Whether to output raw text (unquoted) instead of JSON strings when the emitted values are string types."). - Advanced(). - Default(false), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "jq", jqProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - query, err := conf.FieldString(jqpFieldQuery) - if err != nil { - return nil, err - } - raw, err := conf.FieldBool(jqpFieldRaw) - if err != nil { - return nil, err - } - outputRaw, err := conf.FieldBool(jqpFieldOutputRaw) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - - p, err := newJQ(query, raw, outputRaw, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("jq", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -var jqCompileOptions = []gojq.CompilerOption{ - gojq.WithVariables([]string{"$metadata"}), -} - -type jqProc struct { - inRaw bool - outRaw bool - log log.Modular - code *gojq.Code -} - -func newJQ(queryStr string, raw, outputRaw bool, mgr bundle.NewManagement) (*jqProc, error) { - j := &jqProc{ - inRaw: raw, - outRaw: outputRaw, - log: mgr.Logger(), - } - - query, err := gojq.Parse(queryStr) - if err != nil { - return nil, fmt.Errorf("error parsing jq query: %w", err) - } - - j.code, err = gojq.Compile(query, jqCompileOptions...) - if err != nil { - return nil, fmt.Errorf("error compiling jq query: %w", err) - } - - return j, nil -} - -func (j *jqProc) getPartMetadata(part *message.Part) map[string]any { - metadata := map[string]any{} - _ = part.MetaIterMut(func(k string, v any) error { - metadata[k] = v - return nil - }) - return metadata -} - -func (j *jqProc) getPartValue(part *message.Part, raw bool) (obj any, err error) { - if raw { - return string(part.AsBytes()), nil - } - if obj, err = part.AsStructured(); err != nil { - j.log.Debug("Failed to parse part into json: %v\n", err) - return nil, err - } - return obj, nil -} - -func safeQuery(input any, meta map[string]any, c *gojq.Code) (emitted []any, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("jq panic: %v", r) - } - }() - - iter := c.Run(input, meta) - for { - out, ok := iter.Next() - if !ok { - break - } - if err, ok = out.(error); ok { - _ = err.Error() // This can panic :( - return - } - emitted = append(emitted, out) - } - return -} - -func (j *jqProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - in, err := j.getPartValue(msg, j.inRaw) - if err != nil { - return nil, err - } - metadata := j.getPartMetadata(msg) - - emitted, err := safeQuery(in, metadata, j.code) - if err != nil { - j.log.Debug(err.Error()) - return nil, err - } - - if j.outRaw { - raw, err := j.marshalRaw(emitted) - if err != nil { - j.log.Debug("Failed to marshal raw text: %s", err) - return nil, err - } - - // Sometimes the query result is an empty string. Example: - // echo '{ "foo": "" }' | jq .foo - // In that case we want pass on the empty string instead of treating it as - // an empty message and dropping it - if len(raw) == 0 && len(emitted) == 0 { - return nil, nil - } - - msg.SetBytes(raw) - return []*message.Part{msg}, nil - } else if len(emitted) > 1 { - msg.SetStructuredMut(emitted) - } else if len(emitted) == 1 { - msg.SetStructuredMut(emitted[0]) - } else { - return nil, nil - } - return []*message.Part{msg}, nil -} - -func (*jqProc) Close(ctx context.Context) error { - return nil -} - -func (j *jqProc) marshalRaw(values []any) ([]byte, error) { - buf := bytes.NewBufferString("") - - for index, el := range values { - var rawResult []byte - - val, isString := el.(string) - if isString { - rawResult = []byte(val) - } else { - marshalled, err := json.Marshal(el) - if err != nil { - return nil, fmt.Errorf("failed marshal JQ result at index %d: %w", index, err) - } - - rawResult = marshalled - } - - if _, err := buf.Write(rawResult); err != nil { - return nil, fmt.Errorf("failed to write JQ result at index %d: %w", index, err) - } - } - - bs := buf.Bytes() - return bs, nil -} diff --git a/internal/impl/pure/processor_jq_test.go b/internal/impl/pure/processor_jq_test.go deleted file mode 100644 index 841e27cc72..0000000000 --- a/internal/impl/pure/processor_jq_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package pure_test - -import ( - "context" - "strconv" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestJQAllParts(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jq: - query: .foo.bar -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - msgIn := message.QuickBatch([][]byte{ - []byte(`{"foo":{"bar":0}}`), - []byte(`{"foo":{"bar":1}}`), - []byte(`{"foo":{"bar":2}}`), - }) - msgs, res := jSet.ProcessBatch(context.Background(), msgIn) - require.NoError(t, res) - require.Len(t, msgs, 1) - for i, part := range message.GetAllBytes(msgs[0]) { - assert.Equal(t, strconv.Itoa(i), string(part)) - } -} - -func TestJQValidation(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jq: - query: .foo.bar -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - msgIn := message.QuickBatch([][]byte{[]byte("this is bad json")}) - msgs, res := jSet.ProcessBatch(context.Background(), msgIn) - - require.NoError(t, res) - require.Len(t, msgs, 1) - - assert.Equal(t, "this is bad json", string(message.GetAllBytes(msgs[0])[0])) -} - -func TestJQMutation(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jq: - query: '{foo: .foo} | .foo.bar = "baz"' -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - ogObj := gabs.New() - _, _ = ogObj.Set("is this", "foo", "original", "content") - _, _ = ogObj.Set("remove this", "bar") - ogExp := ogObj.String() - - msgIn := message.QuickBatch(make([][]byte, 1)) - msgIn.Get(0).SetStructured(ogObj.Data()) - msgs, res := jSet.ProcessBatch(context.Background(), msgIn) - require.NoError(t, res) - require.Len(t, msgs, 1) - - assert.Equal(t, `{"foo":{"bar":"baz","original":{"content":"is this"}}}`, string(message.GetAllBytes(msgs[0])[0])) - assert.Equal(t, ogExp, ogObj.String()) -} - -func TestJQ(t *testing.T) { - type jTest struct { - name string - path string - inputStr string - input any - output string - err string - } - - tests := []jTest{ - { - name: "select obj", - path: ".foo.bar", - inputStr: `{"foo":{"bar":{"baz":1}}}`, - output: `{"baz":1}`, - }, - { - name: "select array", - path: ".foo.bar", - inputStr: `{"foo":{"bar":["baz","qux"]}}`, - output: `["baz","qux"]`, - }, - { - name: "select obj as str", - path: ".foo.bar", - inputStr: `{"foo":{"bar":"{\"baz\":1}"}}`, - output: `"{\"baz\":1}"`, - }, - { - name: "select str", - path: ".foo.bar", - inputStr: `{"foo":{"bar":"hello world"}}`, - output: `"hello world"`, - }, - { - name: "select float", - path: ".foo.bar", - inputStr: `{"foo":{"bar":0.123}}`, - output: `0.123`, - }, - { - name: "select int", - path: ".foo.bar", - inputStr: `{"foo":{"bar":123}}`, - output: `123`, - }, - { - name: "select bool", - path: ".foo.bar", - inputStr: `{"foo":{"bar":true}}`, - output: `true`, - }, - { - name: "null result", - path: ".baz.qux", - inputStr: `{"foo":{"bar":true}}`, - output: `null`, - }, - { - name: "empty string", - path: ".foo.bar", - inputStr: `{"foo":{"bar":""}}`, - output: `""`, - }, - { - name: "convert to csv", - path: "[.ts,.id,.msg] | @csv", - inputStr: `{"id":"1054fe28","msg":"sample \"log\"","ts":1641393111}`, - output: `"1641393111,\"1054fe28\",\"sample \"\"log\"\"\""`, - }, - { - name: "invalid type", - path: ".ts | length", - input: map[string]any{ - "ts": time.Unix(1000, 0), - }, - err: "invalid type: time.Time", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jq: - query: '` + test.path + `' -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - var inMsg message.Batch - if test.inputStr != "" { - inMsg = append(inMsg, message.NewPart([]byte(test.inputStr))) - } else { - part := message.NewPart(nil) - part.SetStructuredMut(test.input) - inMsg = append(inMsg, part) - } - - msgs, _ := jSet.ProcessBatch(context.Background(), inMsg) - require.Len(t, msgs, 1) - if test.err == "" { - assert.Equal(t, test.output, string(message.GetAllBytes(msgs[0])[0])) - } else { - assert.Error(t, msgs[0][0].ErrorGet()) - assert.Contains(t, msgs[0][0].ErrorGet().Error(), test.err) - } - }) - } -} - -func TestJQ_OutputRaw(t *testing.T) { - type jTest struct { - name string - path string - input string - output string - } - - tests := []jTest{ - { - name: "select obj", - path: ".foo.bar", - input: `{"foo":{"bar":{"baz":1}}}`, - output: `{"baz":1}`, - }, - { - name: "select array", - path: ".foo.bar", - input: `{"foo":{"bar":["baz","qux"]}}`, - output: `["baz","qux"]`, - }, - { - name: "select obj as str", - path: ".foo.bar", - input: `{"foo":{"bar":"{\"baz\":1}"}}`, - output: `{"baz":1}`, - }, - { - name: "select str", - path: ".foo.bar", - input: `{"foo":{"bar":"hello world"}}`, - output: `hello world`, - }, - { - name: "select float", - path: ".foo.bar", - input: `{"foo":{"bar":0.123}}`, - output: `0.123`, - }, - { - name: "select int", - path: ".foo.bar", - input: `{"foo":{"bar":123}}`, - output: `123`, - }, - { - name: "select bool", - path: ".foo.bar", - input: `{"foo":{"bar":true}}`, - output: `true`, - }, - { - name: "null result", - path: ".baz.qux", - input: `{"foo":{"bar":true}}`, - output: `null`, - }, - { - name: "empty string", - path: ".foo.bar", - input: `{"foo":{"bar":""}}`, - output: ``, - }, - { - name: "convert to csv", - path: "[.ts,.id,.msg] | @csv", - input: `{"id":"1054fe28","msg":"sample \"log\"","ts":1641393111}`, - output: `1641393111,"1054fe28","sample ""log"""`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -jq: - query: '` + test.path + `' - output_raw: true -`) - require.NoError(t, err) - - jSet, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - inMsg := message.QuickBatch( - [][]byte{ - []byte(test.input), - }, - ) - msgs, _ := jSet.ProcessBatch(context.Background(), inMsg) - require.Len(t, msgs, 1) - assert.Equal(t, test.output, string(message.GetAllBytes(msgs[0])[0])) - }) - } -} diff --git a/internal/impl/pure/processor_jsonschema.go b/internal/impl/pure/processor_jsonschema.go deleted file mode 100644 index 336db92a77..0000000000 --- a/internal/impl/pure/processor_jsonschema.go +++ /dev/null @@ -1,184 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" - - jsonschema "github.com/xeipuuv/gojsonschema" -) - -const ( - jschemaPFieldSchemaPath = "schema_path" - jschemaPFieldSchema = "schema" -) - -func jschemaProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Mapping"). - Stable(). - Summary(`Checks messages against a provided JSONSchema definition but does not change the payload under any circumstances. If a message does not match the schema it can be caught using xref:configuration:error_handling.adoc[error handling methods].`). - Description(`Please refer to the https://json-schema.org/[JSON Schema website] for information and tutorials regarding the syntax of the schema.`). - Footnotes(` -== Examples - -With the following JSONSchema document: - -`+"```json"+` -{ - "$id": "https://example.com/person.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -} -`+"```"+` - -And the following Benthos configuration: - -`+"```yaml"+` -pipeline: - processors: - - json_schema: - schema_path: "file://path_to_schema.json" - - catch: - - log: - level: ERROR - message: "Schema validation failed due to: ${!error()}" - - mapping: 'root = deleted()' # Drop messages that fail -`+"```"+` - -If a payload being processed looked like: - -`+"```json"+` -{"firstName":"John","lastName":"Doe","age":-21} -`+"```"+` - -Then a log message would appear explaining the fault and the payload would be -dropped.`). - Fields( - service.NewStringField(jschemaPFieldSchema). - Description("A schema to apply. Use either this or the `schema_path` field."). - Optional(), - service.NewStringField(jschemaPFieldSchemaPath). - Description("The path of a schema document to apply. Use either this or the `schema` field."). - Optional(), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "json_schema", jschemaProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - schemaStr, _ := conf.FieldString(jschemaPFieldSchema) - schemaPath, _ := conf.FieldString(jschemaPFieldSchemaPath) - mgr := interop.UnwrapManagement(res) - p, err := newJSONSchema(schemaStr, schemaPath, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("json_schema", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type jsonSchemaProc struct { - log log.Modular - schema *jsonschema.Schema -} - -func newJSONSchema(schemaStr, schemaPath string, mgr bundle.NewManagement) (processor.AutoObserved, error) { - var schema *jsonschema.Schema - var err error - - // load JSONSchema definition - if schemaPath := schemaPath; schemaPath != "" { - if !(strings.HasPrefix(schemaPath, "file://") || strings.HasPrefix(schemaPath, "http://")) { - return nil, errors.New("invalid schema_path provided, must start with file:// or http://") - } - - schema, err = jsonschema.NewSchema(jsonschema.NewReferenceLoaderFileSystem(schemaPath, ifs.ToHTTP(mgr.FS()))) - if err != nil { - return nil, fmt.Errorf("failed to load JSON schema definition: %v", err) - } - } else if schemaStr != "" { - schema, err = jsonschema.NewSchema(jsonschema.NewStringLoader(schemaStr)) - if err != nil { - return nil, fmt.Errorf("failed to load JSON schema definition: %v", err) - } - } else { - return nil, errors.New("either schema or schema_path must be provided") - } - - return &jsonSchemaProc{ - log: mgr.Logger(), - schema: schema, - }, nil -} - -//------------------------------------------------------------------------------ - -// ProcessMessage applies the processor to a message, either creating >0 -// resulting messages or a response to be sent back to the message source. -func (s *jsonSchemaProc) Process(ctx context.Context, part *message.Part) ([]*message.Part, error) { - jsonPart, err := part.AsStructured() - if err != nil { - s.log.Debug("Failed to parse part into json: %v", err) - return nil, err - } - - partLoader := jsonschema.NewGoLoader(jsonPart) - result, err := s.schema.Validate(partLoader) - if err != nil { - s.log.Debug("Failed to validate json: %v", err) - return nil, err - } - - if !result.Valid() { - s.log.Debug("The document is not valid") - var errStr string - for i, desc := range result.Errors() { - if i > 0 { - errStr += "\n" - } - description := strings.ToLower(desc.Description()) - if property := desc.Details()["property"]; property != nil { - description = property.(string) + strings.TrimPrefix(description, strings.ToLower(property.(string))) - } - errStr += desc.Field() + " " + description - } - return nil, errors.New(errStr) - } - - s.log.Debug("The document is valid") - return []*message.Part{part}, nil -} - -func (s *jsonSchemaProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_jsonschema_test.go b/internal/impl/pure/processor_jsonschema_test.go deleted file mode 100644 index 283972a5a5..0000000000 --- a/internal/impl/pure/processor_jsonschema_test.go +++ /dev/null @@ -1,401 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestJSONSchemaExternalSchemaRelativePath(t *testing.T) { - schema := `{ - "$id": "https://example.com/person.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -}` - - tmpDir := t.TempDir() - - sFileName := filepath.Join(tmpDir, "foo") - require.NoError(t, os.WriteFile(sFileName, []byte(schema), 0o777)) - - cwd, err := os.Getwd() - require.NoError(t, err) - - sFileName, err = filepath.Rel(cwd, sFileName) - require.NoError(t, err) - - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -json_schema: - schema_path: file://%v -`, sFileName)) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - msgs, _ := c.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte(`{"firstName":"John","lastName":"Doe","age":21}`)), - }) - require.Len(t, msgs, 1) - - assert.Equal(t, `{"firstName":"John","lastName":"Doe","age":21}`, string(msgs[0][0].AsBytes())) - assert.NoError(t, msgs[0][0].ErrorGet()) -} - -func TestJSONSchemaExternalSchemaCheck(t *testing.T) { - schema := `{ - "$id": "https://example.com/person.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -}` - - tmpDir := t.TempDir() - - sFileName := filepath.Join(tmpDir, "foo") - require.NoError(t, os.WriteFile(sFileName, []byte(schema), 0o777)) - - type fields struct { - schemaPath string - } - tests := []struct { - name string - fields fields - arg [][]byte - output string - err string - }{ - { - name: "schema match", - fields: fields{ - schemaPath: fmt.Sprintf("file://%s", sFileName), - }, - arg: [][]byte{ - []byte(`{"firstName":"John","lastName":"Doe","age":21}`), - }, - output: `{"firstName":"John","lastName":"Doe","age":21}`, - }, - { - name: "schema no match", - fields: fields{ - schemaPath: fmt.Sprintf("file://%s", sFileName), - }, - arg: [][]byte{ - []byte(`{"firstName":"John","lastName":"Doe","age":-20}`), - }, - output: `{"firstName":"John","lastName":"Doe","age":-20}`, - err: `age must be greater than or equal to 0`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -json_schema: - schema_path: '%v' -`, tt.fields.schemaPath)) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - msgs, _ := c.ProcessBatch(context.Background(), message.QuickBatch(tt.arg)) - - if len(msgs) != 1 { - t.Fatalf("Test '%v' did not succeed", tt.name) - } - - if exp, act := tt.output, string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Wrong result '%v': %v != %v", tt.name, act, exp) - } - _ = msgs[0].Iter(func(i int, part *message.Part) error { - act := part.ErrorGet() - if act != nil && act.Error() != tt.err { - t.Errorf("Wrong error message '%v': %v != %v", tt.name, act, tt.err) - } - return nil - }) - }) - } -} - -func TestJSONSchemaInlineSchemaCheck(t *testing.T) { - schemaDef := `{ - "$id": "https://example.com/person.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -}` - - type fields struct { - schema string - part int - } - tests := []struct { - name string - fields fields - arg [][]byte - output string - err string - }{ - { - name: "schema match", - fields: fields{ - schema: schemaDef, - part: 0, - }, - arg: [][]byte{ - []byte(`{"firstName":"John","lastName":"Doe","age":21}`), - }, - output: `{"firstName":"John","lastName":"Doe","age":21}`, - }, - { - name: "schema no match", - fields: fields{ - schema: schemaDef, - part: 0, - }, - arg: [][]byte{ - []byte(`{"firstName":"John","lastName":"Doe","age":-20}`), - }, - output: `{"firstName":"John","lastName":"Doe","age":-20}`, - err: `age must be greater than or equal to 0`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -json_schema: - schema: | - %v -`, strings.ReplaceAll(tt.fields.schema, "\n", " "))) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - msgs, _ := c.ProcessBatch(context.Background(), message.QuickBatch(tt.arg)) - - if len(msgs) != 1 { - t.Fatalf("Test '%v' did not succeed", tt.name) - } - - if exp, act := tt.output, string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Wrong result '%v': %v != %v", tt.name, act, exp) - } - _ = msgs[0].Iter(func(i int, part *message.Part) error { - act := part.ErrorGet() - if act != nil && act.Error() != tt.err { - t.Errorf("Wrong error message '%v': %v != %v", tt.name, act, tt.err) - } - return nil - }) - }) - } -} - -func TestJSONSchemaLowercaseDescriptionCheck(t *testing.T) { - schema := `{ - "$id": "https://example.com/person.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "addresses": { - "description": "The person's addresses.'", - "type": "array", - "items": { - "type": "object", - "properties": { - "cityName": { - "description": "The city's name'", - "type": "string", - "maxLength": 50 - }, - "postCode": { - "description": "The city's postal code'", - "type": "string", - "maxLength": 50 - } - }, - "required": [ - "cityName" - ] - } - } - } -}` - - type fields struct { - schema string - part int - } - tests := []struct { - name string - fields fields - arg [][]byte - output string - err string - }{ - { - name: "schema match", - fields: fields{ - schema: schema, - part: 0, - }, - arg: [][]byte{ - []byte(`{"firstName":"John","addresses":[{"cityName":"Reading", "postCode":"RG1"},{"cityName":"London", "postCode":"E1"}]}`), - }, - output: `{"firstName":"John","addresses":[{"cityName":"Reading", "postCode":"RG1"},{"cityName":"London", "postCode":"E1"}]}`, - }, - { - name: "schema no match", - fields: fields{ - schema: schema, - part: 0, - }, - arg: [][]byte{ - []byte(`{"firstName":"John","addresses":[{"postCode":"RG1"},{"cityName":"London", "postCode":"E1"}]}`), - }, - output: `{"firstName":"John","addresses":[{"postCode":"RG1"},{"cityName":"London", "postCode":"E1"}]}`, - err: `addresses.0 cityName is required`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -json_schema: - schema: | - %v -`, strings.ReplaceAll(tt.fields.schema, "\n", " "))) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - msgs, _ := c.ProcessBatch(context.Background(), message.QuickBatch(tt.arg)) - - if len(msgs) != 1 { - t.Fatalf("Test '%v' did not succeed", tt.name) - } - - if exp, act := tt.output, string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Wrong result '%v': %v != %v", tt.name, act, exp) - } - _ = msgs[0].Iter(func(i int, part *message.Part) error { - act := part.ErrorGet() - if act != nil && act.Error() != tt.err { - t.Errorf("Wrong error message '%v': %v != %v", tt.name, act, tt.err) - } - return nil - }) - }) - } -} - -func TestJSONSchemaPathNotExist(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -json_schema: - schema_path: file://path_does_not_exist -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - if err == nil { - t.Error("expected error from loading non existent schema file") - } -} - -func TestJSONSchemaInvalidSchema(t *testing.T) { - schema := `{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "any" - }` - - tmpSchemaFile, err := os.CreateTemp("", "benthos_jsonschema_invalid_schema_test") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpSchemaFile.Name()) - - // write schema definition to tmpfile - if _, err := tmpSchemaFile.WriteString(schema); err != nil { - t.Fatal(err) - } - - conf, err := testutil.ProcessorFromYAML(` -json_schema: - schema_path: ` + fmt.Sprintf("file://%s", tmpSchemaFile.Name()) + ` -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - if err == nil { - t.Error("expected error from loading bad schema") - } -} diff --git a/internal/impl/pure/processor_log.go b/internal/impl/pure/processor_log.go deleted file mode 100644 index 59600cb144..0000000000 --- a/internal/impl/pure/processor_log.go +++ /dev/null @@ -1,230 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "sort" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - logPFieldLevel = "level" - logPFieldFields = "fields" - logPFieldFieldsMapping = "fields_mapping" - logPFieldMessage = "message" -) - -func logProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`Prints a log event for each message. Messages always remain unchanged. The log message can be set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries] which allows you to log the contents and metadata of messages.`). - Description(` -The `+"`level`"+` field determines the log level of the printed events and can be any of the following values: TRACE, DEBUG, INFO, WARN, ERROR. - -== Structured fields - -It's also possible add custom fields to logs when the format is set to a structured form such as `+"`json` or `logfmt`"+` with the config field `+"<>"+`: - -`+"```yaml"+` -pipeline: - processors: - - log: - level: DEBUG - message: hello world - fields_mapping: | - root.reason = "cus I wana" - root.id = this.id - root.age = this.user.age - root.kafka_topic = meta("kafka_topic") -`+"```"+` -`). - Fields( - service.NewStringEnumField(logPFieldLevel, "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "ALL"). - Description("The log level to use."). - LintRule(``). - Default("INFO"), - service.NewBloblangField(logPFieldFieldsMapping). - Description("An optional xref:guides:bloblang/about.adoc[Bloblang mapping] that can be used to specify extra fields to add to the log. If log fields are also added with the `fields` field then those values will override matching keys from this mapping."). - Examples( - `root.reason = "cus I wana" -root.id = this.id -root.age = this.user.age.number() -root.kafka_topic = meta("kafka_topic")`, - ). - Optional(), - service.NewInterpolatedStringField(logPFieldMessage). - Description("The message to print."). - Default(""), - service.NewInterpolatedStringMapField(logPFieldFields). - Description("A map of fields to print along with the log message."). - Optional(). - Deprecated(), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "log", logProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - logLevel, err := conf.FieldString(logPFieldLevel) - if err != nil { - return nil, err - } - - messageStr, err := conf.FieldString(logPFieldMessage) - if err != nil { - return nil, err - } - - depFields, _ := conf.FieldStringMap(logPFieldFields) - - fieldsMappingStr, _ := conf.FieldString(logPFieldFieldsMapping) - - mgr := interop.UnwrapManagement(res) - p, err := newLogProcessor(messageStr, logLevel, fieldsMappingStr, depFields, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("log", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type logProcessor struct { - logger log.Modular - level string - message *field.Expression - fields map[string]*field.Expression - printFn func(logger log.Modular, msg string) - fieldsMapping *mapping.Executor -} - -func newLogProcessor(messageStr, levelStr, fieldsMappingStr string, depFields map[string]string, mgr bundle.NewManagement) (processor.AutoObservedBatched, error) { - message, err := mgr.BloblEnvironment().NewField(messageStr) - if err != nil { - return nil, fmt.Errorf("failed to parse message expression: %v", err) - } - l := &logProcessor{ - logger: mgr.Logger(), - level: levelStr, - fields: map[string]*field.Expression{}, - message: message, - } - if len(depFields) > 0 { - for k, v := range depFields { - if l.fields[k], err = mgr.BloblEnvironment().NewField(v); err != nil { - return nil, fmt.Errorf("failed to parse field '%v' expression: %v", k, err) - } - } - } - if fieldsMappingStr != "" { - if l.fieldsMapping, err = mgr.BloblEnvironment().NewMapping(fieldsMappingStr); err != nil { - return nil, fmt.Errorf("failed to parse fields mapping: %w", err) - } - } - if l.printFn, err = l.levelToLogFn(l.level); err != nil { - return nil, err - } - return l, nil -} - -func (l *logProcessor) levelToLogFn(level string) (func(logger log.Modular, msg string), error) { - level = strings.ToUpper(level) - switch level { - case "TRACE": - return func(logger log.Modular, msg string) { - logger.Trace(msg) - }, nil - case "DEBUG": - return func(logger log.Modular, msg string) { - logger.Debug(msg) - }, nil - case "INFO": - return func(logger log.Modular, msg string) { - logger.Info(msg) - }, nil - case "WARN": - return func(logger log.Modular, msg string) { - logger.Warn(msg) - }, nil - case "ERROR": - return func(logger log.Modular, msg string) { - logger.Error(msg) - }, nil - } - return nil, fmt.Errorf("log level not recognised: %v", level) -} - -func (l *logProcessor) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - _ = msg.Iter(func(i int, _ *message.Part) error { - targetLog := l.logger - if l.fieldsMapping != nil { - fieldsMsg, err := l.fieldsMapping.MapPart(i, msg) - if err != nil { - l.logger.Error("Failed to execute fields mapping: %v", err) - return nil - } - - v, err := fieldsMsg.AsStructured() - if err != nil { - l.logger.Error("Failed to extract fields object: %v", err) - return nil - } - - vObj, ok := v.(map[string]any) - if !ok { - l.logger.Error("Fields mapping yielded a non-object result: %T", v) - return nil - } - - keys := make([]string, 0, len(vObj)) - for k := range vObj { - keys = append(keys, k) - } - sort.Strings(keys) - - args := make([]any, 0, len(vObj)*2) - for _, k := range keys { - args = append(args, k, vObj[k]) - } - targetLog = targetLog.With(args...) - } - - if len(l.fields) > 0 { - interpFields := make(map[string]string, len(l.fields)) - for k, vi := range l.fields { - var err error - if interpFields[k], err = vi.String(i, msg); err != nil { - l.logger.Error("Field %v interpolation error: %v", k, err) - return nil - } - } - targetLog = targetLog.WithFields(interpFields) - } - logMsg, err := l.message.String(i, msg) - if err != nil { - l.logger.Error("Message interpolation error: %v", err) - return nil - } - l.printFn(targetLog, logMsg) - return nil - }) - - return []message.Batch{msg}, nil -} - -func (l *logProcessor) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_log_test.go b/internal/impl/pure/processor_log_test.go deleted file mode 100644 index 61bcf4b1d3..0000000000 --- a/internal/impl/pure/processor_log_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type mockLog struct { - traces []string - debugs []string - infos []string - warns []string - errors []string - fields []map[string]string - mappingFields []any -} - -func (m *mockLog) NewModule(prefix string) log.Modular { return m } -func (m *mockLog) WithFields(fields map[string]string) log.Modular { - m.fields = append(m.fields, fields) - return m -} - -func (m *mockLog) With(args ...any) log.Modular { - m.mappingFields = append(m.mappingFields, args...) - return m -} - -func (m *mockLog) Fatal(format string, v ...any) {} -func (m *mockLog) Error(format string, v ...any) { - m.errors = append(m.errors, fmt.Sprintf(format, v...)) -} - -func (m *mockLog) Warn(format string, v ...any) { - m.warns = append(m.warns, fmt.Sprintf(format, v...)) -} - -func (m *mockLog) Info(format string, v ...any) { - m.infos = append(m.infos, fmt.Sprintf(format, v...)) -} - -func (m *mockLog) Debug(format string, v ...any) { - m.debugs = append(m.debugs, fmt.Sprintf(format, v...)) -} - -func (m *mockLog) Trace(format string, v ...any) { - m.traces = append(m.traces, fmt.Sprintf(format, v...)) -} - -func (m *mockLog) Fatalln(message string) {} -func (m *mockLog) Errorln(message string) { - m.errors = append(m.errors, message) -} - -func (m *mockLog) Warnln(message string) { - m.warns = append(m.warns, message) -} - -func (m *mockLog) Infoln(message string) { - m.infos = append(m.infos, message) -} - -func (m *mockLog) Debugln(message string) { - m.debugs = append(m.debugs, message) -} - -func (m *mockLog) Traceln(message string) { - m.traces = append(m.traces, message) -} - -func TestLogBadLevel(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -log: - level: does not exist -`) - require.NoError(t, err) - - if _, err := mock.NewManager().NewProcessor(conf); err == nil { - t.Error("expected err from bad log level") - } -} - -func TestLogLevelTrace(t *testing.T) { - logMock := &mockLog{} - - levels := []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"} - for _, level := range levels { - conf, err := testutil.ProcessorFromYAML(` -log: - message: '${!json("foo")}' - level: ` + level + ` -`) - require.NoError(t, err) - - mgr := mock.NewManager() - mgr.L = logMock - - l, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{[]byte(fmt.Sprintf(`{"foo":"%v"}`, level))}) - expMsgs := []message.Batch{input} - actMsgs, res := l.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - if !reflect.DeepEqual(expMsgs, actMsgs) { - t.Errorf("Wrong message passthrough: %v != %v", actMsgs, expMsgs) - } - } - - if exp, act := []string{"TRACE"}, logMock.traces; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log for trace: %v != %v", act, exp) - } - if exp, act := []string{"DEBUG"}, logMock.debugs; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log for debug: %v != %v", act, exp) - } - if exp, act := []string{"INFO"}, logMock.infos; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log for info: %v != %v", act, exp) - } - if exp, act := []string{"WARN"}, logMock.warns; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log for warn: %v != %v", act, exp) - } - if exp, act := []string{"ERROR"}, logMock.errors; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log for error: %v != %v", act, exp) - } -} - -func TestLogWithFields(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -log: - message: '${!json("foo")}' - level: INFO - fields: - static: foo - dynamic: '${!json("bar")}' -`) - require.NoError(t, err) - - logMock := &mockLog{} - - mgr := mock.NewManager() - mgr.L = logMock - - l, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{[]byte(`{"foo":"info message","bar":"with fields"}`)}) - expMsgs := []message.Batch{input} - actMsgs, res := l.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - if !reflect.DeepEqual(expMsgs, actMsgs) { - t.Errorf("Wrong message passthrough: %v != %v", actMsgs, expMsgs) - } - - if exp, act := []string{"info message"}, logMock.infos; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log output: %v != %v", act, exp) - } - t.Logf("Checking %v\n", logMock.fields) - if exp, act := 1, len(logMock.fields); exp != act { - t.Fatalf("Wrong count of fields: %v != %v", act, exp) - } - if exp, act := map[string]string{"dynamic": "with fields", "static": "foo"}, logMock.fields[0]; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong field output: %v != %v", act, exp) - } - - input = message.QuickBatch([][]byte{[]byte(`{"foo":"info message 2","bar":"with fields 2"}`)}) - expMsgs = []message.Batch{input} - actMsgs, res = l.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - if !reflect.DeepEqual(expMsgs, actMsgs) { - t.Errorf("Wrong message passthrough: %v != %v", actMsgs, expMsgs) - } - - if exp, act := []string{"info message", "info message 2"}, logMock.infos; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong log output: %v != %v", act, exp) - } - t.Logf("Checking %v\n", logMock.fields) - if exp, act := 2, len(logMock.fields); exp != act { - t.Fatalf("Wrong count of fields: %v != %v", act, exp) - } - if exp, act := map[string]string{"dynamic": "with fields", "static": "foo"}, logMock.fields[0]; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong field output: %v != %v", act, exp) - } - if exp, act := map[string]string{"dynamic": "with fields 2", "static": "foo"}, logMock.fields[1]; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong field output: %v != %v", act, exp) - } -} - -func TestLogWithFieldsMapping(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -log: - message: 'hello world' - level: INFO - fields_mapping: | - root.static = "static value" - root.age = this.age + 2 - root.is_cool = this.is_cool -`) - require.NoError(t, err) - - logMock := &mockLog{} - - mgr := mock.NewManager() - mgr.L = logMock - - l, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - input := message.QuickBatch([][]byte{[]byte( - `{"age":10,"is_cool":true,"ignore":"this value please"}`, - )}) - expMsgs := []message.Batch{input} - actMsgs, res := l.ProcessBatch(context.Background(), input) - require.NoError(t, res) - assert.Equal(t, expMsgs, actMsgs) - - assert.Equal(t, []string{"hello world"}, logMock.infos) - assert.Equal(t, []any{ - "age", int64(12), - "is_cool", true, - "static", "static value", - }, logMock.mappingFields) -} diff --git a/internal/impl/pure/processor_mapping.go b/internal/impl/pure/processor_mapping.go deleted file mode 100644 index 7baaae7453..0000000000 --- a/internal/impl/pure/processor_mapping.go +++ /dev/null @@ -1,168 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor( - "mapping", - service.NewConfigSpec(). - Stable(). - Version("4.5.0"). - Categories("Mapping", "Parsing"). - Field(service.NewBloblangField("")). - Summary("Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping on messages, creating a new document that replaces (or filters) the original message."). - Description(` -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information, see xref:guides:bloblang/about.adoc[]. - -If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `+"`from \"\"`"+`, where the path must be absolute, or relative from the location that Benthos is executed from. - -Note: This processor is equivalent to the xref:components:processors/bloblang.adoc#component-rename[Bloblang] one. The latter will be deprecated in a future release. - -== Input document immutability - -Mapping operates by creating an entirely new object during assignments, this has the advantage of treating the original referenced document as immutable and therefore queryable at any stage of your mapping. For example, with the following mapping: - -`+"```coffeescript"+` -root.id = this.id -root.invitees = this.invitees.filter(i -> i.mood >= 0.5) -root.rejected = this.invitees.filter(i -> i.mood < 0.5) -`+"```"+` - -Notice that we mutate the value of `+"`invitees`"+` in the resulting document by filtering out objects with a lower mood. However, even after doing so we're still able to reference the unchanged original contents of this value from the input document in order to populate a second field. Within this mapping we also have the flexibility to reference the mutable mapped document by using the keyword `+"`root` (i.e. `root.invitees`)"+` on the right-hand side instead. - -Mapping documents is advantageous in situations where the result is a document with a dramatically different shape to the input document, since we are effectively rebuilding the document in its entirety and might as well keep a reference to the unchanged input document throughout. However, in situations where we are only performing minor alterations to the input document, the rest of which is unchanged, it might be more efficient to use the `+"xref:components:processors/mutation.adoc[`mutation` processor]"+` instead. - -== Error handling - -Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. - -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired xref:guides:bloblang/about.adoc#error-handling[fallback behavior]. - `). - Example("Mapping", ` -Given JSON documents containing an array of fans: - -`+"```json"+` -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"grace","obsession":0.21}, - {"name":"ali","obsession":0.89}, - {"name":"vic","obsession":0.43} - ] -} -`+"```"+` - -We can reduce the documents down to just the ID and only those fans with an obsession score above 0.5, giving us: - -`+"```json"+` -{ - "id":"foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"ali","obsession":0.89} - ] -} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - mapping: | - root.id = this.id - root.fans = this.fans.filter(fan -> fan.obsession > 0.5) -`). - Example("More Mapping", ` -When receiving JSON documents of the form: - -`+"```json"+` -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -`+"```"+` - -We could collapse the location names from the state of Washington into a field `+"`Cities`"+`: - -`+"```json"+` -{"Cities": "Bellevue, Olympia, Seattle"} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - mapping: | - root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -`), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - mapping, err := conf.FieldBloblang() - if err != nil { - return nil, err - } - - v1Proc := processor.NewAutoObservedBatchedProcessor("mapping", newMapping(mapping, mgr.Logger()), interop.UnwrapManagement(mgr)) - return interop.NewUnwrapInternalBatchProcessor(v1Proc), nil - }) - if err != nil { - panic(err) - } -} - -type mappingProc struct { - exec *mapping.Executor - log *service.Logger -} - -func newMapping(exec *bloblang.Executor, log *service.Logger) *mappingProc { - uw := exec.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - return &mappingProc{ - exec: uw, - log: log, - } -} - -func (m *mappingProc) ProcessBatch(ctx *processor.BatchProcContext, b message.Batch) ([]message.Batch, error) { - newBatch := make(message.Batch, 0, len(b)) - for i, msg := range b { - newPart, err := m.exec.MapPart(i, b) - if err != nil { - ctx.OnError(err, i, msg) - m.log.Errorf("%v", err) - newBatch = append(newBatch, msg) - continue - } - if newPart != nil { - newBatch = append(newBatch, newPart) - } - } - if len(newBatch) == 0 { - return nil, nil - } - return []message.Batch{newBatch}, nil -} - -func (m *mappingProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_mapping_test.go b/internal/impl/pure/processor_mapping_test.go deleted file mode 100644 index b03cef37f3..0000000000 --- a/internal/impl/pure/processor_mapping_test.go +++ /dev/null @@ -1,231 +0,0 @@ -package pure - -import ( - "context" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestMappingCreateCrossfire(t *testing.T) { - tCtx := context.Background() - - inMsg := message.NewPart(nil) - inMsg.SetStructuredMut(map[string]any{ - "foo": map[string]any{ - "bar": map[string]any{ - "baz": "original value", - "qux": "dont change", - }, - }, - }) - inMsg.MetaSetMut("foo", "orig1") - inMsg.MetaSetMut("bar", "orig2") - - inMsg2 := message.NewPart([]byte(`{}`)) - - exec, err := bloblang.Parse(` -foo = json("foo").from(0) -foo.bar_new = "this is swapped now" -foo.bar.baz = "and this changed" -meta foo = meta("foo").from(0) -meta bar = meta("bar").from(0) -meta baz = "new meta" -`) - require.NoError(t, err) - - proc := newMapping(exec, nil) - - inBatch := message.Batch{inMsg, inMsg2} - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 2) - - msgBytes := inMsg.AsBytes() - assert.Equal(t, `{"foo":{"bar":{"baz":"original value","qux":"dont change"}}}`, string(msgBytes)) - v, _ := inMsg.MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = inMsg.MetaGetMut("bar") - assert.Equal(t, "orig2", v) - _, exists := inMsg.MetaGetMut("baz") - assert.False(t, exists) - - msgBytes = inMsg2.AsBytes() - assert.Equal(t, `{}`, string(msgBytes)) - _, exists = inMsg2.MetaGetMut("foo") - assert.False(t, exists) - _, exists = inMsg2.MetaGetMut("bar") - assert.False(t, exists) - _, exists = inMsg2.MetaGetMut("baz") - assert.False(t, exists) - - msgBytes = outBatches[0][0].AsBytes() - assert.Equal(t, `{"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(msgBytes)) - v, _ = outBatches[0][0].MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = outBatches[0][0].MetaGetMut("bar") - assert.Equal(t, "orig2", v) - v, _ = outBatches[0][0].MetaGetMut("baz") - assert.Equal(t, "new meta", v) - - msgBytes = outBatches[0][1].AsBytes() - assert.Equal(t, `{"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(msgBytes)) - v, _ = outBatches[0][1].MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = outBatches[0][1].MetaGetMut("bar") - assert.Equal(t, "orig2", v) - v, _ = outBatches[0][1].MetaGetMut("baz") - assert.Equal(t, "new meta", v) -} - -func TestMappingCreateCustomObject(t *testing.T) { - tCtx := context.Background() - - part := message.NewPart(nil) - - gObj := gabs.New() - _, _ = gObj.ArrayOfSize(3, "foos") - - gObjEle := gabs.New() - _, _ = gObjEle.Set("FROM NEW OBJECT", "foo") - - _, _ = gObj.S("foos").SetIndex(gObjEle.Data(), 0) - _, _ = gObj.S("foos").SetIndex(5, 1) - - part.SetStructuredMut(gObj.Data()) - - exec, err := bloblang.Parse(`root.foos = this.foos`) - require.NoError(t, err) - - proc := newMapping(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, []*message.Part{part}), message.Batch{part}) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 1) - - resPartBytes := outBatches[0][0].AsBytes() - assert.Equal(t, `{"foos":[{"foo":"FROM NEW OBJECT"},5,null]}`, string(resPartBytes)) -} - -func TestMappingCreateFiltering(t *testing.T) { - tCtx := context.Background() - - inBatch := message.Batch{ - message.NewPart([]byte(`{"foo":{"delete":true}}`)), - message.NewPart([]byte(`{"foo":{"dont":"delete me"}}`)), - message.NewPart([]byte(`{"bar":{"delete":true}}`)), - message.NewPart([]byte(`{"bar":{"dont":"delete me"}}`)), - } - - exec, err := bloblang.Parse(` -root = match { - (foo | bar).delete.or(false) => deleted(), -} -`) - require.NoError(t, err) - - proc := newMapping(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 2) - - assert.NoError(t, outBatches[0][0].ErrorGet()) - assert.NoError(t, outBatches[0][1].ErrorGet()) - - msgBytes := outBatches[0][0].AsBytes() - assert.Equal(t, `{"foo":{"dont":"delete me"}}`, string(msgBytes)) - - msgBytes = outBatches[0][1].AsBytes() - assert.Equal(t, `{"bar":{"dont":"delete me"}}`, string(msgBytes)) -} - -func TestMappingCreateFilterAll(t *testing.T) { - tCtx := context.Background() - - inBatch := message.Batch{ - message.NewPart([]byte(`{"foo":{"delete":true}}`)), - message.NewPart([]byte(`{"foo":{"dont":"delete me"}}`)), - message.NewPart([]byte(`{"bar":{"delete":true}}`)), - message.NewPart([]byte(`{"bar":{"dont":"delete me"}}`)), - } - - exec, err := bloblang.Parse(`root = deleted()`) - require.NoError(t, err) - - proc := newMapping(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - assert.NoError(t, err) - assert.Empty(t, outBatches) -} - -func TestMappingCreateJSONError(t *testing.T) { - tCtx := context.Background() - - msg := message.Batch{ - message.NewPart([]byte(`this is not valid json`)), - } - - exec, err := bloblang.Parse(`foo = json().bar`) - require.NoError(t, err) - - proc := newMapping(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, msg), msg) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 1) - - msgBytes := outBatches[0][0].AsBytes() - assert.Equal(t, `this is not valid json`, string(msgBytes)) - - err = outBatches[0][0].ErrorGet() - require.Error(t, err) - assert.Equal(t, `failed assignment (line 1): invalid character 'h' in literal true (expecting 'r')`, err.Error()) -} - -func BenchmarkMappingBasic(b *testing.B) { - blobl, err := bloblang.Parse(` -root = this -root.sum = this.a + this.b -`) - require.NoError(b, err) - - proc := newMapping(blobl, nil) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tmpMsg := message.NewPart(nil) - tmpMsg.SetStructured(map[string]any{ - "a": 5, - "b": 7, - }) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - resBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, nil), message.Batch{tmpMsg.ShallowCopy()}) - require.NoError(b, err) - require.Len(b, resBatches, 1) - require.Len(b, resBatches[0], 1) - - v, err := resBatches[0][0].AsStructured() - require.NoError(b, err) - assert.Equal(b, int64(12), v.(map[string]any)["sum"]) - } - - require.NoError(b, proc.Close(tCtx)) -} diff --git a/internal/impl/pure/processor_metric.go b/internal/impl/pure/processor_metric.go deleted file mode 100644 index 39cca2238d..0000000000 --- a/internal/impl/pure/processor_metric.go +++ /dev/null @@ -1,414 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sort" - "strconv" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - metProcFieldType = "type" - metProcFieldName = "name" - metProcFieldLabels = "labels" - metProcFieldValue = "value" -) - -func metProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary("Emit custom metrics by extracting values from messages."). - Description(` -This processor works by evaluating an xref:configuration:interpolation.adoc#bloblang-queries[interpolated field `+"`value`"+`] for each message and updating a emitted metric according to the <>. - -Custom metrics such as these are emitted along with Benthos internal metrics, where you can customize where metrics are sent, which metric names are emitted and rename them as/when appropriate. For more information see the xref:components:metrics/about.adoc[metrics docs].`). - Footnotes(` -== Types - -=== `+"`counter`"+` - -Increments a counter by exactly 1, the contents of `+"`value`"+` are ignored -by this type. - -=== `+"`counter_by`"+` - -If the contents of `+"`value`"+` can be parsed as a positive integer value -then the counter is incremented by this value. - -For example, the following configuration will increment the value of the -`+"`count.custom.field` metric by the contents of `field.some.value`"+`: - -`+"```yaml"+` -pipeline: - processors: - - metric: - type: counter_by - name: CountCustomField - value: ${!json("field.some.value")} -`+"```"+` - -=== `+"`gauge`"+` - -If the contents of `+"`value`"+` can be parsed as a positive integer value -then the gauge is set to this value. - -For example, the following configuration will set the value of the -`+"`gauge.custom.field` metric to the contents of `field.some.value`"+`: - -`+"```yaml"+` -pipeline: - processors: - - metric: - type: gauge - name: GaugeCustomField - value: ${!json("field.some.value")} -`+"```"+` - -=== `+"`timing`"+` - -Equivalent to `+"`gauge`"+` where instead the metric is a timing. It is recommended that timing values are recorded in nanoseconds in order to be consistent with standard Benthos timing metrics, as in some cases these values are automatically converted into other units such as when exporting timings as histograms with Prometheus metrics.`). - Example( - "Counter", - "In this example we emit a counter metric called `Foos`, which increments for every message processed, and we label the metric with some metadata about where the message came from and a field from the document that states what type it is. We also configure our metrics to emit to CloudWatch, and explicitly only allow our custom metric and some internal Benthos metrics to emit.", - ` -pipeline: - processors: - - metric: - name: Foos - type: counter - labels: - topic: ${! meta("kafka_topic") } - partition: ${! meta("kafka_partition") } - type: ${! json("document.type").or("unknown") } - -metrics: - mapping: | - root = if ![ - "Foos", - "input_received", - "output_sent" - ].contains(this) { deleted() } - aws_cloudwatch: - namespace: ProdConsumer -`, - ). - Example( - "Gauge", - "In this example we emit a gauge metric called `FooSize`, which is given a value extracted from JSON messages at the path `foo.size`. We then also configure our Prometheus metric exporter to only emit this custom metric and nothing else. We also label the metric with some metadata.", - ` -pipeline: - processors: - - metric: - name: FooSize - type: gauge - labels: - topic: ${! meta("kafka_topic") } - value: ${! json("foo.size") } - -metrics: - mapping: 'if this != "FooSize" { deleted() }' - prometheus: {} -`, - ). - Fields( - service.NewStringEnumField(metProcFieldType, "counter", "counter_by", "gauge", "timing"). - Description("The metric <> to create."), - service.NewStringField(metProcFieldName). - Description("The name of the metric to create, this must be unique across all Benthos components otherwise it will overwrite those other metrics."), - service.NewInterpolatedStringMapField(metProcFieldLabels). - Description("A map of label names and values that can be used to enrich metrics. Labels are not supported by some metric destinations, in which case the metrics series are combined."). - Example(map[string]any{ - "type": "${! json(\"doc.type\") }", - "topic": "${! meta(\"kafka_topic\") }", - }). - Optional(), - service.NewInterpolatedStringField(metProcFieldValue). - Description("For some metric types specifies a value to set, increment. Certain metrics exporters such as Prometheus support floating point values, but those that do not will cast a floating point value into an integer."). - Default(""), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "metric", metProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - procTypeStr, err := conf.FieldString(metProcFieldType) - if err != nil { - return nil, err - } - - procName, err := conf.FieldString(metProcFieldName) - if err != nil { - return nil, err - } - - var labelMap map[string]string - if conf.Contains(metProcFieldLabels) { - if labelMap, err = conf.FieldStringMap(metProcFieldLabels); err != nil { - return nil, err - } - } - - valueStr, err := conf.FieldString(metProcFieldValue) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newMetricProcessor(procTypeStr, procName, valueStr, labelMap, mgr) - if err != nil { - return nil, err - } - - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -type metricProcessor struct { - log log.Modular - - value *field.Expression - labels labels - - mCounter metrics.StatCounter - mGauge metrics.StatGauge - mTimer metrics.StatTimer - - mCounterVec metrics.StatCounterVec - mGaugeVec metrics.StatGaugeVec - mTimerVec metrics.StatTimerVec - - handler func(string, int, message.Batch) error -} - -type ( - labels []label - label struct { - name string - value *field.Expression - } -) - -func (l *label) val(index int, msg message.Batch) (string, error) { - return l.value.String(index, msg) -} - -func (l labels) names() []string { - var names []string - for i := range l { - names = append(names, l[i].name) - } - return names -} - -func (l labels) values(index int, msg message.Batch) ([]string, error) { - var values []string - for i := range l { - vStr, err := l[i].val(index, msg) - if err != nil { - return nil, fmt.Errorf("label interpolation error: %w", err) - } - values = append(values, vStr) - } - return values, nil -} - -func newMetricProcessor(typeStr, name, valueStr string, labels map[string]string, mgr bundle.NewManagement) (processor.V1, error) { - value, err := mgr.BloblEnvironment().NewField(valueStr) - if err != nil { - return nil, fmt.Errorf("failed to parse value expression: %v", err) - } - - m := &metricProcessor{ - log: mgr.Logger(), - value: value, - } - - if name == "" { - return nil, errors.New("metric name must not be empty") - } - - labelNames := make([]string, 0, len(labels)) - for n := range labels { - labelNames = append(labelNames, n) - } - sort.Strings(labelNames) - - for _, n := range labelNames { - v, err := mgr.BloblEnvironment().NewField(labels[n]) - if err != nil { - return nil, fmt.Errorf("failed to parse label '%v' expression: %v", n, err) - } - m.labels = append(m.labels, label{ - name: n, - value: v, - }) - } - - stats := mgr.Metrics() - switch strings.ToLower(typeStr) { - case "counter": - if len(m.labels) > 0 { - m.mCounterVec = stats.GetCounterVec(name, m.labels.names()...) - } else { - m.mCounter = stats.GetCounter(name) - } - m.handler = m.handleCounter - case "counter_by": - if len(m.labels) > 0 { - m.mCounterVec = stats.GetCounterVec(name, m.labels.names()...) - } else { - m.mCounter = stats.GetCounter(name) - } - m.handler = m.handleCounterBy - case "gauge": - if len(m.labels) > 0 { - m.mGaugeVec = stats.GetGaugeVec(name, m.labels.names()...) - } else { - m.mGauge = stats.GetGauge(name) - } - m.handler = m.handleGauge - case "timing": - if len(m.labels) > 0 { - m.mTimerVec = stats.GetTimerVec(name, m.labels.names()...) - } else { - m.mTimer = stats.GetTimer(name) - } - m.handler = m.handleTimer - default: - return nil, fmt.Errorf("metric type unrecognised: %v", typeStr) - } - - return m, nil -} - -func (m *metricProcessor) handleCounter(val string, index int, msg message.Batch) error { - if len(m.labels) > 0 { - labelValues, err := m.labels.values(index, msg) - if err != nil { - return err - } - m.mCounterVec.With(labelValues...).Incr(1) - } else { - m.mCounter.Incr(1) - } - return nil -} - -func withNumberStr(val string, ifn func(i int64) error, ffn func(f float64) error) error { - if i, err := strconv.ParseInt(val, 10, 64); err == nil { - if i < 0 { - return fmt.Errorf("value %d is negative", i) - } - return ifn(i) - } - - f, err := strconv.ParseFloat(val, 64) - if err != nil { - return err - } - if f < 0 { - return fmt.Errorf("value %f is negative", f) - } - return ffn(f) -} - -func (m *metricProcessor) handleCounterBy(val string, index int, msg message.Batch) error { - if len(m.labels) > 0 { - labelValues, err := m.labels.values(index, msg) - if err != nil { - return err - } - return withNumberStr(val, func(i int64) error { - m.mCounterVec.With(labelValues...).Incr(i) - return nil - }, func(f float64) error { - m.mCounterVec.With(labelValues...).IncrFloat64(f) - return nil - }) - } - return withNumberStr(val, func(i int64) error { - m.mCounter.Incr(i) - return nil - }, func(f float64) error { - m.mCounter.IncrFloat64(f) - return nil - }) -} - -func (m *metricProcessor) handleGauge(val string, index int, msg message.Batch) error { - if len(m.labels) > 0 { - labelValues, err := m.labels.values(index, msg) - if err != nil { - return err - } - return withNumberStr(val, func(i int64) error { - m.mGaugeVec.With(labelValues...).Set(i) - return nil - }, func(f float64) error { - m.mGaugeVec.With(labelValues...).SetFloat64(f) - return nil - }) - } - return withNumberStr(val, func(i int64) error { - m.mGauge.Set(i) - return nil - }, func(f float64) error { - m.mGauge.SetFloat64(f) - return nil - }) -} - -func (m *metricProcessor) handleTimer(val string, index int, msg message.Batch) error { - i, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return err - } - if i < 0 { - return errors.New("value is negative") - } - if len(m.labels) > 0 { - labelValues, err := m.labels.values(index, msg) - if err != nil { - return err - } - m.mTimerVec.With(labelValues...).Timing(i) - } else { - m.mTimer.Timing(i) - } - return nil -} - -func (m *metricProcessor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - _ = msg.Iter(func(i int, p *message.Part) error { - value, err := m.value.String(i, msg) - if err != nil { - m.log.Error("Value interpolation error: %v", err) - return nil - } - if err := m.handler(value, i, msg); err != nil { - m.log.Error("Handler error: %v", err) - } - return nil - }) - return []message.Batch{msg}, nil -} - -func (m *metricProcessor) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_metric_test.go b/internal/impl/pure/processor_metric_test.go deleted file mode 100644 index 632783f2a5..0000000000 --- a/internal/impl/pure/processor_metric_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestMetricBad(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -metric: - type: bad type - name: some.path -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - require.Error(t, err) - - conf, err = testutil.ProcessorFromYAML(` -type: metric -`) - require.NoError(t, err) - - _, err = mock.NewManager().NewProcessor(conf) - require.Error(t, err) -} - -func TestMetricCounter(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -metric: - type: counter - name: foo.bar - value: '${!json("foo.bar")}' -`) - require.NoError(t, err) - - mockMetrics := metrics.NewLocal() - - mgr := mock.NewManager() - mgr.M = mockMetrics - - proc, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - inputs := [][][]byte{ - {}, - {}, - { - []byte(`{}`), - []byte(`{}`), - }, - { - []byte(`not even json`), - }, - {}, - { - []byte(`{"foo":{"bar":"hello world"}}`), - }, - { - []byte(`{"foo":{"bar":{"baz":"hello world"}}}`), - }, - } - - expMetrics := map[string]int64{ - "foo.bar": 4, - } - - for _, i := range inputs { - msg, res := proc.ProcessBatch(context.Background(), message.QuickBatch(i)) - assert.Len(t, msg, 1) - assert.NoError(t, res) - } - - assert.Equal(t, expMetrics, mockMetrics.FlushCounters()) -} - -func TestMetricCounterBy(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -metric: - type: counter_by - name: foo.bar - value: '${!json("foo.bar")}' -`) - require.NoError(t, err) - - mockMetrics := metrics.NewLocal() - - mgr := mock.NewManager() - mgr.M = mockMetrics - - proc, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - inputs := [][][]byte{ - {}, - {}, - { - []byte(`{"foo":{"bar":2}}`), - []byte(`{}`), - []byte(`{"foo":{"bar":3}}`), - }, - { - []byte(`not even json`), - }, - { - []byte(`{"foo":{"bar":-2}}`), - }, - { - []byte(`{"foo":{"bar":3}}`), - }, - { - []byte(`{"foo":{"bar":{"baz":"hello world"}}}`), - }, - } - - expMetrics := map[string]int64{ - "foo.bar": 8, - } - - for _, i := range inputs { - msg, res := proc.ProcessBatch(context.Background(), message.QuickBatch(i)) - assert.Len(t, msg, 1) - assert.NoError(t, res) - } - - assert.Equal(t, expMetrics, mockMetrics.FlushCounters()) -} - -func TestMetricGauge(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -metric: - type: gauge - name: foo.bar - value: '${!json("foo.bar")}' -`) - require.NoError(t, err) - - mockMetrics := metrics.NewLocal() - - mgr := mock.NewManager() - mgr.M = mockMetrics - - proc, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - inputs := [][][]byte{ - {}, - {}, - { - []byte(`{"foo":{"bar":5}}`), - []byte(`{}`), - }, - { - []byte(`not even json`), - }, - { - []byte(`{"foo":{"bar":-5}}`), - []byte(`{"foo":{"bar":7}}`), - }, - { - []byte(`{"foo":{"bar":"hello world"}}`), - }, - { - []byte(`{"foo":{"bar":{"baz":"hello world"}}}`), - }, - } - - expMetrics := map[string]int64{ - "foo.bar": 7, - } - - for _, i := range inputs { - msg, res := proc.ProcessBatch(context.Background(), message.QuickBatch(i)) - assert.Len(t, msg, 1) - assert.NoError(t, res) - } - - assert.Equal(t, expMetrics, mockMetrics.FlushCounters()) -} - -func TestMetricTiming(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -metric: - type: timing - name: foo.bar - value: '${!json("foo.bar")}' -`) - require.NoError(t, err) - - mockMetrics := metrics.NewLocal() - - mgr := mock.NewManager() - mgr.M = mockMetrics - - proc, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - inputs := [][][]byte{ - {}, - {}, - { - []byte(`{"foo":{"bar":5}}`), - []byte(`{}`), - }, - { - []byte(`not even json`), - }, - { - []byte(`{"foo":{"bar":-5}}`), - []byte(`{"foo":{"bar":7}}`), - }, - { - []byte(`{"foo":{"bar":"hello world"}}`), - }, - { - []byte(`{"foo":{"bar":{"baz":"hello world"}}}`), - }, - } - - for _, i := range inputs { - msg, res := proc.ProcessBatch(context.Background(), message.QuickBatch(i)) - assert.Len(t, msg, 1) - assert.NoError(t, res) - } - - expTimingAvgs := map[string]float64{ - "foo.bar": 6, - } - actTimingAvgs := map[string]float64{} - for k, v := range mockMetrics.FlushTimings() { - actTimingAvgs[k] = v.Mean() - } - - assert.Equal(t, expTimingAvgs, actTimingAvgs) -} diff --git a/internal/impl/pure/processor_mutation.go b/internal/impl/pure/processor_mutation.go deleted file mode 100644 index 50d638a4a9..0000000000 --- a/internal/impl/pure/processor_mutation.go +++ /dev/null @@ -1,172 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor( - "mutation", - service.NewConfigSpec(). - Stable(). - Version("4.5.0"). - Categories("Mapping", "Parsing"). - Field(service.NewBloblangField("")). - Summary("Executes a xref:guides:bloblang/about.adoc[Bloblang] mapping and directly transforms the contents of messages, mutating (or deleting) them."). - Description(` -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information, see xref:guides:bloblang/about.adoc[]. - -If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `+"`from \"\"`"+`, where the path must be absolute, or relative from the location that Benthos is executed from. - -== Input document mutability - -A mutation is a mapping that transforms input documents directly, this has the advantage of reducing the need to copy the data fed into the mapping. However, this also means that the referenced document is mutable and therefore changes throughout the mapping. For example, with the following Bloblang: - -`+"```coffeescript"+` -root.rejected = this.invitees.filter(i -> i.mood < 0.5) -root.invitees = this.invitees.filter(i -> i.mood >= 0.5) -`+"```"+` - -Notice that we create a field `+"`rejected`"+` by copying the array field `+"`invitees`"+` and filtering out objects with a high mood. We then overwrite the field `+"`invitees`"+` by filtering out objects with a low mood, resulting in two array fields that are each a subset of the original. If we were to reverse the ordering of these assignments like so: - -`+"```coffeescript"+` -root.invitees = this.invitees.filter(i -> i.mood >= 0.5) -root.rejected = this.invitees.filter(i -> i.mood < 0.5) -`+"```"+` - -Then the new field `+"`rejected`"+` would be empty as we have already mutated `+"`invitees`"+` to exclude the objects that it would be populated by. We can solve this problem either by carefully ordering our assignments or by capturing the original array using a variable (`+"`let invitees = this.invitees`"+`). - -Mutations are advantageous over a standard mapping in situations where the result is a document with mostly the same shape as the input document, since we can avoid unnecessarily copying data from the referenced input document. However, in situations where we are creating an entirely new document shape it can be more convenient to use the traditional `+"xref:components:processors/mapping.adoc[`mapping` processor]"+` instead. - -== Error handling - -Bloblang mappings can fail, in which case the error is logged and the message is flagged as having failed, allowing you to use xref:configuration:error_handling.adoc[standard processor error handling patterns]. - -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired xref:guides:bloblang/about.adoc#error-handling[fallback behavior]. - `). - Example("Mapping", ` -Given JSON documents containing an array of fans: - -`+"```json"+` -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"grace","obsession":0.21}, - {"name":"ali","obsession":0.89}, - {"name":"vic","obsession":0.43} - ] -} -`+"```"+` - -We can reduce the documents down to just the ID and only those fans with an obsession score above 0.5, giving us: - -`+"```json"+` -{ - "id":"foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"ali","obsession":0.89} - ] -} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - mutation: | - root.description = deleted() - root.fans = this.fans.filter(fan -> fan.obsession > 0.5) -`). - Example("More Mapping", ` -When receiving JSON documents of the form: - -`+"```json"+` -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -`+"```"+` - -We could collapse the location names from the state of Washington into a field `+"`Cities`"+`: - -`+"```json"+` -{"Cities": "Bellevue, Olympia, Seattle"} -`+"```"+` - -With the following config:`, - ` -pipeline: - processors: - - mutation: | - root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -`), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - mapping, err := conf.FieldBloblang() - if err != nil { - return nil, err - } - - v1Proc := processor.NewAutoObservedBatchedProcessor("mutation", newMutation(mapping, mgr.Logger()), interop.UnwrapManagement(mgr)) - return interop.NewUnwrapInternalBatchProcessor(v1Proc), nil - }) - if err != nil { - panic(err) - } -} - -type mutationProc struct { - exec *mapping.Executor - log *service.Logger -} - -func newMutation(exec *bloblang.Executor, log *service.Logger) *mutationProc { - uw := exec.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - return &mutationProc{ - exec: uw, - log: log, - } -} - -func (m *mutationProc) ProcessBatch(ctx *processor.BatchProcContext, b message.Batch) ([]message.Batch, error) { - newBatch := make(message.Batch, 0, len(b)) - for i, msg := range b { - newPart, err := m.exec.MapOnto(msg, i, b) - if err != nil { - ctx.OnError(err, i, msg) - m.log.Errorf("%v", err) - newBatch = append(newBatch, msg) - continue - } - if newPart != nil { - newBatch = append(newBatch, newPart) - } - } - if len(newBatch) == 0 { - return nil, nil - } - return []message.Batch{newBatch}, nil -} - -func (m *mutationProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_mutation_test.go b/internal/impl/pure/processor_mutation_test.go deleted file mode 100644 index f689857648..0000000000 --- a/internal/impl/pure/processor_mutation_test.go +++ /dev/null @@ -1,233 +0,0 @@ -package pure - -import ( - "context" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestMutationCreateCrossfire(t *testing.T) { - tCtx := context.Background() - - inMsg := message.NewPart(nil) - inMsg.SetStructuredMut(map[string]any{ - "foo": map[string]any{ - "bar": map[string]any{ - "baz": "original value", - "qux": "dont change", - }, - }, - }) - inMsg.MetaSetMut("foo", "orig1") - inMsg.MetaSetMut("bar", "orig2") - - inMsg2 := message.NewPart([]byte(`{}`)) - - exec, err := bloblang.Parse(` -a = batch_index() -foo = json("foo").from(0) -foo.bar_new = "this is swapped now" -foo.bar.baz = "and this changed" -meta foo = meta("foo").from(0) -meta bar = meta("bar").from(0) -meta baz = "new meta" -`) - require.NoError(t, err) - - proc := newMutation(exec, nil) - - inBatch := message.Batch{inMsg, inMsg2} - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 2) - - msgBytes := inMsg.AsBytes() - assert.Equal(t, `{"a":0,"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(msgBytes)) - v, _ := inMsg.MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = inMsg.MetaGetMut("bar") - assert.Equal(t, "orig2", v) - v, _ = inMsg.MetaGetMut("baz") - assert.Equal(t, "new meta", v) - - msgBytes = inMsg2.AsBytes() - assert.Equal(t, `{"a":1,"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(msgBytes)) - v, _ = inMsg2.MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = inMsg2.MetaGetMut("bar") - assert.Equal(t, "orig2", v) - v, _ = inMsg2.MetaGetMut("baz") - assert.Equal(t, "new meta", v) - - msgBytes = outBatches[0][0].AsBytes() - assert.Equal(t, `{"a":0,"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(msgBytes)) - v, _ = outBatches[0][0].MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = outBatches[0][0].MetaGetMut("bar") - assert.Equal(t, "orig2", v) - v, _ = outBatches[0][0].MetaGetMut("baz") - assert.Equal(t, "new meta", v) - - msgBytes = outBatches[0][1].AsBytes() - assert.Equal(t, `{"a":1,"foo":{"bar":{"baz":"and this changed","qux":"dont change"},"bar_new":"this is swapped now"}}`, string(msgBytes)) - v, _ = outBatches[0][1].MetaGetMut("foo") - assert.Equal(t, "orig1", v) - v, _ = outBatches[0][1].MetaGetMut("bar") - assert.Equal(t, "orig2", v) - v, _ = outBatches[0][1].MetaGetMut("baz") - assert.Equal(t, "new meta", v) -} - -func TestMutationCreateCustomObject(t *testing.T) { - tCtx := context.Background() - - part := message.NewPart(nil) - - gObj := gabs.New() - _, _ = gObj.ArrayOfSize(3, "foos") - - gObjEle := gabs.New() - _, _ = gObjEle.Set("FROM NEW OBJECT", "foo") - - _, _ = gObj.S("foos").SetIndex(gObjEle.Data(), 0) - _, _ = gObj.S("foos").SetIndex(5, 1) - - part.SetStructuredMut(gObj.Data()) - - exec, err := bloblang.Parse(`root.foos = this.foos`) - require.NoError(t, err) - - proc := newMutation(exec, nil) - - inBatch := message.Batch{part} - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 1) - - resPartBytes := outBatches[0][0].AsBytes() - assert.Equal(t, `{"foos":[{"foo":"FROM NEW OBJECT"},5,null]}`, string(resPartBytes)) -} - -func TestMutationCreateFiltering(t *testing.T) { - tCtx := context.Background() - - inBatch := message.Batch{ - message.NewPart([]byte(`{"foo":{"delete":true}}`)), - message.NewPart([]byte(`{"foo":{"dont":"delete me"}}`)), - message.NewPart([]byte(`{"bar":{"delete":true}}`)), - message.NewPart([]byte(`{"bar":{"dont":"delete me"}}`)), - } - - exec, err := bloblang.Parse(` -root = match { - (foo | bar).delete.or(false) => deleted(), -} -`) - require.NoError(t, err) - - proc := newMutation(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 2) - - assert.NoError(t, outBatches[0][0].ErrorGet()) - assert.NoError(t, outBatches[0][1].ErrorGet()) - - msgBytes := outBatches[0][0].AsBytes() - assert.Equal(t, `{"foo":{"dont":"delete me"}}`, string(msgBytes)) - - msgBytes = outBatches[0][1].AsBytes() - assert.Equal(t, `{"bar":{"dont":"delete me"}}`, string(msgBytes)) -} - -func TestMutationCreateFilterAll(t *testing.T) { - tCtx := context.Background() - - inBatch := message.Batch{ - message.NewPart([]byte(`{"foo":{"delete":true}}`)), - message.NewPart([]byte(`{"foo":{"dont":"delete me"}}`)), - message.NewPart([]byte(`{"bar":{"delete":true}}`)), - message.NewPart([]byte(`{"bar":{"dont":"delete me"}}`)), - } - - exec, err := bloblang.Parse(`root = deleted()`) - require.NoError(t, err) - - proc := newMutation(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, inBatch), inBatch) - assert.NoError(t, err) - assert.Empty(t, outBatches) -} - -func TestMutationCreateJSONError(t *testing.T) { - tCtx := context.Background() - - msg := message.Batch{ - message.NewPart([]byte(`this is not valid json`)), - } - - exec, err := bloblang.Parse(`foo = json().bar`) - require.NoError(t, err) - - proc := newMutation(exec, nil) - - outBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, msg), msg) - require.NoError(t, err) - require.Len(t, outBatches, 1) - require.Len(t, outBatches[0], 1) - - msgBytes := outBatches[0][0].AsBytes() - assert.Equal(t, `this is not valid json`, string(msgBytes)) - - err = outBatches[0][0].ErrorGet() - require.Error(t, err) - assert.Equal(t, `failed assignment (line 1): invalid character 'h' in literal true (expecting 'r')`, err.Error()) -} - -func BenchmarkMutationBasic(b *testing.B) { - blobl, err := bloblang.Parse(` -root = this -root.sum = this.a + this.b -`) - require.NoError(b, err) - - proc := newMutation(blobl, nil) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - tmpMsg := message.NewPart(nil) - tmpMsg.SetStructured(map[string]any{ - "a": 5, - "b": 7, - }) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - resBatches, err := proc.ProcessBatch(processor.TestBatchProcContext(tCtx, nil, nil), message.Batch{tmpMsg.ShallowCopy()}) - require.NoError(b, err) - require.Len(b, resBatches, 1) - require.Len(b, resBatches[0], 1) - - v, err := resBatches[0][0].AsStructured() - require.NoError(b, err) - assert.Equal(b, int64(12), v.(map[string]any)["sum"]) - } - - require.NoError(b, proc.Close(tCtx)) -} diff --git a/internal/impl/pure/processor_noop.go b/internal/impl/pure/processor_noop.go deleted file mode 100644 index 8314ebf7bd..0000000000 --- a/internal/impl/pure/processor_noop.go +++ /dev/null @@ -1,34 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("noop", service.NewConfigSpec(). - Stable(). - Summary("Noop is a processor that does nothing, the message passes through unchanged. Why? Sometimes doing nothing is the braver option."). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - p := &noopProcessor{} - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -type noopProcessor struct{} - -func (c *noopProcessor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - msgs := [1]message.Batch{msg} - return msgs[:], nil -} - -func (c *noopProcessor) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_parallel.go b/internal/impl/pure/processor_parallel.go deleted file mode 100644 index dfd26eb696..0000000000 --- a/internal/impl/pure/processor_parallel.go +++ /dev/null @@ -1,128 +0,0 @@ -package pure - -import ( - "context" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - parProcFieldCap = "cap" - parProcFieldProcessors = "processors" -) - -func init() { - err := service.RegisterBatchProcessor( - "parallel", service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message (similar to the `+"xref:components:processors/for_each.adoc[`for_each`]"+` processor), but where each message is processed in parallel.`). - Description(` -The field `+"`cap`"+`, if greater than zero, caps the maximum number of parallel processing threads. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching in xref:configuration:batching.adoc[].`). - Fields( - service.NewIntField(parProcFieldCap). - Description("The maximum number of messages to have processing at a given time."). - Default(0), - service.NewProcessorListField(parProcFieldProcessors). - Description("A list of child processors to apply."), - ), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - var p parallelProc - var err error - - if p.cap, err = conf.FieldInt(parProcFieldCap); err != nil { - return nil, err - } - - var pChildren []*service.OwnedProcessor - if pChildren, err = conf.FieldProcessorList(parProcFieldProcessors); err != nil { - return nil, err - } - p.children = make([]processor.V1, len(pChildren)) - for i, c := range pChildren { - p.children[i] = interop.UnwrapOwnedProcessor(c) - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("parallel", &p, interop.UnwrapManagement(mgr))), nil - }) - if err != nil { - panic(err) - } -} - -type parallelProc struct { - children []processor.V1 - cap int -} - -func (p *parallelProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - resultMsgs := make([]message.Batch, msg.Len()) - _ = msg.Iter(func(i int, p *message.Part) error { - resultMsgs[i] = message.Batch{p} - return nil - }) - - max := p.cap - if max == 0 || msg.Len() < max { - max = msg.Len() - } - - reqChan := make(chan int) - wg := sync.WaitGroup{} - wg.Add(max) - - for i := 0; i < max; i++ { - go func() { - defer wg.Done() - - for index := range reqChan { - resMsgs, err := processor.ExecuteAll(ctx.Context(), p.children, resultMsgs[index]) - if err != nil { - return - } - resultParts := []*message.Part{} - for _, m := range resMsgs { - _ = m.Iter(func(i int, p *message.Part) error { - resultParts = append(resultParts, p) - return nil - }) - } - resultMsgs[index] = resultParts - } - }() - } - for i := 0; i < msg.Len(); i++ { - reqChan <- i - } - close(reqChan) - wg.Wait() - - if err := ctx.Context().Err(); err != nil { - return nil, err - } - - resMsg := message.QuickBatch(nil) - for _, m := range resultMsgs { - _ = m.Iter(func(i int, p *message.Part) error { - resMsg = append(resMsg, p) - return nil - }) - } - - return []message.Batch{resMsg}, nil -} - -func (p *parallelProc) Close(ctx context.Context) error { - for _, c := range p.children { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_parallel_test.go b/internal/impl/pure/processor_parallel_test.go deleted file mode 100644 index cf85d393de..0000000000 --- a/internal/impl/pure/processor_parallel_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func parseYAMLConf(t testing.TB, formatStr string, args ...any) (conf processor.Config) { - t.Helper() - var err error - conf, err = testutil.ProcessorFromYAML(fmt.Sprintf(formatStr, args...)) - require.NoError(t, err) - return -} - -func TestParallelBasic(t *testing.T) { - wg := sync.WaitGroup{} - wg.Add(5) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wg.Done() - wg.Wait() - _, _ = w.Write([]byte("foobar")) - })) - defer ts.Close() - - conf := parseYAMLConf(t, ` -parallel: - processors: - - http: - url: %v/testpost -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("qux"), - []byte("quz"), - })) - if res != nil { - t.Error(res) - } else if expC, actC := 5, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func TestParallelError(t *testing.T) { - wg := sync.WaitGroup{} - wg.Add(5) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wg.Done() - wg.Wait() - reqBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - if string(reqBytes) == "baz" { - http.Error(w, "test error", http.StatusForbidden) - return - } - _, _ = w.Write([]byte("foobar")) - })) - defer ts.Close() - - conf := parseYAMLConf(t, ` -parallel: - processors: - - http: - url: %v/testpost - retries: 0 -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("qux"), - []byte("quz"), - })) - if res != nil { - t.Error(res) - } - if expC, actC := 5, msgs[0].Len(); actC != expC { - t.Fatalf("Wrong result count: %v != %v", actC, expC) - } - if exp, act := "baz", string(msgs[0].Get(2).AsBytes()); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } - assert.Error(t, msgs[0].Get(2).ErrorGet()) - for _, i := range []int{0, 1, 3, 4} { - if exp, act := "foobar", string(msgs[0].Get(i).AsBytes()); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } - assert.NoError(t, msgs[0].Get(i).ErrorGet()) - } -} - -func TestParallelCapped(t *testing.T) { - var reqs int64 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if req := atomic.AddInt64(&reqs, 1); req > 5 { - t.Errorf("Beyond parallelism cap: %v", req) - } - <-time.After(time.Millisecond * 10) - _, _ = w.Write([]byte("foobar")) - atomic.AddInt64(&reqs, -1) - })) - defer ts.Close() - - conf := parseYAMLConf(t, ` -parallel: - cap: 5 - processors: - - http: - url: %v/testpost -`, ts.URL) - - h, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := h.ProcessBatch(context.Background(), message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("qux"), - []byte("quz"), - []byte("foo2"), - []byte("bar2"), - []byte("baz2"), - []byte("qux2"), - []byte("quz2"), - })) - if res != nil { - t.Error(res) - } else if expC, actC := 10, msgs[0].Len(); actC != expC { - t.Errorf("Wrong result count: %v != %v", actC, expC) - } else if exp, act := "foobar", string(message.GetAllBytes(msgs[0])[0]); act != exp { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} diff --git a/internal/impl/pure/processor_parse_log.go b/internal/impl/pure/processor_parse_log.go deleted file mode 100644 index 92f34fd809..0000000000 --- a/internal/impl/pure/processor_parse_log.go +++ /dev/null @@ -1,317 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "strconv" - "time" - - syslog "github.com/influxdata/go-syslog/v3" - "github.com/influxdata/go-syslog/v3/rfc3164" - "github.com/influxdata/go-syslog/v3/rfc5424" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - plpFieldFormat = "format" - plpFieldCodec = "codec" - plpFieldBestEffort = "best_effort" - plpFieldWithRFC3339 = "allow_rfc3339" - plpFieldWithYear = "default_year" - plpFieldWithTimezone = "default_timezone" -) - -func parseLogSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Parsing"). - Stable(). - Summary(`Parses common log <> into <>. This is easier and often much faster than `+"xref:components:processors/grok.adoc[`grok`]"+`.`). - Footnotes(` -== Codecs - -Currently the only supported structured data codec is `+"`json`"+`. - -== Formats - -=== `+"`syslog_rfc5424`"+` - -Attempts to parse a log following the https://tools.ietf.org/html/rfc5424[Syslog rfc5424] spec. The resulting structured document may contain any of the following fields: - -- `+"`message`"+` (string) -- `+"`timestamp`"+` (string, RFC3339) -- `+"`facility`"+` (int) -- `+"`severity`"+` (int) -- `+"`priority`"+` (int) -- `+"`version`"+` (int) -- `+"`hostname`"+` (string) -- `+"`procid`"+` (string) -- `+"`appname`"+` (string) -- `+"`msgid`"+` (string) -- `+"`structureddata`"+` (object) - -=== `+"`syslog_rfc3164`"+` - -Attempts to parse a log following the https://tools.ietf.org/html/rfc3164[Syslog rfc3164] spec. The resulting structured document may contain any of the following fields: - -- `+"`message`"+` (string) -- `+"`timestamp`"+` (string, RFC3339) -- `+"`facility`"+` (int) -- `+"`severity`"+` (int) -- `+"`priority`"+` (int) -- `+"`hostname`"+` (string) -- `+"`procid`"+` (string) -- `+"`appname`"+` (string) -- `+"`msgid`"+` (string) -`). - Fields( - service.NewStringEnumField(plpFieldFormat, "syslog_rfc5424", "syslog_rfc3164"). - Description("A common log <> to parse."), - service.NewBoolField(plpFieldBestEffort). - Description("Still returns partially parsed messages even if an error occurs."). - Advanced(). - Default(true), - service.NewBoolField(plpFieldWithRFC3339). - Description("Also accept timestamps in rfc3339 format while parsing. Applicable to format `syslog_rfc3164`."). - Advanced(). - Default(true), - service.NewStringField(plpFieldWithYear). - Description("Sets the strategy used to set the year for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. When set to `current` the current year will be set, when set to an integer that value will be used. Leave this field empty to not set a default year at all."). - Advanced(). - Default("current"), - service.NewStringField(plpFieldWithTimezone). - Description("Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the https://golang.org/pkg/time/#LoadLocation[time.LoadLocation] format."). - Advanced(). - Default("UTC"), - service.NewStringField(plpFieldCodec).Deprecated(), - ) -} - -type parseLogConfig struct { - Format string - Codec string - BestEffort bool - WithRFC3339 bool - WithYear string - WithTimezone string -} - -func init() { - err := service.RegisterBatchProcessor( - - "parse_log", parseLogSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - var c parseLogConfig - var err error - - if c.Format, err = conf.FieldString(plpFieldFormat); err != nil { - return nil, err - } - if c.BestEffort, err = conf.FieldBool(plpFieldBestEffort); err != nil { - return nil, err - } - if c.WithRFC3339, err = conf.FieldBool(plpFieldWithRFC3339); err != nil { - return nil, err - } - if c.WithYear, err = conf.FieldString(plpFieldWithYear); err != nil { - return nil, err - } - if c.WithTimezone, err = conf.FieldString(plpFieldWithTimezone); err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newParseLog(c, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("parse_log", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type parserFormat func(body []byte) (map[string]any, error) - -func parserRFC5424(bestEffort bool) parserFormat { - var opts []syslog.MachineOption - if bestEffort { - opts = append(opts, rfc5424.WithBestEffort()) - } - p := rfc5424.NewParser(opts...) - - return func(body []byte) (map[string]any, error) { - resGen, err := p.Parse(body) - if err != nil { - return nil, err - } - res := resGen.(*rfc5424.SyslogMessage) - - resMap := make(map[string]any) - if res.Message != nil { - resMap["message"] = *res.Message - } - if res.Timestamp != nil { - resMap["timestamp"] = res.Timestamp.Format(time.RFC3339Nano) - } - if res.Facility != nil { - resMap["facility"] = *res.Facility - } - if res.Severity != nil { - resMap["severity"] = *res.Severity - } - if res.Priority != nil { - resMap["priority"] = *res.Priority - } - if res.Version != 0 { - resMap["version"] = res.Version - } - if res.Hostname != nil { - resMap["hostname"] = *res.Hostname - } - if res.ProcID != nil { - resMap["procid"] = *res.ProcID - } - if res.Appname != nil { - resMap["appname"] = *res.Appname - } - if res.MsgID != nil { - resMap["msgid"] = *res.MsgID - } - if res.StructuredData != nil { - structuredData := make(map[string]any, len(*res.StructuredData)) - for key, dataItem := range *res.StructuredData { - elements := make(map[string]any, len(dataItem)) - for itemKey, itemVal := range dataItem { - elements[itemKey] = itemVal - } - structuredData[key] = elements - } - resMap["structureddata"] = structuredData - } - - return resMap, nil - } -} - -func parserRFC3164(bestEffort, wrfc3339 bool, year, tz string) (parserFormat, error) { - var opts []syslog.MachineOption - if bestEffort { - opts = append(opts, rfc3164.WithBestEffort()) - } - if wrfc3339 { - opts = append(opts, rfc3164.WithRFC3339()) - } - switch year { - case "current": - opts = append(opts, rfc3164.WithYear(rfc3164.CurrentYear{})) - case "": - // do nothing - default: - iYear, err := strconv.Atoi(year) - if err != nil { - return nil, fmt.Errorf("failed to convert year %s into integer: %v", year, err) - } - opts = append(opts, rfc3164.WithYear(rfc3164.Year{YYYY: iYear})) - } - if tz != "" { - loc, err := time.LoadLocation(tz) - if err != nil { - return nil, fmt.Errorf("failed to lookup timezone %s - %v", loc, err) - } - opts = append(opts, rfc3164.WithTimezone(loc)) - } - - p := rfc3164.NewParser(opts...) - - return func(body []byte) (map[string]any, error) { - resGen, err := p.Parse(body) - if err != nil { - return nil, err - } - res := resGen.(*rfc3164.SyslogMessage) - - resMap := make(map[string]any) - if res.Message != nil { - resMap["message"] = *res.Message - } - if res.Timestamp != nil { - resMap["timestamp"] = res.Timestamp.Format(time.RFC3339Nano) - } - if res.Facility != nil { - resMap["facility"] = *res.Facility - } - if res.Severity != nil { - resMap["severity"] = *res.Severity - } - if res.Priority != nil { - resMap["priority"] = *res.Priority - } - if res.Hostname != nil { - resMap["hostname"] = *res.Hostname - } - if res.ProcID != nil { - resMap["procid"] = *res.ProcID - } - if res.Appname != nil { - resMap["appname"] = *res.Appname - } - if res.MsgID != nil { - resMap["msgid"] = *res.MsgID - } - - return resMap, nil - }, nil -} - -func getParseFormat(parser string, bestEffort, rfc3339 bool, defYear, defTZ string) (parserFormat, error) { - switch parser { - case "syslog_rfc5424": - return parserRFC5424(bestEffort), nil - case "syslog_rfc3164": - return parserRFC3164(bestEffort, rfc3339, defYear, defTZ) - } - return nil, fmt.Errorf("format not recognised: %s", parser) -} - -//------------------------------------------------------------------------------ - -type parseLogProc struct { - format parserFormat - formatStr string - log log.Modular -} - -func newParseLog(conf parseLogConfig, mgr bundle.NewManagement) (processor.AutoObserved, error) { - s := &parseLogProc{ - formatStr: conf.Format, - log: mgr.Logger(), - } - var err error - if s.format, err = getParseFormat(conf.Format, conf.BestEffort, conf.WithRFC3339, - conf.WithYear, conf.WithTimezone); err != nil { - return nil, err - } - return s, nil -} - -func (s *parseLogProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - dataMap, err := s.format(msg.AsBytes()) - if err != nil { - s.log.Debug("Failed to parse message as %s: %v", s.formatStr, err) - return nil, err - } - - msg.SetStructuredMut(dataMap) - return []*message.Part{msg}, nil -} - -func (s *parseLogProc) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_parse_log_test.go b/internal/impl/pure/processor_parse_log_test.go deleted file mode 100644 index bdffce1f8a..0000000000 --- a/internal/impl/pure/processor_parse_log_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestParseLogCases(t *testing.T) { - type testCase struct { - name string - input string - output string - format string - bestEff bool - } - tests := []testCase{ - { - name: "valid syslog_rfc5424 input, valid json output", - format: "syslog_rfc5424", - bestEff: true, - input: `<42>4 2049-10-11T22:14:15.003Z toaster.smarthome myapp - 2 [home01 device_id="43"] failed to make a toast.`, - output: `{"appname":"myapp","facility":5,"hostname":"toaster.smarthome","message":"failed to make a toast.","msgid":"2","priority":42,"severity":2,"structureddata":{"home01":{"device_id":"43"}},"timestamp":"2049-10-11T22:14:15.003Z","version":4}`, - }, - { - name: "invalid syslog_rfc5424 input, invalid json output", - format: "syslog_rfc5424", - bestEff: true, - input: `not a syslog at all.`, - output: `not a syslog at all.`, - }, - { - name: "valid syslog_rfc3164 input, valid json output", - format: "syslog_rfc3164", - bestEff: true, - input: `<28>Dec 2 16:49:23 host app[23410]: Test`, - output: fmt.Sprintf(`{"appname":"app","facility":3,"hostname":"host","message":"Test","priority":28,"procid":"23410","severity":4,"timestamp":"%v-12-02T16:49:23Z"}`, time.Now().Year()), - }, - } - - for _, test := range tests { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -parse_log: - format: %v - best_effort: %v -`, test.format, test.bestEff)) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - t.Run(test.name, func(tt *testing.T) { - msgsOut, res := proc.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte(test.input)})) - if res != nil { - tt.Fatal(res) - } - if len(msgsOut) != 1 { - tt.Fatalf("Wrong count of result messages: %v != 1", len(msgsOut)) - } - if exp, act := test.output, string(msgsOut[0].Get(0).AsBytes()); exp != act { - tt.Errorf("Wrong result: %v != %v", act, exp) - } - }) - } -} - -func TestParseLogRFC5424(t *testing.T) { - type testCase struct { - name string - input string - output string - } - tests := []testCase{ - { - name: "valid syslog_rfc5424 1", - input: `<42>4 2049-10-11T22:14:15.003Z toaster.smarthome myapp - 2 [home01 device_id="43"] failed to make a toast.`, - output: `{"appname":"myapp","facility":5,"hostname":"toaster.smarthome","message":"failed to make a toast.","msgid":"2","priority":42,"severity":2,"structureddata":{"home01":{"device_id":"43"}},"timestamp":"2049-10-11T22:14:15.003Z","version":4}`, - }, - { - name: "valid syslog_rfc5424 2", - input: `<23>4 2032-10-11T22:14:15.003Z foo.bar baz - 10 [home02 device_id="44"] test log.`, - output: `{"appname":"baz","facility":2,"hostname":"foo.bar","message":"test log.","msgid":"10","priority":23,"severity":7,"structureddata":{"home02":{"device_id":"44"}},"timestamp":"2032-10-11T22:14:15.003Z","version":4}`, - }, - } - - conf, err := testutil.ProcessorFromYAML(` -parse_log: - format: syslog_rfc5424 - best_effort: true -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - msgsOut, res := proc.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte(test.input)})) - if res != nil { - tt.Fatal(res) - } - if len(msgsOut) != 1 { - tt.Fatalf("Wrong count of result messages: %v != 1", len(msgsOut)) - } - if exp, act := test.output, string(msgsOut[0].Get(0).AsBytes()); exp != act { - tt.Errorf("Wrong result: %v != %v", act, exp) - } - - exe, err := bloblang.Parse(`json("structureddata").map_each(i -> if i.value.type() == "unknown" { throw("kaboom!") })`) - if err != nil { - tt.Errorf("Failed to parse bloblang: %s", err) - } - if _, err := service.NewInternalMessage(msgsOut[0].Get(0)).BloblangQuery(exe); err != nil { - tt.Errorf("Invalid structureddata field: %s", err) - } - }) - } -} diff --git a/internal/impl/pure/processor_processors.go b/internal/impl/pure/processor_processors.go deleted file mode 100644 index 322a6cdc20..0000000000 --- a/internal/impl/pure/processor_processors.go +++ /dev/null @@ -1,100 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func processorsProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`A processor grouping several sub-processors.`). - Description("This processor is useful in situations where you want to collect several processors under a single resource identifier, whether it is for making your configuration easier to read and navigate, or for improving the testability of your configuration. The behavior of child processors will match exactly the behavior they would have under any other processors block."). - Example( - "Grouped Processing", - "Imagine we have a collection of processors who cover a specific functionality. We could use this processor to group them together and make it easier to read and mock during testing by giving the whole block a label:", - ` -pipeline: - processors: - - label: my_super_feature - processors: - - log: - message: "Let's do something cool" - - archive: - format: json_array - - mapping: root.items = this -`, - ). - Field(service.NewProcessorListField("").Default([]any{})) -} - -func init() { - err := service.RegisterBatchProcessor( - "processors", processorsProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - mgr := interop.UnwrapManagement(res) - childPubProcs, err := conf.FieldProcessorList() - if err != nil { - return nil, err - } - - childProcs := make([]processor.V1, len(childPubProcs)) - for i, p := range childPubProcs { - childProcs[i] = interop.UnwrapOwnedProcessor(p) - } - - pp, err := newProcessorProc(childProcs, mgr) - if err != nil { - return nil, err - } - - p := processor.NewAutoObservedBatchedProcessor("processors", pp, mgr) - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -func newProcessorProc(children []processor.V1, mgr bundle.NewManagement) (*processorProc, error) { - return &processorProc{ - children: children, - log: mgr.Logger(), - }, nil -} - -type processorProc struct { - children []processor.V1 - log log.Modular -} - -func (p *processorProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - if msg.Len() == 0 { - return nil, nil - } - - resultMsgs, err := processor.ExecuteAll(ctx.Context(), p.children, msg) - if err != nil { - return nil, err - } - if len(resultMsgs) == 0 { - return nil, nil - } - return resultMsgs, nil -} - -func (p *processorProc) Close(ctx context.Context) error { - for _, c := range p.children { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_processors_test.go b/internal/impl/pure/processor_processors_test.go deleted file mode 100644 index ddd5d585aa..0000000000 --- a/internal/impl/pure/processor_processors_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestProcessors(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -processors: - - bloblang: 'root = content().uppercase()' - - bloblang: 'root = content().trim()' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - exp := [][][]byte{ - { - []byte(`HELLO FOO WORLD 1`), - []byte(`HELLO WORLD 1`), - []byte(`HELLO BAR WORLD 2`), - }, - } - act := [][][]byte{} - - input := message.QuickBatch([][]byte{ - []byte(` hello foo world 1 `), - []byte(` hello world 1 `), - []byte(` hello bar world 2 `), - }) - msgs, res := proc.ProcessBatch(context.Background(), input) - require.NoError(t, res) - - for _, msg := range msgs { - act = append(act, message.GetAllBytes(msg)) - } - assert.Equal(t, exp, act) -} diff --git a/internal/impl/pure/processor_rate_limit.go b/internal/impl/pure/processor_rate_limit.go deleted file mode 100644 index 3b55f20193..0000000000 --- a/internal/impl/pure/processor_rate_limit.go +++ /dev/null @@ -1,106 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - rlimitFieldResource = "resource" -) - -func rlimitProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`Throttles the throughput of a pipeline according to a specified ` + "xref:components:rate_limits/about.adoc[`rate_limit`]" + ` resource. Rate limits are shared across components and therefore apply globally to all processing pipelines.`). - Field(service.NewStringField(rlimitFieldResource). - Description("The target xref:components:rate_limits/about.adoc[`rate_limit` resource].")) -} - -func init() { - err := service.RegisterBatchProcessor( - "rate_limit", rlimitProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - resStr, err := conf.FieldString(rlimitFieldResource) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - r, err := newRateLimitProc(resStr, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedProcessor("rate_limit", r, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type rateLimitProc struct { - rlName string - mgr bundle.NewManagement - - closeChan chan struct{} - closeOnce sync.Once -} - -func newRateLimitProc(resStr string, mgr bundle.NewManagement) (*rateLimitProc, error) { - if !mgr.ProbeRateLimit(resStr) { - return nil, fmt.Errorf("rate limit resource '%v' was not found", resStr) - } - r := &rateLimitProc{ - rlName: resStr, - mgr: mgr, - closeChan: make(chan struct{}), - } - return r, nil -} - -func (r *rateLimitProc) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - for { - var waitFor time.Duration - var err error - if rerr := r.mgr.AccessRateLimit(ctx, r.rlName, func(rl ratelimit.V1) { - waitFor, err = rl.Access(ctx) - }); rerr != nil { - err = rerr - } - if ctx.Err() != nil { - return nil, err - } - if err != nil { - r.mgr.Logger().Error("Failed to access rate limit: %v", err) - waitFor = time.Second - } - if waitFor == 0 { - return []*message.Part{msg}, nil - } - select { - case <-time.After(waitFor): - case <-ctx.Done(): - return nil, ctx.Err() - case <-r.closeChan: - return nil, component.ErrTypeClosed - } - } -} - -func (r *rateLimitProc) Close(ctx context.Context) error { - r.closeOnce.Do(func() { - close(r.closeChan) - }) - return nil -} diff --git a/internal/impl/pure/processor_rate_limit_test.go b/internal/impl/pure/processor_rate_limit_test.go deleted file mode 100644 index 52f003c998..0000000000 --- a/internal/impl/pure/processor_rate_limit_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package pure_test - -import ( - "context" - "errors" - "reflect" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestRateLimitBasic(t *testing.T) { - var hits int32 - rlFn := func(context.Context) (time.Duration, error) { - atomic.AddInt32(&hits, 1) - return 0, nil - } - - mgr := mock.NewManager() - mgr.RateLimits["foo"] = rlFn - - conf, err := testutil.ProcessorFromYAML(` -rate_limit: - resource: foo -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1","value":"foo 1"}`), - []byte(`{"key":"2","value":"foo 2"}`), - []byte(`{"key":"1","value":"foo 3"}`), - }) - - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Fatal(res) - } - - if len(output) != 1 { - t.Fatalf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := message.GetAllBytes(input), message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - - if exp, act := int32(3), atomic.LoadInt32(&hits); exp != act { - t.Errorf("Wrong count of rate limit hits: %v != %v", act, exp) - } -} - -func TestRateLimitErroredOut(t *testing.T) { - rlFn := func(context.Context) (time.Duration, error) { - return 0, errors.New("omg foo") - } - - mgr := mock.NewManager() - mgr.RateLimits["foo"] = rlFn - - conf, err := testutil.ProcessorFromYAML(` -rate_limit: - resource: foo -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1","value":"foo 1"}`), - []byte(`{"key":"2","value":"foo 2"}`), - []byte(`{"key":"1","value":"foo 3"}`), - }) - - closedChan := make(chan struct{}) - go func() { - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Error(res) - } - - if len(output) != 1 { - t.Errorf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := message.GetAllBytes(input), message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - close(closedChan) - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, proc.Close(ctx)) - - select { - case <-closedChan: - case <-time.After(time.Second): - t.Error("Timed out") - } -} - -func TestRateLimitBlocked(t *testing.T) { - rlFn := func(context.Context) (time.Duration, error) { - return time.Second * 10, nil - } - - mgr := mock.NewManager() - mgr.RateLimits["foo"] = rlFn - - conf, err := testutil.ProcessorFromYAML(` -rate_limit: - resource: foo -`) - require.NoError(t, err) - - proc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - input := message.QuickBatch([][]byte{ - []byte(`{"key":"1","value":"foo 1"}`), - []byte(`{"key":"2","value":"foo 2"}`), - []byte(`{"key":"1","value":"foo 3"}`), - }) - - closedChan := make(chan struct{}) - go func() { - output, res := proc.ProcessBatch(context.Background(), input) - if res != nil { - t.Error(res) - } - - if len(output) != 1 { - t.Errorf("Wrong count of result messages: %v", len(output)) - } - - if exp, act := message.GetAllBytes(input), message.GetAllBytes(output[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result messages: %s != %s", act, exp) - } - close(closedChan) - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, proc.Close(ctx)) - - select { - case <-closedChan: - case <-time.After(time.Second): - t.Error("Timed out") - } -} diff --git a/internal/impl/pure/processor_resource.go b/internal/impl/pure/processor_resource.go deleted file mode 100644 index 95cd82a4ff..0000000000 --- a/internal/impl/pure/processor_resource.go +++ /dev/null @@ -1,94 +0,0 @@ -package pure - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("resource", service.NewConfigSpec(). - Stable(). - Categories("Utility"). - Summary("Resource is a processor type that runs a processor resource identified by its label."). - Description(` -This processor allows you to reference the same configured processor resource in multiple places, and can also tidy up large nested configs. For example, the config: - -`+"```yaml"+` -pipeline: - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() -`+"```"+` - -Is equivalent to: - -`+"```yaml"+` -pipeline: - processors: - - resource: foo_proc - -processor_resources: - - label: foo_proc - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() -`+"```"+` - -You can find out more about resources in xref:configuration:resources.adoc[]`). - Field(service.NewStringField("").Default("")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - name, err := conf.FieldString() - if err != nil { - return nil, err - } - p, err := newResourceProcessor(name, interop.UnwrapManagement(mgr)) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -type resourceProcessor struct { - mgr bundle.NewManagement - name string - log log.Modular -} - -func newResourceProcessor(name string, mgr bundle.NewManagement) (*resourceProcessor, error) { - if !mgr.ProbeProcessor(name) { - return nil, fmt.Errorf("processor resource '%v' was not found", name) - } - return &resourceProcessor{ - mgr: mgr, - name: name, - log: mgr.Logger(), - }, nil -} - -func (r *resourceProcessor) ProcessBatch(ctx context.Context, msg message.Batch) (msgs []message.Batch, res error) { - if err := r.mgr.AccessProcessor(ctx, r.name, func(p processor.V1) { - msgs, res = p.ProcessBatch(ctx, msg) - }); err != nil { - r.log.Error("Failed to obtain processor resource '%v': %v", r.name, err) - return nil, err - } - return msgs, res -} - -func (r *resourceProcessor) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_resource_test.go b/internal/impl/pure/processor_resource_test.go deleted file mode 100644 index 406a836d6c..0000000000 --- a/internal/impl/pure/processor_resource_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestResourceProc(t *testing.T) { - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = `root = "foo: " + content()` - - mgr := mock.NewManager() - - resProc, err := mgr.NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - mgr.Processors["foo"] = func(b message.Batch) ([]message.Batch, error) { - msgs, res := resProc.ProcessBatch(context.Background(), b) - if res != nil { - return nil, res - } - return msgs, nil - } - - nConf := processor.NewConfig() - nConf.Type = "resource" - nConf.Plugin = "foo" - - p, err := mgr.NewProcessor(nConf) - if err != nil { - t.Fatal(err) - } - - msgs, res := p.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - if res != nil { - t.Fatal(res) - } - if len(msgs) != 1 { - t.Error("Expected only 1 message") - } - if exp, act := "foo: bar", string(msgs[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func TestResourceBadName(t *testing.T) { - conf := processor.NewConfig() - conf.Type = "resource" - conf.Plugin = "foo" - - _, err := mock.NewManager().NewProcessor(conf) - if err == nil { - t.Error("expected error from bad resource") - } -} diff --git a/internal/impl/pure/processor_retry.go b/internal/impl/pure/processor_retry.go deleted file mode 100644 index 2d79a79f4f..0000000000 --- a/internal/impl/pure/processor_retry.go +++ /dev/null @@ -1,241 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/cenkalti/backoff/v4" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - rpFieldProcessors = "processors" - rpFieldBackoff = "backoff" - rpFieldParallel = "parallel" -) - -func retryProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Beta(). - Categories("Composition"). - Version("4.27.0"). - Summary(`Attempts to execute a series of child processors until success.`). - Description(` -Executes child processors and if a resulting message is errored then, after a specified backoff period, the same original message will be attempted again through those same processors. If the child processors result in more than one message then the retry mechanism will kick in if _any_ of the resulting messages are errored. - -It is important to note that any mutations performed on the message during these child processors will be discarded for the next retry, and therefore it is safe to assume that each execution of the child processors will always be performed on the data as it was when it first reached the retry processor. - -By default the retry backoff has a specified `+"<>"+`, if this time period is reached during retries and an error still occurs these errored messages will proceed through to the next processor after the retry (or your outputs). Normal xref:configuration:error_handling.adoc[error handling patterns] can be used on these messages. - -In order to avoid permanent loops any error associated with messages as they first enter a retry processor will be cleared. - -[CAUTION] -.Batching -==== -If you wish to wrap a batch-aware series of processors then take a look at the <>. -==== -`). - Footnotes(` -== Batching - -When messages are batched the child processors of a `+"retry"+` are executed for each individual message in isolation, performed serially by default but in parallel when the field `+"<> is set to `true`"+`. This is an intentional limitation of the retry processor and is done in order to ensure that errors are correctly associated with a given input message. Otherwise, the archiving, expansion, grouping, filtering and so on of the child processors could obfuscate this relationship. - -If the target behavior of your retried processors is "batch aware", in that you wish to perform some processing across the entire batch of messages and repeat it in the event of errors, you can use an `+"xref:components:processors/archive.adoc[`archive` processor]"+` to collapse the batch into an individual message. Then, within these child processors either perform your batch aware processing on the archive, or use an `+"xref:components:processors/unarchive.adoc[`unarchive` processor]"+` in order to expand the single message back out into a batch. - -For example, if the retry processor were being used to wrap an HTTP request where the payload data is a batch archived into a JSON array it should look something like this: - -`+"```yaml"+` -pipeline: - processors: - - archive: - format: json_array - - retry: - processors: - - http: - url: example.com/nope - verb: POST - - unarchive: - format: json_array -`+"```"+` -`). - Example("Stop ignoring me Taz", ` -Here we have a config where I generate animal noises and send them to Taz via HTTP. Taz has a tendency to stop his servers whenever I dispatch my animals upon him, and therefore these HTTP requests sometimes fail. However, I have the retry processor and with this super power I can specify a back off policy and it will ensure that for each animal noise the HTTP processor is attempted until either it succeeds or my Benthos instance is stopped. - -I even go as far as to zero-out the maximum elapsed time field, which means that for each animal noise I will wait indefinitely, because I really really want Taz to receive every single animal noise that he is entitled to.`, - ` -input: - generate: - interval: 1s - mapping: 'root.noise = [ "woof", "meow", "moo", "quack" ].index(random_int(min: 0, max: 3))' - -pipeline: - processors: - - retry: - backoff: - initial_interval: 100ms - max_interval: 5s - max_elapsed_time: 0s - processors: - - http: - url: 'http://example.com/try/not/to/dox/taz' - verb: POST - -output: - # Drop everything because it's junk data, I don't want it lol - drop: {} -`, - ). - Fields( - service.NewBackOffField(rpFieldBackoff, true, nil), - service.NewProcessorListField(rpFieldProcessors). - Description("A list of xref:components:processors/about.adoc[processors] to execute on each message."), - service.NewBoolField(rpFieldParallel). - Description("When processing batches of messages these batches are ignored and the processors apply to each message sequentially. However, when this field is set to `true` each message will be processed in parallel. Caution should be made to ensure that batch sizes do not surpass a point where this would cause resource (CPU, memory, API limits) contention."). - Default(false), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "retry", retryProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - mgr := interop.UnwrapManagement(res) - p := &retryProc{ - log: mgr.Logger(), - } - - procList, err := conf.FieldProcessorList(rpFieldProcessors) - if err != nil { - return nil, err - } - if len(procList) == 0 { - return nil, errors.New("at least one child processor must be specified") - } - for _, tmp := range procList { - p.children = append(p.children, interop.UnwrapOwnedProcessor(tmp)) - } - - if p.boff, err = conf.FieldBackOff(rpFieldBackoff); err != nil { - return nil, err - } - - if p.parallel, err = conf.FieldBool(rpFieldParallel); err != nil { - return nil, err - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("retry", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type retryProc struct { - children []processor.V1 - boff *backoff.ExponentialBackOff - parallel bool - log log.Modular -} - -func (r *retryProc) ProcessBatch(ctx *processor.BatchProcContext, msgs message.Batch) ([]message.Batch, error) { - var resMsg message.Batch - if r.parallel { - resBatches := make([][]message.Batch, len(msgs)) - - var wg sync.WaitGroup - wg.Add(len(msgs)) - - for i, tmp := range msgs { - go func(index int, p *message.Part) { - defer wg.Done() - var err error - if resBatches[index], err = r.dispatchMessage(ctx.Context(), p); err != nil { - return - } - }(i, tmp) - } - - wg.Wait() - if err := ctx.Context().Err(); err != nil { - return nil, err - } - - for _, batches := range resBatches { - for _, batch := range batches { - resMsg = append(resMsg, batch...) - } - } - } else { - for _, p := range msgs { - tmp, err := r.dispatchMessage(ctx.Context(), p) - if err != nil { - return nil, err - } - for _, b := range tmp { - resMsg = append(resMsg, b...) - } - } - } - return []message.Batch{resMsg}, nil -} - -func (r *retryProc) dispatchMessage(ctx context.Context, p *message.Part) ([]message.Batch, error) { - // NOTE: We always ensure we start off with a copy of the reference backoff. - boff := *r.boff - boff.Reset() - - // Ensure we do not start off with an error. - p.ErrorSet(nil) - - for { - resBatches, err := processor.ExecuteAll(ctx, r.children, message.Batch{p.ShallowCopy()}) - if err != nil { - return nil, err - } - - hasFailed := false - - errorChecks: - for _, b := range resBatches { - for _, m := range b { - if m.ErrorGet() != nil { - hasFailed = true - break errorChecks - } - } - } - - if !hasFailed { - return resBatches, nil - } - - nextSleep := boff.NextBackOff() - if nextSleep == backoff.Stop { - r.log.With("error", err).Debug("Error occured and maximum wait period was reached.") - return resBatches, nil - } - - r.log.With("error", err, "backoff", nextSleep).Debug("Error occured, sleeping for next backoff period.") - select { - case <-time.After(nextSleep): - case <-ctx.Done(): - return nil, ctx.Err() - } - } -} - -func (r *retryProc) Close(ctx context.Context) error { - for _, c := range r.children { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_retry_test.go b/internal/impl/pure/processor_retry_test.go deleted file mode 100644 index 1529bd6183..0000000000 --- a/internal/impl/pure/processor_retry_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestRetryHappy(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -retry: - processors: - - resource: foo -`) - require.NoError(t, err) - - mockMgr := mock.NewManager() - mockMgr.Processors["foo"] = func(b message.Batch) ([]message.Batch, error) { - b[0].SetBytes([]byte(string(b[0].AsBytes()) + " updated")) - return []message.Batch{ - {b[0]}, - }, nil - } - - p, err := mockMgr.NewProcessor(conf) - require.NoError(t, err) - - resBatches, err := p.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte("hello world a")), - message.NewPart([]byte("hello world b")), - message.NewPart([]byte("hello world c")), - }) - require.NoError(t, err) - require.Len(t, resBatches, 1) - require.Len(t, resBatches[0], 3) - - var resMsgs []string - for _, m := range resBatches[0] { - resMsgs = append(resMsgs, string(m.AsBytes())) - } - assert.Equal(t, []string{ - "hello world a updated", - "hello world b updated", - "hello world c updated", - }, resMsgs) - - require.NoError(t, p.Close(context.Background())) -} - -func TestRetryVerySad(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -retry: - backoff: - initial_interval: 1ms - max_interval: 10ms - max_elapsed_time: 100ms - processors: - - resource: foo -`) - require.NoError(t, err) - - mockMgr := mock.NewManager() - - var fooCalls uint32 - mockMgr.Processors["foo"] = func(b message.Batch) ([]message.Batch, error) { - b[0].SetBytes([]byte(string(b[0].AsBytes()) + " updated")) - b[0].ErrorSet(errors.New("nope")) - atomic.AddUint32(&fooCalls, 1) - return []message.Batch{ - {b[0]}, - }, nil - } - - p, err := mockMgr.NewProcessor(conf) - require.NoError(t, err) - - resBatches, err := p.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte("hello world a")), - message.NewPart([]byte("hello world b")), - message.NewPart([]byte("hello world c")), - }) - require.NoError(t, err) - require.Len(t, resBatches, 1) - require.Len(t, resBatches[0], 3) - - var resMsgs []string - for _, m := range resBatches[0] { - resMsgs = append(resMsgs, string(m.AsBytes())) - } - assert.Equal(t, []string{ - "hello world a updated", - "hello world b updated", - "hello world c updated", - }, resMsgs) - - assert.Greater(t, fooCalls, uint32(6)) - - require.NoError(t, p.Close(context.Background())) -} - -func TestRetryOneFailure(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -retry: - backoff: - initial_interval: 1ms - max_interval: 10ms - processors: - - resource: foo -`) - require.NoError(t, err) - - mockMgr := mock.NewManager() - - var fooCalls uint32 - mockMgr.Processors["foo"] = func(b message.Batch) ([]message.Batch, error) { - b[0].SetBytes([]byte(string(b[0].AsBytes()) + " updated")) - if atomic.AddUint32(&fooCalls, 1) == 1 { - b[0].ErrorSet(errors.New("nope")) - } - return []message.Batch{ - {b[0]}, - }, nil - } - - p, err := mockMgr.NewProcessor(conf) - require.NoError(t, err) - - resBatches, err := p.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte("hello world a")), - }) - require.NoError(t, err) - require.Len(t, resBatches, 1) - require.Len(t, resBatches[0], 1) - - var resMsgs []string - for _, m := range resBatches[0] { - resMsgs = append(resMsgs, string(m.AsBytes())) - } - assert.Equal(t, []string{ - "hello world a updated", - }, resMsgs) - - assert.Greater(t, fooCalls, uint32(1)) - - require.NoError(t, p.Close(context.Background())) -} - -func TestRetryParallelErrors(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -retry: - backoff: - initial_interval: 100ms - max_interval: 100ms - max_elapsed_time: 1s - parallel: true - processors: - - resource: foo -`) - require.NoError(t, err) - - mockMgr := mock.NewManager() - - var fooCalls, barCalls, bazCalls uint32 - mockMgr.Processors["foo"] = func(b message.Batch) ([]message.Batch, error) { - mContent := string(b[0].AsBytes()) - b[0].SetBytes([]byte(mContent + " updated")) - var calls uint32 - switch mContent { - case "foo": - calls = atomic.AddUint32(&fooCalls, 1) - case "bar": - calls = atomic.AddUint32(&barCalls, 1) - case "baz": - calls = atomic.AddUint32(&bazCalls, 1) - } - if calls == 1 { - b[0].ErrorSet(errors.New("nope")) - } - return []message.Batch{ - {b[0]}, - }, nil - } - - p, err := mockMgr.NewProcessor(conf) - require.NoError(t, err) - - tBefore := time.Now() - resBatches, err := p.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - }) - require.NoError(t, err) - require.Len(t, resBatches, 1) - require.Len(t, resBatches[0], 3) - - tTaken := time.Since(tBefore) - assert.Less(t, tTaken, time.Millisecond*200) - - var resMsgs []string - for _, m := range resBatches[0] { - resMsgs = append(resMsgs, string(m.AsBytes())) - } - assert.Equal(t, []string{ - "foo updated", - "bar updated", - "baz updated", - }, resMsgs) - - assert.Equal(t, uint32(2), fooCalls) - assert.Equal(t, uint32(2), barCalls) - assert.Equal(t, uint32(2), bazCalls) - - require.NoError(t, p.Close(context.Background())) -} diff --git a/internal/impl/pure/processor_select_parts.go b/internal/impl/pure/processor_select_parts.go deleted file mode 100644 index 0e232bed70..0000000000 --- a/internal/impl/pure/processor_select_parts.go +++ /dev/null @@ -1,85 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - spFieldParts = "parts" -) - -func init() { - err := service.RegisterBatchProcessor( - "select_parts", service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary("Cherry pick a set of messages from a batch by their index. Indexes larger than the number of messages are simply ignored."). - Description(` -The selected parts are added to the new message batch in the same order as the selection array. E.g. with 'parts' set to [ 2, 0, 1 ] and the message parts [ '0', '1', '2', '3' ], the output will be [ '2', '0', '1' ]. - -If none of the selected parts exist in the input batch (resulting in an empty output message) the batch is dropped entirely. - -Message indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. E.g. if index = -1 then the selected part will be the last part of the message, if index = -2 then the part before the last element with be selected, and so on. - -This processor is only applicable to xref:configuration:batching.adoc[batched messages].`). - Field(service.NewIntListField(spFieldParts). - Description(`An array of message indexes of a batch. Indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1.`). - Default([]any{})), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - partIndexes, err := conf.FieldIntList(spFieldParts) - if err != nil { - return nil, err - } - - proc, err := newSelectParts(partIndexes) - if err != nil { - return nil, err - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("select_parts", proc, interop.UnwrapManagement(mgr))), nil - }) - if err != nil { - panic(err) - } -} - -type selectPartsProc struct { - parts []int -} - -func newSelectParts(parts []int) (*selectPartsProc, error) { - return &selectPartsProc{ - parts: parts, - }, nil -} - -func (m *selectPartsProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - newMsg := message.QuickBatch(nil) - - lParts := msg.Len() - for _, index := range m.parts { - if index < 0 { - // Negative indexes count backwards from the end. - index = lParts + index - } - // Check boundary of part index. - if index < 0 || index >= lParts { - continue - } - newMsg = append(newMsg, msg.Get(index).ShallowCopy()) - } - - if newMsg.Len() == 0 { - return nil, nil - } - return []message.Batch{newMsg}, nil -} - -func (m *selectPartsProc) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_select_parts_test.go b/internal/impl/pure/processor_select_parts_test.go deleted file mode 100644 index 3d6718dc17..0000000000 --- a/internal/impl/pure/processor_select_parts_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "reflect" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestSelectParts(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -select_parts: - parts: [ 1, 3 ] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - type test struct { - in [][]byte - out [][]byte - } - - tests := []test{ - { - in: [][]byte{ - []byte("0"), - []byte("1"), - []byte("2"), - []byte("3"), - }, - out: [][]byte{ - []byte("1"), - []byte("3"), - }, - }, - { - in: [][]byte{ - []byte("0"), - []byte("1"), - }, - out: [][]byte{ - []byte("1"), - }, - }, - { - in: [][]byte{ - []byte("0"), - []byte("1"), - []byte("2"), - []byte("3"), - []byte("4"), - []byte("5"), - []byte("6"), - }, - out: [][]byte{ - []byte("1"), - []byte("3"), - }, - }, - } - - for _, test := range tests { - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(test.in)) - if len(msgs) != 1 { - t.Errorf("Select Parts failed on: %s", test.in) - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if exp, act := test.out, message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected output: %s != %s", act, exp) - } - } -} - -func TestSelectPartsIndexBounds(t *testing.T) { - input := [][]byte{ - []byte("0"), - []byte("1"), - []byte("2"), - []byte("3"), - []byte("4"), - } - - tests := map[int]string{ - -5: "0", - -4: "1", - -3: "2", - -2: "3", - -1: "4", - 0: "0", - 1: "1", - 2: "2", - 3: "3", - 4: "4", - } - - for i, exp := range tests { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -select_parts: - parts: [ %v ] -`, i)) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(input)) - if len(msgs) != 1 { - t.Errorf("Select Parts failed on index: %v", i) - } else if res != nil { - t.Errorf("Expected nil response: %v", res) - } - if act := string(message.GetAllBytes(msgs[0])[0]); exp != act { - t.Errorf("Unexpected output for index %v: %v != %v", i, act, exp) - } - } -} - -func TestSelectPartsEmpty(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -select_parts: - parts: [ 3 ] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - msgs, _ := proc.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("foo")})) - if len(msgs) != 0 { - t.Error("Expected failure with zero parts selected") - } -} diff --git a/internal/impl/pure/processor_sleep.go b/internal/impl/pure/processor_sleep.go deleted file mode 100644 index f89edeb8bc..0000000000 --- a/internal/impl/pure/processor_sleep.go +++ /dev/null @@ -1,98 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - spFieldDuration = "duration" -) - -func init() { - err := service.RegisterBatchProcessor("sleep", service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`Sleep for a period of time specified as a duration string for each message. This processor will interpolate functions within the `+"`duration`"+` field, you can find a list of functions xref:configuration:interpolation.adoc#bloblang-queries[here].`). - Field(service.NewInterpolatedStringField(spFieldDuration). - Description("The duration of time to sleep for each execution.")), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - sleepStr, err := conf.FieldString(spFieldDuration) - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p, err := newSleep(sleepStr, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("sleep", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type sleepProc struct { - closeOnce sync.Once - closeChan chan struct{} - durationStr *field.Expression - log log.Modular -} - -func newSleep(sleepStr string, mgr bundle.NewManagement) (*sleepProc, error) { - durationStr, err := mgr.BloblEnvironment().NewField(sleepStr) - if err != nil { - return nil, fmt.Errorf("failed to parse duration expression: %v", err) - } - t := &sleepProc{ - closeChan: make(chan struct{}), - durationStr: durationStr, - log: mgr.Logger(), - } - return t, nil -} - -func (s *sleepProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - for i := range msg { - periodStr, err := s.durationStr.String(i, msg) - if err != nil { - s.log.Error("Period interpolation error: %v", err) - continue - } - - period, err := time.ParseDuration(periodStr) - if err != nil { - s.log.Error("Failed to parse duration: %v", err) - continue - } - - select { - case <-time.After(period): - case <-ctx.Context().Done(): - return nil, ctx.Context().Err() - case <-s.closeChan: - return nil, errors.New("processor stopped") - } - } - return []message.Batch{msg}, nil -} - -func (s *sleepProc) Close(ctx context.Context) error { - s.closeOnce.Do(func() { - close(s.closeChan) - }) - return nil -} diff --git a/internal/impl/pure/processor_sleep_test.go b/internal/impl/pure/processor_sleep_test.go deleted file mode 100644 index ac285c302b..0000000000 --- a/internal/impl/pure/processor_sleep_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestSleep(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -sleep: - duration: 1ns -`) - require.NoError(t, err) - - slp, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - msgIn := message.QuickBatch([][]byte{[]byte("hello world")}) - msgsOut, err := slp.ProcessBatch(context.Background(), msgIn) - require.NoError(t, err) - require.Len(t, msgsOut, 1) - require.Len(t, msgsOut[0], 1) - assert.Equal(t, "hello world", string(msgsOut[0][0].AsBytes())) -} - -func TestSleepExit(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -sleep: - duration: 10s -`) - require.NoError(t, err) - - slp, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - doneChan := make(chan struct{}) - go func() { - _, _ = slp.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("hello world")})) - close(doneChan) - }() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, slp.Close(ctx)) - - select { - case <-doneChan: - case <-time.After(time.Second): - t.Error("took too long") - } -} - -func TestSleep200Millisecond(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -sleep: - duration: 200ms -`) - require.NoError(t, err) - - slp, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - tBefore := time.Now() - batches, err := slp.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("hello world")})) - tAfter := time.Now() - require.NoError(t, err) - require.Len(t, batches, 1) - - if dur := tAfter.Sub(tBefore); dur < (time.Millisecond * 200) { - t.Errorf("Message didn't take long enough") - } -} - -func TestSleepInterpolated(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -sleep: - duration: '${!json("foo")}ms' -`) - require.NoError(t, err) - - slp, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - tBefore := time.Now() - batches, err := slp.ProcessBatch(context.Background(), message.QuickBatch([][]byte{ - []byte(`{"foo":200}`), - })) - tAfter := time.Now() - require.NoError(t, err) - require.Len(t, batches, 1) - - if dur := tAfter.Sub(tBefore); dur < (time.Millisecond * 200) { - t.Errorf("Message didn't take long enough") - } -} diff --git a/internal/impl/pure/processor_split.go b/internal/impl/pure/processor_split.go deleted file mode 100644 index e013beae3d..0000000000 --- a/internal/impl/pure/processor_split.go +++ /dev/null @@ -1,95 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - splitPFieldSize = "size" - splitPFieldByteSize = "byte_size" -) - -func init() { - err := service.RegisterBatchProcessor( - "split", service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary(`Breaks message batches (synonymous with multiple part messages) into smaller batches. The size of the resulting batches are determined either by a discrete size or, if the field `+"`byte_size`"+` is non-zero, then by total size in bytes (which ever limit is reached first).`). - Description(` -This processor is for breaking batches down into smaller ones. In order to break a single message out into multiple messages use the `+"xref:components:processors/unarchive.adoc[`unarchive` processor]"+`. - -If there is a remainder of messages after splitting a batch the remainder is also sent as a single batch. For example, if your target size was 10, and the processor received a batch of 95 message parts, the result would be 9 batches of 10 messages followed by a batch of 5 messages.`). - Fields( - service.NewIntField(splitPFieldSize). - Description("The target number of messages."). - Default(1), - service.NewIntField(splitPFieldByteSize). - Description("An optional target of total message bytes."). - Default(0), - ), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - mgr := interop.UnwrapManagement(res) - s := &splitProc{log: mgr.Logger()} - - var err error - if s.size, err = conf.FieldInt(splitPFieldSize); err != nil { - return nil, err - } - if s.byteSize, err = conf.FieldInt(splitPFieldByteSize); err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("split", s, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type splitProc struct { - log log.Modular - - size int - byteSize int -} - -func (s *splitProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - if msg.Len() == 0 { - return nil, nil - } - - msgs := []message.Batch{} - - nextMsg := message.QuickBatch(nil) - byteSize := 0 - - _ = msg.Iter(func(i int, p *message.Part) error { - if (s.size > 0 && nextMsg.Len() >= s.size) || - (s.byteSize > 0 && (byteSize+len(p.AsBytes())) > s.byteSize) { - if nextMsg.Len() > 0 { - msgs = append(msgs, nextMsg) - nextMsg = message.QuickBatch(nil) - byteSize = 0 - } else { - s.log.Warn("A single message exceeds the target batch byte size of '%v', actual size: '%v'", s.byteSize, len(p.AsBytes())) - } - } - nextMsg = append(nextMsg, p) - byteSize += len(p.AsBytes()) - return nil - }) - - if nextMsg.Len() > 0 { - msgs = append(msgs, nextMsg) - } - return msgs, nil -} - -func (s *splitProc) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_split_test.go b/internal/impl/pure/processor_split_test.go deleted file mode 100644 index bd929b92a2..0000000000 --- a/internal/impl/pure/processor_split_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package pure_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestSplitToSingleParts(t *testing.T) { - conf := processor.NewConfig() - conf.Type = "split" - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - tests := [][][]byte{ - {}, - { - []byte("foo"), - }, - { - []byte("foo"), - []byte("bar"), - }, - { - []byte("foo"), - []byte("bar"), - []byte("baz"), - }, - } - - for _, tIn := range tests { - inMsg := message.QuickBatch(tIn) - _ = inMsg.Iter(func(i int, p *message.Part) error { - p.MetaSetMut("foo", "bar") - return nil - }) - msgs, _ := proc.ProcessBatch(context.Background(), inMsg) - if exp, act := len(tIn), len(msgs); exp != act { - t.Errorf("Wrong count of messages: %v != %v", act, exp) - continue - } - for i, expBytes := range tIn { - if act, exp := string(msgs[i].Get(0).AsBytes()), string(expBytes); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if act, exp := msgs[i].Get(0).MetaGetStr("foo"), "bar"; act != exp { - t.Errorf("Wrong metadata: %v != %v", act, exp) - } - } - } -} - -func TestSplitToMultipleParts(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -split: - size: 2 -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Error(err) - return - } - - inMsg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - }) - msgs, _ := proc.ProcessBatch(context.Background(), inMsg) - if exp, act := 2, len(msgs); exp != act { - t.Fatalf("Wrong message count: %v != %v", act, exp) - } - if exp, act := 2, msgs[0].Len(); exp != act { - t.Fatalf("Wrong message count: %v != %v", act, exp) - } - if exp, act := 1, msgs[1].Len(); exp != act { - t.Fatalf("Wrong message count: %v != %v", act, exp) - } - if exp, act := "foo", string(msgs[0].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if exp, act := "bar", string(msgs[0].Get(1).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if exp, act := "baz", string(msgs[1].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } -} - -func TestSplitByBytes(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -split: - size: 0 - byte_size: 6 -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - inMsg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - }) - msgs, _ := proc.ProcessBatch(context.Background(), inMsg) - if exp, act := 2, len(msgs); exp != act { - t.Fatalf("Wrong batch count: %v != %v", act, exp) - } - if exp, act := 2, msgs[0].Len(); exp != act { - t.Fatalf("Wrong message 1 count: %v != %v", act, exp) - } - if exp, act := 1, msgs[1].Len(); exp != act { - t.Fatalf("Wrong message 2 count: %v != %v", act, exp) - } - if exp, act := "foo", string(msgs[0].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if exp, act := "bar", string(msgs[0].Get(1).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if exp, act := "baz", string(msgs[1].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } -} - -func TestSplitByBytesTooLarge(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -split: - size: 0 - byte_size: 2 -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - inMsg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - }) - msgs, _ := proc.ProcessBatch(context.Background(), inMsg) - if exp, act := 3, len(msgs); exp != act { - t.Fatalf("Wrong batch count: %v != %v", act, exp) - } - if exp, act := 1, msgs[0].Len(); exp != act { - t.Fatalf("Wrong message 1 count: %v != %v", act, exp) - } - if exp, act := 1, msgs[1].Len(); exp != act { - t.Fatalf("Wrong message 2 count: %v != %v", act, exp) - } - if exp, act := 1, msgs[2].Len(); exp != act { - t.Fatalf("Wrong message 3 count: %v != %v", act, exp) - } - if exp, act := "foo", string(msgs[0].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if exp, act := "bar", string(msgs[1].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } - if exp, act := "baz", string(msgs[2].Get(0).AsBytes()); act != exp { - t.Errorf("Wrong contents: %v != %v", act, exp) - } -} diff --git a/internal/impl/pure/processor_switch.go b/internal/impl/pure/processor_switch.go deleted file mode 100644 index b0bcdec203..0000000000 --- a/internal/impl/pure/processor_switch.go +++ /dev/null @@ -1,245 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "sort" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - spFieldCheck = "check" - spFieldProcessors = "processors" - spFieldFallthrough = "fallthrough" -) - -func switchProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`Conditionally processes messages based on their contents.`). - Description(`For each switch case a xref:guides:bloblang/about.adoc[Bloblang query] is checked and, if the result is true (or the check is empty) the child processors are executed on the message.`). - Footnotes(` -== Batching - -When a switch processor executes on a xref:configuration:batching.adoc[batch of messages] they are checked individually and can be matched independently against cases. During processing the messages matched against a case are processed as a batch, although the ordering of messages during case processing cannot be guaranteed to match the order as received. - -At the end of switch processing the resulting batch will follow the same ordering as the batch was received. If any child processors have split or otherwise grouped messages this grouping will be lost as the result of a switch is always a single batch. In order to perform conditional grouping and/or splitting use the xref:components:processors/group_by.adoc[`+"`group_by`"+` processor].`). - Example("I Hate George", ` -We have a system where we're counting a metric for all messages that pass through our system. However, occasionally we get messages from George where he's rambling about dumb stuff we don't care about. - -For Georges messages we want to instead emit a metric that gauges how angry he is about being ignored and then we drop it.`, - ` -pipeline: - processors: - - switch: - - check: this.user.name.first != "George" - processors: - - metric: - type: counter - name: MessagesWeCareAbout - - - processors: - - metric: - type: gauge - name: GeorgesAnger - value: ${! json("user.anger") } - - mapping: root = deleted() -`, - ). - Field(service.NewObjectListField("", - service.NewBloblangField(spFieldCheck). - Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether a message should have the processors of this case executed on it. If left empty the case always passes. If the check mapping throws an error the message will be flagged xref:configuration:error_handling.adoc[as having failed] and will not be tested against any other cases."). - Examples( - `this.type == "foo"`, - `this.contents.urls.contains("https://benthos.dev/")`, - ). - Default(""), - service.NewProcessorListField(spFieldProcessors). - Description("A list of xref:components:processors/about.adoc[processors] to execute on a message."). - Default([]any{}), - service.NewBoolField(spFieldFallthrough). - Description("Indicates whether, if this case passes for a message, the next case should also be executed."). - Advanced(). - Default(false), - )) -} - -func init() { - err := service.RegisterBatchProcessor( - "switch", switchProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - caseConfs, err := conf.FieldObjectList() - if err != nil { - return nil, err - } - - mgr := interop.UnwrapManagement(res) - p := &switchProc{log: mgr.Logger()} - p.cases = make([]switchCase, len(caseConfs)) - for i, c := range caseConfs { - if p.cases[i], err = switchCaseFromParsed(c, mgr); err != nil { - return nil, fmt.Errorf("case '%v' parse error: %w", i, err) - } - } - - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("switch", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -// switchCase contains a condition, processors and other fields for an -// individual case in the Switch processor. -type switchCase struct { - check *mapping.Executor - processors []processor.V1 - fallThrough bool -} - -func switchCaseFromParsed(conf *service.ParsedConfig, mgr bundle.NewManagement) (c switchCase, err error) { - if checkStr, _ := conf.FieldString(spFieldCheck); checkStr != "" { - if c.check, err = mgr.BloblEnvironment().NewMapping(checkStr); err != nil { - return - } - } - - c.fallThrough, _ = conf.FieldBool(spFieldFallthrough) - - var iProcs []*service.OwnedProcessor - if iProcs, err = conf.FieldProcessorList(spFieldProcessors); err != nil { - return - } - if len(iProcs) == 0 { - err = errors.New("case has no processors, in order to have a no-op case use a `noop` processor") - return - } - - c.processors = make([]processor.V1, len(iProcs)) - for i, proc := range iProcs { - c.processors[i] = interop.UnwrapOwnedProcessor(proc) - } - return -} - -type switchProc struct { - cases []switchCase - log log.Modular -} - -// SwitchReorderFromGroup takes a message sort group and rearranges a slice of -// message parts so that they match up from their origins. -func SwitchReorderFromGroup(group *message.SortGroup, parts []*message.Part) { - partToIndex := map[*message.Part]int{} - for _, p := range parts { - if i := group.GetIndex(p); i >= 0 { - partToIndex[p] = i - } - } - - sort.SliceStable(parts, func(i, j int) bool { - if index, found := partToIndex[parts[i]]; found { - i = index - } - if index, found := partToIndex[parts[j]]; found { - j = index - } - return i < j - }) -} - -func (s *switchProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - var result []*message.Part - var remaining []*message.Part - var carryOver []*message.Part - - sortGroup, sortMsg := message.NewSortGroup(msg) - remaining = make([]*message.Part, sortMsg.Len()) - _ = sortMsg.Iter(func(i int, p *message.Part) error { - remaining[i] = p - return nil - }) - - for i, switchCase := range s.cases { - passed, failed := carryOver, []*message.Part{} - - // Form a message to test against, consisting of fallen through messages - // from prior cases plus remaining messages that haven't passed a case - // yet. - testMsg := message.Batch(remaining) - - for j, p := range remaining { - test := switchCase.check == nil - if !test { - var err error - if test, err = switchCase.check.QueryPart(j, testMsg); err != nil { - s.log.Error("Failed to test case %v: %v\n", i, err) - ctx.OnError(fmt.Errorf("failed to test case %v: %w", i, err), -1, p) - processor.MarkErr(p, nil, err) - result = append(result, p) - continue - } - } - if test { - passed = append(passed, p) - } else { - failed = append(failed, p) - } - } - - carryOver = nil - remaining = failed - - if len(passed) > 0 { - execMsg := message.Batch(passed) - - msgs, res := processor.ExecuteAll(ctx.Context(), switchCase.processors, execMsg) - if res != nil { - return nil, res - } - - for _, m := range msgs { - _ = m.Iter(func(_ int, p *message.Part) error { - if switchCase.fallThrough { - carryOver = append(carryOver, p) - } else { - result = append(result, p) - } - return nil - }) - } - } - } - - result = append(result, remaining...) - if len(result) > 1 { - SwitchReorderFromGroup(sortGroup, result) - } - - resMsg := message.Batch(result) - if resMsg.Len() == 0 { - return nil, nil - } - - return []message.Batch{resMsg}, nil -} - -func (s *switchProc) Close(ctx context.Context) error { - for _, c := range s.cases { - for _, p := range c.processors { - if err := p.Close(ctx); err != nil { - return err - } - } - } - return nil -} diff --git a/internal/impl/pure/processor_switch_test.go b/internal/impl/pure/processor_switch_test.go deleted file mode 100644 index 5346717bcb..0000000000 --- a/internal/impl/pure/processor_switch_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package pure_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestSwitchCases(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -switch: - - check: 'content().contains("A")' - processors: - - bloblang: 'root = "Hit case 0: " + content().string()' - - check: 'content().contains("B")' - processors: - - bloblang: 'root = "Hit case 1: " + content().string()' - fallthrough: true - - check: 'content().contains("C")' - processors: - - bloblang: 'root = "Hit case 2: " + content().string()' -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - defer func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, c.Close(ctx)) - }() - - type testCase struct { - name string - input []string - expected []string - } - tests := []testCase{ - { - name: "switch test 1", - input: []string{"A", "AB"}, - expected: []string{ - "Hit case 0: A", - "Hit case 0: AB", - }, - }, - { - name: "switch test 2", - input: []string{"B", "BC"}, - expected: []string{ - "Hit case 2: Hit case 1: B", - "Hit case 2: Hit case 1: BC", - }, - }, - { - name: "switch test 3", - input: []string{"C", "CD"}, - expected: []string{ - "Hit case 2: C", - "Hit case 2: CD", - }, - }, - { - name: "switch test 4", - input: []string{"A", "B", "C"}, - expected: []string{ - "Hit case 0: A", - "Hit case 2: Hit case 1: B", - "Hit case 2: C", - }, - }, - { - name: "switch test 5", - input: []string{"D"}, - expected: []string{"D"}, - }, - { - name: "switch test 6", - input: []string{"B", "C", "A"}, - expected: []string{ - "Hit case 2: Hit case 1: B", - "Hit case 2: C", - "Hit case 0: A", - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - msg := message.QuickBatch(nil) - for _, s := range test.input { - msg = append(msg, message.NewPart([]byte(s))) - } - msgs, res := c.ProcessBatch(context.Background(), msg) - require.NoError(t, res) - - resStrs := []string{} - for _, b := range message.GetAllBytes(msgs[0]) { - resStrs = append(resStrs, string(b)) - } - assert.Equal(t, test.expected, resStrs) - }) - } -} - -func TestSwitchError(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -switch: - - check: 'this.id.not_empty().contains("foo")' - processors: - - bloblang: 'root = "Hit case 0: " + content().string()' - - check: 'this.content.contains("bar")' - processors: - - bloblang: 'root = "Hit case 1: " + content().string()' -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - defer func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, c.Close(ctx)) - }() - - msg := message.Batch{ - message.NewPart([]byte(`{"id":"foo","content":"just a foo"}`)), - message.NewPart([]byte(`{"content":"bar but doesnt have an id!"}`)), - message.NewPart([]byte(`{"id":"buz","content":"a real foobar"}`)), - } - - msgs, res := c.ProcessBatch(context.Background(), msg) - require.NoError(t, res) - - assert.Len(t, msgs, 1) - assert.Equal(t, 3, msgs[0].Len()) - - resStrs := []string{} - for _, b := range message.GetAllBytes(msgs[0]) { - resStrs = append(resStrs, string(b)) - } - - assert.NoError(t, msgs[0].Get(0).ErrorGet()) - assert.EqualError(t, msgs[0].Get(1).ErrorGet(), "failed assignment (line 1): expected string, array or object value, got null from field `this.id`") - assert.NoError(t, msgs[0].Get(2).ErrorGet()) - - assert.Equal(t, []string{ - `Hit case 0: {"id":"foo","content":"just a foo"}`, - `{"content":"bar but doesnt have an id!"}`, - `Hit case 1: {"id":"buz","content":"a real foobar"}`, - }, resStrs) -} - -func BenchmarkSwitch10(b *testing.B) { - conf, err := testutil.ProcessorFromYAML(` -switch: - - check: 'content().contains("A")' - processors: - - bloblang: 'root = "Hit case 0: " + content().string()' - - check: 'content().contains("B")' - processors: - - bloblang: 'root = "Hit case 1: " + content().string()' - fallthrough: true - - check: 'content().contains("C")' - processors: - - bloblang: 'root = "Hit case 2: " + content().string()' -`) - require.NoError(b, err) - - c, err := mock.NewManager().NewProcessor(conf) - require.NoError(b, err) - defer func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(b, c.Close(ctx)) - }() - - msg := message.QuickBatch([][]byte{ - []byte("A"), - []byte("B"), - []byte("C"), - []byte("D"), - []byte("AB"), - []byte("AC"), - []byte("AD"), - []byte("BC"), - []byte("BD"), - []byte("CD"), - }) - - exp := [][]byte{ - []byte("Hit case 0: A"), - []byte("Hit case 2: Hit case 1: B"), - []byte("Hit case 2: C"), - []byte("D"), - []byte("Hit case 0: AB"), - []byte("Hit case 0: AC"), - []byte("Hit case 0: AD"), - []byte("Hit case 2: Hit case 1: BC"), - []byte("Hit case 2: Hit case 1: BD"), - []byte("Hit case 2: CD"), - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - msgs, res := c.ProcessBatch(context.Background(), msg) - require.NoError(b, res) - assert.Equal(b, exp, message.GetAllBytes(msgs[0])) - } -} - -func BenchmarkSwitch1(b *testing.B) { - conf, err := testutil.ProcessorFromYAML(` -switch: - - check: 'content().contains("A")' - processors: - - bloblang: 'root = "Hit case 0: " + content().string()' - - check: 'content().contains("B")' - processors: - - bloblang: 'root = "Hit case 1: " + content().string()' - fallthrough: true - - check: 'content().contains("C")' - processors: - - bloblang: 'root = "Hit case 2: " + content().string()' -`) - require.NoError(b, err) - - c, err := mock.NewManager().NewProcessor(conf) - require.NoError(b, err) - defer func() { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(b, c.Close(ctx)) - }() - - msgs := []message.Batch{ - message.QuickBatch([][]byte{[]byte("A")}), - message.QuickBatch([][]byte{[]byte("B")}), - message.QuickBatch([][]byte{[]byte("C")}), - message.QuickBatch([][]byte{[]byte("D")}), - message.QuickBatch([][]byte{[]byte("AB")}), - message.QuickBatch([][]byte{[]byte("AC")}), - message.QuickBatch([][]byte{[]byte("AD")}), - message.QuickBatch([][]byte{[]byte("BC")}), - message.QuickBatch([][]byte{[]byte("BD")}), - message.QuickBatch([][]byte{[]byte("CD")}), - } - - exp := [][]byte{ - []byte("Hit case 0: A"), - []byte("Hit case 2: Hit case 1: B"), - []byte("Hit case 2: C"), - []byte("D"), - []byte("Hit case 0: AB"), - []byte("Hit case 0: AC"), - []byte("Hit case 0: AD"), - []byte("Hit case 2: Hit case 1: BC"), - []byte("Hit case 2: Hit case 1: BD"), - []byte("Hit case 2: CD"), - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - resMsgs, res := c.ProcessBatch(context.Background(), msgs[i%len(msgs)]) - require.NoError(b, res) - assert.Equal(b, [][]byte{exp[i%len(exp)]}, message.GetAllBytes(resMsgs[0])) - } -} - -func BenchmarkSortCorrect(b *testing.B) { - sortedParts := make([]*message.Part, b.N) - for i := range sortedParts { - sortedParts[i] = message.NewPart([]byte(fmt.Sprintf("hello world %040d", i))) - } - - group, parts := message.NewSortGroup(sortedParts) - - b.ReportAllocs() - b.ResetTimer() - - pure.SwitchReorderFromGroup(group, parts) -} - -func BenchmarkSortReverse(b *testing.B) { - sortedParts := make([]*message.Part, b.N) - for i := range sortedParts { - sortedParts[i] = message.NewPart([]byte(fmt.Sprintf("hello world %040d", i))) - } - - group, parts := message.NewSortGroup(sortedParts) - unsortedParts := make([]*message.Part, b.N) - for i := range parts { - unsortedParts[i] = parts[len(parts)-i-1] - } - - b.ReportAllocs() - b.ResetTimer() - - pure.SwitchReorderFromGroup(group, unsortedParts) -} diff --git a/internal/impl/pure/processor_sync_response.go b/internal/impl/pure/processor_sync_response.go deleted file mode 100644 index f501c38a3a..0000000000 --- a/internal/impl/pure/processor_sync_response.go +++ /dev/null @@ -1,45 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("sync_response", service.NewConfigSpec(). - Categories("Utility"). - Stable(). - Summary("Adds the payload in its current state as a synchronous response to the input source, where it is dealt with according to that specific input type."). - Description(` -For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this processor even when combining input types that might not have support for sync responses. An example of an input able to utilize this is the `+"`http_server`"+`. - -For more information please read xref:guides:sync_responses.adoc[synchronous responses].`). - Field(service.NewObjectField("").Default(map[string]any{})), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - p := &syncResponseProc{log: interop.UnwrapManagement(mgr).Logger()} - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -type syncResponseProc struct { - log log.Modular -} - -func (s *syncResponseProc) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - if err := transaction.SetAsResponse(msg); err != nil { - s.log.Debug("Failed to store message as a sync response: %v\n", err) - } - return []message.Batch{msg}, nil -} - -func (s *syncResponseProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_try.go b/internal/impl/pure/processor_try.go deleted file mode 100644 index 95ae8b043b..0000000000 --- a/internal/impl/pure/processor_try.go +++ /dev/null @@ -1,131 +0,0 @@ -package pure - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterBatchProcessor("try", service.NewConfigSpec(). - Stable(). - Categories("Composition"). - Summary("Executes a list of child processors on messages only if no prior processors have failed (or the errors have been cleared)."). - Description(` -This processor behaves similarly to the `+"xref:components:processors/for_each.adoc[`for_each`]"+` processor, where a list of child processors are applied to individual messages of a batch. However, if a message has failed any prior processor (before or during the try block) then that message will skip all following processors. - -For example, with the following config: - -`+"```yaml"+` -pipeline: - processors: - - resource: foo - - try: - - resource: bar - - resource: baz - - resource: buz -`+"```"+` - -If the processor `+"`bar`"+` fails for a particular message, that message will skip the processors `+"`baz` and `buz`"+`. Similarly, if `+"`bar`"+` succeeds but `+"`baz`"+` does not then `+"`buz`"+` will be skipped. If the processor `+"`foo`"+` fails for a message then none of `+"`bar`, `baz` or `buz`"+` are executed on that message. - -This processor is useful for when child processors depend on the successful output of previous processors. This processor can be followed with a `+"xref:components:processors/catch.adoc[catch]"+` processor for defining child processors to be applied only to failed messages. - -More information about error handing can be found in xref:configuration:error_handling.adoc[]. - -== Nest within a catch block - -In some cases it might be useful to nest a try block within a catch block, since the `+"xref:components:processors/catch.adoc[`catch` processor]"+` only clears errors _after_ executing its child processors this means a nested try processor will not execute unless the errors are explicitly cleared beforehand. - -This can be done by inserting an empty catch block before the try block like as follows: - -`+"```yaml"+` -pipeline: - processors: - - resource: foo - - catch: - - log: - level: ERROR - message: "Foo failed due to: ${! error() }" - - catch: [] # Clear prior error - - try: - - resource: bar - - resource: baz -`+"```"+``). - Field(service.NewProcessorListField("").Default([]any{})), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - mgr := interop.UnwrapManagement(res) - childPubProcs, err := conf.FieldProcessorList() - if err != nil { - return nil, err - } - - childProcs := make([]processor.V1, len(childPubProcs)) - for i, p := range childPubProcs { - childProcs[i] = interop.UnwrapOwnedProcessor(p) - } - - tp, err := newTryProc(childProcs, mgr) - if err != nil { - return nil, err - } - - p := processor.NewAutoObservedBatchedProcessor("try", tp, mgr) - return interop.NewUnwrapInternalBatchProcessor(p), nil - }) - if err != nil { - panic(err) - } -} - -type tryProc struct { - children []processor.V1 - log log.Modular -} - -func newTryProc(children []processor.V1, mgr bundle.NewManagement) (*tryProc, error) { - return &tryProc{ - children: children, - log: mgr.Logger(), - }, nil -} - -func (p *tryProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) ([]message.Batch, error) { - resultMsgs := make([]message.Batch, msg.Len()) - _ = msg.Iter(func(i int, p *message.Part) error { - resultMsgs[i] = message.Batch{p} - return nil - }) - - var err error - if resultMsgs, err = processor.ExecuteTryAll(ctx.Context(), p.children, resultMsgs...); err != nil || len(resultMsgs) == 0 { - return nil, err - } - - resMsg := message.QuickBatch(nil) - for _, m := range resultMsgs { - _ = m.Iter(func(i int, p *message.Part) error { - resMsg = append(resMsg, p) - return nil - }) - } - if resMsg.Len() == 0 { - return nil, nil - } - - resMsgs := [1]message.Batch{resMsg} - return resMsgs[:], nil -} - -func (p *tryProc) Close(ctx context.Context) error { - for _, c := range p.children { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_try_test.go b/internal/impl/pure/processor_try_test.go deleted file mode 100644 index 5ad615368f..0000000000 --- a/internal/impl/pure/processor_try_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package pure_test - -import ( - "context" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestTryEmpty(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -try: [] -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][]byte{ - []byte("foo bar baz"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(exp)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestTryBasic(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -try: - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("Zm9vIGJhciBiYXo="), - []byte("MSAyIDMgNA=="), - []byte("aGVsbG8gZm9vIHdvcmxk"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestTryFilterSome(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -try: - - bloblang: 'root = if !content().contains("foo") { deleted() }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("foo bar baz"), - []byte("hello foo world"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestTryMultiProcs(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -try: - - bloblang: 'root = if !content().contains("foo") { deleted() }' - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("foo bar baz"), - []byte("1 2 3 4"), - []byte("hello foo world"), - } - exp := [][]byte{ - []byte("Zm9vIGJhciBiYXo="), - []byte("aGVsbG8gZm9vIHdvcmxk"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } -} - -func TestTryFailJSON(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -try: - - jmespath: - query: 'foo' - - bloblang: 'root = if batch_index() == 0 { content().encode("base64") }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte(`{"foo":{"bar":"baz"}}`), - []byte("NOT VALID JSON"), - []byte(`{"foo":{"bar":"baz2"}}`), - } - exp := [][]byte{ - []byte("eyJiYXIiOiJiYXoifQ=="), - []byte("NOT VALID JSON"), - []byte("eyJiYXIiOiJiYXoyIn0="), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if res != nil { - t.Fatal(res) - } - - if len(msgs) != 1 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } - if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong results: %s != %s", act, exp) - } - if msgs[0].Get(0).ErrorGet() != nil { - t.Error("Unexpected part 0 failed flag") - } - if msgs[0].Get(1).ErrorGet() == nil { - t.Error("Unexpected part 1 failed flag") - } - if msgs[0].Get(2).ErrorGet() != nil { - t.Error("Unexpected part 2 failed flag") - } -} - -func TestTryFilterAll(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -try: - - bloblang: 'root = if !content().contains("foo") { deleted() }' -`) - require.NoError(t, err) - - proc, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - parts := [][]byte{ - []byte("bar baz"), - []byte("1 2 3 4"), - []byte("hello world"), - } - msgs, res := proc.ProcessBatch(context.Background(), message.QuickBatch(parts)) - assert.NoError(t, res) - if len(msgs) != 0 { - t.Errorf("Wrong count of result msgs: %v", len(msgs)) - } -} diff --git a/internal/impl/pure/processor_unarchive.go b/internal/impl/pure/processor_unarchive.go deleted file mode 100644 index d1fb4143cb..0000000000 --- a/internal/impl/pure/processor_unarchive.go +++ /dev/null @@ -1,360 +0,0 @@ -package pure - -import ( - "archive/tar" - "archive/zip" - "bytes" - "context" - "encoding/csv" - "encoding/json" - "errors" - "fmt" - "io" - "strings" - - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func unarchiveProcConfig() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Categories("Parsing", "Utility"). - Summary("Unarchives messages according to the selected archive format into multiple messages within a xref:configuration:batching.adoc[batch]."). - Description(` -When a message is unarchived the new messages replace the original message in the batch. Messages that are selected but fail to unarchive (invalid format) will remain unchanged in the message batch but will be flagged as having failed, allowing you to xref:configuration:error_handling.adoc[error handle them]. - -== Metadata - -The metadata found on the messages handled by this processor will be copied into the resulting messages. For the unarchive formats that contain file information (tar, zip), a metadata field is also added to each message called ` + "`archive_filename`" + ` with the extracted filename. -`). - Field(service.NewStringAnnotatedEnumField("format", map[string]string{ - `tar`: `Extract messages from a unix standard tape archive.`, - `zip`: `Extract messages from a zip file.`, - `binary`: `Extract messages from a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format].`, - `lines`: `Extract the lines of a message each into their own message.`, - `json_documents`: `Attempt to parse a message as a stream of concatenated JSON documents. Each parsed document is expanded into a new message.`, - `json_array`: `Attempt to parse a message as a JSON array, and extract each element into its own message.`, - `json_map`: `Attempt to parse the message as a JSON map and for each element of the map expands its contents into a new message. A metadata field is added to each message called ` + "`archive_key`" + ` with the relevant key from the top-level map.`, - `csv`: `Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message.`, - `csv:x`: `Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message using a custom delimiter. The custom delimiter must be a single character, e.g. the format "csv:\t" would consume a tab delimited file.`, - }).Description("The unarchiving format to apply.").LintRule(``)) // NOTE: We disable the linter here because `csv:x` is a dynamic pattern -} - -func init() { - err := service.RegisterProcessor( - "unarchive", unarchiveProcConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return newUnarchiveFromParsed(conf, mgr) - }) - if err != nil { - panic(err) - } -} - -type unarchiveFunc func(part *service.Message) (service.MessageBatch, error) - -func tarUnarchive(part *service.Message) (service.MessageBatch, error) { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - - buf := bytes.NewBuffer(pBytes) - tr := tar.NewReader(buf) - - var newParts []*service.Message - - // Iterate through the files in the archive. - for { - h, err := tr.Next() - if errors.Is(err, io.EOF) { - // end of tar archive - break - } - if err != nil { - return nil, err - } - - newPartBuf := bytes.Buffer{} - if _, err = newPartBuf.ReadFrom(tr); err != nil { - return nil, err - } - - newPart := part.Copy() - newPart.SetBytes(newPartBuf.Bytes()) - newPart.MetaSet("archive_filename", h.Name) - newParts = append(newParts, newPart) - } - - return newParts, nil -} - -func zipUnarchive(part *service.Message) (service.MessageBatch, error) { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - - buf := bytes.NewReader(pBytes) - zr, err := zip.NewReader(buf, int64(buf.Len())) - if err != nil { - return nil, err - } - - var newParts service.MessageBatch - - // Iterate through the files in the archive. - for _, f := range zr.File { - fr, err := f.Open() - if err != nil { - return nil, err - } - - newPartBuf := bytes.Buffer{} - if _, err = newPartBuf.ReadFrom(fr); err != nil { - return nil, err - } - - newPart := part.Copy() - newPart.SetBytes(newPartBuf.Bytes()) - newPart.MetaSet("archive_filename", f.Name) - newParts = append(newParts, newPart) - } - - return newParts, nil -} - -func binaryUnarchive(part *service.Message) (service.MessageBatch, error) { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - - parts, err := message.DeserializeBytes(pBytes) - if err != nil { - return nil, err - } - - batch := make(service.MessageBatch, len(parts)) - for i, p := range parts { - batch[i] = part.Copy() - batch[i].SetBytes(p) - } - return batch, nil -} - -func linesUnarchive(part *service.Message) (service.MessageBatch, error) { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - - lines := bytes.Split(pBytes, []byte("\n")) - - batch := make(service.MessageBatch, len(lines)) - for i, p := range lines { - batch[i] = part.Copy() - batch[i].SetBytes(p) - } - return batch, nil -} - -func jsonDocumentsUnarchive(part *service.Message) (service.MessageBatch, error) { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - - var parts service.MessageBatch - dec := json.NewDecoder(bytes.NewReader(pBytes)) - for { - var m any - if err := dec.Decode(&m); errors.Is(err, io.EOF) { - break - } else if err != nil { - return nil, err - } - newPart := part.Copy() - newPart.SetStructuredMut(m) - parts = append(parts, newPart) - } - return parts, nil -} - -func jsonArrayUnarchive(part *service.Message) (service.MessageBatch, error) { - jDoc, err := part.AsStructuredMut() - if err != nil { - return nil, fmt.Errorf("failed to parse message into JSON array: %v", err) - } - - jArray, ok := jDoc.([]any) - if !ok { - return nil, fmt.Errorf("failed to parse message into JSON array: invalid type '%T'", jDoc) - } - - parts := make(service.MessageBatch, len(jArray)) - for i, ele := range jArray { - newPart := part.Copy() - newPart.SetStructuredMut(ele) - parts[i] = newPart - } - return parts, nil -} - -func jsonMapUnarchive(part *service.Message) (service.MessageBatch, error) { - jDoc, err := part.AsStructuredMut() - if err != nil { - return nil, fmt.Errorf("failed to parse message into JSON map: %v", err) - } - - jMap, ok := jDoc.(map[string]any) - if !ok { - return nil, fmt.Errorf("failed to parse message into JSON map: invalid type '%T'", jDoc) - } - - parts := make(service.MessageBatch, len(jMap)) - i := 0 - for key, ele := range jMap { - newPart := part.Copy() - newPart.SetStructuredMut(ele) - newPart.MetaSet("archive_key", key) - parts[i] = newPart - i++ - } - return parts, nil -} - -func csvUnarchive(customComma *rune) func(*service.Message) (service.MessageBatch, error) { - return func(part *service.Message) (service.MessageBatch, error) { - pBytes, err := part.AsBytes() - if err != nil { - return nil, err - } - - buf := bytes.NewReader(pBytes) - - scanner := csv.NewReader(buf) - scanner.ReuseRecord = true - if customComma != nil { - scanner.Comma = *customComma - } - - var newParts []*service.Message - var headers []string - - for { - var records []string - records, err = scanner.Read() - if err != nil { - break - } - - if headers == nil { - headers = make([]string, len(records)) - copy(headers, records) - continue - } - - if len(records) < len(headers) { - err = errors.New("row has too few values") - break - } - - if len(records) > len(headers) { - err = errors.New("row has too many values") - break - } - - obj := make(map[string]any, len(records)) - for i, r := range records { - obj[headers[i]] = r - } - - newPart := part.Copy() - newPart.SetStructuredMut(obj) - newParts = append(newParts, newPart) - } - - if !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("failed to parse message as csv: %v", err) - } - - return newParts, nil - } -} - -func strToUnarchiver(str string) (unarchiveFunc, error) { - switch str { - case "tar": - return tarUnarchive, nil - case "zip": - return zipUnarchive, nil - case "binary": - return binaryUnarchive, nil - case "lines": - return linesUnarchive, nil - case "json_documents": - return jsonDocumentsUnarchive, nil - case "json_array": - return jsonArrayUnarchive, nil - case "json_map": - return jsonMapUnarchive, nil - case "csv": - return csvUnarchive(nil), nil - } - - if strings.HasPrefix(str, "csv:") { - by := strings.TrimPrefix(str, "csv:") - if by == "" { - return nil, errors.New("csv format requires a non-empty delimiter") - } - byRunes := []rune(by) - if len(byRunes) != 1 { - return nil, errors.New("csv format requires a single character delimiter") - } - byRune := byRunes[0] - return csvUnarchive(&byRune), nil - } - - return nil, fmt.Errorf("archive format not recognised: %v", str) -} - -//------------------------------------------------------------------------------ - -type unarchiveProc struct { - unarchive unarchiveFunc - log *service.Logger -} - -func newUnarchiveFromParsed(conf *service.ParsedConfig, mgr *service.Resources) (*unarchiveProc, error) { - formatStr, err := conf.FieldString("format") - if err != nil { - return nil, err - } - return newUnarchive(mgr, formatStr) -} - -func newUnarchive(nm *service.Resources, format string) (*unarchiveProc, error) { - unarchiver, err := strToUnarchiver(format) - if err != nil { - return nil, err - } - return &unarchiveProc{ - unarchive: unarchiver, - log: nm.Logger(), - }, nil -} - -func (d *unarchiveProc) Process(ctx context.Context, msg *service.Message) (service.MessageBatch, error) { - newParts, err := d.unarchive(msg) - if err != nil { - d.log.Errorf("Failed to unarchive message part: %v\n", err) - return nil, err - } - return newParts, nil -} - -func (d *unarchiveProc) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/processor_unarchive_test.go b/internal/impl/pure/processor_unarchive_test.go deleted file mode 100644 index 1bf25f0b00..0000000000 --- a/internal/impl/pure/processor_unarchive_test.go +++ /dev/null @@ -1,362 +0,0 @@ -package pure - -import ( - "archive/tar" - "archive/zip" - "bytes" - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestUnarchiveBadAlgo(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: does not exist -`, nil) - require.NoError(t, err) - - _, err = newUnarchiveFromParsed(conf, service.MockResources()) - if err == nil { - t.Error("Expected error from bad algo") - } -} - -func TestUnarchiveTar(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: tar -`, nil) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - expNames := []string{} - - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - - for i := range input { - exp = append(exp, input[i]) - - hdr := &tar.Header{ - Name: fmt.Sprintf("testfile%v", i), - Mode: 0o600, - Size: int64(len(input[i])), - } - expNames = append(expNames, hdr.Name) - if err := tw.WriteHeader(hdr); err != nil { - t.Fatal(err) - } - if _, err := tw.Write(input[i]); err != nil { - t.Fatal(err) - } - } - - if err := tw.Close(); err != nil { - t.Fatal(err) - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage(buf.Bytes())) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - key, exists := msgs[i].MetaGet("archive_filename") - require.True(t, exists) - assert.Equal(t, expNames[i], key) - - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, string(e), string(mBytes)) - } -} - -func TestUnarchiveZip(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: zip -`, nil) - require.NoError(t, err) - - input := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - exp := [][]byte{} - expNames := []string{} - - var buf bytes.Buffer - zw := zip.NewWriter(&buf) - - for i := range input { - exp = append(exp, input[i]) - - name := fmt.Sprintf("testfile%v", i) - expNames = append(expNames, name) - if fw, err := zw.Create(name); err != nil { - t.Fatal(err) - } else if _, err := fw.Write(input[i]); err != nil { - t.Fatal(err) - } - } - - if err := zw.Close(); err != nil { - t.Fatal(err) - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage(buf.Bytes())) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - key, exists := msgs[i].MetaGet("archive_filename") - require.True(t, exists) - assert.Equal(t, expNames[i], key) - - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, string(e), string(mBytes)) - } -} - -func TestUnarchiveLines(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: lines -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte("hello world first part"), - []byte("hello world second part"), - []byte("third part"), - []byte("fourth"), - []byte("5"), - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage([]byte( - `hello world first part -hello world second part -third part -fourth -5`, - ))) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, string(e), string(mBytes)) - } -} - -func TestUnarchiveJSONDocuments(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: json_documents -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte(`{"foo":"bar"}`), - []byte(`5`), - []byte(`"testing 123"`), - []byte(`["root","is","an","array"]`), - []byte(`{"bar":"baz"}`), - []byte(`true`), - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage([]byte( - `{"foo":"bar"} 5 "testing 123" ["root", "is", "an", "array"] {"bar": "baz"} true`, - ))) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, string(e), string(mBytes)) - } -} - -func TestUnarchiveJSONArray(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: json_array -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte(`{"foo":"bar"}`), - []byte(`5`), - []byte(`"testing 123"`), - []byte(`["nested","array"]`), - []byte(`true`), - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage([]byte( - `[{"foo":"bar"},5,"testing 123",["nested","array"],true]`, - ))) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, string(e), string(mBytes)) - } -} - -func TestUnarchiveJSONMap(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: json_map -`, nil) - require.NoError(t, err) - - exp := map[string]string{ - "a": `{"foo":"bar"}`, - "b": `5`, - "c": `"testing 123"`, - "d": `["nested","array"]`, - "e": `true`, - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage([]byte( - `{"a":{"foo":"bar"},"b":5,"c":"testing 123","d":["nested","array"],"e":true}`, - ))) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i := 0; i < len(exp); i++ { - key, exists := msgs[i].MetaGet("archive_key") - require.True(t, exists) - - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - - assert.Equal(t, exp[key], string(mBytes)) - delete(exp, key) - } -} - -func TestUnarchiveBinary(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: binary -`, nil) - require.NoError(t, err) - - exp := [][]byte{ - []byte(`{"foo":"bar"}`), - []byte(`5`), - []byte(`"testing 123"`), - []byte(`["nested","array"]`), - []byte(`true`), - } - testMsgBlob := message.SerializeBytes(exp) - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage(testMsgBlob)) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, string(e), string(mBytes)) - } -} - -func TestUnarchiveCSV(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: csv -`, nil) - require.NoError(t, err) - - exp := []string{ - `{"color":"blue","id":"1","name":"foo"}`, - `{"color":"green","id":"2","name":"bar"}`, - `{"color":"red","id":"3","name":"baz"}`, - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage([]byte( - `id,name,color -1,foo,blue -2,bar,green -3,baz,red`, - ))) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, e, string(mBytes)) - } -} - -func TestUnarchiveCSVCustom(t *testing.T) { - conf, err := unarchiveProcConfig().ParseYAML(` -format: csv:| -`, nil) - require.NoError(t, err) - - exp := []string{ - `{"color":"blue","id":"1","name":"foo"}`, - `{"color":"green","id":"2","name":"bar"}`, - `{"color":"red","id":"3","name":"baz"}`, - } - - proc, err := newUnarchiveFromParsed(conf, service.MockResources()) - require.NoError(t, err) - - msgs, res := proc.Process(context.Background(), service.NewMessage([]byte( - `id|name|color -1|foo|blue -2|bar|green -3|baz|red`, - ))) - require.NoError(t, res) - require.Len(t, msgs, len(exp)) - - for i, e := range exp { - mBytes, err := msgs[i].AsBytes() - require.NoError(t, err) - assert.Equal(t, e, string(mBytes)) - } -} diff --git a/internal/impl/pure/processor_while.go b/internal/impl/pure/processor_while.go deleted file mode 100644 index 0949ebe033..0000000000 --- a/internal/impl/pure/processor_while.go +++ /dev/null @@ -1,187 +0,0 @@ -package pure - -import ( - "context" - "errors" - "fmt" - "strconv" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - wpFieldAtLeastOnce = "at_least_once" - wpFieldMaxLoops = "max_loops" - wpFieldCheck = "check" - wpFieldProcessors = "processors" -) - -func whileProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary("A processor that checks a xref:guides:bloblang/about.adoc[Bloblang query] against each batch of messages and executes child processors on them for as long as the query resolves to true."). - Description(` -The field `+"`at_least_once`"+`, if true, ensures that the child processors are always executed at least one time (like a do .. while loop.) - -The field `+"`max_loops`"+`, if greater than zero, caps the number of loops for a message batch to this value. - -If following a loop execution the number of messages in a batch is reduced to zero the loop is exited regardless of the condition result. If following a loop execution there are more than 1 message batches the query is checked against the first batch only. - -The conditions of this processor are applied across entire message batches. You can find out more about batching xref:configuration:batching.adoc[in this doc].`). - Fields( - - service.NewBoolField(wpFieldAtLeastOnce). - Description("Whether to always run the child processors at least one time."). - Default(false), - service.NewIntField(wpFieldMaxLoops). - Description("An optional maximum number of loops to execute. Helps protect against accidentally creating infinite loops."). - Advanced(). - Default(0), - service.NewBloblangField(wpFieldCheck). - Description("A xref:guides:bloblang/about.adoc[Bloblang query] that should return a boolean value indicating whether the while loop should execute again."). - Examples(`errored()`, `this.urls.unprocessed.length() > 0`). - Default(""), - service.NewProcessorListField(wpFieldProcessors). - Description("A list of child processors to execute on each loop."), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "while", whileProcSpec(), - func(conf *service.ParsedConfig, res *service.Resources) (service.BatchProcessor, error) { - maxLoops, err := conf.FieldInt(wpFieldMaxLoops) - if err != nil { - return nil, err - } - - atLeastOnce, err := conf.FieldBool(wpFieldAtLeastOnce) - if err != nil { - return nil, err - } - - checkStr, err := conf.FieldString(wpFieldCheck) - if err != nil { - return nil, err - } - - iProcs, err := conf.FieldProcessorList(wpFieldProcessors) - if err != nil { - return nil, err - } - - children := make([]processor.V1, len(iProcs)) - for i, c := range iProcs { - children[i] = interop.UnwrapOwnedProcessor(c) - } - - mgr := interop.UnwrapManagement(res) - p, err := newWhile(maxLoops, atLeastOnce, checkStr, children, mgr) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(processor.NewAutoObservedBatchedProcessor("while", p, mgr)), nil - }) - if err != nil { - panic(err) - } -} - -type whileProc struct { - maxLoops int - atLeastOnce bool - check *mapping.Executor - children []processor.V1 - log log.Modular - - shutSig *shutdown.Signaller -} - -func newWhile(maxLoops int, atLeastOnce bool, checkStr string, children []processor.V1, mgr bundle.NewManagement) (*whileProc, error) { - var check *mapping.Executor - var err error - - if checkStr != "" { - if check, err = mgr.BloblEnvironment().NewMapping(checkStr); err != nil { - return nil, fmt.Errorf("failed to parse check query: %w", err) - } - } else { - return nil, errors.New("a check query is required") - } - - return &whileProc{ - maxLoops: maxLoops, - atLeastOnce: atLeastOnce, - check: check, - children: children, - log: mgr.Logger(), - shutSig: shutdown.NewSignaller(), - }, nil -} - -func (w *whileProc) checkMsg(msg message.Batch) bool { - c, err := w.check.QueryPart(0, msg) - if err != nil { - c = false - w.log.Error("Query failed for loop: %v", err) - } - return c -} - -func (w *whileProc) ProcessBatch(ctx *processor.BatchProcContext, msg message.Batch) (msgs []message.Batch, res error) { - msgs = []message.Batch{msg} - - loops := 0 - condResult := w.atLeastOnce || w.checkMsg(msg) - for condResult { - if w.shutSig.IsSoftStopSignalled() || ctx.Context().Err() != nil { - return nil, component.ErrTypeClosed - } - if w.maxLoops > 0 && loops >= w.maxLoops { - w.log.Trace("Reached max loops count") - break - } - - w.log.Trace("Looped") - for i := range msg { - ctx.Span(i).LogKV("event", "loop") - } - - msgs, res = processor.ExecuteAll(ctx.Context(), w.children, msgs...) - if len(msgs) == 0 { - return - } - condResult = w.checkMsg(msgs[0]) - loops++ - } - - for i := range msg { - ctx.Span(i).SetTag("result", strconv.FormatBool(condResult)) - } - - totalParts := 0 - for _, msg := range msgs { - totalParts += msg.Len() - } - return -} - -func (w *whileProc) Close(ctx context.Context) error { - w.shutSig.TriggerHardStop() - for _, p := range w.children { - if err := p.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/processor_while_test.go b/internal/impl/pure/processor_while_test.go deleted file mode 100644 index 0de023d46c..0000000000 --- a/internal/impl/pure/processor_while_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package pure_test - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestWhileErrs(t *testing.T) { - conf := processor.NewConfig() - conf.Type = "while" - - _, err := mock.NewManager().NewProcessor(conf) - require.Error(t, err) - require.Contains(t, err.Error(), "a check query is required") -} - -func TestWhileWithCount(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -while: - check: 'count("while_test_1") < 3' - processors: - - insert_part: - content: foo - index: 0 -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - exp := [][]byte{ - []byte(`foo`), - []byte(`foo`), - []byte(`bar`), - } - - msg, res := c.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - require.NoError(t, res) - - assert.Equal(t, exp, message.GetAllBytes(msg[0])) -} - -func TestWhileWithContentCheck(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -while: - check: 'batch_size() <= 3' - processors: - - insert_part: - content: foo - index: 0 -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][]byte{ - []byte(`foo`), - []byte(`foo`), - []byte(`foo`), - []byte(`bar`), - } - - msg, res := c.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - if res != nil { - t.Error(res) - } - if act := message.GetAllBytes(msg[0]); !reflect.DeepEqual(act, exp) { - t.Errorf("Wrong result: %s != %s", act, exp) - } -} - -func TestWhileWithCountALO(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -while: - check: 'count("while_test_2") < 3' - at_least_once: true - processors: - - insert_part: - content: foo - index: 0 -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][]byte{ - []byte(`foo`), - []byte(`foo`), - []byte(`foo`), - []byte(`bar`), - } - - msg, res := c.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - if res != nil { - t.Error(res) - } - if act := message.GetAllBytes(msg[0]); !reflect.DeepEqual(act, exp) { - t.Errorf("Wrong result: %s != %s", act, exp) - } -} - -func TestWhileMaxLoops(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -while: - check: 'true' - max_loops: 3 - processors: - - insert_part: - content: foo - index: 0 -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - exp := [][]byte{ - []byte(`foo`), - []byte(`foo`), - []byte(`foo`), - []byte(`bar`), - } - - msg, res := c.ProcessBatch(context.Background(), message.QuickBatch([][]byte{[]byte("bar")})) - if res != nil { - t.Error(res) - } - if act := message.GetAllBytes(msg[0]); !reflect.DeepEqual(act, exp) { - t.Errorf("Wrong result: %s != %s", act, exp) - } -} - -func TestWhileWithStaticTrue(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -while: - check: 'true' - processors: - - insert_part: - content: 'foo' - index: 0 - - sleep: - duration: 100ms -`) - require.NoError(t, err) - - c, err := mock.NewManager().NewProcessor(conf) - if err != nil { - t.Fatal(err) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - go func() { - <-time.After(time.Millisecond * 100) - assert.NoError(t, c.Close(ctx)) - }() - - _, err = c.ProcessBatch(ctx, message.QuickBatch([][]byte{[]byte("bar")})) - assert.NoError(t, err) -} diff --git a/internal/impl/pure/processor_workflow.go b/internal/impl/pure/processor_workflow.go deleted file mode 100644 index 6c7c4fa764..0000000000 --- a/internal/impl/pure/processor_workflow.go +++ /dev/null @@ -1,591 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "sort" - "sync" - "time" - - "github.com/Jeffail/gabs/v2" - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/interop" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - wflowProcFieldMetaPath = "meta_path" - wflowProcFieldOrder = "order" - wflowProcFieldBranchResources = "branch_resources" - wflowProcFieldBranches = "branches" -) - -func workflowProcSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Categories("Composition"). - Stable(). - Summary(`Executes a topology of `+"xref:components:processors/branch.adoc[`branch` processors]"+`, performing them in parallel where possible.`). - Description(` -== Why use a workflow - -=== Performance - -Most of the time the best way to compose processors is also the simplest, just configure them in series. This is because processors are often CPU bound, low-latency, and you can gain vertical scaling by increasing the number of processor pipeline threads, allowing Benthos to process xref:configuration:processing_pipelines.adoc[multiple messages in parallel]. - -However, some processors such as `+"xref:components:processors/http.adoc[`http`], xref:components:processors/aws_lambda.adoc[`aws_lambda`] or xref:components:processors/cache.adoc[`cache`]"+` interact with external services and therefore spend most of their time waiting for a response. These processors tend to be high-latency and low CPU activity, which causes messages to process slowly. - -When a processing pipeline contains multiple network processors that aren't dependent on each other we can benefit from performing these processors in parallel for each individual message, reducing the overall message processing latency. - -=== Simplifying processor topology - -A workflow is often expressed as a https://en.wikipedia.org/wiki/Directed_acyclic_graph[DAG] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. - -For example, if we had processing stages A, B, C and D, where stage A could result in either stage B or C being next, always followed by D, it might look something like this: - -`+"```text"+` - /--> B --\ -A --| |--> D - \--> C --/ -`+"```"+` - -This flow would be easy to express in a standard Benthos config, we could simply use a `+"xref:components:processors/switch.adoc[`switch` processor]"+` to route to either B or C depending on a condition on the result of A. However, this method of flow control quickly becomes unfeasible as the DAG gets more complicated, imagine expressing this flow using switch processors: - -`+"```text"+` - /--> B -------------|--> D - / / -A --| /--> E --| - \--> C --| \ - \----------|--> F -`+"```"+` - -And imagine doing so knowing that the diagram is subject to change over time. Yikes! Instead, with a workflow we can either trust it to automatically resolve the DAG or express it manually as simply as `+"`order: [ [ A ], [ B, C ], [ E ], [ D, F ] ]`"+`, and the conditional logic for determining if a stage is executed is defined as part of the branch itself.`). - Footnotes(` -== Structured metadata - -When the field `+"`meta_path`"+` is non-empty the workflow processor creates an object describing which workflows were successful, skipped or failed for each message and stores the object within the message at the end. - -The object is of the following form: - -`+"```json"+` -{ - "succeeded": [ "foo" ], - "skipped": [ "bar" ], - "failed": { - "baz": "the error message from the branch" - } -} -`+"```"+` - -If a message already has a meta object at the given path when it is processed then the object is used in order to determine which branches have already been performed on the message (or skipped) and can therefore be skipped on this run. - -This is a useful pattern when replaying messages that have failed some branches previously. For example, given the above example object the branches foo and bar would automatically be skipped, and baz would be reattempted. - -The previous meta object will also be preserved in the field `+"`.previous`"+` when the new meta object is written, preserving a full record of all workflow executions. - -If a field `+"`.apply`"+` exists in the meta object for a message and is an array then it will be used as an explicit list of stages to apply, all other stages will be skipped. - -== Resources - -It's common to configure processors (and other components) xref:configuration:resources.adoc[as resources] in order to keep the pipeline configuration cleaner. With the workflow processor you can include branch processors configured as resources within your workflow either by specifying them by name in the field `+"`order`"+`, if Benthos doesn't find a branch within the workflow configuration of that name it'll refer to the resources. - -Alternatively, if you do not wish to have an explicit ordering, you can add resource names to the field `+"`branch_resources`"+` and they will be included in the workflow with automatic DAG resolution along with any branches configured in the `+"`branches`"+` field. - -=== Resource error conditions - -There are two error conditions that could potentially occur when resources included in your workflow are mutated, and if you are planning to mutate resources in your workflow it is important that you understand them. - -The first error case is that a resource in the workflow is removed and not replaced, when this happens the workflow will still be executed but the individual branch will fail. This should only happen if you explicitly delete a branch resource, as any mutation operation will create the new resource before removing the old one. - -The second error case is when automatic DAG resolution is being used and a resource in the workflow is changed in a way that breaks the DAG (circular dependencies, etc). When this happens it is impossible to execute the workflow and therefore the processor will fail, which is possible to capture and handle using xref:configuration:error_handling.adoc[standard error handling patterns]. - -== Error handling - -The recommended approach to handle failures within a workflow is to query against the <> it provides, as it provides granular information about exactly which branches failed and which ones succeeded and therefore aren't necessary to perform again. - -For example, if our meta object is stored at the path `+"`meta.workflow`"+` and we wanted to check whether a message has failed for any branch we can do that using a xref:guides:bloblang/about.adoc[Bloblang query] like `+"`this.meta.workflow.failed.length() | 0 > 0`"+`, or to check whether a specific branch failed we can use `+"`this.exists(\"meta.workflow.failed.foo\")`"+`. - -However, if structured metadata is disabled by setting the field `+"`meta_path`"+` to empty then the workflow processor instead adds a general error flag to messages when any executed branch fails. In this case it's possible to handle failures using xref:configuration:error_handling.adoc[standard error handling patterns]. - -`). - Example("Automatic Ordering", ` -When the field `+"`order`"+` is omitted a best attempt is made to determine a dependency tree between branches based on their request and result mappings. In the following example the branches foo and bar will be executed first in parallel, and afterwards the branch baz will be executed.`, ` -pipeline: - processors: - - workflow: - meta_path: meta.workflow - branches: - foo: - request_map: 'root = ""' - processors: - - http: - url: TODO - result_map: 'root.foo = this' - - bar: - request_map: 'root = this.body' - processors: - - aws_lambda: - function: TODO - result_map: 'root.bar = this' - - baz: - request_map: | - root.fooid = this.foo.id - root.barstuff = this.bar.content - processors: - - cache: - resource: TODO - operator: set - key: ${! json("fooid") } - value: ${! json("barstuff") } -`). - Example("Conditional Branches", ` -Branches of a workflow are skipped when the `+"`request_map`"+` assigns `+"`deleted()`"+` to the root. In this example the branch A is executed when the document type is "foo", and branch B otherwise. Branch C is executed afterwards and is skipped unless either A or B successfully provided a result at `+"`tmp.result`"+`.`, ` -pipeline: - processors: - - workflow: - branches: - A: - request_map: | - root = if this.document.type != "foo" { - deleted() - } - processors: - - http: - url: TODO - result_map: 'root.tmp.result = this' - - B: - request_map: | - root = if this.document.type == "foo" { - deleted() - } - processors: - - aws_lambda: - function: TODO - result_map: 'root.tmp.result = this' - - C: - request_map: | - root = if this.tmp.result != null { - deleted() - } - processors: - - http: - url: TODO_SOMEWHERE_ELSE - result_map: 'root.tmp.result = this' -`). - Example("Resources", ` -The `+"`order`"+` field can be used in order to refer to <>, this can sometimes make your pipeline configuration cleaner, as well as allowing you to reuse branch configurations in order places. It's also possible to mix and match branches configured within the workflow and configured as resources.`, ` -pipeline: - processors: - - workflow: - order: [ [ foo, bar ], [ baz ] ] - branches: - bar: - request_map: 'root = this.body' - processors: - - aws_lambda: - function: TODO - result_map: 'root.bar = this' - -processor_resources: - - label: foo - branch: - request_map: 'root = ""' - processors: - - http: - url: TODO - result_map: 'root.foo = this' - - - label: baz - branch: - request_map: | - root.fooid = this.foo.id - root.barstuff = this.bar.content - processors: - - cache: - resource: TODO - operator: set - key: ${! json("fooid") } - value: ${! json("barstuff") } -`). - Fields( - service.NewStringField(wflowProcFieldMetaPath). - Description("A xref:configuration:field_paths.adoc[dot path] indicating where to store and reference <> about the workflow execution."). - Default("meta.workflow"), - service.NewStringListOfListsField(wflowProcFieldOrder). - Description("An explicit declaration of branch ordered tiers, which describes the order in which parallel tiers of branches should be executed. Branches should be identified by the name as they are configured in the field `branches`. It's also possible to specify branch processors configured <>."). - Examples( - []any{[]any{"foo", "bar"}, []any{"baz"}}, - []any{[]any{"foo"}, []any{"bar"}, []any{"baz"}}, - ). - Default([]any{}), - service.NewStringListField(wflowProcFieldBranchResources). - Description("An optional list of xref:components:processors/branch.adoc[`branch` processor] names that are configured as <>. These resources will be included in the workflow with any branches configured inline within the <> field. The order and parallelism in which branches are executed is automatically resolved based on the mappings of each branch. When using resources with an explicit order it is not necessary to list resources in this field."). - Version("3.38.0"). - Advanced(). - Default([]any{}), - service.NewObjectMapField(wflowProcFieldBranches, branchSpecFields()...). - Description("An object of named xref:components:processors/branch.adoc[`branch` processors] that make up the workflow. The order and parallelism in which branches are executed can either be made explicit with the field `order`, or if omitted an attempt is made to automatically resolve an ordering based on the mappings of each branch."). - Default(map[string]any{}), - ) -} - -func init() { - err := service.RegisterBatchProcessor( - "workflow", workflowProcSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - w, err := NewWorkflow(conf, interop.UnwrapManagement(mgr)) - if err != nil { - return nil, err - } - return interop.NewUnwrapInternalBatchProcessor(w), nil - }) - if err != nil { - panic(err) - } -} - -//------------------------------------------------------------------------------ - -// Workflow is a processor that applies a list of child processors to a new -// payload mapped from the original, and after processing attempts to overlay -// the results back onto the original payloads according to more mappings. -type Workflow struct { - log log.Modular - tracer trace.TracerProvider - - children *workflowBranchMap - allStages map[string]struct{} - metaPath []string - - // Metrics - mReceived metrics.StatCounter - mBatchReceived metrics.StatCounter - mSent metrics.StatCounter - mBatchSent metrics.StatCounter - mError metrics.StatCounter - mLatency metrics.StatTimer -} - -// NewWorkflow instanciates a new workflow processor. -func NewWorkflow(conf *service.ParsedConfig, mgr bundle.NewManagement) (*Workflow, error) { - stats := mgr.Metrics() - w := &Workflow{ - log: mgr.Logger(), - tracer: mgr.Tracer(), - - metaPath: nil, - allStages: map[string]struct{}{}, - - mReceived: stats.GetCounter("processor_received"), - mBatchReceived: stats.GetCounter("processor_batch_received"), - mSent: stats.GetCounter("processor_sent"), - mBatchSent: stats.GetCounter("processor_batch_sent"), - mError: stats.GetCounter("processor_error"), - mLatency: stats.GetTimer("processor_latency_ns"), - } - - metaStr, err := conf.FieldString(wflowProcFieldMetaPath) - if err != nil { - return nil, err - } - if metaStr != "" { - w.metaPath = gabs.DotPathToSlice(metaStr) - } - - if w.children, err = newWorkflowBranchMap(conf, mgr); err != nil { - return nil, err - } - for k := range w.children.dynamicBranches { - w.allStages[k] = struct{}{} - } - - return w, nil -} - -// Flow returns the calculated workflow as a 2D slice. -func (w *Workflow) Flow() [][]string { - return w.children.dag -} - -//------------------------------------------------------------------------------ - -type resultTracker struct { - succeeded map[string]struct{} - skipped map[string]struct{} - failed map[string]string - sync.Mutex -} - -func trackerFromTree(tree [][]string) *resultTracker { - r := &resultTracker{ - succeeded: map[string]struct{}{}, - skipped: map[string]struct{}{}, - failed: map[string]string{}, - } - for _, layer := range tree { - for _, k := range layer { - r.succeeded[k] = struct{}{} - } - } - return r -} - -func (r *resultTracker) Skipped(k string) { - r.Lock() - delete(r.succeeded, k) - - r.skipped[k] = struct{}{} - r.Unlock() -} - -func (r *resultTracker) Failed(k, why string) { - r.Lock() - delete(r.succeeded, k) - delete(r.skipped, k) - - r.failed[k] = why - r.Unlock() -} - -func (r *resultTracker) ToObject() map[string]any { - succeeded := make([]any, 0, len(r.succeeded)) - skipped := make([]any, 0, len(r.skipped)) - failed := make(map[string]any, len(r.failed)) - - for k := range r.succeeded { - succeeded = append(succeeded, k) - } - sort.Slice(succeeded, func(i, j int) bool { - return succeeded[i].(string) < succeeded[j].(string) - }) - for k := range r.skipped { - skipped = append(skipped, k) - } - sort.Slice(skipped, func(i, j int) bool { - return skipped[i].(string) < skipped[j].(string) - }) - for k, v := range r.failed { - failed[k] = v - } - - m := map[string]any{} - if len(succeeded) > 0 { - m["succeeded"] = succeeded - } - if len(skipped) > 0 { - m["skipped"] = skipped - } - if len(failed) > 0 { - m["failed"] = failed - } - return m -} - -// Returns a map of enrichment IDs that should be skipped for this payload. -func (w *Workflow) skipFromMeta(root any) map[string]struct{} { - skipList := map[string]struct{}{} - if len(w.metaPath) == 0 { - return skipList - } - - gObj := gabs.Wrap(root) - - // If a whitelist is provided for this flow then skip stages that aren't - // within it. - if apply, ok := gObj.S(append(w.metaPath, "apply")...).Data().([]any); ok { - if len(apply) > 0 { - for k := range w.allStages { - skipList[k] = struct{}{} - } - for _, id := range apply { - if idStr, isString := id.(string); isString { - delete(skipList, idStr) - } - } - } - } - - // Skip stages that already succeeded in a previous run of this workflow. - if succeeded, ok := gObj.S(append(w.metaPath, "succeeded")...).Data().([]any); ok { - for _, id := range succeeded { - if idStr, isString := id.(string); isString { - if _, exists := w.allStages[idStr]; exists { - skipList[idStr] = struct{}{} - } - } - } - } - - // Skip stages that were already skipped in a previous run of this workflow. - if skipped, ok := gObj.S(append(w.metaPath, "skipped")...).Data().([]any); ok { - for _, id := range skipped { - if idStr, isString := id.(string); isString { - if _, exists := w.allStages[idStr]; exists { - skipList[idStr] = struct{}{} - } - } - } - } - - return skipList -} - -// ProcessBatch applies workflow stages to each part of a message type. -func (w *Workflow) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - w.mReceived.Incr(int64(msg.Len())) - w.mBatchReceived.Incr(1) - startedAt := time.Now() - - // Prevent resourced branches from being updated mid-flow. - dag, children, unlock, err := w.children.Lock() - if err != nil { - w.mError.Incr(1) - w.log.Error("Failed to establish workflow: %v\n", err) - - _ = msg.Iter(func(i int, p *message.Part) error { - p.ErrorSet(err) - return nil - }) - w.mSent.Incr(int64(msg.Len())) - w.mBatchSent.Incr(1) - return []message.Batch{msg}, nil - } - defer unlock() - - skipOnMeta := make([]map[string]struct{}, msg.Len()) - _ = msg.Iter(func(i int, p *message.Part) error { - // TODO: Do we want to evaluate bytes here? And metadata? - if jObj, err := p.AsStructured(); err == nil { - skipOnMeta[i] = w.skipFromMeta(jObj) - } else { - skipOnMeta[i] = map[string]struct{}{} - } - return nil - }) - - propMsg, _ := tracing.WithChildSpans(w.tracer, "workflow", msg) - - records := make([]*resultTracker, msg.Len()) - for i := range records { - records[i] = trackerFromTree(dag) - } - - for _, layer := range dag { - results := make([][]*message.Part, len(layer)) - errors := make([]error, len(layer)) - - wg := sync.WaitGroup{} - wg.Add(len(layer)) - for i, eid := range layer { - branchMsg, branchSpans := tracing.WithChildSpans(w.tracer, eid, propMsg.ShallowCopy()) - go func(id string, index int) { - branchParts := make([]*message.Part, branchMsg.Len()) - _ = branchMsg.Iter(func(partIndex int, part *message.Part) error { - // Remove errors so that they aren't propagated into the - // branch. - part.ErrorSet(nil) - if _, exists := skipOnMeta[partIndex][id]; !exists { - branchParts[partIndex] = part - } - return nil - }) - - var mapErrs []branchMapError - results[index], mapErrs, errors[index] = children[id].createResult(ctx, branchParts, propMsg.ShallowCopy()) - for _, s := range branchSpans { - s.Finish() - } - for j, p := range results[index] { - if p == nil { - records[j].Skipped(id) - } - } - for _, e := range mapErrs { - records[e.index].Failed(id, e.err.Error()) - } - wg.Done() - }(eid, i) - } - wg.Wait() - - if err := ctx.Err(); err != nil { - return nil, err - } - - for i, id := range layer { - var failed []branchMapError - err := errors[i] - if err == nil { - failed, err = children[id].overlayResult(msg, results[i]) - } - if err != nil { - w.mError.Incr(1) - w.log.Error("Failed to perform enrichment '%v': %v\n", id, err) - for j := range records { - records[j].Failed(id, err.Error()) - } - continue - } - for _, e := range failed { - records[e.index].Failed(id, e.err.Error()) - } - } - } - - // Finally, set the meta records of each document. - if len(w.metaPath) > 0 { - _ = msg.Iter(func(i int, p *message.Part) error { - pJSON, err := p.AsStructuredMut() - if err != nil { - w.mError.Incr(1) - w.log.Error("Failed to parse message for meta update: %v\n", err) - p.ErrorSet(err) - return nil - } - - gObj := gabs.Wrap(pJSON) - previous := gObj.S(w.metaPath...).Data() - current := records[i].ToObject() - if previous != nil { - current["previous"] = previous - } - _, _ = gObj.Set(current, w.metaPath...) - - p.SetStructuredMut(gObj.Data()) - return nil - }) - } else { - _ = msg.Iter(func(i int, p *message.Part) error { - if lf := len(records[i].failed); lf > 0 { - failed := make([]string, 0, lf) - for k := range records[i].failed { - failed = append(failed, k) - } - sort.Strings(failed) - p.ErrorSet(fmt.Errorf("workflow branches failed: %v", failed)) - } - return nil - }) - } - - tracing.FinishSpans(propMsg) - - w.mSent.Incr(int64(msg.Len())) - w.mBatchSent.Incr(1) - w.mLatency.Timing(time.Since(startedAt).Nanoseconds()) - return []message.Batch{msg}, nil -} - -// Close shuts down the processor and stops processing requests. -func (w *Workflow) Close(ctx context.Context) error { - return w.children.Close(ctx) -} diff --git a/internal/impl/pure/processor_workflow_branch_map.go b/internal/impl/pure/processor_workflow_branch_map.go deleted file mode 100644 index ae83e9de40..0000000000 --- a/internal/impl/pure/processor_workflow_branch_map.go +++ /dev/null @@ -1,312 +0,0 @@ -package pure - -import ( - "context" - "fmt" - "regexp" - "sort" - "sync" - - "github.com/quipo/dependencysolver" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/public/service" -) - -type workflowBranch interface { - lock() (*Branch, func()) -} - -//------------------------------------------------------------------------------ - -type workflowBranchMap struct { - static bool - dag [][]string - staticBranches map[string]*Branch - - dynamicBranches map[string]workflowBranch -} - -func lockAll(dynBranches map[string]workflowBranch) (branches map[string]*Branch, unlockFn func(), err error) { - unlocks := make([]func(), 0, len(dynBranches)) - unlockFn = func() { - for _, u := range unlocks { - if u != nil { - u() - } - } - } - - branches = make(map[string]*Branch, len(dynBranches)) - for k, v := range dynBranches { - var branchUnlock func() - branches[k], branchUnlock = v.lock() - unlocks = append(unlocks, branchUnlock) - if branches[k] == nil { - err = fmt.Errorf("missing branch resource: %v", k) - unlockFn() - return - } - } - return -} - -// Locks all branches contained in the branch map and returns the latest DAG, a -// map of resources, and a func to unlock the resources that were locked. If -// any error occurs in locked each branch (the resource is missing, or the DAG -// is malformed) then an error is returned instead. -func (w *workflowBranchMap) Lock() (dag [][]string, branches map[string]*Branch, unlockFn func(), err error) { - if w.static { - return w.dag, w.staticBranches, func() {}, nil - } - - if branches, unlockFn, err = lockAll(w.dynamicBranches); err != nil { - return - } - if len(w.dag) > 0 { - dag = w.dag - return - } - - if dag, err = resolveDynamicBranchDAG(branches); err != nil { - unlockFn() - err = fmt.Errorf("failed to resolve DAG: %w", err) - } - return -} - -func (w *workflowBranchMap) Close(ctx context.Context) error { - for _, c := range w.staticBranches { - if err := c.Close(ctx); err != nil { - return err - } - } - return nil -} - -//------------------------------------------------------------------------------ - -var processDAGStageName = regexp.MustCompile("[a-zA-Z0-9-_]+") - -func newWorkflowBranchMap(conf *service.ParsedConfig, mgr bundle.NewManagement) (*workflowBranchMap, error) { - branchObjMap, err := conf.FieldObjectMap(wflowProcFieldBranches) - if err != nil { - return nil, err - } - - dynamicBranches, staticBranches := map[string]workflowBranch{}, map[string]*Branch{} - for k, v := range branchObjMap { - if len(processDAGStageName.FindString(k)) != len(k) { - return nil, fmt.Errorf("workflow branch name '%v' contains invalid characters", k) - } - - child, err := newBranchFromParsed(v, mgr.IntoPath("workflow", "branches", k)) - if err != nil { - return nil, err - } - - dynamicBranches[k] = &normalBranch{child} - staticBranches[k] = child - } - - branchResources, err := conf.FieldStringList(wflowProcFieldBranchResources) - if err != nil { - return nil, err - } - for _, k := range branchResources { - if _, exists := dynamicBranches[k]; exists { - return nil, fmt.Errorf("branch resource name '%v' collides with an explicit branch", k) - } - if !mgr.ProbeProcessor(k) { - return nil, fmt.Errorf("processor resource '%v' was not found", k) - } - dynamicBranches[k] = &resourcedBranch{ - name: k, - mgr: mgr, - } - } - - // When order is specified we infer that names missing from our explicit - // branches are resources. - order, err := conf.FieldStringListOfLists(wflowProcFieldOrder) - if err != nil { - return nil, err - } - for _, tier := range order { - for _, k := range tier { - if _, exists := dynamicBranches[k]; !exists { - if !mgr.ProbeProcessor(k) { - return nil, fmt.Errorf("processor resource '%v' was not found", k) - } - dynamicBranches[k] = &resourcedBranch{ - name: k, - mgr: mgr, - } - } - } - } - - static := len(dynamicBranches) == len(staticBranches) - - var dag [][]string - if len(order) > 0 { - dag = order - if err := verifyStaticBranchDAG(dag, dynamicBranches); err != nil { - return nil, err - } - } else if static { - var err error - if dag, err = resolveDynamicBranchDAG(staticBranches); err != nil { - return nil, err - } - mgr.Logger().Info("Automatically resolved workflow DAG: %v", dag) - } - - return &workflowBranchMap{ - static: static, - dag: dag, - staticBranches: staticBranches, - dynamicBranches: dynamicBranches, - }, nil -} - -//------------------------------------------------------------------------------ - -type resourcedBranch struct { - name string - mgr bundle.NewManagement -} - -func (r *resourcedBranch) lock() (branch *Branch, unlockFn func()) { - var openOnce, releaseOnce sync.Once - open, release := make(chan struct{}), make(chan struct{}) - unlockFn = func() { - releaseOnce.Do(func() { - close(release) - }) - } - - go func() { - _ = r.mgr.AccessProcessor(context.Background(), r.name, func(p processor.V1) { - branch, _ = processor.Unwrap(p).(*Branch) - openOnce.Do(func() { - close(open) - }) - <-release - }) - openOnce.Do(func() { - close(open) - }) - }() - - <-open - return -} - -//------------------------------------------------------------------------------ - -type normalBranch struct { - *Branch -} - -func (r *normalBranch) lock() (branch *Branch, unlockFn func()) { - return r.Branch, nil -} - -//------------------------------------------------------------------------------ - -func depHasPrefix(wanted, provided []string) bool { - if len(wanted) < len(provided) { - return false - } - for i, s := range provided { - if wanted[i] != s { - return false - } - } - return true -} - -func getBranchDeps(id string, wanted [][]string, branches map[string]*Branch) []string { - dependencies := []string{} - - for k, b := range branches { - if k == id { - continue - } - for _, tp := range b.targetsProvided() { - for _, tn := range wanted { - if depHasPrefix(tn, tp) { - dependencies = append(dependencies, k) - break - } - } - } - } - - return dependencies -} - -func verifyStaticBranchDAG(order [][]string, branches map[string]workflowBranch) error { - remaining := map[string]struct{}{} - seen := map[string]struct{}{} - for id := range branches { - remaining[id] = struct{}{} - } - for i, tier := range order { - if len(tier) == 0 { - return fmt.Errorf("explicit order tier '%v' was empty", i) - } - for _, t := range tier { - if _, exists := seen[t]; exists { - return fmt.Errorf("branch specified in order listed multiple times: %v", t) - } - seen[t] = struct{}{} - delete(remaining, t) - } - } - if len(remaining) > 0 { - names := make([]string, 0, len(remaining)) - for k := range remaining { - names = append(names, k) - } - return fmt.Errorf("the following branches were missing from order: %v", names) - } - return nil -} - -func resolveDynamicBranchDAG(branches map[string]*Branch) ([][]string, error) { - if len(branches) == 0 { - return [][]string{}, nil - } - remaining := map[string]struct{}{} - - var entries []dependencysolver.Entry - for id, b := range branches { - wanted := b.targetsUsed() - - remaining[id] = struct{}{} - entries = append(entries, dependencysolver.Entry{ - ID: id, Deps: getBranchDeps(id, wanted, branches), - }) - } - - layers := dependencysolver.LayeredTopologicalSort(entries) - for _, l := range layers { - for _, id := range l { - delete(remaining, id) - } - } - - if len(remaining) > 0 { - var tProcs []string - for k := range remaining { - tProcs = append(tProcs, k) - } - sort.Strings(tProcs) - return nil, fmt.Errorf("failed to automatically resolve DAG, circular dependencies detected for branches: %v", tProcs) - } - - return layers, nil -} diff --git a/internal/impl/pure/processor_workflow_test.go b/internal/impl/pure/processor_workflow_test.go deleted file mode 100644 index c21465cbd6..0000000000 --- a/internal/impl/pure/processor_workflow_test.go +++ /dev/null @@ -1,1052 +0,0 @@ -package pure_test - -import ( - "context" - "errors" - "fmt" - "sort" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/impl/pure" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestWorkflowDeps(t *testing.T) { - tests := []struct { - branches [][2]string - inputOrdering [][]string - ordering [][]string - errContains string - }{ - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.bar", - "root.baz = this", - }, - { - "root = this.baz", - "root.buz = this", - }, - }, - ordering: [][]string{ - {"0"}, {"1"}, {"2"}, - }, - }, - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.bar", - "root.baz = this", - }, - { - "root = this.baz", - "root.buz = this", - }, - }, - inputOrdering: [][]string{ - {"1", "2"}, {"0"}, - }, - ordering: [][]string{ - {"1", "2"}, {"0"}, - }, - }, - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.bar", - "root.baz = this", - }, - { - "root = this.baz", - "root.buz = this", - }, - }, - ordering: [][]string{ - {"0"}, {"1"}, {"2"}, - }, - }, - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.foo", - "root.baz = this", - }, - { - "root = this.baz", - "root.foo = this", - }, - }, - errContains: "failed to automatically resolve DAG, circular dependencies detected for branches: [0 1 2]", - }, - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.bar", - "root.baz = this", - }, - { - "root = this.baz", - "root.buz = this", - }, - }, - inputOrdering: [][]string{ - {"1"}, {"0"}, - }, - errContains: "the following branches were missing from order: [2]", - }, - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.bar", - "root.baz = this", - }, - { - "root = this.baz", - "root.buz = this", - }, - }, - inputOrdering: [][]string{ - {"1"}, {"0", "2"}, {"1"}, - }, - errContains: "branch specified in order listed multiple times: 1", - }, - { - branches: [][2]string{ - { - "root = this.foo", - "root.bar = this", - }, - { - "root = this.foo", - "root.baz = this", - }, - { - `root.bar = this.bar -root.baz = this.baz`, - "root.buz = this", - }, - }, - ordering: [][]string{ - {"0", "1"}, {"2"}, - }, - }, - } - - for i, test := range tests { - test := test - t.Run(strconv.Itoa(i), func(t *testing.T) { - if test.inputOrdering == nil { - test.inputOrdering = [][]string{} - } - confStr := fmt.Sprintf(` -workflow: - order: %v - branches: -`, gabs.Wrap(test.inputOrdering).String()) - - for j, mappings := range test.branches { - confStr += fmt.Sprintf(` - %v: - request_map: | - %v - processors: - - bloblang: root = this - result_map: | - %v -`, - strconv.Itoa(j), - strings.ReplaceAll(mappings[0], "\n", "\n "), - strings.ReplaceAll(mappings[1], "\n", "\n "), - ) - } - - conf, err := testutil.ProcessorFromYAML(confStr) - require.NoError(t, err) - - p, err := mock.NewManager().NewProcessor(conf) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - - dag := p.(*pure.Workflow).Flow() - for _, d := range dag { - sort.Strings(d) - } - assert.Equal(t, test.ordering, dag) - } - }) - } -} - -func newMockProcProvider(t *testing.T, confs map[string]processor.Config) bundle.NewManagement { - t.Helper() - - resConf := manager.NewResourceConfig() - for k, v := range confs { - v.Label = k - resConf.ResourceProcessors = append(resConf.ResourceProcessors, v) - } - mgr, err := manager.New(resConf) - require.NoError(t, err) - - return mgr -} - -func quickTestBranches(t testing.TB, branches ...[4]string) map[string]processor.Config { - t.Helper() - m := map[string]processor.Config{} - for _, b := range branches { - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -branch: - request_map: | - %v - processors: - - bloblang: | - %v - result_map: | - %v -`, - strings.ReplaceAll(b[1], "\n", "\n "), - strings.ReplaceAll(b[2], "\n", "\n "), - strings.ReplaceAll(b[3], "\n", "\n "), - )) - require.NoError(t, err) - - m[b[0]] = conf - } - return m -} - -func TestWorkflowMissingResources(t *testing.T) { - conf, err := testutil.ProcessorFromYAML(` -workflow: - order: [[ foo, bar, baz ]] - branches: - bar: - request_map: root = this - processors: - - bloblang: root = this - result_map: root = this -`) - require.NoError(t, err) - - branchConf, err := testutil.ProcessorFromYAML(` -branch: - request_map: root = this - processors: - - bloblang: root = this - result_map: root = this -`) - require.NoError(t, err) - - mgr := newMockProcProvider(t, map[string]processor.Config{ - "baz": branchConf, - }) - - _, err = mgr.NewProcessor(conf) - require.Error(t, err) - require.Contains(t, err.Error(), "processor resource 'foo' was not found") -} - -type mockMsg struct { - content string - meta map[string]string - err error -} - -func (m mockMsg) withErr(err error) mockMsg { - m.err = err - return m -} - -func TestWorkflows(t *testing.T) { - msg := func(content string, meta ...string) mockMsg { - t.Helper() - m := mockMsg{ - content: content, - meta: map[string]string{}, - } - for i, v := range meta { - if i%2 == 1 { - m.meta[meta[i-1]] = v - } - } - return m - } - - // To make configs simpler they break branches down into three mappings, the - // request map, a bloblang processor, and a result map. - tests := []struct { - branches [][3]string - order [][]string - input []mockMsg - output []mockMsg - err string - }{ - { - branches: [][3]string{ - { - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - }, - input: []mockMsg{ - msg(`{}`), - msg(`{"foo":"not a number"}`), - msg(`{"foo":"5"}`), - }, - output: []mockMsg{ - msg(`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"}}}}`), - msg(`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax"}}}}`), - msg(`{"bar":5,"foo":"5","meta":{"workflow":{"succeeded":["0"]}}}`), - }, - }, - { - branches: [][3]string{ - { - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - }, - input: []mockMsg{ - msg(`{}`), - msg(`{"foo":"not a number"}`), - msg(`{"foo":"5"}`), - }, - output: []mockMsg{ - msg(`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`), - msg(`{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`), - msg(`{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`), - }, - }, - { - branches: [][3]string{ - { - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - }, - input: []mockMsg{ - msg(`{"meta":{"workflow":{"apply":["2"]}},"baz":2}`), - msg(`{"meta":{"workflow":{"skipped":["0"]}},"bar":3}`), - msg(`{"meta":{"workflow":{"succeeded":["1"]}},"baz":9}`), - }, - output: []mockMsg{ - msg(`{"baz":2,"buz":4,"meta":{"workflow":{"previous":{"apply":["2"]},"skipped":["0","1"],"succeeded":["2"]}}}`), - msg(`{"bar":3,"baz":8,"buz":10,"meta":{"workflow":{"previous":{"skipped":["0"]},"skipped":["0"],"succeeded":["1","2"]}}}`), - msg(`{"baz":9,"buz":11,"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"},"previous":{"succeeded":["1"]},"skipped":["1"],"succeeded":["2"]}}}`), - }, - }, - { - branches: [][3]string{ - { - "root = this.foo.not_null()", - "root = this", - "root.bar = this.number() + 2", - }, - { - "root = this.foo.not_null()", - "root = this", - "root.baz = this.number() + 3", - }, - { - `root.bar = this.bar.not_null() -root.baz = this.baz.not_null()`, - "root = this", - "root.buz = this.bar + this.baz", - }, - }, - input: []mockMsg{ - msg(`{"foo":2}`), - msg(`{}`), - msg(`not even a json object`), - }, - output: []mockMsg{ - msg(`{"bar":4,"baz":5,"buz":9,"foo":2,"meta":{"workflow":{"succeeded":["0","1","2"]}}}`), - msg(`{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null"}}}}`), - msg(`not even a json object`).withErr(errors.New("invalid character 'o' in literal null (expecting 'u')")), - }, - }, - { - branches: [][3]string{ - { - `root = this`, - `root = this -root.name_upper = this.name.uppercase()`, - `root.result = if this.failme.bool(false) { - throw("this is a branch error") -} else { - this.name_upper -}`, - }, - }, - input: []mockMsg{ - msg(`{"id":0,"name":"first"}`).withErr(errors.New("this is a pre-existing failure")), - msg(`{"failme":true,"id":1,"name":"second"}`), - msg(`{"failme":true,"id":2,"name":"third"}`).withErr(errors.New("this is a pre-existing failure")), - }, - output: []mockMsg{ - msg(`{"id":0,"meta":{"workflow":{"succeeded":["0"]}},"name":"first","result":"FIRST"}`).withErr(errors.New("this is a pre-existing failure")), - msg( - `{"failme":true,"id":1,"meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): this is a branch error"}}},"name":"second"}`, - ), - msg(`{"failme":true,"id":2,"meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): this is a branch error"}}},"name":"third"}`).withErr(errors.New("this is a pre-existing failure")), - }, - }, - } - - for i, test := range tests { - test := test - t.Run(strconv.Itoa(i), func(t *testing.T) { - if test.order == nil { - test.order = [][]string{} - } - confStr := fmt.Sprintf(` -workflow: - order: %v - branches: -`, gabs.Wrap(test.order).String()) - - for j, mappings := range test.branches { - confStr += fmt.Sprintf(` - %v: - request_map: | - %v - processors: - - bloblang: | - %v - result_map: | - %v -`, - strconv.Itoa(j), - strings.ReplaceAll(mappings[0], "\n", "\n "), - strings.ReplaceAll(mappings[1], "\n", "\n "), - strings.ReplaceAll(mappings[2], "\n", "\n "), - ) - } - - conf, err := testutil.ProcessorFromYAML(confStr) - require.NoError(t, err) - - p, err := mock.NewManager().NewProcessor(conf) - require.NoError(t, err) - - inputMsg := message.QuickBatch(nil) - for _, m := range test.input { - part := message.NewPart([]byte(m.content)) - if m.meta != nil { - for k, v := range m.meta { - part.MetaSetMut(k, v) - } - } - if m.err != nil { - part.ErrorSet(m.err) - } - inputMsg = append(inputMsg, part) - } - - msgs, res := p.ProcessBatch(context.Background(), inputMsg.ShallowCopy()) - if test.err != "" { - require.Error(t, res) - require.EqualError(t, res, test.err) - } else { - require.Len(t, msgs, 1) - assert.Equal(t, len(test.output), msgs[0].Len()) - for i, out := range test.output { - comparePart := mockMsg{ - content: string(msgs[0].Get(i).AsBytes()), - meta: map[string]string{}, - } - - _ = msgs[0].Get(i).MetaIterStr(func(k, v string) error { - comparePart.meta[k] = v - return nil - }) - - if out.err != nil { - assert.EqualError(t, msgs[0].Get(i).ErrorGet(), out.err.Error()) - } else { - assert.NoError(t, msgs[0].Get(i).ErrorGet()) - } - msgs[0].Get(i).ErrorSet(nil) - out.err = nil - - assert.Equal(t, out, comparePart, "part: %v", i) - } - } - - // Ensure nothing changed - for i, m := range test.input { - assert.Equal(t, m.content, string(inputMsg.Get(i).AsBytes())) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, p.Close(ctx)) - }) - } -} - -func TestWorkflowsWithResources(t *testing.T) { - // To make configs simpler they break branches down into three mappings, the - // request map, a bloblang processor, and a result map. - tests := []struct { - branches [][4]string - input []string - output []string - err string - }{ - { - branches: [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - }, - input: []string{ - `{}`, - `{"foo":"not a number"}`, - `{"foo":"5"}`, - }, - output: []string{ - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"}}}}`, - `{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax"}}}}`, - `{"bar":5,"foo":"5","meta":{"workflow":{"succeeded":["0"]}}}`, - }, - }, - { - branches: [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "1", - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "2", - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - }, - input: []string{ - `{}`, - `{"foo":"not a number"}`, - `{"foo":"5"}`, - }, - output: []string{ - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`, - `{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`, - `{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`, - }, - }, - { - branches: [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "1", - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "2", - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - }, - input: []string{ - `{"meta":{"workflow":{"apply":["2"]}},"baz":2}`, - `{"meta":{"workflow":{"skipped":["0"]}},"bar":3}`, - `{"meta":{"workflow":{"succeeded":["1"]}},"baz":9}`, - }, - output: []string{ - `{"baz":2,"buz":4,"meta":{"workflow":{"previous":{"apply":["2"]},"skipped":["0","1"],"succeeded":["2"]}}}`, - `{"bar":3,"baz":8,"buz":10,"meta":{"workflow":{"previous":{"skipped":["0"]},"skipped":["0"],"succeeded":["1","2"]}}}`, - `{"baz":9,"buz":11,"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"},"previous":{"succeeded":["1"]},"skipped":["1"],"succeeded":["2"]}}}`, - }, - }, - { - branches: [][4]string{ - { - "0", - "root = this.foo.not_null()", - "root = this", - "root.bar = this.number() + 2", - }, - { - "1", - "root = this.foo.not_null()", - "root = this", - "root.baz = this.number() + 3", - }, - { - "2", - `root.bar = this.bar.not_null() - root.baz = this.baz.not_null()`, - "root = this", - "root.buz = this.bar + this.baz", - }, - }, - input: []string{ - `{"foo":2}`, - `{}`, - `not even a json object`, - }, - output: []string{ - `{"bar":4,"baz":5,"buz":9,"foo":2,"meta":{"workflow":{"succeeded":["0","1","2"]}}}`, - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null"}}}}`, - `not even a json object`, - }, - }, - } - - for i, test := range tests { - test := test - t.Run(strconv.Itoa(i), func(t *testing.T) { - var branchNames []string - for _, b := range test.branches { - branchNames = append(branchNames, b[0]) - } - - mgr := newMockProcProvider(t, quickTestBranches(t, test.branches...)) - - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -workflow: - branch_resources: %v -`, gabs.Wrap(branchNames).String())) - require.NoError(t, err) - - p, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - var parts [][]byte - for _, input := range test.input { - parts = append(parts, []byte(input)) - } - - msgs, res := p.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if test.err != "" { - require.Error(t, res) - require.EqualError(t, res, test.err) - } else { - require.Len(t, msgs, 1) - var output []string - for _, b := range message.GetAllBytes(msgs[0]) { - output = append(output, string(b)) - } - assert.Equal(t, test.output, output) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, p.Close(ctx)) - }) - } -} - -func TestWorkflowsParallel(t *testing.T) { - branches := [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "1", - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "2", - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - } - input := []string{ - `{}`, - `{"foo":"not a number"}`, - `{"foo":"5"}`, - } - output := []string{ - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`, - `{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`, - `{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`, - } - - var branchNames []string - for _, b := range branches { - branchNames = append(branchNames, b[0]) - } - - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -workflow: - branch_resources: %v -`, gabs.Wrap(branchNames).String())) - require.NoError(t, err) - - for loops := 0; loops < 10; loops++ { - mgr := newMockProcProvider(t, quickTestBranches(t, branches...)) - p, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - startChan := make(chan struct{}) - wg := sync.WaitGroup{} - - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-startChan - - for j := 0; j < 100; j++ { - var parts [][]byte - for _, input := range input { - parts = append(parts, []byte(input)) - } - - msgs, res := p.ProcessBatch(context.Background(), message.QuickBatch(parts)) - require.NoError(t, res) - require.Len(t, msgs, 1) - var actual []string - for _, b := range message.GetAllBytes(msgs[0]) { - actual = append(actual, string(b)) - } - assert.Equal(t, output, actual) - } - }() - } - - close(startChan) - wg.Wait() - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - assert.NoError(t, p.Close(ctx)) - done() - } -} - -func TestWorkflowsWithOrderResources(t *testing.T) { - // To make configs simpler they break branches down into three mappings, the - // request map, a bloblang processor, and a result map. - tests := []struct { - branches [][4]string - order [][]string - input []string - output []string - err string - }{ - { - branches: [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - }, - order: [][]string{ - {"0"}, - }, - input: []string{ - `{}`, - `{"foo":"not a number"}`, - `{"foo":"5"}`, - }, - output: []string{ - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"}}}}`, - `{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax"}}}}`, - `{"bar":5,"foo":"5","meta":{"workflow":{"succeeded":["0"]}}}`, - }, - }, - { - branches: [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "1", - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "2", - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - }, - order: [][]string{ - {"0"}, - {"1"}, - {"2"}, - }, - input: []string{ - `{}`, - `{"foo":"not a number"}`, - `{"foo":"5"}`, - }, - output: []string{ - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`, - `{"foo":"not a number","meta":{"workflow":{"failed":{"0":"result mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: strconv.ParseFloat: parsing \"not a number\": invalid syntax","1":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.baz`" + `: value is null"}}}}`, - `{"bar":5,"baz":10,"buz":12,"foo":"5","meta":{"workflow":{"succeeded":["0","1","2"]}}}`, - }, - }, - { - branches: [][4]string{ - { - "0", - "root.foo = this.foo.not_null()", - "root = this", - "root.bar = this.foo.number()", - }, - { - "1", - "root.bar = this.bar.not_null()", - "root = this", - "root.baz = this.bar.number() + 5", - }, - { - "2", - "root.baz = this.baz.not_null()", - "root = this", - "root.buz = this.baz.number() + 2", - }, - }, - order: [][]string{ - {"0"}, - {"1"}, - {"2"}, - }, - input: []string{ - `{"meta":{"workflow":{"apply":["2"]}},"baz":2}`, - `{"meta":{"workflow":{"skipped":["0"]}},"bar":3}`, - `{"meta":{"workflow":{"succeeded":["1"]}},"baz":9}`, - }, - output: []string{ - `{"baz":2,"buz":4,"meta":{"workflow":{"previous":{"apply":["2"]},"skipped":["0","1"],"succeeded":["2"]}}}`, - `{"bar":3,"baz":8,"buz":10,"meta":{"workflow":{"previous":{"skipped":["0"]},"skipped":["0"],"succeeded":["1","2"]}}}`, - `{"baz":9,"buz":11,"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null"},"previous":{"succeeded":["1"]},"skipped":["1"],"succeeded":["2"]}}}`, - }, - }, - { - branches: [][4]string{ - { - "0", - "root = this.foo.not_null()", - "root = this", - "root.bar = this.number() + 2", - }, - { - "1", - "root = this.foo.not_null()", - "root = this", - "root.baz = this.number() + 3", - }, - { - "2", - `root.bar = this.bar.not_null() - root.baz = this.baz.not_null()`, - "root = this", - "root.buz = this.bar + this.baz", - }, - }, - order: [][]string{ - {"0", "1"}, - {"2"}, - }, - input: []string{ - `{"foo":2}`, - `{}`, - `not even a json object`, - }, - output: []string{ - `{"bar":4,"baz":5,"buz":9,"foo":2,"meta":{"workflow":{"succeeded":["0","1","2"]}}}`, - `{"meta":{"workflow":{"failed":{"0":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","1":"request mapping failed: failed assignment (line 1): field ` + "`this.foo`" + `: value is null","2":"request mapping failed: failed assignment (line 1): field ` + "`this.bar`" + `: value is null"}}}}`, - `not even a json object`, - }, - }, - } - - for i, test := range tests { - test := test - t.Run(strconv.Itoa(i), func(t *testing.T) { - if test.order == nil { - test.order = [][]string{} - } - conf, err := testutil.ProcessorFromYAML(fmt.Sprintf(` -workflow: - order: %v -`, gabs.Wrap(test.order).String())) - require.NoError(t, err) - - mgr := newMockProcProvider(t, quickTestBranches(t, test.branches...)) - p, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - var parts [][]byte - for _, input := range test.input { - parts = append(parts, []byte(input)) - } - - msgs, res := p.ProcessBatch(context.Background(), message.QuickBatch(parts)) - if test.err != "" { - require.Error(t, res) - require.EqualError(t, res, test.err) - } else { - require.Len(t, msgs, 1) - var output []string - for _, b := range message.GetAllBytes(msgs[0]) { - output = append(output, string(b)) - } - assert.Equal(t, test.output, output) - } - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - assert.NoError(t, p.Close(ctx)) - }) - } -} - -func TestWorkflowUnwrapResourceBranches(t *testing.T) { - strmBuilder := service.NewStreamBuilder() - - require.NoError(t, strmBuilder.AddResourcesYAML(` -processor_resources: - - label: fooproc - branch: - request_map: 'root = this.id' - processors: - - label: innerproc - mapping: 'root.id = content().uppercase().string()' - result_map: 'root.id = this.id' -`)) - - require.NoError(t, strmBuilder.AddProcessorYAML(` -label: barproc -workflow: - branch_resources: [ fooproc ] -`)) - - inFunc, err := strmBuilder.AddProducerFunc() - require.NoError(t, err) - - var outValue string - require.NoError(t, strmBuilder.AddConsumerFunc(func(ctx context.Context, m *service.Message) error { - outBytes, err := m.AsBytes() - require.NoError(t, err) - outValue = string(outBytes) - return nil - })) - - strm, tracer, err := strmBuilder.BuildTraced() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - go func() { - assert.NoError(t, strm.Run(tCtx)) - }() - require.NoError(t, inFunc(tCtx, service.NewMessage([]byte(`{"id":"hello world","content":"waddup"}`)))) - require.NoError(t, strm.Stop(tCtx)) - - assert.Equal(t, `{"content":"waddup","id":"HELLO WORLD","meta":{"workflow":{"succeeded":["fooproc"]}}}`, outValue) - assert.Equal(t, map[string][]service.TracingEvent{ - "barproc": { - {Type: "CONSUME", Content: "{\"id\":\"hello world\",\"content\":\"waddup\"}", Meta: map[string]interface{}{}}, - {Type: "PRODUCE", Content: "{\"content\":\"waddup\",\"id\":\"HELLO WORLD\",\"meta\":{\"workflow\":{\"succeeded\":[\"fooproc\"]}}}", Meta: map[string]interface{}{}}, - }, - "fooproc": {}, - "innerproc": { - {Type: "CONSUME", Content: "hello world", Meta: map[string]interface{}{}}, - {Type: "PRODUCE", Content: "{\"id\":\"HELLO WORLD\"}", Meta: map[string]interface{}{}}, - }, - }, tracer.ProcessorEvents()) -} diff --git a/internal/impl/pure/rate_limit_local.go b/internal/impl/pure/rate_limit_local.go deleted file mode 100644 index 689bbd1241..0000000000 --- a/internal/impl/pure/rate_limit_local.go +++ /dev/null @@ -1,93 +0,0 @@ -package pure - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func localRatelimitConfig() *service.ConfigSpec { - spec := service.NewConfigSpec(). - Stable(). - Summary(`The local rate limit is a simple X every Y type rate limit that can be shared across any number of components within the pipeline but does not support distributed rate limits across multiple running instances of Benthos.`). - Field(service.NewIntField("count"). - Description("The maximum number of requests to allow for a given period of time."). - Default(1000)). - Field(service.NewDurationField("interval"). - Description("The time window to limit requests by."). - Default("1s")) - - return spec -} - -func init() { - err := service.RegisterRateLimit( - "local", localRatelimitConfig(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.RateLimit, error) { - return newLocalRatelimitFromConfig(conf) - }) - if err != nil { - panic(err) - } -} - -func newLocalRatelimitFromConfig(conf *service.ParsedConfig) (*localRatelimit, error) { - count, err := conf.FieldInt("count") - if err != nil { - return nil, err - } - interval, err := conf.FieldDuration("interval") - if err != nil { - return nil, err - } - return newLocalRatelimit(count, interval) -} - -//------------------------------------------------------------------------------ - -type localRatelimit struct { - mut sync.Mutex - bucket int - lastRefresh time.Time - - size int - period time.Duration -} - -func newLocalRatelimit(count int, interval time.Duration) (*localRatelimit, error) { - if count <= 0 { - return nil, errors.New("count must be larger than zero") - } - return &localRatelimit{ - bucket: count, - lastRefresh: time.Now(), - size: count, - period: interval, - }, nil -} - -func (r *localRatelimit) Access(ctx context.Context) (time.Duration, error) { - r.mut.Lock() - r.bucket-- - - if r.bucket < 0 { - r.bucket = 0 - remaining := r.period - time.Since(r.lastRefresh) - - if remaining > 0 { - r.mut.Unlock() - return remaining, nil - } - r.bucket = r.size - 1 - r.lastRefresh = time.Now() - } - r.mut.Unlock() - return 0, nil -} - -func (r *localRatelimit) Close(ctx context.Context) error { - return nil -} diff --git a/internal/impl/pure/rate_limit_local_test.go b/internal/impl/pure/rate_limit_local_test.go deleted file mode 100644 index c859974a7a..0000000000 --- a/internal/impl/pure/rate_limit_local_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package pure - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLocalRateLimitConfErrors(t *testing.T) { - conf, err := localRatelimitConfig().ParseYAML(`count: -1`, nil) - require.NoError(t, err) - - _, err = newLocalRatelimitFromConfig(conf) - require.Error(t, err) - - _, err = localRatelimitConfig().ParseYAML(`interval: nope`, nil) - require.NoError(t, err) - - _, err = newLocalRatelimitFromConfig(conf) - require.Error(t, err) -} - -func TestLocalRateLimitBasic(t *testing.T) { - conf, err := localRatelimitConfig().ParseYAML(` -count: 10 -interval: 1s -`, nil) - require.NoError(t, err) - - rl, err := newLocalRatelimitFromConfig(conf) - require.NoError(t, err) - - ctx := context.Background() - - for i := 0; i < 10; i++ { - period, _ := rl.Access(ctx) - assert.LessOrEqual(t, period, time.Duration(0)) - } - - if period, _ := rl.Access(ctx); period == 0 { - t.Error("Expected limit on final request") - } else if period > time.Second { - t.Errorf("Period beyond interval: %v", period) - } -} - -func TestLocalRateLimitRefresh(t *testing.T) { - conf, err := localRatelimitConfig().ParseYAML(` -count: 10 -interval: 10ms -`, nil) - require.NoError(t, err) - - rl, err := newLocalRatelimitFromConfig(conf) - require.NoError(t, err) - - ctx := context.Background() - - for i := 0; i < 10; i++ { - period, _ := rl.Access(ctx) - if period > 0 { - t.Errorf("Period above zero: %v", period) - } - } - - if period, _ := rl.Access(ctx); period == 0 { - t.Error("Expected limit on final request") - } else if period > time.Second { - t.Errorf("Period beyond interval: %v", period) - } - - <-time.After(time.Millisecond * 15) - - for i := 0; i < 10; i++ { - period, _ := rl.Access(ctx) - if period != 0 { - t.Errorf("Rate limited on get %v", i) - } - } - - if period, _ := rl.Access(ctx); period == 0 { - t.Error("Expected limit on final request") - } else if period > time.Second { - t.Errorf("Period beyond interval: %v", period) - } -} - -//------------------------------------------------------------------------------ - -func BenchmarkRateLimit(b *testing.B) { - /* A rate limit is typically going to be protecting a networked resource - * where the request will likely be measured at least in hundreds of - * microseconds. It would be reasonable to assume the rate limit might be - * shared across tens of components. - * - * Therefore, we can probably sit comfortably with lock contention across - * one hundred or so parallel components adding an overhead of single digit - * microseconds. Since this benchmark doesn't take into account the actual - * request duration after receiving a rate limit I've set the number of - * components to ten in order to compensate. - */ - b.ReportAllocs() - - nParallel := 10 - startChan := make(chan struct{}) - wg := sync.WaitGroup{} - wg.Add(nParallel) - - conf, err := localRatelimitConfig().ParseYAML(` -count: 1000 -interval: 1ns -`, nil) - require.NoError(b, err) - - rl, err := newLocalRatelimitFromConfig(conf) - require.NoError(b, err) - - ctx := context.Background() - - for i := 0; i < nParallel; i++ { - go func() { - <-startChan - for j := 0; j < b.N; j++ { - period, _ := rl.Access(ctx) - if period > 0 { - time.Sleep(period) - } - } - wg.Done() - }() - } - - b.ResetTimer() - close(startChan) - wg.Wait() -} diff --git a/internal/impl/pure/scanner_chunker.go b/internal/impl/pure/scanner_chunker.go deleted file mode 100644 index 8109b5309e..0000000000 --- a/internal/impl/pure/scanner_chunker.go +++ /dev/null @@ -1,94 +0,0 @@ -package pure - -import ( - "bytes" - "context" - "errors" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const scFieldSize = "size" - -func chunkerScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Split an input stream into chunks of a given number of bytes."). - Fields( - service.NewIntField(scFieldSize). - Description("The size of each chunk in bytes."), - ) -} - -func init() { - err := service.RegisterBatchScannerCreator("chunker", chunkerScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return chunkerScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func chunkerScannerFromParsed(conf *service.ParsedConfig) (l *chunkerScannerCreator, err error) { - l = &chunkerScannerCreator{} - if l.size, err = conf.FieldInt(scFieldSize); err != nil { - return - } - return -} - -type chunkerScannerCreator struct { - size int -} - -func (c *chunkerScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - return service.AutoAggregateBatchScannerAcks(&chunkerScanner{ - r: rdr, - size: int64(c.size), - buf: bytes.NewBuffer(make([]byte, 0, c.size)), - }, aFn), nil -} - -func (c *chunkerScannerCreator) Close(context.Context) error { - return nil -} - -type chunkerScanner struct { - size int64 - buf *bytes.Buffer - r io.ReadCloser -} - -func (c *chunkerScanner) NextBatch(ctx context.Context) (service.MessageBatch, error) { - if c.r == nil { - return nil, io.EOF - } - - _, err := io.CopyN(c.buf, c.r, c.size) - if err != nil && !errors.Is(err, io.EOF) { - return nil, err - } - - if c.buf.Len() == 0 { - return nil, io.EOF - } - - bytesCopy := make([]byte, c.buf.Len()) - copy(bytesCopy, c.buf.Bytes()) - - c.buf.Reset() - if err != nil { - _ = c.r.Close() - c.r = nil - } - return service.MessageBatch{service.NewMessage(bytesCopy)}, nil -} - -func (c *chunkerScanner) Close(ctx context.Context) error { - if c.r == nil { - return nil - } - return c.r.Close() -} diff --git a/internal/impl/pure/scanner_chunker_test.go b/internal/impl/pure/scanner_chunker_test.go deleted file mode 100644 index 09e023b6ed..0000000000 --- a/internal/impl/pure/scanner_chunker_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package pure_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestLinesChunkerSuite(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - chunker: - size: 4 -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`abcdefghijklmnopqrstuvwxyz`), "abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx", "yz") -} diff --git a/internal/impl/pure/scanner_csv.go b/internal/impl/pure/scanner_csv.go deleted file mode 100644 index 7fb8945537..0000000000 --- a/internal/impl/pure/scanner_csv.go +++ /dev/null @@ -1,163 +0,0 @@ -package pure - -import ( - "context" - "encoding/csv" - "errors" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - scsvFieldCustomDelimiter = "custom_delimiter" - scsvFieldParseHeaderRow = "parse_header_row" - scsvFieldLazyQuotes = "lazy_quotes" - scsvFieldContinueOnError = "continue_on_error" -) - -func csvScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Consume comma-separated values row by row, including support for custom delimiters."). - Description(` -== Metadata - -This scanner adds the following metadata to each message: - -- `+"`csv_row`"+` The index of each row, beginning at 0. - -`). - Fields( - service.NewStringField(scsvFieldCustomDelimiter). - Description("Use a provided custom delimiter instead of the default comma."). - Optional(), - service.NewBoolField(scsvFieldParseHeaderRow). - Description("Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, each message will consist of an array of values from the corresponding CSV row."). - Default(true), - service.NewBoolField(scsvFieldLazyQuotes). - Description("If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field."). - Default(false), - service.NewBoolField(scsvFieldContinueOnError). - Description("If a row fails to parse due to any error emit an empty message marked with the error and then continue consuming subsequent rows when possible. This can sometimes be useful in situations where input data contains individual rows which are malformed. However, when a row encounters a parsing error it is impossible to guarantee that following rows are valid, as this indicates that the input data is unreliable and could potentially emit misaligned rows."). - Default(false), - ) -} - -func init() { - err := service.RegisterBatchScannerCreator("csv", csvScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return csvScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func csvScannerFromParsed(conf *service.ParsedConfig) (l *csvScannerCreator, err error) { - l = &csvScannerCreator{} - if conf.Contains(scsvFieldCustomDelimiter) { - if l.customDelim, err = conf.FieldString(scsvFieldCustomDelimiter); err != nil { - return - } - } - if l.parseHeaderRow, err = conf.FieldBool(scsvFieldParseHeaderRow); err != nil { - return - } - if l.lazyQuotes, err = conf.FieldBool(scsvFieldLazyQuotes); err != nil { - return - } - if l.continueOnError, err = conf.FieldBool(scsvFieldContinueOnError); err != nil { - return - } - return -} - -type csvScannerCreator struct { - customDelim string - parseHeaderRow bool - lazyQuotes bool - continueOnError bool -} - -func (c *csvScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - cRdr := csv.NewReader(rdr) - cRdr.LazyQuotes = c.lazyQuotes - if c.customDelim != "" { - cRdr.Comma = []rune(c.customDelim)[0] - } - - var headers []string - if c.parseHeaderRow { - tmpHeaders, err := cRdr.Read() - if err != nil { - return nil, err - } - headers = make([]string, len(tmpHeaders)) - _ = copy(headers, tmpHeaders) - } - - return service.AutoAggregateBatchScannerAcks(&csvScanner{ - r: rdr, - c: cRdr, - headers: headers, - continueOnError: c.continueOnError, - }, aFn), nil -} - -func (c *csvScannerCreator) Close(context.Context) error { - return nil -} - -type csvScanner struct { - c *csv.Reader - r io.ReadCloser - - headers []string - row int - continueOnError bool -} - -func (c *csvScanner) NextBatch(ctx context.Context) (service.MessageBatch, error) { - if c.r == nil { - return nil, io.EOF - } - - recordStrs, err := c.c.Read() - if err != nil { - if errors.Is(err, io.EOF) || !c.continueOnError { - return nil, err - } - } - - msg := service.NewMessage(nil) - msg.MetaSetMut("csv_row", c.row) - if err != nil { - msg.SetError(err) - } - if len(c.headers) > 0 { - a := make(map[string]any, len(recordStrs)) - for i, v := range recordStrs { - if len(c.headers) > i { - a[c.headers[i]] = v - } - } - msg.SetStructuredMut(a) - } else { - a := make([]any, len(recordStrs)) - for i, v := range recordStrs { - a[i] = v - } - msg.SetStructuredMut(a) - } - c.row++ - - return service.MessageBatch{msg}, nil -} - -func (c *csvScanner) Close(ctx context.Context) error { - if c.r == nil { - return nil - } - return c.r.Close() -} diff --git a/internal/impl/pure/scanner_csv_test.go b/internal/impl/pure/scanner_csv_test.go deleted file mode 100644 index d4b3c31e62..0000000000 --- a/internal/impl/pure/scanner_csv_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package pure_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestCSVScannerDefault(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - csv: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`a,b,c -a1,b1,c1 -a2,b2,c2 -a3,b3,c3 -a4,b4,c4 -`), - `{"a":"a1","b":"b1","c":"c1"}`, - `{"a":"a2","b":"b2","c":"c2"}`, - `{"a":"a3","b":"b3","c":"c3"}`, - `{"a":"a4","b":"b4","c":"c4"}`, - ) -} - -func TestCSVScannerCustomDelim(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - csv: - custom_delimiter: '|' -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`a|b|c -a1|b1|c1 -a2|b2|c2 -a3|b3|c3 -a4|b4|c4 -`), - `{"a":"a1","b":"b1","c":"c1"}`, - `{"a":"a2","b":"b2","c":"c2"}`, - `{"a":"a3","b":"b3","c":"c3"}`, - `{"a":"a4","b":"b4","c":"c4"}`, - ) -} - -func TestCSVScannerNoHeaderRow(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - csv: - parse_header_row: false -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`a1,b1,c1 -a2,b2,c2 -a3,b3,c3 -a4,b4,c4 -`), - `["a1","b1","c1"]`, - `["a2","b2","c2"]`, - `["a3","b3","c3"]`, - `["a4","b4","c4"]`, - ) -} diff --git a/internal/impl/pure/scanner_decompress.go b/internal/impl/pure/scanner_decompress.go deleted file mode 100644 index 10661cafeb..0000000000 --- a/internal/impl/pure/scanner_decompress.go +++ /dev/null @@ -1,72 +0,0 @@ -package pure - -import ( - "context" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - sdFieldAlgorithm = "algorithm" - sdFieldChild = "into" -) - -func decompressScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Decompress the stream of bytes according to an algorithm, before feeding it into a child scanner."). - Fields( - service.NewStringField(sdFieldAlgorithm). - Description("One of `gzip`, `pgzip`, `zlib`, `bzip2`, `flate`, `snappy`, `lz4`, `zstd`."), - service.NewScannerField(sdFieldChild). - Description("The child scanner to feed the decompressed stream into."). - Default(map[string]any{"to_the_end": map[string]any{}}), - ) -} - -func init() { - err := service.RegisterBatchScannerCreator("decompress", decompressScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return decompressScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func decompressScannerFromParsed(conf *service.ParsedConfig) (l *decompressScannerCreator, err error) { - l = &decompressScannerCreator{} - var decompAlg string - if decompAlg, err = conf.FieldString(sdFieldAlgorithm); err != nil { - return - } - if l.decompReaderCtor, err = strToDecompressReader(decompAlg); err != nil { - return - } - if l.child, err = conf.FieldScanner(sdFieldChild); err != nil { - return - } - return -} - -type decompressScannerCreator struct { - decompReaderCtor DecompressReader - child *service.OwnedScannerCreator -} - -func (c *decompressScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - dRdr, err := c.decompReaderCtor(rdr) - if err != nil { - return nil, err - } - cRdr, ok := dRdr.(io.ReadCloser) - if !ok { - cRdr = io.NopCloser(dRdr) - } - return c.child.Create(cRdr, aFn, details) -} - -func (c *decompressScannerCreator) Close(context.Context) error { - return nil -} diff --git a/internal/impl/pure/scanner_decompress_test.go b/internal/impl/pure/scanner_decompress_test.go deleted file mode 100644 index 9ed5fc2bf9..0000000000 --- a/internal/impl/pure/scanner_decompress_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package pure_test - -import ( - "encoding/hex" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestDecompressScannerSuite(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - decompress: - algorithm: gzip - into: - lines: - custom_delimiter: X -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - inputBytes, err := hex.DecodeString("1f8b080000096e8800ff001e00e1ff68656c6c6f58776f726c64587468697358697358636f6d7072657373656403009104d92d1e000000") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, inputBytes, "hello", "world", "this", "is", "compressed") -} diff --git a/internal/impl/pure/scanner_json.go b/internal/impl/pure/scanner_json.go deleted file mode 100644 index 555bd08fab..0000000000 --- a/internal/impl/pure/scanner_json.go +++ /dev/null @@ -1,71 +0,0 @@ -package pure - -import ( - "context" - "encoding/json" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func jsonDocumentScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Version("4.27.0"). - Summary("Consumes a stream of one or more JSON documents."). - // Just a placeholder empty object as we don't have any fields yet - Field(service.NewObjectField("").Default(map[string]any{})) -} - -func init() { - err := service.RegisterBatchScannerCreator("json_documents", jsonDocumentScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return &jsonDocumentScannerCreator{}, nil - }) - if err != nil { - panic(err) - } -} - -type jsonDocumentScannerCreator struct{} - -func (js *jsonDocumentScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - return service.AutoAggregateBatchScannerAcks(&jsonDocumentScanner{ - d: json.NewDecoder(rdr), - r: rdr, - }, aFn), nil -} - -func (js *jsonDocumentScannerCreator) Close(context.Context) error { - return nil -} - -type jsonDocumentScanner struct { - d *json.Decoder - r io.ReadCloser -} - -func (js *jsonDocumentScanner) NextBatch(ctx context.Context) (service.MessageBatch, error) { - if js.r == nil { - return nil, io.EOF - } - - var jsonDocObj any - if err := js.d.Decode(&jsonDocObj); err != nil { - _ = js.r.Close() - js.r = nil - return nil, err - } - - msg := service.NewMessage(nil) - msg.SetStructuredMut(jsonDocObj) - - return service.MessageBatch{msg}, nil -} - -func (js *jsonDocumentScanner) Close(ctx context.Context) error { - if js.r == nil { - return nil - } - return js.r.Close() -} diff --git a/internal/impl/pure/scanner_json_test.go b/internal/impl/pure/scanner_json_test.go deleted file mode 100644 index aa87162d50..0000000000 --- a/internal/impl/pure/scanner_json_test.go +++ /dev/null @@ -1,273 +0,0 @@ -package pure_test - -import ( - "context" - "io" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestJSONScannerDefault(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`{"a":"a0"} -{"a":"a1"} -{"a":"a2"} -{"a":"a3"} -{"a":"a4"} -`), - `{"a":"a0"}`, - `{"a":"a1"}`, - `{"a":"a2"}`, - `{"a":"a3"}`, - `{"a":"a4"}`, - ) -} - -func TestJSONScannerBadData(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - var ack error - - scanner, err := rdr.Create(io.NopCloser(strings.NewReader(`{"a":"a0"} -nope !@ not good json -{"a":"a1"} -`)), func(ctx context.Context, err error) error { - ack = err - return nil - }, &service.ScannerSourceDetails{}) - require.NoError(t, err) - - resBatch, aFn, err := scanner.NextBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, aFn(context.Background(), nil)) - require.Len(t, resBatch, 1) - mBytes, err := resBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, `{"a":"a0"}`, string(mBytes)) - - _, _, err = scanner.NextBatch(context.Background()) - assert.Error(t, err) - - _, _, err = scanner.NextBatch(context.Background()) - assert.ErrorIs(t, err, io.EOF) - - assert.ErrorContains(t, ack, "invalid character") -} - -func TestJSONScannerFormatted(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`{ - "a":"a0" - } -{ - "a":"a1" -} -{ - "a":"a2" -} -{ - "a":"a3" -} -{ - "a":"a4" -} -`), - `{"a":"a0"}`, - `{"a":"a1"}`, - `{"a":"a2"}`, - `{"a":"a3"}`, - `{"a":"a4"}`, - ) -} - -func TestJSONScannerNested(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`{"a":{"b":"ab0"}} -{"a":{"b":"ab1"}} -{"a":{"b":"ab2"}} -{"a":{"b":"ab3"}} -{"a":{"b":"ab4"}} -`), - `{"a":{"b":"ab0"}}`, - `{"a":{"b":"ab1"}}`, - `{"a":{"b":"ab2"}}`, - `{"a":{"b":"ab3"}}`, - `{"a":{"b":"ab4"}}`, - ) -} - -func TestJSONScannerNestedAndFormatted(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`{ - "a": { - "b": "ab0" - } -} -{ - "a": { - "b": "ab1" - } -} -{ - "a": { - "b": "ab2" - } -} -{ - "a": { - "b": "ab3" - } -} -{ - "a": { - "b": "ab4" - } -} -`), - `{"a":{"b":"ab0"}}`, - `{"a":{"b":"ab1"}}`, - `{"a":{"b":"ab2"}}`, - `{"a":{"b":"ab3"}}`, - `{"a":{"b":"ab4"}}`, - ) -} - -func TestJSONScannerMultipleValuesAndFormatted(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`{ - "a": "a0", - "b": "b0" - } - { - "b": "b1", - "a": "a1" - } - { - "a": "a2", - "b": "b2" - } -`), - `{"a":"a0","b":"b0"}`, - `{"a":"a1","b":"b1"}`, - `{"a":"a2","b":"b2"}`, - ) -} - -func TestJSONScannerArrayElement(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`{ - "a": ["a0","a1","a2"], - "b": "b0" - } - { - "a": "a1", - "b": "b1" - } - { - "a": "a2", - "b": "b2" - } -`), - `{"a":["a0","a1","a2"],"b":"b0"}`, - `{"a":"a1","b":"b1"}`, - `{"a":"a2","b":"b2"}`, - ) -} - -func TestJSONScannerArray(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - json_documents: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`[ - { - "a": "a0", - "b": "b0" - }, - { - "a": "a1", - "b": "b1" - }, - { - "a": "a2", - "b": "b2" - } - ] -`), - `[{"a":"a0","b":"b0"},{"a":"a1","b":"b1"},{"a":"a2","b":"b2"}]`, - ) -} diff --git a/internal/impl/pure/scanner_lines.go b/internal/impl/pure/scanner_lines.go deleted file mode 100644 index 241044f31c..0000000000 --- a/internal/impl/pure/scanner_lines.go +++ /dev/null @@ -1,119 +0,0 @@ -package pure - -import ( - "bufio" - "bytes" - "context" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - slFieldCustomDelimiter = "custom_delimiter" - slFieldMaxBufferSize = "max_buffer_size" -) - -func linesScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Split an input stream into a message per line of data."). - Fields( - service.NewStringField(slFieldCustomDelimiter). - Description("Use a provided custom delimiter for detecting the end of a line rather than a single line break."). - Optional(), - service.NewIntField(slFieldMaxBufferSize). - Description("Set the maximum buffer size for storing line data, this limits the maximum size that a line can be without causing an error."). - Default(bufio.MaxScanTokenSize), - ) -} - -func init() { - err := service.RegisterBatchScannerCreator("lines", linesScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return linesScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func linesScannerFromParsed(conf *service.ParsedConfig) (l *linesScanner, err error) { - l = &linesScanner{} - if conf.Contains(slFieldCustomDelimiter) { - if l.customDelim, err = conf.FieldString(slFieldCustomDelimiter); err != nil { - return - } - } - if l.maxScanTokenSize, err = conf.FieldInt(slFieldMaxBufferSize); err != nil { - return - } - return -} - -type linesScanner struct { - maxScanTokenSize int - customDelim string -} - -func (l *linesScanner) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - scanner := bufio.NewScanner(rdr) - if l.maxScanTokenSize != bufio.MaxScanTokenSize { - scanner.Buffer([]byte{}, l.maxScanTokenSize) - } - - if l.customDelim != "" { - delimBytes := []byte(l.customDelim) - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - if i := bytes.Index(data, delimBytes); i >= 0 { - // We have a full terminated line. - return i + len(delimBytes), data[0:i], nil - } - - // If we're at EOF, we have a final, non-terminated line. Return it. - if atEOF { - return len(data), data, nil - } - - // Request more data. - return 0, nil, nil - }) - } - - return service.AutoAggregateBatchScannerAcks(&linesReaderStream{ - buf: scanner, - r: rdr, - }, aFn), nil -} - -func (l *linesScanner) Close(context.Context) error { - return nil -} - -type linesReaderStream struct { - buf *bufio.Scanner - r io.ReadCloser -} - -func (l *linesReaderStream) NextBatch(ctx context.Context) (service.MessageBatch, error) { - scanned := l.buf.Scan() - if scanned { - bytesCopy := make([]byte, len(l.buf.Bytes())) - copy(bytesCopy, l.buf.Bytes()) - return service.MessageBatch{service.NewMessage(bytesCopy)}, nil - } - - err := l.buf.Err() - if err == nil { - err = io.EOF - } - return nil, err -} - -func (l *linesReaderStream) Close(ctx context.Context) error { - return l.r.Close() -} diff --git a/internal/impl/pure/scanner_lines_test.go b/internal/impl/pure/scanner_lines_test.go deleted file mode 100644 index d67a2ed527..0000000000 --- a/internal/impl/pure/scanner_lines_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package pure_test - -import ( - "bytes" - "context" - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestLinesScanner(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - lines: - custom_delimiter: 'X' - max_buffer_size: 200 -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - buf := bytes.NewReader([]byte(`firstXsecondXthird`)) - var acked bool - strm, err := rdr.Create(io.NopCloser(buf), func(ctx context.Context, err error) error { - acked = true - return nil - }, service.NewScannerSourceDetails()) - require.NoError(t, err) - - for _, s := range []string{ - "first", "second", "third", - } { - m, aFn, err := strm.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, m, 1) - mBytes, err := m[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, s, string(mBytes)) - require.NoError(t, aFn(context.Background(), nil)) - assert.False(t, acked) - } - - _, _, err = strm.NextBatch(context.Background()) - require.Equal(t, io.EOF, err) - - require.NoError(t, strm.Close(context.Background())) - assert.True(t, acked) -} - -func TestLinesScannerSuite(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - lines: - custom_delimiter: 'X' - max_buffer_size: 200 -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`firstXsecondXthird`), "first", "second", "third") - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`firstXsecondXXthird`), "first", "second", "", "third") -} diff --git a/internal/impl/pure/scanner_re_match.go b/internal/impl/pure/scanner_re_match.go deleted file mode 100644 index de273be8a3..0000000000 --- a/internal/impl/pure/scanner_re_match.go +++ /dev/null @@ -1,128 +0,0 @@ -package pure - -import ( - "bufio" - "context" - "io" - "regexp" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - sremFieldPattern = "pattern" - sremFieldMaxBufferSize = "max_buffer_size" -) - -func reMatchScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Split an input stream into segments matching against a regular expression."). - Fields( - service.NewStringField(sremFieldPattern). - Description("The pattern to match against."). - Example("(?m)^\\d\\d:\\d\\d:\\d\\d"), - service.NewIntField(sremFieldMaxBufferSize). - Description("Set the maximum buffer size for storing line data, this limits the maximum size that a message can be without causing an error."). - Default(bufio.MaxScanTokenSize), - ) -} - -func init() { - err := service.RegisterBatchScannerCreator("re_match", reMatchScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return reMatchScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func reMatchScannerFromParsed(conf *service.ParsedConfig) (l *reMatchScanner, err error) { - l = &reMatchScanner{} - var regex string - if regex, err = conf.FieldString(sremFieldPattern); err != nil { - return - } - if l.maxScanTokenSize, err = conf.FieldInt(sremFieldMaxBufferSize); err != nil { - return - } - - if l.regex, err = regexp.Compile(regex); err != nil { - return nil, err - } - return -} - -type reMatchScanner struct { - maxScanTokenSize int - regex *regexp.Regexp -} - -func (l *reMatchScanner) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - scanner := bufio.NewScanner(rdr) - if l.maxScanTokenSize != bufio.MaxScanTokenSize { - scanner.Buffer([]byte{}, l.maxScanTokenSize) - } - - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - loc := l.regex.FindAllIndex(data, 2) - if loc == nil { - if atEOF { - return len(data), data, nil - } - return 0, nil, nil - } - - if len(loc) == 1 { - if atEOF { - if loc[0][0] == 0 { - return len(data), data, nil - } - return loc[0][0], data[0:loc[0][0]], nil - } - return 0, nil, nil - } - if loc[0][0] == 0 { - return loc[1][0], data[0:loc[1][0]], nil - } - return loc[0][0], data[0:loc[0][0]], nil - }) - - return service.AutoAggregateBatchScannerAcks(&reMatchReaderStream{ - buf: scanner, - r: rdr, - }, aFn), nil -} - -func (l *reMatchScanner) Close(context.Context) error { - return nil -} - -type reMatchReaderStream struct { - buf *bufio.Scanner - r io.ReadCloser -} - -func (l *reMatchReaderStream) NextBatch(ctx context.Context) (service.MessageBatch, error) { - scanned := l.buf.Scan() - if scanned { - bytesCopy := make([]byte, len(l.buf.Bytes())) - copy(bytesCopy, l.buf.Bytes()) - return service.MessageBatch{service.NewMessage(bytesCopy)}, nil - } - - err := l.buf.Err() - if err == nil { - err = io.EOF - } - return nil, err -} - -func (l *reMatchReaderStream) Close(ctx context.Context) error { - return l.r.Close() -} diff --git a/internal/impl/pure/scanner_re_match_test.go b/internal/impl/pure/scanner_re_match_test.go deleted file mode 100644 index 6f05bf695c..0000000000 --- a/internal/impl/pure/scanner_re_match_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package pure_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestReMatchScannerSuite(t *testing.T) { - testREPattern := func(pattern, input string, expected ...string) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(fmt.Sprintf(` -test: - re_match: - pattern: '%v' - max_buffer_size: 200 -`, pattern), nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(input), expected...) - } - - testREPattern("(?m)^", "foo\nbar\nbaz", "foo\n", "bar\n", "baz") - - testREPattern("split", "foo\nbar\nsplit\nbaz\nsplitsplit", "foo\nbar\n", "split\nbaz\n", "split", "split") - - testREPattern("\\n", "split", "split") - testREPattern("split", "split", "split") - - testREPattern("\\n", "foo\nbar\nsplit\nbaz\nsplitsplit", "foo", "\nbar", "\nsplit", "\nbaz", "\nsplitsplit") - - testREPattern("\\n", "foo\nbar\nsplit\nbaz", "foo", "\nbar", "\nsplit", "\nbaz") - - testREPattern("\\n\\d", "20:20:22 ERROR\nCode\n20:20:21 INFO\n20:20:21 INFO\n20:20:22 ERROR\nCode\n", "20:20:22 ERROR\nCode", "\n20:20:21 INFO", "\n20:20:21 INFO", "\n20:20:22 ERROR\nCode\n") - - testREPattern("(?m)^\\d\\d:\\d\\d:\\d\\d", "20:20:22 ERROR\nCode\n20:20:21 INFO\n20:20:21 INFO\n20:20\n20:20:22 ERROR\nCode\n2022", "20:20:22 ERROR\nCode\n", "20:20:21 INFO\n", "20:20:21 INFO\n20:20\n", "20:20:22 ERROR\nCode\n2022") - - testREPattern("split", "") -} diff --git a/internal/impl/pure/scanner_skip_bom.go b/internal/impl/pure/scanner_skip_bom.go deleted file mode 100644 index 0204244f44..0000000000 --- a/internal/impl/pure/scanner_skip_bom.go +++ /dev/null @@ -1,153 +0,0 @@ -package pure - -import ( - "context" - "io" - "sort" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - ssbFieldChild = "into" -) - -func ssbScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Skip one or more byte order marks for each opened child scanner."). - Fields( - service.NewScannerField(ssbFieldChild). - Description("The child scanner to feed the resulting stream into."). - Default(map[string]any{"to_the_end": map[string]any{}}), - ) -} - -func init() { - err := service.RegisterBatchScannerCreator("skip_bom", ssbScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return ssbScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func ssbScannerFromParsed(conf *service.ParsedConfig) (l *ssbScannerCreator, err error) { - l = &ssbScannerCreator{} - if l.child, err = conf.FieldScanner(sdFieldChild); err != nil { - return - } - return -} - -type ssbScannerCreator struct { - child *service.OwnedScannerCreator -} - -func (c *ssbScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - return c.child.Create(skipBOM(rdr), aFn, details) -} - -func (c *ssbScannerCreator) Close(context.Context) error { - return nil -} - -//------------------------------------------------------------------------------ - -func skipBOM(r io.ReadCloser) io.ReadCloser { - return skipGroup(r, - []byte{0x00, 0x00, 0xFE, 0xFF}, // UTF32BigEndianBOM4 - []byte{0xFF, 0xFE, 0x00, 0x00}, // UTF32LittleEndianBOM4 - []byte{0xEF, 0xBB, 0xBF}, // UTF8BOM3 - []byte{0xFE, 0xFF}, // UTF16BigEndianBOM2 - []byte{0xFF, 0xFE}, // UTF16LittleEndianBOM2 - ) -} - -func skipGroup(rd io.ReadCloser, groups ...[]byte) io.ReadCloser { - if len(groups) == 0 { - return rd - } - - sort.Slice(groups, func(i, j int) bool { - return len(groups[i]) > len(groups[j]) - }) - - buf, err := readUpToMax(rd, len(groups[0])) - -groupLoop: - for _, g := range groups { - if len(buf) < len(g) { - continue - } - for i, b := range g { - if buf[i] != b { - continue groupLoop - } - } - if buf = buf[len(g):]; len(buf) == 0 { - buf = nil - } - break - } - - return &bufPriorityReader{ - rd: rd, - buf: buf, - err: err, - } -} - -func readUpToMax(r io.Reader, max int) (buf []byte, err error) { - if max == 0 { - return - } - - buf = make([]byte, max) - - var readLen int - for err == nil && readLen < max { - var n int - n, err = r.Read(buf[readLen:]) - readLen += n - } - buf = buf[:readLen] - return -} - -//------------------------------------------------------------------------------ - -// Reads from a buf and err as priority over the underlying io.Reader. -type bufPriorityReader struct { - rd io.Reader - buf []byte - err error -} - -func (r *bufPriorityReader) Read(p []byte) (n int, err error) { - if len(p) == 0 { - return - } - - if r.buf == nil { - if err = r.err; err != nil { - r.err = nil - return - } - return r.rd.Read(p) - } - - n = copy(p, r.buf) - if r.buf = r.buf[n:]; len(r.buf) == 0 { - r.buf = nil - } - return -} - -func (r *bufPriorityReader) Close() error { - if c, ok := r.rd.(io.Closer); ok { - return c.Close() - } - return nil -} diff --git a/internal/impl/pure/scanner_skip_bom_test.go b/internal/impl/pure/scanner_skip_bom_test.go deleted file mode 100644 index 05eec1442b..0000000000 --- a/internal/impl/pure/scanner_skip_bom_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package pure - -import ( - "bytes" - "fmt" - "io" - "testing" - "testing/iotest" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestReadUpTo(t *testing.T) { - for _, test := range []struct { - name string - input string - skips [][]byte - expected string - }{ - { - name: "no groups", - input: "foo", - expected: "foo", - }, - { - name: "no input", - input: "", - skips: [][]byte{ - []byte("hello "), - }, - expected: "", - }, - { - name: "an empty group", - input: "hello world", - skips: [][]byte{ - []byte("not this"), - []byte(""), - }, - expected: "hello world", - }, - { - name: "easy match", - input: "hello world", - expected: "world", - skips: [][]byte{ - []byte("hello "), - }, - }, - { - name: "exact skip match", - input: "foo", - expected: "", - skips: [][]byte{ - []byte("foo"), - }, - }, - { - name: "max is bigger", - input: "foa", - expected: "a", - skips: [][]byte{ - []byte("fo"), - []byte("what this is huge"), - }, - }, - { - name: "order doesnt matter", - input: "helloworld", - expected: "ld", - skips: [][]byte{ - []byte("hellowoa"), - []byte("hella"), - []byte("hello"), - []byte("hellowor"), - []byte("hea"), - }, - }, - } { - test := test - for _, readWrapper := range []struct { - name string - fn func(io.Reader) io.Reader - }{ - {"full", func(r io.Reader) io.Reader { return r }}, - {"one_byte", iotest.OneByteReader}, - } { - t.Run(fmt.Sprintf("%v_%v", test.name, readWrapper.name), func(t *testing.T) { - testReader := skipGroup(io.NopCloser(bytes.NewReader([]byte(test.input))), test.skips...) - output, err := io.ReadAll(testReader) - require.NoError(t, err) - assert.Equal(t, test.expected, string(output)) - }) - } - } -} diff --git a/internal/impl/pure/scanner_switch.go b/internal/impl/pure/scanner_switch.go deleted file mode 100644 index 5d6e01d683..0000000000 --- a/internal/impl/pure/scanner_switch.go +++ /dev/null @@ -1,132 +0,0 @@ -package pure - -import ( - "context" - "errors" - "io" - "regexp" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - ssFieldSwitchREMatchName = "re_match_name" - ssFieldSwitchChild = "scanner" -) - -func switchScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Select a child scanner dynamically for source data based on factors such as the filename."). - Description("This scanner outlines a list of potential child scanner candidates to be chosen, and for each source of data the first candidate to pass will be selected. A candidate without any conditions acts as a catch-all and will pass for every source, it is recommended to always have a catch-all scanner at the end of your list. If a given source of data does not pass a candidate an error is returned and the data is rejected."). - Field(service.NewObjectListField("", - service.NewStringField(ssFieldSwitchREMatchName). - Description("A regular expression to test against the name of each source of data fed into the scanner (filename or equivalent). If this pattern matches the child scanner is selected."). - Optional(), - service.NewScannerField(ssFieldSwitchChild). - Description("The scanner to activate if this candidate passes."), - )). - Example( - "Switch based on file name", - "In this example a file input chooses a scanner based on the extension of each file", ` -input: - file: - paths: [ ./data/* ] - scanner: - switch: - - re_match_name: '\.avro$' - scanner: { avro: {} } - - - re_match_name: '\.csv$' - scanner: { csv: {} } - - - re_match_name: '\.csv.gz$' - scanner: - decompress: - algorithm: gzip - into: - csv: {} - - - re_match_name: '\.tar$' - scanner: { tar: {} } - - - re_match_name: '\.tar.gz$' - scanner: - decompress: - algorithm: gzip - into: - tar: {} - - - scanner: { to_the_end: {} } -`) -} - -func init() { - err := service.RegisterBatchScannerCreator("switch", switchScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return switchScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func switchScannerFromParsed(conf *service.ParsedConfig) (l *switchScannerCreator, err error) { - l = &switchScannerCreator{} - var pConfs []*service.ParsedConfig - if pConfs, err = conf.FieldObjectList(); err != nil { - return - } - - for _, pConf := range pConfs { - var c scannerSwitchCase - if c.child, err = pConf.FieldScanner(ssFieldSwitchChild); err != nil { - return - } - if pConf.Contains(ssFieldSwitchREMatchName) { - var namePatternStr string - if namePatternStr, err = pConf.FieldString(ssFieldSwitchREMatchName); err != nil { - return - } - if c.namePattern, err = regexp.Compile(namePatternStr); err != nil { - return - } - } - l.matchCases = append(l.matchCases, &c) - } - return -} - -type scannerSwitchCase struct { - namePattern *regexp.Regexp - child *service.OwnedScannerCreator -} - -func (s *scannerSwitchCase) test(details *service.ScannerSourceDetails) bool { - if s.namePattern != nil { - return s.namePattern.MatchString(details.Name()) - } - return true -} - -type switchScannerCreator struct { - matchCases []*scannerSwitchCase -} - -func (c *switchScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - for _, v := range c.matchCases { - if v.test(details) { - return v.child.Create(rdr, aFn, details) - } - } - return nil, errors.New("source details did not match against any scanners") -} - -func (c *switchScannerCreator) Close(ctx context.Context) error { - for _, v := range c.matchCases { - if err := v.child.Close(ctx); err != nil { - return err - } - } - return nil -} diff --git a/internal/impl/pure/scanner_switch_test.go b/internal/impl/pure/scanner_switch_test.go deleted file mode 100644 index 80e8e7a307..0000000000 --- a/internal/impl/pure/scanner_switch_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package pure_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestSwitchScanner(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - switch: - - re_match_name: '\.json$' - scanner: { to_the_end: {} } - - re_match_name: '\.csv$' - scanner: { csv: {} } - - re_match_name: '\.chunks$' - scanner: - chunker: - size: 4 - - scanner: { to_the_end: {} } - -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - details := service.NewScannerSourceDetails() - details.SetName("a/b/foo.csv") - testutil.ScannerTestSuite(t, rdr, details, []byte(`a,b,c -a1,b1,c1 -a2,b2,c2 -a3,b3,c3 -`), - `{"a":"a1","b":"b1","c":"c1"}`, - `{"a":"a2","b":"b2","c":"c2"}`, - `{"a":"a3","b":"b3","c":"c3"}`, - ) - - details = service.NewScannerSourceDetails() - details.SetName("woof/meow.chunks") - testutil.ScannerTestSuite(t, rdr, details, []byte(`abcdefghijklmnopqrstuvwxyz`), "abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx", "yz") - - details = service.NewScannerSourceDetails() - details.SetName("./meow.json") - testutil.ScannerTestSuite(t, rdr, details, []byte(`{"hello":"world"}`), `{"hello":"world"}`) -} diff --git a/internal/impl/pure/scanner_tar.go b/internal/impl/pure/scanner_tar.go deleted file mode 100644 index e23a1f8547..0000000000 --- a/internal/impl/pure/scanner_tar.go +++ /dev/null @@ -1,86 +0,0 @@ -package pure - -import ( - "archive/tar" - "bytes" - "context" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func tarScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Consume a tar archive file by file."). - Description(` -== Metadata - -This scanner adds the following metadata to each message: - -- ` + "`tar_name`" + ` - -`). - Field(service.NewObjectField("").Default(map[string]any{})) -} - -func init() { - err := service.RegisterBatchScannerCreator("tar", tarScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return tarScannerFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func tarScannerFromParsed(conf *service.ParsedConfig) (l *tarScannerCreator, err error) { - l = &tarScannerCreator{} - return -} - -type tarScannerCreator struct{} - -func (c *tarScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - return service.AutoAggregateBatchScannerAcks(&tarScanner{ - r: rdr, - t: tar.NewReader(rdr), - }, aFn), nil -} - -func (c *tarScannerCreator) Close(context.Context) error { - return nil -} - -type tarScanner struct { - t *tar.Reader - r io.ReadCloser -} - -func (c *tarScanner) NextBatch(ctx context.Context) (service.MessageBatch, error) { - if c.r == nil { - return nil, io.EOF - } - - hdr, err := c.t.Next() - if err != nil { - return nil, err - } - - var buf bytes.Buffer - if _, err := io.Copy(&buf, c.t); err != nil { - return nil, err - } - - msg := service.NewMessage(buf.Bytes()) - msg.MetaSetMut("tar_name", hdr.Name) - - return service.MessageBatch{msg}, nil -} - -func (c *tarScanner) Close(ctx context.Context) error { - if c.r == nil { - return nil - } - return c.r.Close() -} diff --git a/internal/impl/pure/scanner_tar_test.go b/internal/impl/pure/scanner_tar_test.go deleted file mode 100644 index b80fa2c0de..0000000000 --- a/internal/impl/pure/scanner_tar_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package pure_test - -import ( - "archive/tar" - "bytes" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestTarScannerSuite(t *testing.T) { - input := []string{ - "first document", - "second document", - "third document", - } - - var tarBuf bytes.Buffer - tw := tar.NewWriter(&tarBuf) - for i := range input { - hdr := &tar.Header{ - Name: fmt.Sprintf("testfile%v", i), - Mode: 0o600, - Size: int64(len(input[i])), - } - - err := tw.WriteHeader(hdr) - require.NoError(t, err) - - _, err = tw.Write([]byte(input[i])) - require.NoError(t, err) - } - require.NoError(t, tw.Close()) - - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - tar: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, tarBuf.Bytes(), input...) -} diff --git a/internal/impl/pure/scanner_to_the_end.go b/internal/impl/pure/scanner_to_the_end.go deleted file mode 100644 index fce00f4b5e..0000000000 --- a/internal/impl/pure/scanner_to_the_end.go +++ /dev/null @@ -1,70 +0,0 @@ -package pure - -import ( - "context" - "io" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func toTheEndScannerSpec() *service.ConfigSpec { - return service.NewConfigSpec(). - Stable(). - Summary("Read the input stream all the way until the end and deliver it as a single message."). - Description(` -[CAUTION] -==== -Some sources of data may not have a logical end, therefore caution should be made to exclusively use this scanner when the end of an input stream is clearly defined (and well within memory). -==== -`). - Field(service.NewObjectField("").Default(map[string]any{})) -} - -func init() { - err := service.RegisterBatchScannerCreator("to_the_end", toTheEndScannerSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return toTheEndScannerCreatorFromParsed(conf) - }) - if err != nil { - panic(err) - } -} - -func toTheEndScannerCreatorFromParsed(conf *service.ParsedConfig) (s *toTheEndScannerCreator, err error) { - s = &toTheEndScannerCreator{} - return -} - -type toTheEndScannerCreator struct{} - -func (l *toTheEndScannerCreator) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (service.BatchScanner, error) { - return service.AutoAggregateBatchScannerAcks(&toTheEndScanner{r: rdr}, aFn), nil -} - -func (l *toTheEndScannerCreator) Close(context.Context) error { - return nil -} - -type toTheEndScanner struct { - r io.ReadCloser -} - -func (t *toTheEndScanner) NextBatch(ctx context.Context) (service.MessageBatch, error) { - if t.r == nil { - return nil, io.EOF - } - mBytes, err := io.ReadAll(t.r) - if err != nil { - return nil, err - } - _ = t.r.Close() - t.r = nil - return service.MessageBatch{service.NewMessage(mBytes)}, err -} - -func (t *toTheEndScanner) Close(ctx context.Context) error { - if t.r == nil { - return nil - } - return t.r.Close() -} diff --git a/internal/impl/pure/scanner_to_the_end_test.go b/internal/impl/pure/scanner_to_the_end_test.go deleted file mode 100644 index 451296d2c0..0000000000 --- a/internal/impl/pure/scanner_to_the_end_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package pure_test - -import ( - "bytes" - "context" - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/scanner/testutil" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestToTheEndScanner(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - to_the_end: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - buf := bytes.NewReader([]byte(`firstXsecondXthird`)) - var acked bool - strm, err := rdr.Create(io.NopCloser(buf), func(ctx context.Context, err error) error { - acked = true - return nil - }, service.NewScannerSourceDetails()) - require.NoError(t, err) - - for _, s := range []string{ - "firstXsecondXthird", - } { - m, aFn, err := strm.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, m, 1) - mBytes, err := m[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, s, string(mBytes)) - require.NoError(t, aFn(context.Background(), nil)) - assert.False(t, acked) - } - - _, _, err = strm.NextBatch(context.Background()) - require.Equal(t, io.EOF, err) - - require.NoError(t, strm.Close(context.Background())) - assert.True(t, acked) -} - -func TestToTheEndScannerSuite(t *testing.T) { - confSpec := service.NewConfigSpec().Field(service.NewScannerField("test")) - pConf, err := confSpec.ParseYAML(` -test: - to_the_end: {} -`, nil) - require.NoError(t, err) - - rdr, err := pConf.FieldScanner("test") - require.NoError(t, err) - - testutil.ScannerTestSuite(t, rdr, nil, []byte(`firstXsecondXthird`), "firstXsecondXthird") -} diff --git a/internal/impl/pure/tracer_none.go b/internal/impl/pure/tracer_none.go deleted file mode 100644 index c917d8279e..0000000000 --- a/internal/impl/pure/tracer_none.go +++ /dev/null @@ -1,24 +0,0 @@ -package pure - -import ( - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func init() { - err := service.RegisterOtelTracerProvider( - "none", service.NewConfigSpec(). - Stable(). - Summary(`Do not send tracing events anywhere.`). - Field( - service.NewObjectField("").Default(map[string]any{}), - ), - func(conf *service.ParsedConfig) (trace.TracerProvider, error) { - return noop.NewTracerProvider(), nil - }) - if err != nil { - panic(err) - } -} diff --git a/internal/impl/sql/buffer_sqlite_test.go b/internal/impl/sql/buffer_sqlite_test.go index bcddf8f27f..52ffafa2c6 100644 --- a/internal/impl/sql/buffer_sqlite_test.go +++ b/internal/impl/sql/buffer_sqlite_test.go @@ -14,9 +14,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/internal/impl/sql" "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/connect/v4/internal/impl/sql" + _ "github.com/benthosdev/benthos/v4/public/components/pure/extended" ) diff --git a/internal/impl/sql/integration_test.go b/internal/impl/sql/integration_test.go index 98a4bf803f..bdece14fcc 100644 --- a/internal/impl/sql/integration_test.go +++ b/internal/impl/sql/integration_test.go @@ -14,10 +14,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - isql "github.com/benthosdev/benthos/v4/internal/impl/sql" "github.com/benthosdev/benthos/v4/public/service" "github.com/benthosdev/benthos/v4/public/service/integration" + isql "github.com/redpanda-data/connect/v4/internal/impl/sql" + _ "github.com/benthosdev/benthos/v4/public/components/pure" _ "github.com/benthosdev/benthos/v4/public/components/sql" ) diff --git a/internal/log/config.go b/internal/log/config.go deleted file mode 100644 index 4bf1ecb0d1..0000000000 --- a/internal/log/config.go +++ /dev/null @@ -1,110 +0,0 @@ -package log - -import "github.com/benthosdev/benthos/v4/internal/docs" - -const ( - fieldLogLevel = "level" - fieldFormat = "format" - fieldAddTimeStamp = "add_timestamp" - fieldLevelName = "level_name" - fieldMessageName = "message_name" - fieldTimestampName = "timestamp_name" - fieldStaticFields = "static_fields" - fieldFile = "file" - fieldFilePath = "path" - fieldFileRotate = "rotate" - fieldFileRotateMaxAge = "rotate_max_age_days" -) - -// Config holds configuration options for a logger object. -type Config struct { - LogLevel string `yaml:"level"` - Format string `yaml:"format"` - AddTimeStamp bool `yaml:"add_timestamp"` - LevelName string `yaml:"level_name"` - MessageName string `yaml:"message_name"` - TimestampName string `yaml:"timestamp_name"` - StaticFields map[string]string `yaml:"static_fields"` - File File `yaml:"file"` -} - -// File contains configuration for file based logging. -type File struct { - Path string `yaml:"path"` - Rotate bool `yaml:"rotate"` - RotateMaxAge int `yaml:"rotate_max_age_days"` -} - -// NewConfig returns a config struct with the default values for each field. -func NewConfig() Config { - return Config{ - LogLevel: "INFO", - Format: "logfmt", - AddTimeStamp: false, - LevelName: "level", - TimestampName: "time", - MessageName: "msg", - StaticFields: map[string]string{ - "@service": "benthos", - }, - } -} - -// UnmarshalYAML ensures that when parsing configs that are in a slice the -// default values are still applied. -func (conf *Config) UnmarshalYAML(unmarshal func(any) error) error { - type confAlias Config - aliased := confAlias(NewConfig()) - - defaultFields := aliased.StaticFields - aliased.StaticFields = nil - - if err := unmarshal(&aliased); err != nil { - return err - } - - if aliased.StaticFields == nil { - aliased.StaticFields = defaultFields - } - - *conf = Config(aliased) - return nil -} - -func FromParsed(pConf *docs.ParsedConfig) (conf Config, err error) { - if conf.LogLevel, err = pConf.FieldString(fieldLogLevel); err != nil { - return - } - if conf.Format, err = pConf.FieldString(fieldFormat); err != nil { - return - } - if conf.AddTimeStamp, err = pConf.FieldBool(fieldAddTimeStamp); err != nil { - return - } - if conf.LevelName, err = pConf.FieldString(fieldLevelName); err != nil { - return - } - if conf.MessageName, err = pConf.FieldString(fieldMessageName); err != nil { - return - } - if conf.TimestampName, err = pConf.FieldString(fieldTimestampName); err != nil { - return - } - if conf.StaticFields, err = pConf.FieldStringMap(fieldStaticFields); err != nil { - return - } - - if pConf.Contains(fieldFile) { - fConf := pConf.Namespace(fieldFile) - if conf.File.Path, err = fConf.FieldString(fieldFilePath); err != nil { - return - } - if conf.File.Rotate, err = fConf.FieldBool(fieldFileRotate); err != nil { - return - } - if conf.File.RotateMaxAge, err = fConf.FieldInt(fieldFileRotateMaxAge); err != nil { - return - } - } - return -} diff --git a/internal/log/docs.adoc b/internal/log/docs.adoc deleted file mode 100644 index 068288b354..0000000000 --- a/internal/log/docs.adoc +++ /dev/null @@ -1,42 +0,0 @@ -= Logger - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the contents of: - internal/log/docs.adoc -//// - -{page-component-title} logging prints to stdout (or stderr if your output is stdout) and is formatted as https://brandur.org/logfmt[logfmt] by default. Use these configuration options to change both the logging formats as well as the destination of logs. - -[tabs] -====== -Logfmt to Stdout:: -+ --- -```yaml -logger: - level: INFO - format: logfmt - add_timestamp: false - static_fields: - '@service': benthos -``` --- -JSON to File:: -+ --- -```yaml -logger: - level: WARN - format: json - file: - path: ./logs/benthos.ndjson - rotate: true -``` --- -====== - -== Fields - diff --git a/internal/log/docs.go b/internal/log/docs.go deleted file mode 100644 index c15ad5edaa..0000000000 --- a/internal/log/docs.go +++ /dev/null @@ -1,51 +0,0 @@ -package log - -import ( - "bytes" - "text/template" - - "github.com/benthosdev/benthos/v4/internal/docs" - - _ "embed" -) - -// Spec returns a field spec for the logger configuration fields. -func Spec() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString(fieldLogLevel, "Set the minimum severity level for emitting logs.").HasOptions( - "OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "ALL", "NONE", - ).HasDefault("INFO").LinterFunc(nil), - docs.FieldString(fieldFormat, "Set the format of emitted logs.").HasOptions("json", "logfmt").HasDefault("logfmt"), - docs.FieldBool(fieldAddTimeStamp, "Whether to include timestamps in logs.").HasDefault(false), - docs.FieldString(fieldLevelName, "The name of the level field added to logs when the `format` is `json`.").HasDefault("level"), - docs.FieldString(fieldTimestampName, "The name of the timestamp field added to logs when `add_timestamp` is set to `true` and the `format` is `json`.").HasDefault("time"), - docs.FieldString(fieldMessageName, "The name of the message field added to logs when the `format` is `json`.").HasDefault("msg"), - docs.FieldString(fieldStaticFields, "A map of key/value pairs to add to each structured log.").Map().HasDefault(map[string]any{ - "@service": "benthos", - }), - docs.FieldObject(fieldFile, "Experimental: Specify fields for optionally writing logs to a file.").WithChildren( - docs.FieldString(fieldFilePath, "The file path to write logs to, if the file does not exist it will be created. Leave this field empty or unset to disable file based logging.").HasDefault(""), - docs.FieldBool(fieldFileRotate, "Whether to rotate log files automatically.").HasDefault(false), - docs.FieldInt(fieldFileRotateMaxAge, "The maximum number of days to retain old log files based on the timestamp encoded in their filename, after which they are deleted. Setting to zero disables this mechanism.").HasDefault(0), - ), - } -} - -//go:embed docs.adoc -var loggerDocs string - -type loggerContext struct { - Fields []docs.FieldSpecCtx -} - -// DocsMarkdown returns a markdown document for the logger documentation. -func DocsMarkdown() ([]byte, error) { - var buf bytes.Buffer - buf.WriteString(loggerDocs) - - err := template.Must(template.New("logger").Parse(docs.FieldsTemplate(false)+`{{template "field_docs" . -}}`)).Execute(&buf, loggerContext{ - Fields: docs.FieldObject("", "").WithChildren(Spec()...).FlattenChildrenForDocs(), - }) - - return buf.Bytes(), err -} diff --git a/internal/log/interface.go b/internal/log/interface.go deleted file mode 100644 index 67bc40c14c..0000000000 --- a/internal/log/interface.go +++ /dev/null @@ -1,14 +0,0 @@ -package log - -// Modular is a log printer that allows you to branch new modules. -type Modular interface { - WithFields(fields map[string]string) Modular - With(keyValues ...any) Modular - - Fatal(format string, v ...any) - Error(format string, v ...any) - Warn(format string, v ...any) - Info(format string, v ...any) - Debug(format string, v ...any) - Trace(format string, v ...any) -} diff --git a/internal/log/logrus.go b/internal/log/logrus.go deleted file mode 100644 index c80bfa4c2e..0000000000 --- a/internal/log/logrus.go +++ /dev/null @@ -1,171 +0,0 @@ -package log - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "github.com/sirupsen/logrus" - "gopkg.in/natefinch/lumberjack.v2" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -// Logger is an object with support for levelled logging and modular components. -type Logger struct { - entry *logrus.Entry -} - -// New returns a new logger from a config, or returns an error if the config -// is invalid. -func New(stream io.Writer, fs ifs.FS, config Config) (Modular, error) { - if config.File.Path != "" { - if config.File.Rotate { - stream = &lumberjack.Logger{ - Filename: config.File.Path, - MaxSize: 10, - MaxAge: config.File.RotateMaxAge, - MaxBackups: 1, - Compress: true, - } - } else { - fw, err := ifs.OS().OpenFile(config.File.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666) - if err == nil { - var isw bool - if stream, isw = fw.(io.Writer); !isw { - err = errors.New("failed to open a writeable file") - } - } - if err != nil { - return nil, err - } - } - } - - logger := logrus.New() - logger.Out = stream - - switch config.Format { - case "json": - logger.SetFormatter(&logrus.JSONFormatter{ - DisableTimestamp: !config.AddTimeStamp, - FieldMap: logrus.FieldMap{ - logrus.FieldKeyTime: config.TimestampName, - logrus.FieldKeyMsg: config.MessageName, - logrus.FieldKeyLevel: config.LevelName, - }, - }) - case "logfmt": - logger.SetFormatter(&logrus.TextFormatter{ - DisableTimestamp: !config.AddTimeStamp, - QuoteEmptyFields: true, - FullTimestamp: config.AddTimeStamp, - FieldMap: logrus.FieldMap{ - logrus.FieldKeyTime: config.TimestampName, - logrus.FieldKeyMsg: config.MessageName, - logrus.FieldKeyLevel: config.LevelName, - }, - }) - default: - return nil, fmt.Errorf("log format '%v' not recognized", config.Format) - } - - switch strings.ToUpper(config.LogLevel) { - case "OFF", "NONE": - logger.Level = logrus.PanicLevel - case "FATAL": - logger.Level = logrus.FatalLevel - case "ERROR": - logger.Level = logrus.ErrorLevel - case "WARN": - logger.Level = logrus.WarnLevel - case "INFO": - logger.Level = logrus.InfoLevel - case "DEBUG": - logger.Level = logrus.DebugLevel - case "TRACE", "ALL": - logger.Level = logrus.TraceLevel - logger.Level = logrus.TraceLevel - } - - sFields := logrus.Fields{} - for k, v := range config.StaticFields { - sFields[k] = v - } - logEntry := logger.WithFields(sFields) - - return &Logger{entry: logEntry}, nil -} - -//------------------------------------------------------------------------------ - -// Noop creates and returns a new logger object that writes nothing. -func Noop() Modular { - logger := logrus.New() - logger.Out = io.Discard - return &Logger{entry: logger.WithFields(logrus.Fields{})} -} - -// WithFields returns a logger with new fields added to the JSON formatted -// output. -func (l *Logger) WithFields(inboundFields map[string]string) Modular { - newFields := make(logrus.Fields, len(inboundFields)) - for k, v := range inboundFields { - newFields[k] = v - } - - newLogger := *l - newLogger.entry = l.entry.WithFields(newFields) - return &newLogger -} - -// With returns a copy of the logger with new labels added to the logging -// context. -func (l *Logger) With(keyValues ...any) Modular { - newEntry := l.entry.WithFields(logrus.Fields{}) - for i := 0; i < (len(keyValues) - 1); i += 2 { - key, ok := keyValues[i].(string) - if !ok { - continue - } - newEntry = newEntry.WithField(key, keyValues[i+1]) - } - - newLogger := *l - newLogger.entry = newEntry - return &newLogger -} - -//------------------------------------------------------------------------------ - -// Fatal prints a fatal message to the console. Does NOT cause panic. -func (l *Logger) Fatal(format string, v ...any) { - l.entry.Fatalf(strings.TrimSuffix(format, "\n"), v...) -} - -// Error prints an error message to the console. -func (l *Logger) Error(format string, v ...any) { - l.entry.Errorf(strings.TrimSuffix(format, "\n"), v...) -} - -// Warn prints a warning message to the console. -func (l *Logger) Warn(format string, v ...any) { - l.entry.Warnf(strings.TrimSuffix(format, "\n"), v...) -} - -// Info prints an information message to the console. -func (l *Logger) Info(format string, v ...any) { - l.entry.Infof(strings.TrimSuffix(format, "\n"), v...) -} - -// Debug prints a debug message to the console. -func (l *Logger) Debug(format string, v ...any) { - l.entry.Debugf(strings.TrimSuffix(format, "\n"), v...) -} - -// Trace prints a trace message to the console. -func (l *Logger) Trace(format string, v ...any) { - l.entry.Tracef(strings.TrimSuffix(format, "\n"), v...) -} diff --git a/internal/log/logrus_test.go b/internal/log/logrus_test.go deleted file mode 100644 index 2ae67e9ab3..0000000000 --- a/internal/log/logrus_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package log - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -func TestLoggerWith(t *testing.T) { - loggerConfig := NewConfig() - loggerConfig.AddTimeStamp = false - loggerConfig.Format = "logfmt" - loggerConfig.LogLevel = "WARN" - loggerConfig.StaticFields = map[string]string{ - "@service": "benthos_service", - "@system": "foo", - } - - var buf bytes.Buffer - - logger, err := New(&buf, ifs.OS(), loggerConfig) - require.NoError(t, err) - - logger.Warn("Warning message root module") - - logger2 := logger.WithFields(map[string]string{ - "foo": "bar", "count": "10", "thing": "is a string", "iscool": "true", - }) - require.NoError(t, err) - logger2.Warn("Warning message foo fields") - - logger.Warn("Warning message root module\n") - - expected := `level=warning msg="Warning message root module" @service=benthos_service @system=foo -level=warning msg="Warning message foo fields" @service=benthos_service @system=foo count=10 foo=bar iscool=true thing="is a string" -level=warning msg="Warning message root module" @service=benthos_service @system=foo -` - - assert.Equal(t, expected, buf.String()) -} - -func TestLoggerWithOddArgs(t *testing.T) { - loggerConfig := NewConfig() - loggerConfig.AddTimeStamp = false - loggerConfig.Format = "logfmt" - loggerConfig.LogLevel = "WARN" - loggerConfig.StaticFields = map[string]string{ - "@service": "benthos_service", - "@system": "foo", - } - - var buf bytes.Buffer - - logger, err := New(&buf, ifs.OS(), loggerConfig) - require.NoError(t, err) - - logger = logger.WithFields(map[string]string{ - "foo": "bar", "count": "10", "thing": "is a string", "iscool": "true", - }) - require.NoError(t, err) - - logger.Warn("Warning message foo fields") - - expected := `level=warning msg="Warning message foo fields" @service=benthos_service @system=foo count=10 foo=bar iscool=true thing="is a string" -` - - assert.Equal(t, expected, buf.String()) -} - -func TestLoggerWithNonStringKeys(t *testing.T) { - loggerConfig := NewConfig() - loggerConfig.AddTimeStamp = false - loggerConfig.Format = "logfmt" - loggerConfig.LogLevel = "WARN" - loggerConfig.StaticFields = map[string]string{ - "@service": "benthos_service", - "@system": "foo", - } - - var buf bytes.Buffer - - logger, err := New(&buf, ifs.OS(), loggerConfig) - require.NoError(t, err) - - logger = logger.WithFields(map[string]string{ - "component": "meow", - "foo": "bar", - "thing": "is a string", - "iscool": "true", - }) - - logger.Warn("Warning message foo fields") - - expected := `level=warning msg="Warning message foo fields" @service=benthos_service @system=foo component=meow foo=bar iscool=true thing="is a string" -` - - assert.Equal(t, expected, buf.String()) -} - -func TestLoggerWithOtherNames(t *testing.T) { - loggerConfig := NewConfig() - loggerConfig.AddTimeStamp = false - loggerConfig.Format = "json" - loggerConfig.LogLevel = "WARN" - loggerConfig.StaticFields = map[string]string{ - "@service": "benthos_service", - "@system": "foo", - } - loggerConfig.LevelName = "severity" - loggerConfig.MessageName = "message" - - var buf bytes.Buffer - - logger, err := New(&buf, ifs.OS(), loggerConfig) - require.NoError(t, err) - - logger = logger.WithFields(map[string]string{ - "foo": "bar", - }) - require.NoError(t, err) - - logger.Warn("Warning message foo fields") - - expected := `{"@service":"benthos_service","@system":"foo","foo":"bar","message":"Warning message foo fields","severity":"warning"} -` - - require.JSONEq(t, expected, buf.String()) -} - -type logCounter struct { - count int -} - -func (l *logCounter) Write(p []byte) (n int, err error) { - l.count++ - return len(p), nil -} - -func TestLogLevels(t *testing.T) { - for i, lvl := range []string{ - "FATAL", - "ERROR", - "WARN", - "INFO", - "DEBUG", - "TRACE", - } { - loggerConfig := NewConfig() - loggerConfig.LogLevel = lvl - - buf := logCounter{} - - logger, err := New(&buf, ifs.OS(), loggerConfig) - require.NoError(t, err) - - logger.Error("error test") - logger.Warn("warn test") - logger.Info("info test") - logger.Debug("info test") - logger.Trace("trace test") - - if i != buf.count { - t.Errorf("Wrong log count for [%v], %v != %v", loggerConfig.LogLevel, i, buf.count) - } - } -} diff --git a/internal/log/slog.go b/internal/log/slog.go deleted file mode 100644 index f97dd8c32b..0000000000 --- a/internal/log/slog.go +++ /dev/null @@ -1,64 +0,0 @@ -//go:build go1.21 - -package log - -import ( - "fmt" - "log/slog" - "os" -) - -type logHandler struct { - slog *slog.Logger -} - -func NewBenthosLogAdapter(l *slog.Logger) *logHandler { - return &logHandler{slog: l} -} - -func (l *logHandler) WithFields(fields map[string]string) Modular { - tmp := l.slog - for k, v := range fields { - tmp = tmp.With(slog.String(k, v)) - } - - c := l.clone() - c.slog = tmp - return c -} - -func (l *logHandler) With(keyValues ...any) Modular { - c := l.clone() - c.slog = l.slog.With(keyValues...) - return c -} - -func (l *logHandler) Fatal(format string, v ...any) { - l.slog.Error(fmt.Sprintf(format, v...)) - os.Exit(1) -} - -func (l *logHandler) Error(format string, v ...any) { - l.slog.Error(fmt.Sprintf(format, v...)) -} - -func (l *logHandler) Warn(format string, v ...any) { - l.slog.Warn(fmt.Sprintf(format, v...)) -} - -func (l *logHandler) Info(format string, v ...any) { - l.slog.Info(fmt.Sprintf(format, v...)) -} - -func (l *logHandler) Debug(format string, v ...any) { - l.slog.Debug(fmt.Sprintf(format, v...)) -} - -func (l *logHandler) Trace(format string, v ...any) { - l.slog.Debug(fmt.Sprintf(format, v...)) -} - -func (l *logHandler) clone() *logHandler { - c := *l - return &c -} diff --git a/internal/log/slog_test.go b/internal/log/slog_test.go deleted file mode 100644 index bdad9f632d..0000000000 --- a/internal/log/slog_test.go +++ /dev/null @@ -1,85 +0,0 @@ -//go:build go1.21 - -package log - -import ( - "bytes" - "log/slog" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Clear slog's time attribute for easier testing -var clearTimeAttr = func(_ []string, a slog.Attr) slog.Attr { - if a.Key == "time" { - return slog.String("time", "") - } - return a -} - -func TestSlogToBenthosLoggerAdapter(t *testing.T) { - var buf bytes.Buffer - h := slog.NewTextHandler(&buf, &slog.HandlerOptions{ReplaceAttr: clearTimeAttr}) - s := slog.New(h) - - s = s.With("foo", "bar", "count", "10", "thing", "is a string", "iscool", "true") - - var logger Modular = NewBenthosLogAdapter(s) - require.NotNil(t, logger) - - logger.Warn("Warning message foo fields") - logger.Warn("Warning message root module\n") - - expected := "time=\"\" level=WARN msg=\"Warning message foo fields\" foo=bar count=10 thing=\"is a string\" iscool=true\ntime=\"\" level=WARN msg=\"Warning message root module\\n\" foo=bar count=10 thing=\"is a string\" iscool=true\n" - assert.Equal(t, expected, buf.String()) -} - -func TestSlogToBenthosLoggerAdapterMapKV(t *testing.T) { - var buf bytes.Buffer - h := slog.NewTextHandler(&buf, &slog.HandlerOptions{ReplaceAttr: clearTimeAttr}) - s := slog.New(h) - - var logger Modular = NewBenthosLogAdapter(s) - require.NotNil(t, logger) - - logger = logger.WithFields(map[string]string{ - "foo": "bar", - "count": "10", - }) - - logger = logger.With("thing", "is a string", "iscool", "true") - - logger.Warn("Warning message foo fields") - logger.Warn("Warning message root module\n") - - bufStr := buf.String() - - for _, exp := range []string{ - "time=\"\" level=WARN msg=\"Warning message foo fields\"", - "foo=bar", - "count=10", - "thing=\"is a string\" iscool=true", - "time=\"\" level=WARN msg=\"Warning message root module\\n\"", - } { - assert.Contains(t, bufStr, exp) - } -} - -func TestSlogMessageFormatting(t *testing.T) { - var buf bytes.Buffer - h := slog.NewTextHandler(&buf, &slog.HandlerOptions{ReplaceAttr: clearTimeAttr, Level: slog.LevelDebug}) - s := slog.New(h) - - var logger Modular = NewBenthosLogAdapter(s) - require.NotNil(t, logger) - - logger.Debug("Hello %s %d", "World", 1) - logger.Info("Hello %s %d", "World", 2) - logger.Warn("Hello %s %d", "World", 3) - logger.Error("Hello %s %d", "World", 4) - - expected := "time=\"\" level=DEBUG msg=\"Hello World 1\"\ntime=\"\" level=INFO msg=\"Hello World 2\"\ntime=\"\" level=WARN msg=\"Hello World 3\"\ntime=\"\" level=ERROR msg=\"Hello World 4\"\n" - assert.Equal(t, expected, buf.String()) -} diff --git a/internal/log/tee.go b/internal/log/tee.go deleted file mode 100644 index e70fe5b21f..0000000000 --- a/internal/log/tee.go +++ /dev/null @@ -1,53 +0,0 @@ -package log - -type teeLogger struct { - a, b Modular -} - -func TeeLogger(a, b Modular) Modular { - return &teeLogger{a: a, b: b} -} - -func (t *teeLogger) WithFields(fields map[string]string) Modular { - return &teeLogger{ - a: t.a.WithFields(fields), - b: t.b.WithFields(fields), - } -} - -func (t *teeLogger) With(keyValues ...any) Modular { - return &teeLogger{ - a: t.a.With(keyValues...), - b: t.b.With(keyValues...), - } -} - -func (t *teeLogger) Fatal(format string, v ...any) { - t.a.Fatal(format, v...) - t.b.Fatal(format, v...) -} - -func (t *teeLogger) Error(format string, v ...any) { - t.a.Error(format, v...) - t.b.Error(format, v...) -} - -func (t *teeLogger) Warn(format string, v ...any) { - t.a.Warn(format, v...) - t.b.Warn(format, v...) -} - -func (t *teeLogger) Info(format string, v ...any) { - t.a.Info(format, v...) - t.b.Info(format, v...) -} - -func (t *teeLogger) Debug(format string, v ...any) { - t.a.Debug(format, v...) - t.b.Debug(format, v...) -} - -func (t *teeLogger) Trace(format string, v ...any) { - t.a.Trace(format, v...) - t.b.Trace(format, v...) -} diff --git a/internal/log/tee_test.go b/internal/log/tee_test.go deleted file mode 100644 index d806280452..0000000000 --- a/internal/log/tee_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package log - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -func TestLoggerTee(t *testing.T) { - var bufA, bufB bytes.Buffer - - loggerConfig := NewConfig() - loggerConfig.AddTimeStamp = false - loggerConfig.Format = "logfmt" - loggerConfig.LogLevel = "WARN" - loggerConfig.StaticFields = map[string]string{ - "@service": "benthos_service", - "@system": "foo", - } - - loggerA, err := New(&bufA, ifs.OS(), loggerConfig) - require.NoError(t, err) - - loggerConfig = NewConfig() - loggerConfig.AddTimeStamp = false - loggerConfig.Format = "logfmt" - loggerConfig.LogLevel = "DEBUG" - loggerConfig.StaticFields = map[string]string{ - "@service": "benthos_service", - "@system": "bar", - } - - loggerB, err := New(&bufB, ifs.OS(), loggerConfig) - require.NoError(t, err) - - logger := TeeLogger(loggerA, loggerB) - - logger.Warn("Warning message root module") - logger.Debug("Debug log root module") - - logger2 := logger.WithFields(map[string]string{ - "foo": "bar", "count": "10", "thing": "is a string", "iscool": "true", - }) - require.NoError(t, err) - logger2.Debug("Debug log foo fields") - logger2.Warn("Warning message foo fields") - - logger.Warn("Warning message root module\n") - logger.Debug("Debug message root again") - - expectedA := `level=warning msg="Warning message root module" @service=benthos_service @system=foo -level=warning msg="Warning message foo fields" @service=benthos_service @system=foo count=10 foo=bar iscool=true thing="is a string" -level=warning msg="Warning message root module" @service=benthos_service @system=foo -` - assert.Equal(t, expectedA, bufA.String()) - - expectedB := `level=warning msg="Warning message root module" @service=benthos_service @system=bar -level=debug msg="Debug log root module" @service=benthos_service @system=bar -level=debug msg="Debug log foo fields" @service=benthos_service @system=bar count=10 foo=bar iscool=true thing="is a string" -level=warning msg="Warning message foo fields" @service=benthos_service @system=bar count=10 foo=bar iscool=true thing="is a string" -level=warning msg="Warning message root module" @service=benthos_service @system=bar -level=debug msg="Debug message root again" @service=benthos_service @system=bar -` - assert.Equal(t, expectedB, bufB.String()) -} diff --git a/internal/log/wrap.go b/internal/log/wrap.go deleted file mode 100644 index 13f0317892..0000000000 --- a/internal/log/wrap.go +++ /dev/null @@ -1,143 +0,0 @@ -package log - -// PrintFormatter is an interface implemented by standard loggers. -type PrintFormatter interface { - Printf(format string, v ...any) - Println(v ...any) -} - -//------------------------------------------------------------------------------ - -// Logger level constants. -const ( - LogOff int = 0 - LogFatal int = 1 - LogError int = 2 - LogWarn int = 3 - LogInfo int = 4 - LogDebug int = 5 - LogTrace int = 6 - LogAll int = 7 -) - -// wrapped is an object with support for levelled logging and modular components. -type wrapped struct { - pf PrintFormatter - level int -} - -// Wrap a PrintFormatter with a log.Modular implementation. Log level is set to -// INFO, use WrapAtLevel to set this explicitly. -func Wrap(l PrintFormatter) Modular { - return &wrapped{ - pf: l, - level: LogInfo, - } -} - -// WrapAtLevel wraps a PrintFormatter with a log.Modular implementation with an -// explicit log level. -func WrapAtLevel(l PrintFormatter, level int) Modular { - return &wrapped{ - pf: l, - level: level, - } -} - -//------------------------------------------------------------------------------ - -// WithFields is a no-op. -func (l *wrapped) WithFields(fields map[string]string) Modular { - return l -} - -// With is a no-op. -func (l *wrapped) With(keyValues ...any) Modular { - return l -} - -// Fatal prints a fatal message to the console. Does NOT cause panic. -func (l *wrapped) Fatal(format string, v ...any) { - if LogFatal <= l.level { - l.pf.Printf(format, v...) - } -} - -// Error prints an error message to the console. -func (l *wrapped) Error(format string, v ...any) { - if LogError <= l.level { - l.pf.Printf(format, v...) - } -} - -// Warn prints a warning message to the console. -func (l *wrapped) Warn(format string, v ...any) { - if LogWarn <= l.level { - l.pf.Printf(format, v...) - } -} - -// Info prints an information message to the console. -func (l *wrapped) Info(format string, v ...any) { - if LogInfo <= l.level { - l.pf.Printf(format, v...) - } -} - -// Debug prints a debug message to the console. -func (l *wrapped) Debug(format string, v ...any) { - if LogDebug <= l.level { - l.pf.Printf(format, v...) - } -} - -// Trace prints a trace message to the console. -func (l *wrapped) Trace(format string, v ...any) { - if LogTrace <= l.level { - l.pf.Printf(format, v...) - } -} - -//------------------------------------------------------------------------------ - -// Fatalln prints a fatal message to the console. Does NOT cause panic. -func (l *wrapped) Fatalln(message string) { - if LogFatal <= l.level { - l.pf.Println(message) - } -} - -// Errorln prints an error message to the console. -func (l *wrapped) Errorln(message string) { - if LogError <= l.level { - l.pf.Println(message) - } -} - -// Warnln prints a warning message to the console. -func (l *wrapped) Warnln(message string) { - if LogWarn <= l.level { - l.pf.Println(message) - } -} - -// Infoln prints an information message to the console. -func (l *wrapped) Infoln(message string) { - if LogInfo <= l.level { - l.pf.Println(message) - } -} - -// Debugln prints a debug message to the console. -func (l *wrapped) Debugln(message string) { - if LogDebug <= l.level { - l.pf.Println(message) - } -} - -// Traceln prints a trace message to the console. -func (l *wrapped) Traceln(message string) { - if LogTrace <= l.level { - l.pf.Println(message) - } -} diff --git a/internal/manager/config.go b/internal/manager/config.go deleted file mode 100644 index 53730afb1e..0000000000 --- a/internal/manager/config.go +++ /dev/null @@ -1,136 +0,0 @@ -package manager - -import ( - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -const ( - fieldResourceInputs = "input_resources" - fieldResourceProcessors = "processor_resources" - fieldResourceOutputs = "output_resources" - fieldResourceCaches = "cache_resources" - fieldResourceRateLimits = "rate_limit_resources" -) - -// ResourceConfig contains fields for specifying resource components at the root -// of a Benthos config. -type ResourceConfig struct { - ResourceInputs []input.Config `yaml:"input_resources,omitempty"` - ResourceProcessors []processor.Config `yaml:"processor_resources,omitempty"` - ResourceOutputs []output.Config `yaml:"output_resources,omitempty"` - ResourceCaches []cache.Config `yaml:"cache_resources,omitempty"` - ResourceRateLimits []ratelimit.Config `yaml:"rate_limit_resources,omitempty"` -} - -// NewResourceConfig creates a ResourceConfig with default values. -func NewResourceConfig() ResourceConfig { - return ResourceConfig{ - ResourceInputs: []input.Config{}, - ResourceProcessors: []processor.Config{}, - ResourceOutputs: []output.Config{}, - ResourceCaches: []cache.Config{}, - ResourceRateLimits: []ratelimit.Config{}, - } -} - -// AddFrom takes another Config and adds all of its resources to itself. If -// there are any resource name collisions an error is returned. -func (r *ResourceConfig) AddFrom(extra *ResourceConfig) error { - r.ResourceInputs = append(r.ResourceInputs, extra.ResourceInputs...) - r.ResourceProcessors = append(r.ResourceProcessors, extra.ResourceProcessors...) - r.ResourceOutputs = append(r.ResourceOutputs, extra.ResourceOutputs...) - r.ResourceCaches = append(r.ResourceCaches, extra.ResourceCaches...) - r.ResourceRateLimits = append(r.ResourceRateLimits, extra.ResourceRateLimits...) - return nil -} - -func FromAny(prov docs.Provider, v any) (conf ResourceConfig, err error) { - var pConf *docs.ParsedConfig - if pConf, err = Spec().ParsedConfigFromAny(v); err != nil { - return - } - return FromParsed(prov, pConf) -} - -func FromParsed(prov docs.Provider, pConf *docs.ParsedConfig) (conf ResourceConfig, err error) { - conf = NewResourceConfig() - - var l []*docs.ParsedConfig - var v any - - if l, err = pConf.FieldAnyList(fieldResourceInputs); err != nil { - return - } - for _, p := range l { - if v, err = p.FieldAny(); err != nil { - return - } - var c input.Config - if c, err = input.FromAny(prov, v); err != nil { - return - } - conf.ResourceInputs = append(conf.ResourceInputs, c) - } - - if l, err = pConf.FieldAnyList(fieldResourceProcessors); err != nil { - return - } - for _, p := range l { - if v, err = p.FieldAny(); err != nil { - return - } - var c processor.Config - if c, err = processor.FromAny(prov, v); err != nil { - return - } - conf.ResourceProcessors = append(conf.ResourceProcessors, c) - } - - if l, err = pConf.FieldAnyList(fieldResourceOutputs); err != nil { - return - } - for _, p := range l { - if v, err = p.FieldAny(); err != nil { - return - } - var c output.Config - if c, err = output.FromAny(prov, v); err != nil { - return - } - conf.ResourceOutputs = append(conf.ResourceOutputs, c) - } - - if l, err = pConf.FieldAnyList(fieldResourceCaches); err != nil { - return - } - for _, p := range l { - if v, err = p.FieldAny(); err != nil { - return - } - var c cache.Config - if c, err = cache.FromAny(prov, v); err != nil { - return - } - conf.ResourceCaches = append(conf.ResourceCaches, c) - } - - if l, err = pConf.FieldAnyList(fieldResourceRateLimits); err != nil { - return - } - for _, p := range l { - if v, err = p.FieldAny(); err != nil { - return - } - var c ratelimit.Config - if c, err = ratelimit.FromAny(prov, v); err != nil { - return - } - conf.ResourceRateLimits = append(conf.ResourceRateLimits, c) - } - return -} diff --git a/internal/manager/config_test.go b/internal/manager/config_test.go deleted file mode 100644 index d5d34065f2..0000000000 --- a/internal/manager/config_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package manager_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager" -) - -func TestConfigParseYAML(t *testing.T) { - tests := []struct { - name string - input string - errContains string - validateFn func(t testing.TB, v manager.ResourceConfig) - }{ - { - name: "one of everything", - input: ` -input_resources: - - label: a - generate: - count: 1 - mapping: 'root.id = "a"' - interval: 1s -processor_resources: - - label: b - mapping: 'root.id = "b"' -output_resources: - - label: c - reject: "c rejected" -cache_resources: - - label: d - memory: - init_values: - static: "d value" -rate_limit_resources: - - label: e - local: - count: 123 - interval: 100ms -`, - validateFn: func(t testing.TB, v manager.ResourceConfig) { - require.Len(t, v.ResourceCaches, 1) - require.Len(t, v.ResourceRateLimits, 1) - require.Len(t, v.ResourceInputs, 1) - require.Len(t, v.ResourceOutputs, 1) - require.Len(t, v.ResourceProcessors, 1) - - assert.Equal(t, "a", v.ResourceInputs[0].Label) - assert.Equal(t, "generate", v.ResourceInputs[0].Type) - - assert.Equal(t, "b", v.ResourceProcessors[0].Label) - assert.Equal(t, "mapping", v.ResourceProcessors[0].Type) - - assert.Equal(t, "c", v.ResourceOutputs[0].Label) - assert.Equal(t, "reject", v.ResourceOutputs[0].Type) - - assert.Equal(t, "d", v.ResourceCaches[0].Label) - assert.Equal(t, "memory", v.ResourceCaches[0].Type) - - assert.Equal(t, "e", v.ResourceRateLimits[0].Label) - assert.Equal(t, "local", v.ResourceRateLimits[0].Type) - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - conf, err := testutil.ManagerFromYAML(test.input) - if test.errContains == "" { - require.NoError(t, err) - test.validateFn(t, conf) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } - }) - } -} diff --git a/internal/manager/docs.go b/internal/manager/docs.go deleted file mode 100644 index 09872ab73e..0000000000 --- a/internal/manager/docs.go +++ /dev/null @@ -1,48 +0,0 @@ -package manager - -import ( - "errors" - - "github.com/Jeffail/gabs/v2" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func lintResource(ctx docs.LintContext, line, col int, v any) []docs.Lint { - if _, ok := v.(map[string]any); !ok { - return nil - } - gObj := gabs.Wrap(v) - label, _ := gObj.S("label").Data().(string) - if label == "" { - return []docs.Lint{ - docs.NewLintError(line, docs.LintBadLabel, errors.New("the label field for resources must be unique and not empty")), - } - } - return nil -} - -// Spec returns a field spec for the manager configuration. -func Spec() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldInput( - "input_resources", "A list of input resources, each must have a unique label.", - ).Array().LinterFunc(lintResource).HasDefault([]any{}).Advanced(), - - docs.FieldProcessor( - "processor_resources", "A list of processor resources, each must have a unique label.", - ).Array().LinterFunc(lintResource).HasDefault([]any{}).Advanced(), - - docs.FieldOutput( - "output_resources", "A list of output resources, each must have a unique label.", - ).Array().LinterFunc(lintResource).HasDefault([]any{}).Advanced(), - - docs.FieldCache( - "cache_resources", "A list of cache resources, each must have a unique label.", - ).Array().LinterFunc(lintResource).HasDefault([]any{}).Advanced(), - - docs.FieldRateLimit( - "rate_limit_resources", "A list of rate limit resources, each must have a unique label.", - ).Array().LinterFunc(lintResource).HasDefault([]any{}).Advanced(), - } -} diff --git a/internal/manager/initialization_test.go b/internal/manager/initialization_test.go deleted file mode 100644 index fb741d357c..0000000000 --- a/internal/manager/initialization_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package manager - -import ( - "context" - "errors" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func TestInitialization(t *testing.T) { - env := bundle.NewEnvironment() - - require.NoError(t, env.BufferAdd(func(c buffer.Config, mgr bundle.NewManagement) (buffer.Streamed, error) { - return nil, errors.New("not this buffer") - }, docs.ComponentSpec{ - Name: "testbuffer", - })) - - require.NoError(t, env.CacheAdd(func(c cache.Config, mgr bundle.NewManagement) (cache.V1, error) { - return nil, errors.New("not this cache") - }, docs.ComponentSpec{ - Name: "testcache", - })) - - require.NoError(t, env.InputAdd(func(c input.Config, mgr bundle.NewManagement) (input.Streamed, error) { - return nil, errors.New("not this input") - }, docs.ComponentSpec{ - Name: "testinput", - })) - - lenOutputProcs := 0 - require.NoError(t, env.OutputAdd(func(c output.Config, mgr bundle.NewManagement, p ...processor.PipelineConstructorFunc) (output.Streamed, error) { - lenOutputProcs = len(p) - return nil, errors.New("not this output") - }, docs.ComponentSpec{ - Name: "testoutput", - })) - - require.NoError(t, env.ProcessorAdd(func(c processor.Config, mgr bundle.NewManagement) (processor.V1, error) { - return nil, errors.New("not this processor") - }, docs.ComponentSpec{ - Name: "testprocessor", - })) - - require.NoError(t, env.RateLimitAdd(func(c ratelimit.Config, mgr bundle.NewManagement) (ratelimit.V1, error) { - return nil, errors.New("not this rate limit") - }, docs.ComponentSpec{ - Name: "testratelimit", - })) - - mgr, err := New(NewResourceConfig(), OptSetEnvironment(env)) - require.NoError(t, err) - - bConf := buffer.NewConfig() - bConf.Type = "testbuffer" - _, err = mgr.NewBuffer(bConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "not this buffer") - - cConf := cache.NewConfig() - cConf.Type = "testcache" - _, err = mgr.NewCache(cConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "not this cache") - - iConf := input.NewConfig() - iConf.Type = "testinput" - _, err = mgr.NewInput(iConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "not this input") - - oConf := output.NewConfig() - oConf.Type = "testoutput" - _, err = mgr.NewOutput(oConf, nil, nil, nil) - require.Error(t, err) - assert.Contains(t, err.Error(), "not this output") - assert.Equal(t, 3, lenOutputProcs) - - pConf := processor.NewConfig() - pConf.Type = "testprocessor" - _, err = mgr.NewProcessor(pConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "not this processor") - - rConf := ratelimit.NewConfig() - rConf.Type = "testratelimit" - _, err = mgr.NewRateLimit(rConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "not this rate limit") -} - -func TestInitializationOrdering(t *testing.T) { - env := bundle.NewEnvironment() - - var wg sync.WaitGroup - wg.Add(2) - - require.NoError(t, env.InputAdd(func(c input.Config, mgr bundle.NewManagement) (input.Streamed, error) { - go func() { - defer wg.Done() - err := mgr.AccessRateLimit(context.Background(), "testratelimit", func(rl ratelimit.V1) {}) - _ = assert.Error(t, err) && assert.Contains(t, err.Error(), "unable to locate") - }() - return nil, nil - }, docs.ComponentSpec{ - Name: "testinput", - })) - - require.NoError(t, env.ProcessorAdd(func(c processor.Config, mgr bundle.NewManagement) (processor.V1, error) { - go func() { - defer wg.Done() - err := mgr.AccessRateLimit(context.Background(), "fooratelimit", func(rl ratelimit.V1) {}) - _ = assert.Error(t, err) && assert.Contains(t, err.Error(), "unable to locate") - }() - return nil, nil - }, docs.ComponentSpec{ - Name: "testprocessor", - })) - - require.NoError(t, env.RateLimitAdd(func(c ratelimit.Config, mgr bundle.NewManagement) (ratelimit.V1, error) { - return nil, nil - }, docs.ComponentSpec{ - Name: "testratelimit", - })) - - inConf := input.NewConfig() - inConf.Label = "fooinput" - inConf.Type = "testinput" - - procConf := processor.NewConfig() - procConf.Label = "fooproc" - procConf.Type = "testprocessor" - - rlConf := ratelimit.NewConfig() - rlConf.Label = "fooratelimit" - rlConf.Type = "testratelimit" - - resConf := NewResourceConfig() - resConf.ResourceInputs = append(resConf.ResourceInputs, inConf) - resConf.ResourceProcessors = append(resConf.ResourceProcessors, procConf) - resConf.ResourceRateLimits = append(resConf.ResourceRateLimits, rlConf) - - _, err := New(resConf, OptSetEnvironment(env)) - require.NoError(t, err) - - wg.Wait() -} diff --git a/internal/manager/input_wrapper.go b/internal/manager/input_wrapper.go deleted file mode 100644 index 26b76beb20..0000000000 --- a/internal/manager/input_wrapper.go +++ /dev/null @@ -1,161 +0,0 @@ -package manager - -import ( - "context" - "sync" - "sync/atomic" - "time" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ input.Streamed = &InputWrapper{} - -type inputCtrl struct { - input input.Streamed - closedForSwap *int32 -} - -type InputWrapper struct { - ctrl *inputCtrl - inputLock sync.Mutex - - tranChan chan message.Transaction - shutSig *shutdown.Signaller -} - -func WrapInput(i input.Streamed) *InputWrapper { - var s int32 - w := &InputWrapper{ - ctrl: &inputCtrl{ - input: i, - closedForSwap: &s, - }, - tranChan: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - go w.loop() - return w -} - -func (w *InputWrapper) CloseExistingInput(ctx context.Context, forSwap bool) error { - w.inputLock.Lock() - tmpInput := w.ctrl.input - if forSwap { - atomic.StoreInt32(w.ctrl.closedForSwap, 1) - } else { - atomic.StoreInt32(w.ctrl.closedForSwap, 0) - } - w.inputLock.Unlock() - - if tmpInput == nil { - return nil - } - - tmpInput.TriggerStopConsuming() - return tmpInput.WaitForClose(ctx) -} - -func (w *InputWrapper) SwapInput(i input.Streamed) { - var s int32 - w.inputLock.Lock() - w.ctrl = &inputCtrl{ - input: i, - closedForSwap: &s, - } - w.inputLock.Unlock() -} - -func (w *InputWrapper) TransactionChan() <-chan message.Transaction { - return w.tranChan -} - -func (w *InputWrapper) Connected() bool { - w.inputLock.Lock() - con := w.ctrl.input != nil && w.ctrl.input.Connected() - w.inputLock.Unlock() - return con -} - -func (w *InputWrapper) loop() { - defer func() { - w.inputLock.Lock() - tmpInput := w.ctrl.input - w.inputLock.Unlock() - - if tmpInput != nil { - tmpInput.TriggerStopConsuming() - _ = tmpInput.WaitForClose(context.Background()) - } - - close(w.tranChan) - w.shutSig.TriggerHasStopped() - }() - - for { - var tChan <-chan message.Transaction - var closedForSwap *int32 - - w.inputLock.Lock() - if w.ctrl.input != nil { - tChan = w.ctrl.input.TransactionChan() - closedForSwap = w.ctrl.closedForSwap - } - w.inputLock.Unlock() - - var t message.Transaction - var open bool - - if tChan != nil { - select { - case t, open = <-tChan: - // If closed and is natural (not closed for swap) then exit - // gracefully. - if !open && atomic.LoadInt32(closedForSwap) == 0 { - return - } - case <-w.shutSig.SoftStopChan(): - return - } - } - - if !open { - select { - case <-time.After(time.Millisecond * 100): - case <-w.shutSig.SoftStopChan(): - return - } - continue - } - - select { - case w.tranChan <- t: - case <-w.shutSig.SoftStopChan(): - ctx, done := w.shutSig.HardStopCtx(context.Background()) - _ = t.Ack(ctx, component.ErrTypeClosed) - done() - return - } - } -} - -func (w *InputWrapper) TriggerStopConsuming() { - w.shutSig.TriggerSoftStop() -} - -func (w *InputWrapper) TriggerCloseNow() { - w.shutSig.TriggerHardStop() -} - -func (w *InputWrapper) WaitForClose(ctx context.Context) error { - select { - case <-w.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/manager/input_wrapper_test.go b/internal/manager/input_wrapper_test.go deleted file mode 100644 index b8783e98ce..0000000000 --- a/internal/manager/input_wrapper_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package manager_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager" - bmock "github.com/benthosdev/benthos/v4/internal/manager/mock" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestInputWrapperSwap(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - conf, err := testutil.InputFromYAML(` -generate: - interval: 10ms - mapping: 'root.name = "from root generate"' -`) - require.NoError(t, err) - - bMgr := bmock.NewManager() - - iWrapped, err := bMgr.NewInput(conf) - require.NoError(t, err) - - iWrapper := manager.WrapInput(iWrapped) - select { - case tran, open := <-iWrapper.TransactionChan(): - require.True(t, open) - assert.Equal(t, `{"name":"from root generate"}`, string(tran.Payload.Get(0).AsBytes())) - assert.NoError(t, tran.Ack(ctx, nil)) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - - for i := 0; i < 5; i++ { - conf, err := testutil.InputFromYAML(fmt.Sprintf(` -generate: - interval: 10ms - mapping: 'root.name = "from generate %v"' -`, i)) - require.NoError(t, err) - - go func() { - assert.NoError(t, iWrapper.CloseExistingInput(ctx, true)) - - iWrapped, err = bMgr.NewInput(conf) - assert.NoError(t, err) - - iWrapper.SwapInput(iWrapped) - }() - - expected := fmt.Sprintf(`{"name":"from generate %v"}`, i) - consumeLoop: - for { - select { - case tran, open := <-iWrapper.TransactionChan(): - require.True(t, open, i) - - actual := string(tran.Payload.Get(0).AsBytes()) - assert.NoError(t, tran.Ack(ctx, nil), i) - if expected == actual { - break consumeLoop - } - case <-ctx.Done(): - t.Fatal(ctx.Err(), i) - } - } - } - - iWrapper.TriggerStopConsuming() - require.NoError(t, iWrapper.WaitForClose(ctx)) -} diff --git a/internal/manager/live_resources.go b/internal/manager/live_resources.go deleted file mode 100644 index 16d3fa215d..0000000000 --- a/internal/manager/live_resources.go +++ /dev/null @@ -1,159 +0,0 @@ -package manager - -import ( - "sync" -) - -type liveResource[T any] struct { - res *T - m sync.RWMutex -} - -// Access the underlying resource in writeable mode, where mutations within the -// provided closure are safe on the resource, and the set function can be used -// to change or delete (nil argument) the underlying resource. Returns true if -// the resource remains non-nil after it was accessed. -func (l *liveResource[T]) Access(fn func(t *T, set func(t *T))) bool { - if l == nil { - return false - } - - l.m.Lock() - defer l.m.Unlock() - - fn(l.res, func(t *T) { - l.res = t - }) - return l.res != nil -} - -// RAccess grants a closure function access to the underlying resource, but -// mutations must not be performed on the resource itself. Returns true if the -// resource is non-nil. -func (l *liveResource[T]) RAccess(fn func(t T)) bool { - l.m.RLock() - defer l.m.RUnlock() - - if l.res == nil { - return false - } - - fn(*l.res) - return true -} - -//------------------------------------------------------------------------------ - -type liveResources[T any] struct { - resources map[string]*liveResource[T] - m sync.RWMutex -} - -func newLiveResources[T any]() *liveResources[T] { - return &liveResources[T]{ - resources: map[string]*liveResource[T]{}, - } -} - -// Probe checks whether a given resource name is known. This, however, does not -// check that the underlying resource exists at this moment in time. -func (l *liveResources[T]) Probe(name string) bool { - l.m.RLock() - _, exists := l.resources[name] - l.m.RUnlock() - return exists -} - -// Add a resource with a given name. -func (l *liveResources[T]) Add(name string, t *T) { - l.m.Lock() - l.resources[name] = &liveResource[T]{ - res: t, - } - l.m.Unlock() -} - -// Walk all resources, executing a closure function that is permitted to mutate -// (or delete) the resource. -func (l *liveResources[T]) Walk(fn func(name string, t *T, set func(t *T)) error) (err error) { - l.m.Lock() - defer l.m.Unlock() - - for k, v := range l.resources { - if exists := v.Access(func(t *T, set func(t *T)) { - err = fn(k, t, set) - }); !exists { - delete(l.resources, k) - } - if err != nil { - return - } - } - return nil -} - -// RWalk walks all resources, executing a closure function with each resource. -func (l *liveResources[T]) RWalk(fn func(name string, t T) error) (err error) { - l.m.RLock() - defer l.m.RUnlock() - - for k, v := range l.resources { - _ = v.RAccess(func(t T) { - err = fn(k, t) - }) - if err != nil { - return - } - } - return nil -} - -// Access a resource by name in writeable mode, where mutations within the -// provided closure are safe on the resource, and the set function can be used -// to change or delete (nil argument) the underlying resource. If create is set -// to true the resource is created if it does not yet exist. -func (l *liveResources[T]) Access(name string, create bool, fn func(t *T, set func(t *T))) error { - l.m.RLock() - rl, exists := l.resources[name] - l.m.RUnlock() - - if !exists { - if !create { - return ErrResourceNotFound(name) - } - l.m.Lock() - rl = &liveResource[T]{} - l.resources[name] = rl - l.m.Unlock() - } - - if rl.Access(fn) { - return nil - } - - // If the underlying resource is deleted we can clean up the resources - // map and prevent unbounded growth. - l.m.Lock() - if !l.resources[name].Access(func(t *T, set func(t *T)) {}) { - delete(l.resources, name) - } - l.m.Unlock() - return nil -} - -// RAccess grants a closure function access to a named resource, but mutations -// must not be performed on the resource itself. -func (l *liveResources[T]) RAccess(name string, fn func(t T)) error { - l.m.RLock() - rl, exists := l.resources[name] - l.m.RUnlock() - - if !exists { - return ErrResourceNotFound(name) - } - - if !rl.RAccess(fn) { - return ErrResourceNotFound(name) - } - return nil -} diff --git a/internal/manager/mock/cache.go b/internal/manager/mock/cache.go deleted file mode 100644 index 7f6b0ae467..0000000000 --- a/internal/manager/mock/cache.go +++ /dev/null @@ -1,72 +0,0 @@ -package mock - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" -) - -// CacheItem represents a cached key/ttl pair. -type CacheItem struct { - Value string - TTL *time.Duration -} - -// Cache provides a mock cache implementation. -type Cache struct { - Values map[string]CacheItem -} - -// Get a mock cache item. -func (c *Cache) Get(ctx context.Context, key string) ([]byte, error) { - i, ok := c.Values[key] - if !ok { - return nil, component.ErrKeyNotFound - } - return []byte(i.Value), nil -} - -// Set a mock cache item. -func (c *Cache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - c.Values[key] = CacheItem{ - Value: string(value), - TTL: ttl, - } - return nil -} - -// SetMulti sets multiple mock cache items. -func (c *Cache) SetMulti(ctx context.Context, kvs map[string]cache.TTLItem) error { - for k, v := range kvs { - c.Values[k] = CacheItem{ - Value: string(v.Value), - TTL: v.TTL, - } - } - return nil -} - -// Add a mock cache item. -func (c *Cache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if _, ok := c.Values[key]; ok { - return component.ErrKeyAlreadyExists - } - c.Values[key] = CacheItem{ - Value: string(value), - TTL: ttl, - } - return nil -} - -// Delete a mock cache item. -func (c *Cache) Delete(ctx context.Context, key string) error { - delete(c.Values, key) - return nil -} - -// Close does nothing. -func (c *Cache) Close(ctx context.Context) error { - return nil -} diff --git a/internal/manager/mock/cache_test.go b/internal/manager/mock/cache_test.go deleted file mode 100644 index 76d8d34cf1..0000000000 --- a/internal/manager/mock/cache_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package mock_test - -import ( - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -var _ cache.V1 = &mock.Cache{} diff --git a/internal/manager/mock/input.go b/internal/manager/mock/input.go deleted file mode 100644 index cb556a3277..0000000000 --- a/internal/manager/mock/input.go +++ /dev/null @@ -1,57 +0,0 @@ -package mock - -import ( - "context" - "sync" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Input provides a mocked input implementation. -type Input struct { - TChan chan message.Transaction - closeOnce sync.Once -} - -// NewInput creates a new mock input that will return transactions containing a -// list of batches, then exit. -func NewInput(batches []message.Batch) *Input { - ts := make(chan message.Transaction, len(batches)) - resChan := make(chan error, len(batches)) - go func() { - defer close(ts) - for _, b := range batches { - ts <- message.NewTransaction(b, resChan) - } - }() - return &Input{TChan: ts} -} - -// Connected always returns true. -func (f *Input) Connected() bool { - return true -} - -// TransactionChan returns a transaction channel. -func (f *Input) TransactionChan() <-chan message.Transaction { - return f.TChan -} - -// TriggerStopConsuming closes the input transaction channel. -func (f *Input) TriggerStopConsuming() { - f.closeOnce.Do(func() { - close(f.TChan) - }) -} - -// TriggerCloseNow closes the input transaction channel. -func (f *Input) TriggerCloseNow() { - f.closeOnce.Do(func() { - close(f.TChan) - }) -} - -// WaitForClose does nothing. -func (f *Input) WaitForClose(ctx context.Context) error { - return nil -} diff --git a/internal/manager/mock/input_test.go b/internal/manager/mock/input_test.go deleted file mode 100644 index 77674bb186..0000000000 --- a/internal/manager/mock/input_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package mock_test - -import ( - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -var _ input.Streamed = &mock.Input{} diff --git a/internal/manager/mock/manager.go b/internal/manager/mock/manager.go deleted file mode 100644 index 2fa87c9497..0000000000 --- a/internal/manager/mock/manager.go +++ /dev/null @@ -1,371 +0,0 @@ -package mock - -import ( - "context" - "net/http" - "sync" - - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Manager provides a mock benthos manager that components can use to test -// interactions with fake resources. -type Manager struct { - Version string - - Inputs map[string]*Input - Caches map[string]map[string]CacheItem - RateLimits map[string]RateLimit - Outputs map[string]OutputWriter - Processors map[string]Processor - Pipes map[string]<-chan message.Transaction - lock sync.Mutex - - // OnRegisterEndpoint can be set in order to intercept endpoints registered - // by components. - OnRegisterEndpoint func(path string, h http.HandlerFunc) - - CustomFS ifs.FS - M metrics.Type - L log.Modular - T trace.TracerProvider -} - -// NewManager provides a new mock manager. -func NewManager() *Manager { - return &Manager{ - Version: "mock", - Inputs: map[string]*Input{}, - Caches: map[string]map[string]CacheItem{}, - RateLimits: map[string]RateLimit{}, - Outputs: map[string]OutputWriter{}, - Processors: map[string]Processor{}, - Pipes: map[string]<-chan message.Transaction{}, - CustomFS: ifs.OS(), - M: metrics.Noop(), - L: log.Noop(), - T: noop.NewTracerProvider(), - } -} - -func (m *Manager) EngineVersion() string { - return m.Version -} - -// ForStream returns the same mock manager. -func (m *Manager) ForStream(id string) bundle.NewManagement { return m } - -// IntoPath returns the same mock manager. -func (m *Manager) IntoPath(segments ...string) bundle.NewManagement { return m } - -// WithAddedMetrics returns the same mock manager. -func (m *Manager) WithAddedMetrics(m2 metrics.Type) bundle.NewManagement { return m } - -// NewBuffer always errors on invalid type. -func (m *Manager) NewBuffer(conf buffer.Config) (buffer.Streamed, error) { - return nil, component.ErrInvalidType("buffer", conf.Type) -} - -// NewCache always errors on invalid type. -func (m *Manager) NewCache(conf cache.Config) (cache.V1, error) { - return bundle.AllCaches.Init(conf, m) -} - -// StoreCache always errors on invalid type. -func (m *Manager) StoreCache(ctx context.Context, name string, conf cache.Config) error { - return component.ErrInvalidType("cache", conf.Type) -} - -// NewInput always errors on invalid type. -func (m *Manager) NewInput(conf input.Config) (input.Streamed, error) { - return bundle.AllInputs.Init(conf, m) -} - -// StoreInput always errors on invalid type. -func (m *Manager) StoreInput(ctx context.Context, name string, conf input.Config) error { - return component.ErrInvalidType("input", conf.Type) -} - -// NewProcessor always errors on invalid type. -func (m *Manager) NewProcessor(conf processor.Config) (processor.V1, error) { - return bundle.AllProcessors.Init(conf, m) -} - -// StoreProcessor always errors on invalid type. -func (m *Manager) StoreProcessor(ctx context.Context, name string, conf processor.Config) error { - return component.ErrInvalidType("processor", conf.Type) -} - -// NewOutput always errors on invalid type. -func (m *Manager) NewOutput(conf output.Config, pipelines ...processor.PipelineConstructorFunc) (output.Streamed, error) { - return bundle.AllOutputs.Init(conf, m, pipelines...) -} - -// StoreOutput always errors on invalid type. -func (m *Manager) StoreOutput(ctx context.Context, name string, conf output.Config) error { - return component.ErrInvalidType("output", conf.Type) -} - -// NewRateLimit always errors on invalid type. -func (m *Manager) NewRateLimit(conf ratelimit.Config) (ratelimit.V1, error) { - return bundle.AllRateLimits.Init(conf, m) -} - -// StoreRateLimit always errors on invalid type. -func (m *Manager) StoreRateLimit(ctx context.Context, name string, conf ratelimit.Config) error { - return component.ErrInvalidType("rate_limit", conf.Type) -} - -// NewScanner attempts to create a new scanner component from a config. -func (m *Manager) NewScanner(conf scanner.Config) (scanner.Creator, error) { - return bundle.AllScanners.Init(conf, m) -} - -// Path always returns empty. -func (m *Manager) Path() []string { return nil } - -// Label always returns empty. -func (m *Manager) Label() string { return "" } - -// Metrics returns a no-op metrics. -func (m *Manager) Metrics() metrics.Type { return m.M } - -// Logger returns a no-op logger. -func (m *Manager) Logger() log.Modular { return m.L } - -// Tracer returns a no-op tracer. -func (m *Manager) Tracer() trace.TracerProvider { return m.T } - -// RegisterEndpoint registers a server wide HTTP endpoint. -func (m *Manager) RegisterEndpoint(path, desc string, h http.HandlerFunc) { - if m.OnRegisterEndpoint != nil { - m.OnRegisterEndpoint(path, h) - } -} - -// FS returns CustomFS, which wraps the os package unless overridden. -func (m *Manager) FS() ifs.FS { - return m.CustomFS -} - -// Environment always returns the global environment. -func (m *Manager) Environment() *bundle.Environment { - return bundle.GlobalEnvironment -} - -// BloblEnvironment always returns the global environment. -func (m *Manager) BloblEnvironment() *bloblang.Environment { - return bloblang.GlobalEnvironment() -} - -// ProbeCache returns true if a cache resource exists under the provided name. -func (m *Manager) ProbeCache(name string) bool { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Caches[name] - return exists -} - -// AccessCache executes a closure on a cache resource. -func (m *Manager) AccessCache(ctx context.Context, name string, fn func(cache.V1)) error { - m.lock.Lock() - defer m.lock.Unlock() - - values, ok := m.Caches[name] - if !ok { - return component.ErrCacheNotFound - } - fn(&Cache{Values: values}) - return nil -} - -// RemoveCache removes a resource. -func (m *Manager) RemoveCache(ctx context.Context, name string) error { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Caches[name] - if !exists { - return component.ErrCacheNotFound - } - delete(m.Caches, name) - return nil -} - -// ProbeRateLimit returns true if a rate limit resource exists under the -// provided name. -func (m *Manager) ProbeRateLimit(name string) bool { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.RateLimits[name] - return exists -} - -// AccessRateLimit executes a closure on a rate limit resource. -func (m *Manager) AccessRateLimit(ctx context.Context, name string, fn func(ratelimit.V1)) error { - m.lock.Lock() - defer m.lock.Unlock() - - r, ok := m.RateLimits[name] - if !ok { - return component.ErrRateLimitNotFound - } - fn(r) - return nil -} - -// RemoveRateLimit removes a resource. -func (m *Manager) RemoveRateLimit(ctx context.Context, name string) error { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.RateLimits[name] - if !exists { - return component.ErrRateLimitNotFound - } - delete(m.RateLimits, name) - return nil -} - -// ProbeInput returns true if an input resource exists under the provided name. -func (m *Manager) ProbeInput(name string) bool { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Inputs[name] - return exists -} - -// AccessInput executes a closure on an input resource. -func (m *Manager) AccessInput(ctx context.Context, name string, fn func(input.Streamed)) error { - m.lock.Lock() - defer m.lock.Unlock() - - i, exists := m.Inputs[name] - if !exists { - return component.ErrInputNotFound - } - fn(i) - return nil -} - -// RemoveInput removes a resource. -func (m *Manager) RemoveInput(ctx context.Context, name string) error { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Inputs[name] - if !exists { - return component.ErrInputNotFound - } - delete(m.Inputs, name) - return nil -} - -// ProbeProcessor returns true if a processor resource exists under the provided -// name. -func (m *Manager) ProbeProcessor(name string) bool { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Processors[name] - return exists -} - -// AccessProcessor executes a closure on a processor resource. -func (m *Manager) AccessProcessor(ctx context.Context, name string, fn func(processor.V1)) error { - m.lock.Lock() - defer m.lock.Unlock() - - p, ok := m.Processors[name] - if !ok { - return component.ErrProcessorNotFound - } - fn(p) - return nil -} - -// RemoveProcessor removes a resource. -func (m *Manager) RemoveProcessor(ctx context.Context, name string) error { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Processors[name] - if !exists { - return component.ErrProcessorNotFound - } - delete(m.Processors, name) - return nil -} - -// ProbeOutput returns true if an output resource exists under the provided -// name. -func (m *Manager) ProbeOutput(name string) bool { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Outputs[name] - return exists -} - -// AccessOutput executes a closure on an output resource. -func (m *Manager) AccessOutput(ctx context.Context, name string, fn func(output.Sync)) error { - m.lock.Lock() - defer m.lock.Unlock() - - o, exists := m.Outputs[name] - if !exists { - return component.ErrOutputNotFound - } - fn(o) - return nil -} - -// RemoveOutput removes an output resource. -func (m *Manager) RemoveOutput(ctx context.Context, name string) error { - m.lock.Lock() - defer m.lock.Unlock() - - _, exists := m.Outputs[name] - if !exists { - return component.ErrOutputNotFound - } - delete(m.Outputs, name) - return nil -} - -// GetPipe attempts to find a service wide transaction chan by its name. -func (m *Manager) GetPipe(name string) (<-chan message.Transaction, error) { - if p, ok := m.Pipes[name]; ok { - return p, nil - } - return nil, component.ErrPipeNotFound -} - -// SetPipe registers a transaction chan under a name. -func (m *Manager) SetPipe(name string, t <-chan message.Transaction) { - m.Pipes[name] = t -} - -// UnsetPipe removes a named transaction chan. -func (m *Manager) UnsetPipe(name string, t <-chan message.Transaction) { - delete(m.Pipes, name) -} diff --git a/internal/manager/mock/manager_test.go b/internal/manager/mock/manager_test.go deleted file mode 100644 index 3b39be9393..0000000000 --- a/internal/manager/mock/manager_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package mock_test - -import ( - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -var _ bundle.NewManagement = &mock.Manager{} diff --git a/internal/manager/mock/output.go b/internal/manager/mock/output.go deleted file mode 100644 index 8d6837c341..0000000000 --- a/internal/manager/mock/output.go +++ /dev/null @@ -1,59 +0,0 @@ -package mock - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// OutputWriter provides a mock implementation of types.OutputWriter. -type OutputWriter func(context.Context, message.Transaction) error - -// WriteTransaction attempts to write a transaction to an output. -func (o OutputWriter) WriteTransaction(ctx context.Context, t message.Transaction) error { - return o(ctx, t) -} - -// Connected always returns true. -func (o OutputWriter) Connected() bool { - return true -} - -// TriggerStopConsuming does nothing. -func (o OutputWriter) TriggerStopConsuming() { -} - -// TriggerCloseNow does nothing. -func (o OutputWriter) TriggerCloseNow() { -} - -// WaitForClose does nothing. -func (o OutputWriter) WaitForClose(ctx context.Context) error { - return nil -} - -// OutputChanneled implements the output.Type interface around an exported -// transaction channel. -type OutputChanneled struct { - TChan <-chan message.Transaction -} - -// Connected returns true. -func (m *OutputChanneled) Connected() bool { - return true -} - -// Consume sets the read channel. This implementation is NOT thread safe. -func (m *OutputChanneled) Consume(msgs <-chan message.Transaction) error { - m.TChan = msgs - return nil -} - -// TriggerCloseNow does nothing. -func (m *OutputChanneled) TriggerCloseNow() { -} - -// WaitForClose does nothing. -func (m OutputChanneled) WaitForClose(ctx context.Context) error { - return nil -} diff --git a/internal/manager/mock/output_test.go b/internal/manager/mock/output_test.go deleted file mode 100644 index 9b6f6ac464..0000000000 --- a/internal/manager/mock/output_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package mock_test - -import ( - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -var _ output.Sync = mock.OutputWriter(nil) diff --git a/internal/manager/mock/processor.go b/internal/manager/mock/processor.go deleted file mode 100644 index 2878347146..0000000000 --- a/internal/manager/mock/processor.go +++ /dev/null @@ -1,20 +0,0 @@ -package mock - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Processor provides a mock processor implementation around a closure. -type Processor func(message.Batch) ([]message.Batch, error) - -// ProcessBatch returns the closure result executed on a batch. -func (p Processor) ProcessBatch(ctx context.Context, b message.Batch) ([]message.Batch, error) { - return p(b) -} - -// Close does nothing. -func (p Processor) Close(context.Context) error { - return nil -} diff --git a/internal/manager/mock/processor_test.go b/internal/manager/mock/processor_test.go deleted file mode 100644 index c5ad0a4fcf..0000000000 --- a/internal/manager/mock/processor_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package mock_test - -import ( - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -var _ processor.V1 = mock.Processor(nil) diff --git a/internal/manager/mock/ratelimit.go b/internal/manager/mock/ratelimit.go deleted file mode 100644 index 4e4811f316..0000000000 --- a/internal/manager/mock/ratelimit.go +++ /dev/null @@ -1,19 +0,0 @@ -package mock - -import ( - "context" - "time" -) - -// RateLimit provides a mock rate limit implementation around a closure. -type RateLimit func(context.Context) (time.Duration, error) - -// Access the rate limit. -func (r RateLimit) Access(ctx context.Context) (time.Duration, error) { - return r(ctx) -} - -// Close does nothing. -func (r RateLimit) Close(ctx context.Context) error { - return nil -} diff --git a/internal/manager/mock/ratelimit_test.go b/internal/manager/mock/ratelimit_test.go deleted file mode 100644 index d4c49de93f..0000000000 --- a/internal/manager/mock/ratelimit_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package mock_test - -import ( - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -var _ ratelimit.V1 = mock.RateLimit(nil) diff --git a/internal/manager/output_wrapper.go b/internal/manager/output_wrapper.go deleted file mode 100644 index 9db97e139b..0000000000 --- a/internal/manager/output_wrapper.go +++ /dev/null @@ -1,70 +0,0 @@ -package manager - -import ( - "context" - "sync" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - ioutput "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var _ ioutput.Sync = &outputWrapper{} - -type outputWrapper struct { - output ioutput.Streamed - shutSig *shutdown.Signaller - - tranChan chan message.Transaction - tranMut sync.RWMutex -} - -func wrapOutput(o ioutput.Streamed) (*outputWrapper, error) { - tranChan := make(chan message.Transaction) - if err := o.Consume(tranChan); err != nil { - return nil, err - } - return &outputWrapper{ - output: o, - shutSig: shutdown.NewSignaller(), - tranChan: tranChan, - }, nil -} - -func (w *outputWrapper) WriteTransaction(ctx context.Context, t message.Transaction) error { - w.tranMut.RLock() - defer w.tranMut.RUnlock() - select { - case w.tranChan <- t: - case <-w.shutSig.SoftStopChan(): - case <-ctx.Done(): - return component.ErrTimeout - } - return nil -} - -// Connected returns a boolean indicating whether this output is currently -// connected to its target. -func (w *outputWrapper) Connected() bool { - return w.output.Connected() -} - -func (w *outputWrapper) TriggerStopConsuming() { - w.shutSig.TriggerSoftStop() - w.tranMut.Lock() - if w.tranChan != nil { - close(w.tranChan) - w.tranChan = nil - } - w.tranMut.Unlock() -} - -func (w *outputWrapper) TriggerCloseNow() { - w.output.TriggerCloseNow() -} - -func (w *outputWrapper) WaitForClose(ctx context.Context) error { - return w.output.WaitForClose(ctx) -} diff --git a/internal/manager/output_wrapper_test.go b/internal/manager/output_wrapper_test.go deleted file mode 100644 index 7eccd97f9c..0000000000 --- a/internal/manager/output_wrapper_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package manager - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestOutputWrapperShutdown(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mOutput := &mock.OutputChanneled{ - TChan: make(<-chan message.Transaction), - } - - mWrapped, err := wrapOutput(mOutput) - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - for ts := range mOutput.TChan { - assert.NoError(t, ts.Ack(tCtx, nil)) - } - wg.Done() - }() - - // Trigger Async Shutdown - go func() { - time.Sleep(time.Millisecond * 50) - mWrapped.TriggerStopConsuming() - }() - - for i := 0; i < 1000; i++ { - require.NoError(t, mWrapped.WriteTransaction(tCtx, message.NewTransactionFunc(message.Batch{ - message.NewPart([]byte("hello world")), - }, func(ctx context.Context, err error) error { - return nil - }))) - } - - wg.Wait() -} diff --git a/internal/manager/package.go b/internal/manager/package.go deleted file mode 100644 index 49fd2149db..0000000000 --- a/internal/manager/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package manager implements the types.Manager interface used for creating and -// sharing resources across a Benthos service. -package manager diff --git a/internal/manager/type.go b/internal/manager/type.go deleted file mode 100644 index 928c6c939e..0000000000 --- a/internal/manager/type.go +++ /dev/null @@ -1,998 +0,0 @@ -package manager - -import ( - "context" - "fmt" - "net/http" - "path" - "sync" - - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// ErrResourceNotFound represents an error where a named resource could not be -// accessed because it was not found by the manager. -type ErrResourceNotFound string - -// Error implements the standard error interface. -func (e ErrResourceNotFound) Error() string { - return fmt.Sprintf("unable to locate resource: %v", string(e)) -} - -//------------------------------------------------------------------------------ - -// APIReg is an interface representing an API builder. -type APIReg interface { - RegisterEndpoint(path, desc string, h http.HandlerFunc) -} - -//------------------------------------------------------------------------------ - -// Type is an implementation of types.Manager, which is expected by Benthos -// components that need to register service wide behaviours such as HTTP -// endpoints and event listeners, and obtain service wide shared resources such -// as caches and other resources. -type Type struct { - // An optional identifier given to a manager that is used by a unique stream - // and if specified should be used as a path prefix for API endpoints, and - // added as a label to logs and metrics. - stream string - - // Opt that determines whether HTTP endpoints registered from within a - // stream should be prefixed with the stream name. - namespaceStreamEndpoints bool - - // We allow this version string to be dynamic, as plugin authors might want - // their own custom versioning scheme. - engineVersion string - - // Keeps track of the full configuration path of the component that holds - // the manager. This value is used only in observability and therefore it - // is acceptable that this does not fully represent reality. - componentPath []string - - // Keeps track of the label of the component holding this manager. - label string - - apiReg APIReg - fs ifs.FS - - inputs *liveResources[*InputWrapper] - caches *liveResources[cache.V1] - processors *liveResources[processor.V1] - outputs *liveResources[*outputWrapper] - rateLimits *liveResources[ratelimit.V1] - - // Collections of component constructors - env *bundle.Environment - bloblEnv *bloblang.Environment - - logger log.Modular - stats *metrics.Namespaced - tracer trace.TracerProvider - - pipes map[string]<-chan message.Transaction - pipeLock *sync.RWMutex -} - -// OptFunc is an opt setting for a manager type. -type OptFunc func(*Type) - -// OptSetAPIReg sets the multiplexer used by components of this manager for -// registering their own HTTP endpoints. -func OptSetAPIReg(r APIReg) OptFunc { - return func(t *Type) { - t.apiReg = r - } -} - -// OptSetStreamHTTPNamespacing determines whether HTTP endpoints registered from -// within a stream should be prefixed with the stream name. -func OptSetStreamHTTPNamespacing(enabled bool) OptFunc { - return func(t *Type) { - t.namespaceStreamEndpoints = enabled - } -} - -// OptSetEngineVersion sets the engine version reported to components. This -// can be any scheme, or no scheme at all. -func OptSetEngineVersion(v string) OptFunc { - return func(t *Type) { - t.engineVersion = v - } -} - -// OptSetLogger sets the logger from which the manager emits log events for -// components. -func OptSetLogger(logger log.Modular) OptFunc { - return func(t *Type) { - t.logger = logger - } -} - -// OptSetMetrics sets the metrics exporter from which the manager creates -// metrics for components. -func OptSetMetrics(stats *metrics.Namespaced) OptFunc { - return func(t *Type) { - t.stats = stats - } -} - -// OptSetTracer sets the tracer provider from which the manager creates tracing -// spans. -func OptSetTracer(tracer trace.TracerProvider) OptFunc { - return func(t *Type) { - t.tracer = tracer - } -} - -// OptSetEnvironment determines the environment from which the manager -// initializes components and resources. This option is for internal use only. -func OptSetEnvironment(e *bundle.Environment) OptFunc { - return func(t *Type) { - t.env = e - } -} - -// OptSetBloblangEnvironment determines the environment from which the manager -// parses bloblang functions and methods. This option is for internal use only. -func OptSetBloblangEnvironment(env *bloblang.Environment) OptFunc { - return func(t *Type) { - t.bloblEnv = env - } -} - -// OptSetStreamsMode marks the manager as being created for running streams mode -// resources. This ensures that a label "stream" is added to metrics. -func OptSetStreamsMode(b bool) OptFunc { - return func(t *Type) { - if b { - t.stats = t.stats.WithLabels("stream", "") - } - } -} - -// OptSetFS determines which ifs.FS implementation to use for its filesystem. -// This can be used to override the default os based filesystem implementation. -func OptSetFS(fs ifs.FS) OptFunc { - return func(t *Type) { - t.fs = fs - } -} - -// New returns an instance of manager.Type, which can be shared amongst -// components and logical threads of a Benthos service. -func New(conf ResourceConfig, opts ...OptFunc) (*Type, error) { - t := &Type{ - apiReg: mock.NewManager(), - namespaceStreamEndpoints: true, - - inputs: newLiveResources[*InputWrapper](), - caches: newLiveResources[cache.V1](), - processors: newLiveResources[processor.V1](), - outputs: newLiveResources[*outputWrapper](), - rateLimits: newLiveResources[ratelimit.V1](), - - // Environment defaults to global (everything that was imported). - env: bundle.GlobalEnvironment, - bloblEnv: bloblang.GlobalEnvironment(), - - logger: log.Noop(), - stats: metrics.Noop(), - tracer: noop.NewTracerProvider(), - - fs: ifs.OS(), - - pipes: map[string]<-chan message.Transaction{}, - pipeLock: &sync.RWMutex{}, - } - - for _, opt := range opts { - opt(t) - } - - seen := map[string]struct{}{} - - checkLabel := func(typeStr, label string) error { - if label == "" { - return fmt.Errorf("%v resource has an empty label", typeStr) - } - if _, exists := seen[label]; exists { - return fmt.Errorf("%v resource label '%v' collides with a previously defined resource", typeStr, label) - } - seen[label] = struct{}{} - return nil - } - - // Sometimes resources of a type might refer to other resources of the same - // type. When they are constructed they will check with the manager to - // ensure the resource they point to is valid, but not keep the reference. - // Since we cannot guarantee an order of initialisation we create - // placeholders during construction. - for _, c := range conf.ResourceInputs { - if err := checkLabel("input", c.Label); err != nil { - return nil, err - } - t.inputs.Add(c.Label, nil) - } - for _, c := range conf.ResourceCaches { - if err := checkLabel("cache", c.Label); err != nil { - return nil, err - } - t.caches.Add(c.Label, nil) - } - for _, c := range conf.ResourceProcessors { - if err := checkLabel("processor", c.Label); err != nil { - return nil, err - } - t.processors.Add(c.Label, nil) - } - for _, c := range conf.ResourceOutputs { - if err := checkLabel("output", c.Label); err != nil { - return nil, err - } - t.outputs.Add(c.Label, nil) - } - for _, c := range conf.ResourceRateLimits { - if err := checkLabel("rate limit", c.Label); err != nil { - return nil, err - } - t.rateLimits.Add(c.Label, nil) - } - - // Labels validated, begin construction - for _, conf := range conf.ResourceRateLimits { - if err := t.StoreRateLimit(context.Background(), conf.Label, conf); err != nil { - return nil, err - } - } - - for _, conf := range conf.ResourceCaches { - if err := t.StoreCache(context.Background(), conf.Label, conf); err != nil { - return nil, err - } - } - - // TODO: Prevent recursive processors. - for _, conf := range conf.ResourceProcessors { - if err := t.StoreProcessor(context.Background(), conf.Label, conf); err != nil { - return nil, err - } - } - - for _, conf := range conf.ResourceInputs { - if err := t.StoreInput(context.Background(), conf.Label, conf); err != nil { - return nil, err - } - } - - for _, conf := range conf.ResourceOutputs { - if err := t.StoreOutput(context.Background(), conf.Label, conf); err != nil { - return nil, err - } - } - - return t, nil -} - -// EngineVersion returns the stored version string for the engine. This version -// string could be any format. -func (t *Type) EngineVersion() string { - return t.engineVersion -} - -//------------------------------------------------------------------------------ - -// ForStream returns a variant of this manager to be used by a particular stream -// identifier, where APIs registered will be namespaced by that id. -func (t *Type) ForStream(id string) bundle.NewManagement { - return t.forStream(id) -} - -func (t *Type) forStream(id string) *Type { - newT := *t - newT.stream = id - newT.logger = t.logger.WithFields(map[string]string{ - "stream": id, - }) - newT.stats = t.stats.WithLabels("stream", id) - return &newT -} - -func (t *Type) forLabel(name string) *Type { - newT := *t - newT.label = name - newT.logger = t.logger.WithFields(map[string]string{ - "label": name, - }) - newT.stats = t.stats.WithLabels("label", name) - return &newT -} - -// IntoPath returns a variant of this manager to be used by a particular -// component path, which is a child of the current component, where -// observability components will be automatically tagged with the new path. -func (t *Type) IntoPath(segments ...string) bundle.NewManagement { - return t.intoPath(segments...) -} - -func (t *Type) intoPath(segments ...string) *Type { - newT := *t - newComponentPath := make([]string, 0, len(t.componentPath)+len(segments)) - newComponentPath = append(newComponentPath, t.componentPath...) - newComponentPath = append(newComponentPath, segments...) - newT.componentPath = newComponentPath - - pathStr := "root." + query.SliceToDotPath(newComponentPath...) - newT.logger = t.logger.WithFields(map[string]string{ - "path": pathStr, - }) - newT.stats = t.stats.WithLabels("path", pathStr) - return &newT -} - -// Path returns the current component path held by a manager. -func (t *Type) Path() []string { - return t.componentPath -} - -// Label returns the current component label held by a manager. -func (t *Type) Label() string { - return t.label -} - -// WithAddedMetrics returns a modified version of the manager where metrics are -// registered to both the current metrics target as well as the provided one. -func (t *Type) WithAddedMetrics(m metrics.Type) bundle.NewManagement { - newT := *t - newT.stats = newT.stats.WithStats(metrics.Combine(newT.stats.Child(), m)) - return &newT -} - -//------------------------------------------------------------------------------ - -// RegisterEndpoint registers a server wide HTTP endpoint. -func (t *Type) RegisterEndpoint(apiPath, desc string, h http.HandlerFunc) { - if t.stream != "" && t.namespaceStreamEndpoints { - apiPath = path.Join("/", t.stream, apiPath) - } - if t.apiReg != nil { - t.apiReg.RegisterEndpoint(apiPath, desc, h) - } -} - -// FS returns an ifs.FS implementation that provides access to a filesystem. By -// default this simply access the os package, with relative paths resolved from -// the directory that the process is running from. -func (t *Type) FS() ifs.FS { - return t.fs -} - -// SetPipe registers a new transaction chan to a named pipe. -func (t *Type) SetPipe(name string, tran <-chan message.Transaction) { - t.pipeLock.Lock() - t.pipes[name] = tran - t.pipeLock.Unlock() -} - -// GetPipe attempts to obtain and return a named output Pipe. -func (t *Type) GetPipe(name string) (<-chan message.Transaction, error) { - t.pipeLock.RLock() - pipe, exists := t.pipes[name] - t.pipeLock.RUnlock() - if exists { - return pipe, nil - } - return nil, component.ErrPipeNotFound -} - -// UnsetPipe removes a named pipe transaction chan. -func (t *Type) UnsetPipe(name string, tran <-chan message.Transaction) { - t.pipeLock.Lock() - if otran, exists := t.pipes[name]; exists && otran == tran { - delete(t.pipes, name) - } - t.pipeLock.Unlock() -} - -//------------------------------------------------------------------------------ - -// WithMetricsMapping returns a manager with the stored metrics exporter wrapped -// with a mapping. -func (t *Type) WithMetricsMapping(m *metrics.Mapping) *Type { - newT := *t - newT.stats = t.stats.WithMapping(m) - return &newT -} - -// Metrics returns an aggregator preset with the current component context. -func (t *Type) Metrics() metrics.Type { - return t.stats -} - -// Logger returns a logger preset with the current component context. -func (t *Type) Logger() log.Modular { - return t.logger -} - -// Tracer returns a tracer provider with the current component context. -func (t *Type) Tracer() trace.TracerProvider { - return t.tracer -} - -// Environment returns a bundle environment used by the manager. This is for -// internal use only. -func (t *Type) Environment() *bundle.Environment { - return t.env -} - -// BloblEnvironment returns a Bloblang environment used by the manager. This is -// for internal use only. -func (t *Type) BloblEnvironment() *bloblang.Environment { - return t.bloblEnv -} - -//------------------------------------------------------------------------------ - -// GetDocs returns a documentation spec for an implementation of a component. -func (t *Type) GetDocs(name string, ctype docs.Type) (docs.ComponentSpec, bool) { - return t.env.GetDocs(name, ctype) -} - -//------------------------------------------------------------------------------ - -// NewBuffer attempts to create a new buffer component from a config. -func (t *Type) NewBuffer(conf buffer.Config) (buffer.Streamed, error) { - // Buffers currently never have a label - return t.env.BufferInit(conf, t.forLabel("")) -} - -//------------------------------------------------------------------------------ - -// ProbeCache returns true if a cache resource exists under the provided name. -func (t *Type) ProbeCache(name string) bool { - return t.caches.Probe(name) -} - -// AccessCache attempts to access a cache resource by a unique identifier and -// executes a closure function with the cache as an argument. Returns an error -// if the cache does not exist (or is otherwise inaccessible). -// -// During the execution of the provided closure it is guaranteed that the -// resource will not be closed or removed. However, it is possible for the -// resource to be accessed by any number of components in parallel. -func (t *Type) AccessCache(ctx context.Context, name string, fn func(cache.V1)) (err error) { - if rerr := t.caches.RAccess(name, func(t cache.V1) { - if t == nil { - err = ErrResourceNotFound(name) - return - } - fn(t) - }); rerr != nil { - err = rerr - } - return -} - -// NewCache attempts to create a new cache component from a config. -func (t *Type) NewCache(conf cache.Config) (cache.V1, error) { - return t.env.CacheInit(conf, t.forLabel(conf.Label)) -} - -// StoreCache attempts to store a new cache resource. If an existing resource -// has the same name it is closed and removed _before_ the new one is -// initialized in order to avoid duplicate connections. -func (t *Type) StoreCache(ctx context.Context, name string, conf cache.Config) error { - var initErr error - if err := t.caches.Access(name, true, func(c *cache.V1, set func(*cache.V1)) { - if c != nil { - // If a previous resource exists with the same name then we do NOT allow - // it to be replaced unless it can be successfully closed. This ensures - // that we do not leak connections. - if initErr = (*c).Close(ctx); initErr != nil { - return - } - } - - var newCache cache.V1 - if newCache, initErr = t.intoPath("cache_resources").NewCache(conf); initErr != nil { - return - } - set(&newCache) - }); err != nil { - return err - } - return initErr -} - -// RemoveCache attempts to close and remove an existing cache resource. -func (t *Type) RemoveCache(ctx context.Context, name string) error { - var closeErr error - if err := t.caches.Access(name, false, func(c *cache.V1, set func(c *cache.V1)) { - if c == nil { - return - } - if closeErr = (*c).Close(ctx); closeErr != nil { - return - } - set(nil) - }); err != nil { - return err - } - return closeErr -} - -//------------------------------------------------------------------------------ - -// ProbeInput returns true if an input resource exists under the provided name. -func (t *Type) ProbeInput(name string) bool { - return t.inputs.Probe(name) -} - -// AccessInput attempts to access an input resource by a unique identifier and -// executes a closure function with the input as an argument. Returns an error -// if the input does not exist (or is otherwise inaccessible). -// -// During the execution of the provided closure it is guaranteed that the -// resource will not be closed or removed. However, it is possible for the -// resource to be accessed by any number of components in parallel. -func (t *Type) AccessInput(ctx context.Context, name string, fn func(input.Streamed)) (err error) { - if rerr := t.inputs.RAccess(name, func(t *InputWrapper) { - if t == nil { - err = ErrResourceNotFound(name) - return - } - fn(t) - }); rerr != nil { - err = rerr - } - return -} - -// NewInput attempts to create a new input component from a config. -func (t *Type) NewInput(conf input.Config) (input.Streamed, error) { - return t.env.InputInit(conf, t.forLabel(conf.Label)) -} - -// StoreInput attempts to store a new input resource. If an existing resource -// has the same name it is closed and removed _before_ the new one is -// initialized in order to avoid duplicate connections. -func (t *Type) StoreInput(ctx context.Context, name string, conf input.Config) error { - var initErr error - if err := t.inputs.Access(name, true, func(i **InputWrapper, set func(**InputWrapper)) { - if i != nil { - // If a previous resource exists with the same name then we do NOT allow - // it to be replaced unless it can be successfully closed. This ensures - // that we do not leak connections. - if initErr = (*i).CloseExistingInput(ctx, true); initErr != nil { - return - } - } - - if conf.Label != "" && conf.Label != name { - initErr = fmt.Errorf("label '%v' must be empty or match the resource name '%v'", conf.Label, name) - return - } - - var newInput input.Streamed - if newInput, initErr = t.intoPath("input_resources").NewInput(conf); initErr != nil { - return - } - - if i != nil { - (*i).SwapInput(newInput) - } else { - ni := WrapInput(newInput) - set(&ni) - } - }); err != nil { - return err - } - return initErr -} - -// RemoveInput attempts to close and remove an existing input resource. -func (t *Type) RemoveInput(ctx context.Context, name string) error { - var closeErr error - if err := t.inputs.Access(name, false, func(i **InputWrapper, set func(i **InputWrapper)) { - if i == nil { - return - } - if closeErr = (*i).CloseExistingInput(ctx, false); closeErr != nil { - return - } - set(nil) - }); err != nil { - return err - } - return closeErr -} - -//------------------------------------------------------------------------------ - -// ProbeProcessor returns true if a processor resource exists under the provided -// name. -func (t *Type) ProbeProcessor(name string) bool { - return t.processors.Probe(name) -} - -// AccessProcessor attempts to access a processor resource by a unique -// identifier and executes a closure function with the processor as an argument. -// Returns an error if the processor does not exist (or is otherwise -// inaccessible). -// -// During the execution of the provided closure it is guaranteed that the -// resource will not be closed or removed. However, it is possible for the -// resource to be accessed by any number of components in parallel. -func (t *Type) AccessProcessor(ctx context.Context, name string, fn func(processor.V1)) (err error) { - if rerr := t.processors.RAccess(name, func(t processor.V1) { - if t == nil { - err = ErrResourceNotFound(name) - return - } - fn(t) - }); rerr != nil { - err = rerr - } - return -} - -// NewProcessor attempts to create a new processor component from a config. -func (t *Type) NewProcessor(conf processor.Config) (processor.V1, error) { - return t.env.ProcessorInit(conf, t.forLabel(conf.Label)) -} - -// StoreProcessor attempts to store a new processor resource. If an existing -// resource has the same name it is closed and removed _before_ the new one is -// initialized in order to avoid duplicate connections. -func (t *Type) StoreProcessor(ctx context.Context, name string, conf processor.Config) error { - var initErr error - if err := t.processors.Access(name, true, func(p *processor.V1, set func(*processor.V1)) { - if p != nil { - // If a previous resource exists with the same name then we do NOT allow - // it to be replaced unless it can be successfully closed. This ensures - // that we do not leak connections. - if initErr = (*p).Close(ctx); initErr != nil { - return - } - } - - var newProc processor.V1 - if newProc, initErr = t.intoPath("processor_resources").NewProcessor(conf); initErr != nil { - return - } - set(&newProc) - }); err != nil { - return err - } - return initErr -} - -// RemoveProcessor attempts to close and remove an existing processor resource. -func (t *Type) RemoveProcessor(ctx context.Context, name string) error { - var closeErr error - if err := t.processors.Access(name, false, func(p *processor.V1, set func(p *processor.V1)) { - if p == nil { - return - } - if closeErr = (*p).Close(ctx); closeErr != nil { - return - } - set(nil) - }); err != nil { - return err - } - return closeErr -} - -//------------------------------------------------------------------------------ - -// ProbeOutput returns true if an output resource exists under the provided -// name. -func (t *Type) ProbeOutput(name string) bool { - return t.outputs.Probe(name) -} - -// AccessOutput attempts to access an output resource by a unique identifier and -// executes a closure function with the output as an argument. Returns an error -// if the output does not exist (or is otherwise inaccessible). -// -// During the execution of the provided closure it is guaranteed that the -// resource will not be closed or removed. However, it is possible for the -// resource to be accessed by any number of components in parallel. -func (t *Type) AccessOutput(ctx context.Context, name string, fn func(output.Sync)) (err error) { - if rerr := t.outputs.RAccess(name, func(t *outputWrapper) { - if t == nil { - err = ErrResourceNotFound(name) - return - } - fn(t) - }); rerr != nil { - err = rerr - } - return -} - -// NewOutput attempts to create a new output component from a config. -func (t *Type) NewOutput(conf output.Config, pipelines ...processor.PipelineConstructorFunc) (output.Streamed, error) { - return t.env.OutputInit(conf, t.forLabel(conf.Label), pipelines...) -} - -// StoreOutput attempts to store a new output resource. If an existing resource -// has the same name it is closed and removed _before_ the new one is -// initialized in order to avoid duplicate connections. -func (t *Type) StoreOutput(ctx context.Context, name string, conf output.Config) error { - var initErr error - if err := t.outputs.Access(name, true, func(o **outputWrapper, set func(**outputWrapper)) { - if o != nil { - // If a previous resource exists with the same name then we do NOT allow - // it to be replaced unless it can be successfully closed. This ensures - // that we do not leak connections. - (*o).TriggerStopConsuming() - if initErr = (*o).WaitForClose(ctx); initErr != nil { - return - } - } - - if conf.Label != "" && conf.Label != name { - initErr = fmt.Errorf("label '%v' must be empty or match the resource name '%v'", conf.Label, name) - return - } - - var newOutput output.Streamed - if newOutput, initErr = t.intoPath("output_resources").NewOutput(conf); initErr != nil { - return - } - - var wrappedOutput *outputWrapper - if wrappedOutput, initErr = wrapOutput(newOutput); initErr != nil { - newOutput.TriggerCloseNow() - return - } - - set(&wrappedOutput) - }); err != nil { - return err - } - return initErr -} - -// RemoveOutput attempts to close and remove an existing output resource. -func (t *Type) RemoveOutput(ctx context.Context, name string) error { - var closeErr error - if err := t.outputs.Access(name, false, func(o **outputWrapper, set func(o **outputWrapper)) { - if o == nil { - return - } - - (*o).TriggerStopConsuming() - if closeErr = (*o).WaitForClose(ctx); closeErr != nil { - return - } - set(nil) - }); err != nil { - return err - } - return closeErr -} - -//------------------------------------------------------------------------------ - -// ProbeRateLimit returns true if a rate limit resource exists under the -// provided name. -func (t *Type) ProbeRateLimit(name string) bool { - return t.rateLimits.Probe(name) -} - -// AccessRateLimit attempts to access a rate limit resource by a unique -// identifier and executes a closure function with the rate limit as an -// argument. Returns an error if the rate limit does not exist (or is otherwise -// inaccessible). -// -// During the execution of the provided closure it is guaranteed that the -// resource will not be closed or removed. However, it is possible for the -// resource to be accessed by any number of components in parallel. -func (t *Type) AccessRateLimit(ctx context.Context, name string, fn func(ratelimit.V1)) (err error) { - if rerr := t.rateLimits.RAccess(name, func(t ratelimit.V1) { - if t == nil { - err = ErrResourceNotFound(name) - return - } - fn(t) - }); rerr != nil { - err = rerr - } - return -} - -// NewRateLimit attempts to create a new rate limit component from a config. -func (t *Type) NewRateLimit(conf ratelimit.Config) (ratelimit.V1, error) { - return t.env.RateLimitInit(conf, t.forLabel(conf.Label)) -} - -// StoreRateLimit attempts to store a new rate limit resource. If an existing -// resource has the same name it is closed and removed _before_ the new one is -// initialized in order to avoid duplicate connections. -func (t *Type) StoreRateLimit(ctx context.Context, name string, conf ratelimit.Config) error { - var initErr error - if err := t.rateLimits.Access(name, true, func(r *ratelimit.V1, set func(*ratelimit.V1)) { - if r != nil { - // If a previous resource exists with the same name then we do NOT allow - // it to be replaced unless it can be successfully closed. This ensures - // that we do not leak connections. - if initErr = (*r).Close(ctx); initErr != nil { - return - } - } - - var newRL ratelimit.V1 - if newRL, initErr = t.intoPath("rate_limit_resources").NewRateLimit(conf); initErr != nil { - return - } - set(&newRL) - }); err != nil { - return err - } - return initErr -} - -// RemoveRateLimit attempts to close and remove an existing rate limit resource. -func (t *Type) RemoveRateLimit(ctx context.Context, name string) error { - var closeErr error - if err := t.rateLimits.Access(name, false, func(r *ratelimit.V1, set func(r *ratelimit.V1)) { - if r == nil { - return - } - if closeErr = (*r).Close(ctx); closeErr != nil { - return - } - set(nil) - }); err != nil { - return err - } - return closeErr -} - -//------------------------------------------------------------------------------ - -// NewScanner attempts to create a new scanner component from a config. -func (t *Type) NewScanner(conf scanner.Config) (scanner.Creator, error) { - return t.env.ScannerInit(conf, t) -} - -//------------------------------------------------------------------------------ - -// CloseObservability attempts to clean up observability (metrics, tracing, etc) -// components owned by the manager. This should only be called when the manager -// itself has finished shutting down and when it is the sole owner of the -// observability components. -func (t *Type) CloseObservability(ctx context.Context) error { - if t.tracer != nil { - if shutter, ok := t.tracer.(interface { - Shutdown(context.Context) error - }); ok { - _ = shutter.Shutdown(ctx) - } - } - if t.stats != nil { - if err := t.stats.Close(); err != nil { - return err - } - } - return nil -} - -// TriggerStopConsuming instructs the manager to stop resource inputs and -// outputs from consuming data. This call does not block. -func (t *Type) TriggerStopConsuming() { - _ = t.inputs.RWalk(func(name string, i *InputWrapper) error { - i.TriggerStopConsuming() - return nil - }) - _ = t.outputs.RWalk(func(name string, o *outputWrapper) error { - o.TriggerStopConsuming() - return nil - }) -} - -// TriggerCloseNow triggers the absolute shut down of this component but should -// not block the calling goroutine. -func (t *Type) TriggerCloseNow() { - _ = t.inputs.RWalk(func(name string, i *InputWrapper) error { - i.TriggerCloseNow() - return nil - }) - _ = t.outputs.RWalk(func(name string, o *outputWrapper) error { - o.TriggerCloseNow() - return nil - }) -} - -// WaitForClose is a blocking call to wait until the component has finished -// shutting down and cleaning up resources. -func (t *Type) WaitForClose(ctx context.Context) error { - if err := t.inputs.Walk(func(name string, i **InputWrapper, set func(i **InputWrapper)) error { - if i == nil { - return nil - } - if err := (*i).WaitForClose(ctx); err != nil { - return fmt.Errorf("resource '%s' failed to cleanly shutdown: %v", name, err) - } - set(nil) - return nil - }); err != nil { - return err - } - - if err := t.caches.Walk(func(name string, c *cache.V1, set func(c *cache.V1)) error { - if c == nil { - return nil - } - if err := (*c).Close(ctx); err != nil { - return fmt.Errorf("resource '%s' failed to cleanly shutdown: %v", name, err) - } - set(nil) - return nil - }); err != nil { - return err - } - - if err := t.processors.Walk(func(name string, p *processor.V1, set func(p *processor.V1)) error { - if p == nil { - return nil - } - if err := (*p).Close(ctx); err != nil { - return fmt.Errorf("resource '%s' failed to cleanly shutdown: %v", name, err) - } - set(nil) - return nil - }); err != nil { - return err - } - - if err := t.rateLimits.Walk(func(name string, r *ratelimit.V1, set func(r *ratelimit.V1)) error { - if r == nil { - return nil - } - if err := (*r).Close(ctx); err != nil { - return fmt.Errorf("resource '%s' failed to cleanly shutdown: %v", name, err) - } - set(nil) - return nil - }); err != nil { - return err - } - - if err := t.outputs.Walk(func(name string, o **outputWrapper, set func(o **outputWrapper)) error { - if o == nil { - return nil - } - if err := (*o).WaitForClose(ctx); err != nil { - return fmt.Errorf("resource '%s' failed to cleanly shutdown: %v", name, err) - } - set(nil) - return nil - }); err != nil { - return err - } - return nil -} diff --git a/internal/manager/type_stream_test.go b/internal/manager/type_stream_test.go deleted file mode 100644 index 85d861dcec..0000000000 --- a/internal/manager/type_stream_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package manager_test - -import ( - "context" - "testing" - "time" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestManagerStreamPipelines(t *testing.T) { - for _, test := range []struct { - name string - conf string - closeErr string - }{ - { - name: "basic pipeline all resource types", - conf: ` -input: - resource: fooinput - -pipeline: - processors: - - rate_limit: - resource: fooratelimit - - resource: fooproc - -output: - resource: foooutput - -input_resources: - - label: fooinput - generate: - interval: 1ms - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - -processor_resources: - - label: fooproc - cache: - operator: set - resource: foocache - key: fookey - value: '${! this.id }' - -cache_resources: - - label: foocache - memory: {} - -rate_limit_resources: - - label: fooratelimit - local: - count: 10 - interval: 1s - -output_resources: - - label: foooutput - drop: {} -`, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - builder := service.NewStreamBuilder() - require.NoError(t, builder.SetYAML(test.conf)) - require.NoError(t, builder.SetLoggerYAML(`level: none`)) - - strm, err := builder.Build() - require.NoError(t, err) - - cancelledCtx, done := context.WithCancel(context.Background()) - done() - - assert.Equal(t, cancelledCtx.Err(), strm.Run(cancelledCtx)) - - stopErr := strm.StopWithin(time.Millisecond * 100) - if test.closeErr == "" { - require.NoError(t, stopErr) - } else { - require.Error(t, stopErr) - assert.ErrorContains(t, stopErr, test.closeErr) - } - }) - } -} diff --git a/internal/manager/type_test.go b/internal/manager/type_test.go deleted file mode 100644 index 28d7a926e1..0000000000 --- a/internal/manager/type_test.go +++ /dev/null @@ -1,541 +0,0 @@ -package manager_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -var _ bundle.NewManagement = &manager.Type{} - -func TestManagerProcessorLabels(t *testing.T) { - t.Skip("No longer validating labels at construction") - - goodLabels := []string{ - "foo", - "foo_bar", - "foo_bar_baz_buz", - "foo__", - "foo123__45", - } - for _, l := range goodLabels { - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = "root = this" - conf.Label = l - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewProcessor(conf) - assert.NoError(t, err, "label: %v", l) - } - - badLabels := []string{ - "_foo", - "foo-bar", - "FOO", - "foo.bar", - } - for _, l := range badLabels { - conf := processor.NewConfig() - conf.Type = "bloblang" - conf.Plugin = "root = this" - conf.Label = l - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewProcessor(conf) - assert.EqualError(t, err, docs.ErrBadLabel.Error(), "label: %v", l) - } -} - -func TestManagerCache(t *testing.T) { - conf := manager.NewResourceConfig() - - fooCache := cache.NewConfig() - fooCache.Label = "foo" - conf.ResourceCaches = append(conf.ResourceCaches, fooCache) - - barCache := cache.NewConfig() - barCache.Label = "bar" - conf.ResourceCaches = append(conf.ResourceCaches, barCache) - - mgr, err := manager.New(conf) - if err != nil { - t.Fatal(err) - } - - require.True(t, mgr.ProbeCache("foo")) - require.True(t, mgr.ProbeCache("bar")) - require.False(t, mgr.ProbeCache("baz")) -} - -func TestManagerResourceCRUD(t *testing.T) { - conf := manager.NewResourceConfig() - - mgr, err := manager.New(conf) - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - inConf := input.NewConfig() - inConf.Type = "inproc" - inConf.Plugin = "meow" - - outConf := output.NewConfig() - outConf.Type = "drop" - - require.False(t, mgr.ProbeCache("foo")) - require.False(t, mgr.ProbeInput("foo")) - require.False(t, mgr.ProbeOutput("foo")) - require.False(t, mgr.ProbeProcessor("foo")) - require.False(t, mgr.ProbeRateLimit("foo")) - - require.NoError(t, mgr.StoreCache(tCtx, "foo", cache.NewConfig())) - require.NoError(t, mgr.StoreInput(tCtx, "foo", inConf)) - require.NoError(t, mgr.StoreOutput(tCtx, "foo", outConf)) - require.NoError(t, mgr.StoreProcessor(tCtx, "foo", processor.NewConfig())) - require.NoError(t, mgr.StoreRateLimit(tCtx, "foo", ratelimit.NewConfig())) - - require.True(t, mgr.ProbeCache("foo")) - require.True(t, mgr.ProbeInput("foo")) - require.True(t, mgr.ProbeOutput("foo")) - require.True(t, mgr.ProbeProcessor("foo")) - require.True(t, mgr.ProbeRateLimit("foo")) - - require.NoError(t, mgr.RemoveCache(tCtx, "foo")) - - require.False(t, mgr.ProbeCache("foo")) - require.True(t, mgr.ProbeInput("foo")) - require.True(t, mgr.ProbeOutput("foo")) - require.True(t, mgr.ProbeProcessor("foo")) - require.True(t, mgr.ProbeRateLimit("foo")) - - require.NoError(t, mgr.RemoveInput(tCtx, "foo")) - - require.False(t, mgr.ProbeCache("foo")) - require.False(t, mgr.ProbeInput("foo")) - require.True(t, mgr.ProbeOutput("foo")) - require.True(t, mgr.ProbeProcessor("foo")) - require.True(t, mgr.ProbeRateLimit("foo")) - - require.NoError(t, mgr.RemoveOutput(tCtx, "foo")) - - require.False(t, mgr.ProbeCache("foo")) - require.False(t, mgr.ProbeInput("foo")) - require.False(t, mgr.ProbeOutput("foo")) - require.True(t, mgr.ProbeProcessor("foo")) - require.True(t, mgr.ProbeRateLimit("foo")) - - require.NoError(t, mgr.RemoveProcessor(tCtx, "foo")) - - require.False(t, mgr.ProbeCache("foo")) - require.False(t, mgr.ProbeInput("foo")) - require.False(t, mgr.ProbeOutput("foo")) - require.False(t, mgr.ProbeProcessor("foo")) - require.True(t, mgr.ProbeRateLimit("foo")) - - require.NoError(t, mgr.RemoveRateLimit(tCtx, "foo")) - - require.False(t, mgr.ProbeCache("foo")) - require.False(t, mgr.ProbeInput("foo")) - require.False(t, mgr.ProbeOutput("foo")) - require.False(t, mgr.ProbeProcessor("foo")) - require.False(t, mgr.ProbeRateLimit("foo")) -} - -func TestManagerCacheList(t *testing.T) { - cacheFoo := cache.NewConfig() - cacheFoo.Label = "foo" - - cacheBar := cache.NewConfig() - cacheBar.Label = "bar" - - conf := manager.NewResourceConfig() - conf.ResourceCaches = append(conf.ResourceCaches, cacheFoo, cacheBar) - - mgr, err := manager.New(conf) - require.NoError(t, err) - - err = mgr.AccessCache(context.Background(), "foo", func(cache.V1) {}) - require.NoError(t, err) - - err = mgr.AccessCache(context.Background(), "bar", func(cache.V1) {}) - require.NoError(t, err) - - err = mgr.AccessCache(context.Background(), "baz", func(cache.V1) {}) - assert.EqualError(t, err, "unable to locate resource: baz") -} - -func TestManagerCacheListErrors(t *testing.T) { - cFoo := cache.NewConfig() - cFoo.Label = "foo" - - cBar := cache.NewConfig() - cBar.Label = "foo" - - conf := manager.NewResourceConfig() - conf.ResourceCaches = append(conf.ResourceCaches, cFoo, cBar) - - _, err := manager.New(conf) - require.EqualError(t, err, "cache resource label 'foo' collides with a previously defined resource") - - cEmpty := cache.NewConfig() - conf = manager.NewResourceConfig() - conf.ResourceCaches = append(conf.ResourceCaches, cEmpty) - - _, err = manager.New(conf) - require.EqualError(t, err, "cache resource has an empty label") -} - -func TestManagerBadCache(t *testing.T) { - conf := manager.NewResourceConfig() - - badConf := cache.NewConfig() - badConf.Label = "bad" - badConf.Type = "notexist" - conf.ResourceCaches = append(conf.ResourceCaches, badConf) - - if _, err := manager.New(conf); err == nil { - t.Fatal("Expected error from bad cache") - } -} - -func TestManagerRateLimit(t *testing.T) { - conf := manager.NewResourceConfig() - - fooRL := ratelimit.NewConfig() - fooRL.Label = "foo" - conf.ResourceRateLimits = append(conf.ResourceRateLimits, fooRL) - - barRL := ratelimit.NewConfig() - barRL.Label = "bar" - conf.ResourceRateLimits = append(conf.ResourceRateLimits, barRL) - - mgr, err := manager.New(conf) - if err != nil { - t.Fatal(err) - } - - require.True(t, mgr.ProbeRateLimit("foo")) - require.True(t, mgr.ProbeRateLimit("bar")) - require.False(t, mgr.ProbeRateLimit("baz")) -} - -func TestManagerRateLimitList(t *testing.T) { - cFoo := ratelimit.NewConfig() - cFoo.Label = "foo" - - cBar := ratelimit.NewConfig() - cBar.Label = "bar" - - conf := manager.NewResourceConfig() - conf.ResourceRateLimits = append(conf.ResourceRateLimits, cFoo, cBar) - - mgr, err := manager.New(conf) - require.NoError(t, err) - - err = mgr.AccessRateLimit(context.Background(), "foo", func(ratelimit.V1) {}) - require.NoError(t, err) - - err = mgr.AccessRateLimit(context.Background(), "bar", func(ratelimit.V1) {}) - require.NoError(t, err) - - err = mgr.AccessRateLimit(context.Background(), "baz", func(ratelimit.V1) {}) - assert.EqualError(t, err, "unable to locate resource: baz") -} - -func TestManagerRateLimitListErrors(t *testing.T) { - cFoo := ratelimit.NewConfig() - cFoo.Label = "foo" - - cBar := ratelimit.NewConfig() - cBar.Label = "foo" - - conf := manager.NewResourceConfig() - conf.ResourceRateLimits = append(conf.ResourceRateLimits, cFoo, cBar) - - _, err := manager.New(conf) - require.EqualError(t, err, "rate limit resource label 'foo' collides with a previously defined resource") - - cEmpty := ratelimit.NewConfig() - conf = manager.NewResourceConfig() - conf.ResourceRateLimits = append(conf.ResourceRateLimits, cEmpty) - - _, err = manager.New(conf) - require.EqualError(t, err, "rate limit resource has an empty label") -} - -func TestManagerBadRateLimit(t *testing.T) { - conf := manager.NewResourceConfig() - badConf := ratelimit.NewConfig() - badConf.Type = "notexist" - badConf.Label = "bad" - conf.ResourceRateLimits = append(conf.ResourceRateLimits, badConf) - - if _, err := manager.New(conf); err == nil { - t.Fatal("Expected error from bad rate limit") - } -} - -func TestManagerProcessor(t *testing.T) { - conf := manager.NewResourceConfig() - - fooProc := processor.NewConfig() - fooProc.Label = "foo" - conf.ResourceProcessors = append(conf.ResourceProcessors, fooProc) - - barProc := processor.NewConfig() - barProc.Label = "bar" - conf.ResourceProcessors = append(conf.ResourceProcessors, barProc) - - mgr, err := manager.New(conf) - if err != nil { - t.Fatal(err) - } - - require.True(t, mgr.ProbeProcessor("foo")) - require.True(t, mgr.ProbeProcessor("bar")) - require.False(t, mgr.ProbeProcessor("baz")) -} - -func TestManagerProcessorList(t *testing.T) { - cFoo := processor.NewConfig() - cFoo.Label = "foo" - - cBar := processor.NewConfig() - cBar.Label = "bar" - - conf := manager.NewResourceConfig() - conf.ResourceProcessors = append(conf.ResourceProcessors, cFoo, cBar) - - mgr, err := manager.New(conf) - require.NoError(t, err) - - err = mgr.AccessProcessor(context.Background(), "foo", func(processor.V1) {}) - require.NoError(t, err) - - err = mgr.AccessProcessor(context.Background(), "bar", func(processor.V1) {}) - require.NoError(t, err) - - err = mgr.AccessProcessor(context.Background(), "baz", func(processor.V1) {}) - assert.EqualError(t, err, "unable to locate resource: baz") -} - -func TestManagerProcessorListErrors(t *testing.T) { - cFoo := processor.NewConfig() - cFoo.Label = "foo" - - cBar := processor.NewConfig() - cBar.Label = "foo" - - conf := manager.NewResourceConfig() - conf.ResourceProcessors = append(conf.ResourceProcessors, cFoo, cBar) - - _, err := manager.New(conf) - require.EqualError(t, err, "processor resource label 'foo' collides with a previously defined resource") - - cEmpty := processor.NewConfig() - conf = manager.NewResourceConfig() - conf.ResourceProcessors = append(conf.ResourceProcessors, cEmpty) - - _, err = manager.New(conf) - require.EqualError(t, err, "processor resource has an empty label") -} - -func TestManagerInputList(t *testing.T) { - cFoo, err := testutil.InputFromYAML(` -label: foo -generate: - mapping: 'root = {}' -`) - require.NoError(t, err) - - cBar, err := testutil.InputFromYAML(` -label: bar -generate: - mapping: 'root = {}' -`) - require.NoError(t, err) - - conf := manager.NewResourceConfig() - conf.ResourceInputs = append(conf.ResourceInputs, cFoo, cBar) - - mgr, err := manager.New(conf) - require.NoError(t, err) - - err = mgr.AccessInput(context.Background(), "foo", func(i input.Streamed) {}) - require.NoError(t, err) - - err = mgr.AccessInput(context.Background(), "bar", func(i input.Streamed) {}) - require.NoError(t, err) - - err = mgr.AccessInput(context.Background(), "baz", func(i input.Streamed) {}) - assert.EqualError(t, err, "unable to locate resource: baz") -} - -func TestManagerInputListErrors(t *testing.T) { - cFoo := input.NewConfig() - cFoo.Label = "foo" - - cBar := input.NewConfig() - cBar.Label = "foo" - - conf := manager.NewResourceConfig() - conf.ResourceInputs = append(conf.ResourceInputs, cFoo, cBar) - - _, err := manager.New(conf) - require.EqualError(t, err, "input resource label 'foo' collides with a previously defined resource") - - cEmpty := input.NewConfig() - conf = manager.NewResourceConfig() - conf.ResourceInputs = append(conf.ResourceInputs, cEmpty) - - _, err = manager.New(conf) - require.EqualError(t, err, "input resource has an empty label") -} - -func TestManagerOutputList(t *testing.T) { - cFoo := output.NewConfig() - cFoo.Type = "drop" - cFoo.Label = "foo" - - cBar := output.NewConfig() - cBar.Type = "drop" - cBar.Label = "bar" - - conf := manager.NewResourceConfig() - conf.ResourceOutputs = append(conf.ResourceOutputs, cFoo, cBar) - - mgr, err := manager.New(conf) - require.NoError(t, err) - - err = mgr.AccessOutput(context.Background(), "foo", func(ow output.Sync) {}) - require.NoError(t, err) - - err = mgr.AccessOutput(context.Background(), "bar", func(ow output.Sync) {}) - require.NoError(t, err) - - err = mgr.AccessOutput(context.Background(), "baz", func(ow output.Sync) {}) - assert.EqualError(t, err, "unable to locate resource: baz") -} - -func TestManagerOutputListErrors(t *testing.T) { - cFoo := output.NewConfig() - cFoo.Label = "foo" - - cBar := output.NewConfig() - cBar.Label = "foo" - - conf := manager.NewResourceConfig() - conf.ResourceOutputs = append(conf.ResourceOutputs, cFoo, cBar) - - _, err := manager.New(conf) - require.EqualError(t, err, "output resource label 'foo' collides with a previously defined resource") - - cEmpty := output.NewConfig() - conf = manager.NewResourceConfig() - conf.ResourceOutputs = append(conf.ResourceOutputs, cEmpty) - - _, err = manager.New(conf) - require.EqualError(t, err, "output resource has an empty label") -} - -func TestManagerPipeErrors(t *testing.T) { - conf := manager.NewResourceConfig() - mgr, err := manager.New(conf) - if err != nil { - t.Fatal(err) - } - - if _, err = mgr.GetPipe("does not exist"); err != component.ErrPipeNotFound { - t.Errorf("Wrong error returned: %v != %v", err, component.ErrPipeNotFound) - } -} - -func TestManagerPipeGetSet(t *testing.T) { - conf := manager.NewResourceConfig() - mgr, err := manager.New(conf) - if err != nil { - t.Fatal(err) - } - - t1 := make(chan message.Transaction) - t2 := make(chan message.Transaction) - t3 := make(chan message.Transaction) - - mgr.SetPipe("foo", t1) - mgr.SetPipe("bar", t3) - - var p <-chan message.Transaction - if p, err = mgr.GetPipe("foo"); err != nil { - t.Fatal(err) - } - if p != t1 { - t.Error("Wrong transaction chan returned") - } - - // Should be a noop - mgr.UnsetPipe("foo", t2) - if p, err = mgr.GetPipe("foo"); err != nil { - t.Fatal(err) - } - if p != t1 { - t.Error("Wrong transaction chan returned") - } - if p, err = mgr.GetPipe("bar"); err != nil { - t.Fatal(err) - } - if p != t3 { - t.Error("Wrong transaction chan returned") - } - - mgr.UnsetPipe("foo", t1) - if _, err = mgr.GetPipe("foo"); err != component.ErrPipeNotFound { - t.Errorf("Wrong error returned: %v != %v", err, component.ErrPipeNotFound) - } - - // Back to before - mgr.SetPipe("foo", t1) - if p, err = mgr.GetPipe("foo"); err != nil { - t.Fatal(err) - } - if p != t1 { - t.Error("Wrong transaction chan returned") - } - - // Now replace pipe - mgr.SetPipe("foo", t2) - if p, err = mgr.GetPipe("foo"); err != nil { - t.Fatal(err) - } - if p != t2 { - t.Error("Wrong transaction chan returned") - } - if p, err = mgr.GetPipe("bar"); err != nil { - t.Fatal(err) - } - if p != t3 { - t.Error("Wrong transaction chan returned") - } -} diff --git a/internal/message/data.go b/internal/message/data.go deleted file mode 100644 index 2c3f4d1215..0000000000 --- a/internal/message/data.go +++ /dev/null @@ -1,201 +0,0 @@ -package message - -// Contains underlying allocated data for messages. -type messageData struct { - rawBytes []byte // Contents are always read-only - err error - - // Mutable when readOnlyStructured = false - readOnlyStructured bool - structured any // Sometimes mutable - - // Mutable when readOnlyMeta = false - readOnlyMeta bool - metadata map[string]any -} - -func newMessageBytes(content []byte) *messageData { - return &messageData{ - rawBytes: content, - metadata: nil, - err: nil, - } -} - -func (m *messageData) SetBytes(d []byte) { - m.rawBytes = d - m.structured = nil -} - -func (m *messageData) AsBytes() []byte { - if len(m.rawBytes) == 0 && m.structured != nil { - m.rawBytes = encodeJSON(m.structured) - } - return m.rawBytes -} - -func (m *messageData) SetStructured(jObj any) { - m.rawBytes = nil - if jObj == nil { - m.rawBytes = []byte(`null`) - m.structured = nil - m.readOnlyStructured = false - return - } - m.rawBytes = nil - m.structured = jObj - m.readOnlyStructured = true -} - -func (m *messageData) SetStructuredMut(jObj any) { - m.SetStructured(jObj) - m.readOnlyStructured = false -} - -func (m *messageData) AsStructured() (any, error) { - if m.structured != nil { - return m.structured, nil - } - - if len(m.rawBytes) == 0 { - return nil, ErrMessagePartNotExist // TODO: Need this? - } - - var err error - m.structured, err = decodeJSON(m.rawBytes) - return m.structured, err -} - -func (m *messageData) AsStructuredMut() (any, error) { - if m.readOnlyStructured { - if m.structured != nil { - m.structured = cloneGeneric(m.structured) - } - m.readOnlyStructured = true - } - - v, err := m.AsStructured() - if err != nil { - return nil, err - } - - // Bytes need resetting as our structured form may change - m.rawBytes = nil - return v, nil -} - -// ShallowCopy returns a copy of the message data that can be mutated without -// mutating the original message contents (metadata and structured data). -func (m *messageData) ShallowCopy() *messageData { - return &messageData{ - rawBytes: m.rawBytes, - err: m.err, - - readOnlyStructured: true, - structured: m.structured, - - readOnlyMeta: true, - metadata: m.metadata, - } -} - -// DeepCopy returns a copy of the message data that can be mutated without -// mutating the original message contents (metadata and structured data), and -// all underlying reference types are deeply copied to eliminate any parental -// ownership. -// -// This is worth doing on values persisted outside of the lifetime of a -// transaction unless some other strategy is used for persistence. -func (m *messageData) DeepCopy() *messageData { - var clonedMeta map[string]any - if m.metadata != nil { - clonedMeta = make(map[string]any, len(m.metadata)) - for k, v := range m.metadata { - clonedMeta[k] = cloneGeneric(v) - } - } - - var bytesCopy []byte - if len(m.rawBytes) > 0 { - bytesCopy = make([]byte, len(m.rawBytes)) - copy(bytesCopy, m.rawBytes) - } - - var structuredCopy any - if m.structured != nil { - structuredCopy = cloneGeneric(m.structured) - } - - return &messageData{ - rawBytes: bytesCopy, - err: m.err, - structured: structuredCopy, - metadata: clonedMeta, - } -} - -func (m *messageData) IsEmpty() bool { - return len(m.rawBytes) == 0 && m.structured == nil -} - -func (m *messageData) writeableMeta() { - if !m.readOnlyMeta { - return - } - - var clonedMeta map[string]any - if m.metadata != nil { - clonedMeta = make(map[string]any, len(m.metadata)) - for k, v := range m.metadata { - // NOTE: All metadata is store as mutable so no need to deep clone. - clonedMeta[k] = v - } - } - - m.metadata = clonedMeta - m.readOnlyMeta = false -} - -func (m *messageData) MetaGetMut(key string) (any, bool) { - if m.metadata == nil { - return nil, false - } - s, exists := m.metadata[key] - if !exists { - return nil, false - } - return s, true -} - -func (m *messageData) MetaSetMut(key string, value any) { - m.writeableMeta() - if m.metadata == nil { - m.metadata = map[string]any{ - key: value, - } - return - } - m.metadata[key] = value -} - -func (m *messageData) MetaDelete(key string) { - m.writeableMeta() - delete(m.metadata, key) -} - -func (m *messageData) MetaIterMut(f func(k string, v any) error) error { - for ak, av := range m.metadata { - if err := f(ak, av); err != nil { - return err - } - } - return nil -} - -func (m *messageData) ErrorGet() error { - return m.err -} - -func (m *messageData) ErrorSet(err error) { - m.err = err -} diff --git a/internal/message/data_test.go b/internal/message/data_test.go deleted file mode 100644 index 0271cf52fc..0000000000 --- a/internal/message/data_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package message - -import ( - "sync" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConcurrentMutationsFromNil(t *testing.T) { - source := newMessageBytes(nil) - kickOffChan := make(chan struct{}) - - var wg sync.WaitGroup - for i := 0; i < 100; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-kickOffChan - - local := source.ShallowCopy() - local.MetaSetMut("foo", "bar") - local.MetaSetMut("bar", "baz") - _ = local.MetaIterMut(func(k string, v any) error { - return nil - }) - local.MetaDelete("foo") - - local.SetBytes([]byte(`new thing`)) - local.SetStructuredMut(map[string]any{ - "foo": "bar", - }) - - vThing, err := local.AsStructuredMut() - require.NoError(t, err) - - _, err = gabs.Wrap(vThing).Set("baz", "foo") - require.NoError(t, err) - - vBytes := local.AsBytes() - assert.Equal(t, `{"foo":"baz"}`, string(vBytes)) - }() - } - - close(kickOffChan) - wg.Wait() -} - -func TestConcurrentMutationsFromStructured(t *testing.T) { - source := newMessageBytes(nil) - source.MetaSetMut("foo", "foo1") - source.MetaSetMut("bar", "bar1") - source.SetStructuredMut(map[string]any{ - "foo": "bar", - }) - - kickOffChan := make(chan struct{}) - - var wg sync.WaitGroup - for i := 0; i < 100; i++ { - wg.Add(1) - go func() { - defer wg.Done() - <-kickOffChan - - local := source.ShallowCopy() - local.MetaSetMut("foo", "foo2") - - v, exists := local.MetaGetMut("foo") - assert.True(t, exists) - assert.Equal(t, "foo2", v) - - v, exists = local.MetaGetMut("bar") - assert.True(t, exists) - assert.Equal(t, "bar1", v) - - _ = local.MetaIterMut(func(k string, v any) error { - return nil - }) - local.MetaDelete("foo") - - _, exists = local.MetaGetMut("foo") - assert.False(t, exists) - - vThing, err := local.AsStructuredMut() - require.NoError(t, err) - - _, err = gabs.Wrap(vThing).Set("baz", "foo") - require.NoError(t, err) - - vBytes := local.AsBytes() - assert.Equal(t, `{"foo":"baz"}`, string(vBytes)) - }() - } - - close(kickOffChan) - wg.Wait() -} - -func TestSetNil(t *testing.T) { - source := newMessageBytes(nil) - source.SetStructured(map[string]any{ - "foo": "bar", - }) - - v, err := source.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{"foo": "bar"}, v) - - source.SetStructured(nil) - - v, err = source.AsStructured() - require.NoError(t, err) - assert.Nil(t, v) -} diff --git a/internal/message/errors.go b/internal/message/errors.go deleted file mode 100644 index b4f4596ad9..0000000000 --- a/internal/message/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package message - -import ( - "errors" -) - -// Errors returned by the message type. -var ( - ErrMessagePartNotExist = errors.New("target message part does not exist") - ErrBadMessageBytes = errors.New("serialised message bytes were in unexpected format") -) diff --git a/internal/message/message.go b/internal/message/message.go deleted file mode 100644 index 6b10b272b1..0000000000 --- a/internal/message/message.go +++ /dev/null @@ -1,177 +0,0 @@ -package message - -// Batch represents zero or more messages. -type Batch []*Part - -// QuickBatch initializes a new message batch from a 2D byte slice, the slice -// can be nil, in which case the batch will start empty. -func QuickBatch(bslice [][]byte) Batch { - parts := make([]*Part, len(bslice)) - for i, v := range bslice { - parts[i] = NewPart(v) - } - return parts -} - -//------------------------------------------------------------------------------ - -// Len returns the length of the batch. -func (m Batch) Len() int { - return len(m) -} - -// ShallowCopy creates a new shallow copy of the message. Parts can be -// re-arranged in the new copy and JSON parts can be get/set without impacting -// other message copies. -func (m Batch) ShallowCopy() Batch { - parts := make([]*Part, len(m)) - for i, v := range m { - parts[i] = v.ShallowCopy() - } - return parts -} - -// DeepCopy creates a new deep copy of the message. This can be considered an -// entirely new object that is safe to use anywhere. -func (m Batch) DeepCopy() Batch { - parts := make([]*Part, len(m)) - for i, v := range m { - parts[i] = v.DeepCopy() - } - return parts -} - -//------------------------------------------------------------------------------ - -// Get returns a message part at a particular index, indexes can be negative. -func (m Batch) Get(index int) *Part { - if len(m) == 0 { - return NewPart(nil) - } - if index < 0 { - index = len(m) + index - } - if index < 0 || index >= len(m) { - return NewPart(nil) - } - if m[index] == nil { - m[index] = NewPart(nil) - } - return m[index] -} - -// Iter will iterate all parts of the message, calling f for each. -func (m Batch) Iter(f func(i int, p *Part) error) error { - for i, p := range m { - if p == nil { - p = NewPart(nil) - m[i] = p - } - if err := f(i, p); err != nil { - return err - } - } - return nil -} - -//------------------------------------------------------------------------------ - -/* -Internal message blob format: - -- Four bytes containing number of message parts in big endian -- For each message part: - + Four bytes containing length of message part in big endian - + Content of message part - - # Of bytes in message part 2 - | -# Of message parts (u32 big endian) | Content of message part 2 -| | | -v v v -| 0| 0| 0| 2| 0| 0| 0| 5| h| e| l| l| o| 0| 0| 0| 5| w| o| r| l| d| - 0 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 17 18 19 20 21 22 - ^ ^ - | | - | Content of message part 1 - | - # Of bytes in message part 1 (u32 big endian) -*/ - -// Reserve bytes for our length counter (4 * 8 = 32 bit). -var intLen uint32 = 4 - -// SerializeBytes returns a 2D byte-slice serialized. -func SerializeBytes(parts [][]byte) []byte { - lenParts := uint32(len(parts)) - - l := (lenParts + 1) * intLen - for _, p := range parts { - l += uint32(len(p)) - } - b := make([]byte, l) - - b[0] = byte(lenParts >> 24) - b[1] = byte(lenParts >> 16) - b[2] = byte(lenParts >> 8) - b[3] = byte(lenParts) - - b2 := b[intLen:] - - for _, p := range parts { - le := uint32(len(p)) - - b2[0] = byte(le >> 24) - b2[1] = byte(le >> 16) - b2[2] = byte(le >> 8) - b2[3] = byte(le) - - b2 = b2[intLen:] - - copy(b2, p) - b2 = b2[len(p):] - } - - return b -} - -// DeserializeBytes rebuilds a 2D byte array from a binary serialized blob. -func DeserializeBytes(b []byte) ([][]byte, error) { - if len(b) < 4 { - return nil, ErrBadMessageBytes - } - - numParts := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) - if numParts >= uint32(len(b)) { - return nil, ErrBadMessageBytes - } - - b = b[4:] - - parts := make([][]byte, numParts) - - for i := uint32(0); i < numParts; i++ { - if len(b) < 4 { - return nil, ErrBadMessageBytes - } - partSize := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) - b = b[4:] - - if uint32(len(b)) < partSize { - return nil, ErrBadMessageBytes - } - - parts[i] = b[:partSize] - b = b[partSize:] - } - return parts, nil -} - -// FromBytes deserialises a Message from a byte array. -func FromBytes(b []byte) (Batch, error) { - parts, err := DeserializeBytes(b) - if err != nil { - return nil, err - } - return QuickBatch(parts), nil -} diff --git a/internal/message/message_test.go b/internal/message/message_test.go deleted file mode 100644 index e557554c24..0000000000 --- a/internal/message/message_test.go +++ /dev/null @@ -1,524 +0,0 @@ -package message - -import ( - "context" - "errors" - "reflect" - "testing" - - "github.com/Jeffail/gabs/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMessageSerialization(t *testing.T) { - input := [][]byte{ - []byte("hello"), - []byte("world"), - []byte("12345"), - } - - m2, err := FromBytes(SerializeBytes(input)) - if err != nil { - t.Error(err) - return - } - - if !reflect.DeepEqual(input, GetAllBytes(m2)) { - t.Errorf("Messages not equal: %v != %v", input, m2) - } -} - -func TestNew(t *testing.T) { - m := QuickBatch(nil) - if act := len(m); act > 0 { - t.Errorf("New returned more than zero message parts: %v", act) - } -} - -func TestIter(t *testing.T) { - parts := [][]byte{ - []byte(`foo`), - []byte(`bar`), - []byte(`baz`), - } - m := QuickBatch(parts) - iters := 0 - _ = m.Iter(func(index int, b *Part) error { - if exp, act := string(parts[index]), string(b.AsBytes()); exp != act { - t.Errorf("Unexpected part: %v != %v", act, exp) - } - iters++ - return nil - }) - if exp, act := 3, iters; exp != act { - t.Errorf("Wrong count of iterations: %v != %v", act, exp) - } -} - -func TestMessageInvalidBytesFormat(t *testing.T) { - cases := [][]byte{ - []byte(``), - []byte(`this is invalid`), - {0x00, 0x00}, - {0x00, 0x00, 0x00, 0x05}, - {0x00, 0x00, 0x00, 0x01, 0x00, 0x00}, - {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}, - {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00}, - } - - for _, c := range cases { - if _, err := FromBytes(c); err == nil { - t.Errorf("Received nil error from invalid byte sequence: %s", c) - } - } -} - -func TestMessageIncompleteJSON(t *testing.T) { - tests := []struct { - message string - err string - }{ - {message: "{}"}, - { - message: "{} not foo", - err: "invalid character 'o' in literal null (expecting 'u')", - }, - { - message: "{} {}", - err: "message contains multiple valid documents", - }, - {message: `["foo"] `}, - {message: ` ["foo"] `}, - {message: ` ["foo"] - - `}, - { - message: ` ["foo"] - - - - {}`, - err: "message contains multiple valid documents", - }, - } - - for _, test := range tests { - msg := QuickBatch([][]byte{[]byte(test.message)}) - _, err := msg.Get(0).AsStructuredMut() - if test.err == "" { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, test.err) - } - } -} - -func TestMessageJSONGet(t *testing.T) { - msg := QuickBatch( - [][]byte{[]byte(`{"foo":{"bar":"baz"}}`)}, - ) - - _, err := msg.Get(1).AsStructuredMut() - require.Error(t, err) - - jObj, err := msg.Get(0).AsStructuredMut() - require.NoError(t, err) - - exp := map[string]any{ - "foo": map[string]any{ - "bar": "baz", - }, - } - assert.Equal(t, exp, jObj) - - msg.Get(0).SetBytes([]byte(`{"foo":{"bar":"baz2"}}`)) - - jObj, err = msg.Get(0).AsStructuredMut() - require.NoError(t, err) - - exp = map[string]any{ - "foo": map[string]any{ - "bar": "baz2", - }, - } - assert.Equal(t, exp, jObj) -} - -func TestMessageJSONSet(t *testing.T) { - msg := QuickBatch([][]byte{[]byte(`hello world`)}) - - msg.Get(1).SetStructured(nil) - - p1Obj := map[string]any{ - "foo": map[string]any{ - "bar": "baz", - }, - } - p1Str := `{"foo":{"bar":"baz"}}` - - p2Obj := map[string]any{ - "baz": map[string]any{ - "bar": "foo", - }, - } - p2Str := `{"baz":{"bar":"foo"}}` - - msg.Get(0).SetStructured(p1Obj) - if exp, act := p1Str, string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong json blob: %v != %v", act, exp) - } - - msg.Get(0).SetStructured(p2Obj) - if exp, act := p2Str, string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong json blob: %v != %v", act, exp) - } - - msg.Get(0).SetStructured(p1Obj) - if exp, act := p1Str, string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong json blob: %v != %v", act, exp) - } -} - -func TestMessageMetadata(t *testing.T) { - m := QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - }) - - m.Get(0).MetaSetMut("foo", "bar") - if exp, act := "bar", m.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - m.Get(0).MetaSetMut("foo", "bar2") - if exp, act := "bar2", m.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - m.Get(0).MetaSetMut("bar", "baz") - m.Get(0).MetaSetMut("baz", "qux") - - exp := map[string]string{ - "foo": "bar2", - "bar": "baz", - "baz": "qux", - } - act := map[string]string{} - require.NoError(t, m.Get(0).MetaIterStr(func(k, v string) error { - act[k] = v - return nil - })) - if !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } -} - -func TestMessageCopy(t *testing.T) { - m := QuickBatch([][]byte{ - []byte(`foo`), - []byte(`bar`), - }) - m.Get(0).MetaSetMut("foo", "bar") - - m2 := m.ShallowCopy() - if exp, act := [][]byte{[]byte(`foo`), []byte(`bar`)}, GetAllBytes(m2); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } - if exp, act := "bar", m2.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - m2.Get(0).MetaSetMut("foo", "bar2") - m2.Get(0).SetBytes([]byte(`baz`)) - if exp, act := [][]byte{[]byte(`baz`), []byte(`bar`)}, GetAllBytes(m2); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } - if exp, act := "bar2", m2.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := [][]byte{[]byte(`foo`), []byte(`bar`)}, GetAllBytes(m); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } - if exp, act := "bar", m.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func TestMessageErrors(t *testing.T) { - p1 := NewPart([]byte("foo")) - assert.NoError(t, p1.ErrorGet()) - - p2 := p1.WithContext(context.Background()) - assert.NoError(t, p2.ErrorGet()) - - p3 := p2.ShallowCopy() - assert.NoError(t, p3.ErrorGet()) - - p1.ErrorSet(errors.New("err1")) - assert.EqualError(t, p1.ErrorGet(), "err1") - assert.EqualError(t, p2.ErrorGet(), "err1") - assert.NoError(t, p3.ErrorGet()) - - p2.ErrorSet(errors.New("err2")) - assert.EqualError(t, p1.ErrorGet(), "err2") - assert.EqualError(t, p2.ErrorGet(), "err2") - assert.NoError(t, p3.ErrorGet()) - - p3.ErrorSet(errors.New("err3")) - assert.EqualError(t, p1.ErrorGet(), "err2") - assert.EqualError(t, p2.ErrorGet(), "err2") - assert.EqualError(t, p3.ErrorGet(), "err3") -} - -func TestMessageDeepCopy(t *testing.T) { - m := QuickBatch([][]byte{ - []byte(`foo`), - []byte(`bar`), - }) - m.Get(0).MetaSetMut("foo", "bar") - - m2 := m.DeepCopy() - if exp, act := [][]byte{[]byte(`foo`), []byte(`bar`)}, GetAllBytes(m2); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } - if exp, act := "bar", m2.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - m2.Get(0).MetaSetMut("foo", "bar2") - m2.Get(0).SetBytes([]byte(`baz`)) - if exp, act := [][]byte{[]byte(`baz`), []byte(`bar`)}, GetAllBytes(m2); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } - if exp, act := "bar2", m2.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := [][]byte{[]byte(`foo`), []byte(`bar`)}, GetAllBytes(m); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } - if exp, act := "bar", m.Get(0).MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func TestMessageJSONSetGet(t *testing.T) { - msg := QuickBatch([][]byte{[]byte(`hello world`)}) - - p1Obj := map[string]any{ - "foo": map[string]any{ - "bar": "baz", - }, - } - p1Str := `{"foo":{"bar":"baz"}}` - - p2Obj := map[string]any{ - "baz": map[string]any{ - "bar": "foo", - }, - } - p2Str := `{"baz":{"bar":"foo"}}` - - var err error - var jObj any - - msg.Get(0).SetStructured(p1Obj) - if exp, act := p1Str, string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong json blob: %v != %v", act, exp) - } - if jObj, err = msg.Get(0).AsStructuredMut(); err != nil { - t.Fatal(err) - } - if exp, act := p1Obj, jObj; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong json obj: %v != %v", act, exp) - } - - msg.Get(0).SetStructured(p2Obj) - if exp, act := p2Str, string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong json blob: %v != %v", act, exp) - } - if jObj, err = msg.Get(0).AsStructuredMut(); err != nil { - t.Fatal(err) - } - if exp, act := p2Obj, jObj; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong json obj: %v != %v", act, exp) - } - - msg.Get(0).SetStructured(p1Obj) - if exp, act := p1Str, string(msg.Get(0).AsBytes()); exp != act { - t.Errorf("Wrong json blob: %v != %v", act, exp) - } - if jObj, err = msg.Get(0).AsStructuredMut(); err != nil { - t.Fatal(err) - } - if exp, act := p1Obj, jObj; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong json obj: %v != %v", act, exp) - } -} - -func TestMessageSplitJSON(t *testing.T) { - msg1 := QuickBatch([][]byte{ - []byte("Foo plain text"), - []byte(`nothing here`), - }) - - msg1.Get(1).SetStructured(map[string]any{"foo": "bar"}) - msg2 := msg1.ShallowCopy() - - if exp, act := GetAllBytes(msg1), GetAllBytes(msg2); !reflect.DeepEqual(exp, act) { - t.Errorf("Parts unmatched from shallow copy: %s != %s", act, exp) - } - - msg2.Get(0).SetBytes([]byte("Bar different text")) - - if exp, act := "Foo plain text", string(msg1.Get(0).AsBytes()); exp != act { - t.Errorf("Original content was changed from shallow copy: %v != %v", act, exp) - } - - msg1.Get(1).SetStructured(map[string]any{"foo": "baz"}) - jCont, err := msg1.Get(1).AsStructuredMut() - if err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"foo": "baz"}, jCont; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected json content: %v != %v", exp, act) - } - if exp, act := `{"foo":"baz"}`, string(msg1.Get(1).AsBytes()); exp != act { - t.Errorf("Unexpected original content: %v != %v", act, exp) - } - - jCont, err = msg2.Get(1).AsStructuredMut() - if err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"foo": "bar"}, jCont; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected json content: %v != %v", exp, act) - } - if exp, act := `{"foo":"bar"}`, string(msg2.Get(1).AsBytes()); exp != act { - t.Errorf("Unexpected shallow content: %v != %v", act, exp) - } - - msg2.Get(1).SetStructured(map[string]any{"foo": "baz2"}) - jCont, err = msg2.Get(1).AsStructuredMut() - if err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"foo": "baz2"}, jCont; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected json content: %v != %v", exp, act) - } - if exp, act := `{"foo":"baz2"}`, string(msg2.Get(1).AsBytes()); exp != act { - t.Errorf("Unexpected shallow copy content: %v != %v", act, exp) - } - - jCont, err = msg1.Get(1).AsStructuredMut() - if err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"foo": "baz"}, jCont; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected original json content: %v != %v", exp, act) - } - if exp, act := `{"foo":"baz"}`, string(msg1.Get(1).AsBytes()); exp != act { - t.Errorf("Unexpected original content: %v != %v", act, exp) - } -} - -func TestMessageCrossContaminateJSON(t *testing.T) { - msg1 := QuickBatch([][]byte{ - []byte(`{"foo":"bar"}`), - }) - - jCont1, err := msg1.Get(0).AsStructuredMut() - require.NoError(t, err) - - msg2 := msg1.ShallowCopy() - jCont2, err := msg2.Get(0).AsStructuredMut() - require.NoError(t, err) - - _, err = gabs.Wrap(jCont2).Set("baz", "foo") - require.NoError(t, err) - - if exp, act := map[string]any{"foo": "bar"}, jCont1; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected json content: %v != %v", exp, act) - } - if exp, act := `{"foo":"bar"}`, string(msg1.Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected raw content: %v != %v", exp, act) - } - - if exp, act := map[string]any{"foo": "baz"}, jCont2; !reflect.DeepEqual(exp, act) { - t.Errorf("Unexpected json content: %v != %v", exp, act) - } - if exp, act := `{"foo":"baz"}`, string(msg2.Get(0).AsBytes()); exp != act { - t.Errorf("Unexpected raw content: %v != %v", exp, act) - } -} - -func BenchmarkJSONGet(b *testing.B) { - sample1 := []byte(`{ - "foo":{ - "bar":"baz", - "this":{ - "will":{ - "be":{ - "very":{ - "nested":true - } - }, - "dont_forget":"me" - }, - "dont_forget":"me" - }, - "dont_forget":"me" - }, - "numbers": [0,1,2,3,4,5,6,7] -}`) - sample2 := []byte(`{ - "foo2":{ - "bar":"baz2", - "this":{ - "will":{ - "be":{ - "very":{ - "nested":false - } - }, - "dont_forget":"me too" - }, - "dont_forget":"me too" - }, - "dont_forget":"me too" - }, - "numbers": [0,1,2,3,4,5,6,7] -}`) - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - msg := QuickBatch([][]byte{sample1}) - - jObj, err := msg.Get(0).AsStructuredMut() - if err != nil { - b.Error(err) - } - if _, ok := jObj.(map[string]any); !ok { - b.Error("Couldn't cast to map") - } - - jObj, err = msg.Get(0).AsStructuredMut() - if err != nil { - b.Error(err) - } - if _, ok := jObj.(map[string]any); !ok { - b.Error("Couldn't cast to map") - } - - msg.Get(0).SetBytes(sample2) - - jObj, err = msg.Get(0).AsStructuredMut() - if err != nil { - b.Error(err) - } - if _, ok := jObj.(map[string]any); !ok { - b.Error("Couldn't cast to map") - } - } -} diff --git a/internal/message/part.go b/internal/message/part.go deleted file mode 100644 index 4572864a23..0000000000 --- a/internal/message/part.go +++ /dev/null @@ -1,171 +0,0 @@ -package message - -import ( - "context" -) - -// Part represents a single Benthos message. -type Part struct { - data *messageData - ctx context.Context -} - -// NewPart initializes a new message part. -func NewPart(data []byte) *Part { - return &Part{ - data: newMessageBytes(data), - ctx: context.Background(), - } -} - -//------------------------------------------------------------------------------ - -// ShallowCopy creates a shallow copy of the message part. -func (p *Part) ShallowCopy() *Part { - return &Part{ - data: p.data.ShallowCopy(), - ctx: p.ctx, - } -} - -// DeepCopy creates a new deep copy of the message part. -func (p *Part) DeepCopy() *Part { - return &Part{ - data: p.data.DeepCopy(), - ctx: p.ctx, - } -} - -//------------------------------------------------------------------------------ - -// GetContext either returns a context attached to the message part, or -// context.Background() if one hasn't been previously attached. -func GetContext(p *Part) context.Context { - return p.ctx -} - -// WithContext returns the same message part wrapped with a context, this -// context can subsequently be received with GetContext. -func WithContext(ctx context.Context, p *Part) *Part { - return p.WithContext(ctx) -} - -// GetContext returns the underlying context attached to this message part. -func (p *Part) GetContext() context.Context { - return p.ctx -} - -// WithContext returns the underlying message part with a different context -// attached. -func (p *Part) WithContext(ctx context.Context) *Part { - newP := *p - newP.ctx = ctx - return &newP -} - -//------------------------------------------------------------------------------ - -// ErrorGet returns an error associated with the message, or nil if none exists. -func (p *Part) ErrorGet() error { - return p.data.ErrorGet() -} - -// ErrorSet modifies the error associated with a message. Errors attached to -// messages are used to indicate that processing has failed at some point in the -// processing pipeline. -func (p *Part) ErrorSet(err error) { - p.data.ErrorSet(err) -} - -// AsBytes returns the body of the message part. -func (p *Part) AsBytes() []byte { - return p.data.AsBytes() -} - -// AsStructuredMut returns the structured format of the message if already set, -// or attempts to parse the raw bytes as a JSON document if not. The returned -// structure is mutable and therefore safe to mutate directly. -func (p *Part) AsStructuredMut() (any, error) { - return p.data.AsStructuredMut() -} - -// AsStructured returns the structured format of the message if already set, or -// attempts to parse the raw bytes as a JSON document if not. The returned -// structure should be considered read-only and therefore not be mutated. -func (p *Part) AsStructured() (any, error) { - return p.data.AsStructured() -} - -// SetBytes the value of the message part as a raw byte slice. -func (p *Part) SetBytes(data []byte) *Part { - p.data.SetBytes(data) - return p -} - -// SetStructuredMut sets the value of the message to a structured value, this -// value is mutable and subsequent mutations will be performed directly on the -// provided data. -func (p *Part) SetStructuredMut(jObj any) { - p.data.SetStructuredMut(jObj) -} - -// SetStructured sets the value of the message to a structured value, this -// value is read-only and subsequent mutations will require cloning of the -// entire data structure. -func (p *Part) SetStructured(jObj any) { - p.data.SetStructured(jObj) -} - -//------------------------------------------------------------------------------ - -// MetaGetStr returns a metadata value if a key exists as a string, otherwise an -// empty string. -func (p *Part) MetaGetStr(key string) string { - v, exists := p.data.MetaGetMut(key) - if !exists { - return "" - } - return metaToString(v) -} - -// MetaGetMut returns a metadata value if a key exists. -func (p *Part) MetaGetMut(key string) (any, bool) { - v, exists := p.data.MetaGetMut(key) - if !exists { - return nil, false - } - return v, true -} - -// MetaSetMut sets the value of a metadata key to any value. -func (p *Part) MetaSetMut(key string, value any) { - p.data.MetaSetMut(key, value) -} - -// MetaDelete removes the value of a metadata key. -func (p *Part) MetaDelete(key string) { - p.data.MetaDelete(key) -} - -// MetaIterStr iterates each metadata key/value pair with the value serialised -// as a string. -func (p *Part) MetaIterStr(f func(string, string) error) error { - return p.data.MetaIterMut(func(k string, v any) error { - vStr := metaToString(v) - return f(k, vStr) - }) -} - -// MetaIterMut iterates each metadata key/value pair. -func (p *Part) MetaIterMut(f func(string, any) error) error { - return p.data.MetaIterMut(func(k string, v any) error { - return f(k, v) - }) -} - -//------------------------------------------------------------------------------ - -// IsEmpty returns true if the message part is empty. -func (p *Part) IsEmpty() bool { - return p.data.IsEmpty() -} diff --git a/internal/message/part_test.go b/internal/message/part_test.go deleted file mode 100644 index be6befcf91..0000000000 --- a/internal/message/part_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package message - -import ( - "reflect" - "testing" -) - -func TestPartBasic(t *testing.T) { - p := NewPart([]byte(`{"hello":"world"}`)) - p.MetaSetMut("foo", "bar") - p.MetaSetMut("foo2", "bar2") - - if exp, act := `{"hello":"world"}`, string(p.AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := "bar", p.MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := "bar2", p.MetaGetStr("foo2"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - jObj, err := p.AsStructuredMut() - if err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"hello": "world"}, jObj; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %v != %v", act, exp) - } - p.data.rawBytes = nil - if jObj, err = p.AsStructuredMut(); err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"hello": "world"}, jObj; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %v != %v", act, exp) - } - - p.SetBytes([]byte("hello world")) - if exp, act := `hello world`, string(p.AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if _, err = p.AsStructuredMut(); err == nil { - t.Errorf("Expected error from bad JSON") - } - - p.SetStructured(map[string]any{ - "foo": "bar", - }) - if exp, act := `{"foo":"bar"}`, string(p.AsBytes()); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := "bar", p.MetaGetStr("foo"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } - if exp, act := "bar2", p.MetaGetStr("foo2"); exp != act { - t.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func TestPartShallowCopy(t *testing.T) { - p := NewPart([]byte(`{"hello":"world"}`)) - p.MetaSetMut("foo", "bar") - p.MetaSetMut("foo2", "bar2") - - if _, err := p.AsStructuredMut(); err != nil { - t.Fatal(err) - } - - p2 := p.ShallowCopy() - if exp, act := string(p2.data.rawBytes), string(p.data.rawBytes); exp != act { - t.Error("Part slices diverged") - } - if exp, act := p.data.structured, p2.data.structured; !reflect.DeepEqual(exp, act) { - t.Errorf("Unmatched json docs: %v != %v", act, exp) - } - if exp, act := p.data.metadata, p2.data.metadata; !reflect.DeepEqual(exp, act) { - t.Errorf("Unmatched metadata types: %v != %v", act, exp) - } - - p2.MetaSetMut("foo", "new") - if exp, act := "bar", p.MetaGetStr("foo"); exp != act { - t.Errorf("Metadata changed after copy: %v != %v", act, exp) - } -} - -func TestPartJSONMarshal(t *testing.T) { - p := NewPart(nil) - p.SetStructured(map[string]any{ - "foo": "contains tags & 😊 emojis", - }) - if exp, act := `{"foo":"contains tags & 😊 emojis"}`, string(p.AsBytes()); exp != act { - t.Errorf("Wrong marshalled json: %v != %v", act, exp) - } - - p.SetStructured(nil) - if exp, act := `null`, string(p.AsBytes()); exp != act { - t.Errorf("Wrong marshalled json: %v != %v", act, exp) - } - - p.SetStructured("") - if exp, act := `""`, string(p.AsBytes()); exp != act { - t.Errorf("Wrong marshalled json: %v != %v", act, exp) - } -} diff --git a/internal/message/part_with_context_test.go b/internal/message/part_with_context_test.go deleted file mode 100644 index 623a08c8bc..0000000000 --- a/internal/message/part_with_context_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package message - -import ( - "context" - "testing" -) - -func TestPartWithContext(t *testing.T) { - p1 := NewPart([]byte(`foobar`)) - if exp, act := context.Background(), GetContext(p1); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - - type testKey string - - ctx := context.WithValue(context.Background(), testKey("foo"), "bar") - p2 := WithContext(ctx, p1) - - if exp, act := false, p2.IsEmpty(); exp != act { - t.Errorf("Wrong value: %v != %v", act, exp) - } - if exp, act := "foobar", string(p2.AsBytes()); exp != act { - t.Errorf("Wrong value: %v != %v", act, exp) - } - p2.SetBytes([]byte(`barbaz`)) - if exp, act := "barbaz", string(p1.AsBytes()); exp != act { - t.Errorf("Wrong value: %v != %v", act, exp) - } - - if exp, act := ctx, GetContext(p2); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - if exp, act := ctx, GetContext(p2.ShallowCopy()); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - if exp, act := ctx, GetContext(p2.DeepCopy()); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - if exp, act := ctx, GetContext(p2.ShallowCopy().DeepCopy().ShallowCopy()); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - - ctx = context.WithValue(ctx, testKey("bar"), "baz") - p3 := WithContext(ctx, p2) - - if exp, act := "barbaz", string(p3.AsBytes()); exp != act { - t.Errorf("Wrong value: %v != %v", act, exp) - } - p3.SetBytes([]byte(`bazqux`)) - if exp, act := "bazqux", string(p1.AsBytes()); exp != act { - t.Errorf("Wrong value: %v != %v", act, exp) - } - if exp, act := "bazqux", string(p2.AsBytes()); exp != act { - t.Errorf("Wrong value: %v != %v", act, exp) - } - - if exp, act := ctx, GetContext(p3); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - if exp, act := ctx, GetContext(p3.ShallowCopy()); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - if exp, act := ctx, GetContext(p3.DeepCopy()); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } - if exp, act := ctx, GetContext(p3.ShallowCopy().DeepCopy().ShallowCopy()); exp != act { - t.Errorf("Wrong context returned: %v != %v", act, exp) - } -} diff --git a/internal/message/sort_group.go b/internal/message/sort_group.go deleted file mode 100644 index 3dbc040ea9..0000000000 --- a/internal/message/sort_group.go +++ /dev/null @@ -1,109 +0,0 @@ -package message - -import ( - "context" -) - -// SortGroup associates a tag of a part with the original group. -type SortGroup struct { - Len int -} - -// NewSortGroup creates a sort group associated with a slice of parts. -func NewSortGroup(parts Batch) (*SortGroup, Batch) { - g := &SortGroup{Len: len(parts)} - newParts := make([]*Part, len(parts)) - - for i, part := range parts { - tag := &tag{ - Index: i, - Group: g, - } - - ctx := GetContext(part) - - var prev tagChecker - if v, ok := ctx.Value(tagKey).(tagChecker); ok { - prev = v - } - - ctx = context.WithValue(ctx, tagKey, tagValue{ - tag: tag, - previous: prev, - }) - - newParts[i] = WithContext(ctx, part) - } - - return g, newParts -} - -// GetIndex attempts to determine the original index of a message part relative -// to a sort group. -func (g *SortGroup) GetIndex(p *Part) int { - v, ok := p.GetContext().Value(tagKey).(tagChecker) - if !ok { - return -1 - } - return v.IndexForGroup(g) -} - -// TopLevelSortGroup returns the newest sort group to be associated with the -// given message part, or nil if there is none. -func TopLevelSortGroup(p *Part) *SortGroup { - v, ok := p.GetContext().Value(tagKey).(tagChecker) - if !ok { - return nil - } - return v.TopLevelGroup() -} - -//------------------------------------------------------------------------------ - -type tag struct { - Index int - Group groupType -} - -type tagType *tag - -type groupType *SortGroup - -type tagKeyType int - -const tagKey tagKeyType = iota - -type tagChecker interface { - TopLevelGroup() groupType - IndexForGroup(g groupType) int - HasTag(t tagType) bool -} - -type tagValue struct { - tag tagType - previous tagChecker -} - -func (t tagValue) TopLevelGroup() groupType { - return t.tag.Group -} - -func (t tagValue) IndexForGroup(g groupType) int { - if t.tag.Group == g { - return t.tag.Index - } - if t.previous != nil { - return t.previous.IndexForGroup(g) - } - return -1 -} - -func (t tagValue) HasTag(tag tagType) bool { - if t.tag == tag { - return true - } - if t.previous != nil { - return t.previous.HasTag(tag) - } - return false -} diff --git a/internal/message/sort_group_test.go b/internal/message/sort_group_test.go deleted file mode 100644 index e415c9732f..0000000000 --- a/internal/message/sort_group_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package message - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNestedSortGroups(t *testing.T) { - msg := Batch{ - NewPart([]byte("first")), - NewPart([]byte("second")), - } - - group1, msg1 := NewSortGroup(msg) - - assert.Equal(t, -1, group1.GetIndex(msg.Get(0))) - assert.Equal(t, -1, group1.GetIndex(msg.Get(1))) - - assert.Equal(t, 0, group1.GetIndex(msg1.Get(0))) - assert.Equal(t, 1, group1.GetIndex(msg1.Get(1))) - - msg1Reordered := Batch{msg1[1], msg1[0]} - - assert.Equal(t, group1, TopLevelSortGroup(msg1[1])) - - group2, msg2 := NewSortGroup(msg1Reordered) - - assert.Equal(t, -1, group1.GetIndex(msg.Get(0))) - assert.Equal(t, -1, group1.GetIndex(msg.Get(1))) - - assert.Equal(t, 0, group1.GetIndex(msg1.Get(0))) - assert.Equal(t, 1, group1.GetIndex(msg1.Get(1))) - - assert.Equal(t, -1, group2.GetIndex(msg.Get(0))) - assert.Equal(t, -1, group2.GetIndex(msg.Get(1))) - - assert.Equal(t, -1, group2.GetIndex(msg1.Get(0))) - assert.Equal(t, -1, group2.GetIndex(msg1.Get(1))) - - assert.Equal(t, 0, group2.GetIndex(msg2.Get(0))) - assert.Equal(t, 1, group2.GetIndex(msg2.Get(1))) - - assert.Equal(t, 1, group1.GetIndex(msg2.Get(0))) - assert.Equal(t, 0, group1.GetIndex(msg2.Get(1))) - - assert.Equal(t, group1, TopLevelSortGroup(msg1[1])) - assert.Equal(t, group2, TopLevelSortGroup(msg2[1])) -} diff --git a/internal/message/transaction.go b/internal/message/transaction.go deleted file mode 100644 index 86196eeb18..0000000000 --- a/internal/message/transaction.go +++ /dev/null @@ -1,96 +0,0 @@ -package message - -import "context" - -// Transaction is a component that associates a batch of one or more messages -// with a mechanism that is able to propagate an acknowledgement of delivery -// back to the source of the batch. -// -// This allows batches to be routed through complex component networks of -// buffers, processing pipelines and output brokers without losing the -// association. -// -// It would not be sufficient to associate acknowledgement to the message (or -// batch of messages) itself as it would then not be possible to expand and -// split message batches (grouping, etc) without loosening delivery guarantees. -// -// The proper way to do such things would be to create a new transaction for -// each resulting batch, and only when all derivative transactions are -// acknowledged is the source transaction acknowledged in turn. -type Transaction struct { - // Payload is the message payload of this transaction. - Payload Batch - - // responseChan should receive a response at the end of a transaction (once - // the message is no longer owned by the receiver.) The response itself - // indicates whether the message has been propagated successfully. - responseChan chan<- error - - // responseFunc should be called with an error at the end of a transaction - // (once the message is no longer owned by the receiver.) The error - // indicates whether the message has been propagated successfully. - responseFunc func(context.Context, error) error - - // Used for cancelling transactions. When cancelled it is up to the receiver - // of this transaction to abort any attempt to deliver the transaction - // message. - ctx context.Context -} - -//------------------------------------------------------------------------------ - -// NewTransaction creates a new transaction object from a message payload and a -// response channel. -func NewTransaction(payload Batch, resChan chan<- error) Transaction { - return Transaction{ - Payload: payload, - responseChan: resChan, - ctx: context.Background(), - } -} - -// NewTransactionFunc creates a new transaction object that associates a message -// batch payload with a func used to acknowledge delivery of the message batch. -func NewTransactionFunc(payload Batch, fn func(context.Context, error) error) Transaction { - return Transaction{ - Payload: payload, - responseFunc: fn, - ctx: context.Background(), - } -} - -// Context returns a context that indicates the cancellation of a transaction. -// It is optional for receivers of a transaction to honour this context, and is -// worth doing in cases where the transaction is blocked (on reconnect loops, -// etc) as it is often used as a fail-fast mechanism. -// -// When a transaction is aborted due to cancellation it is still required that -// acknowledgment is made, and should be done so with t.Context().Err(). -func (t *Transaction) Context() context.Context { - return t.ctx -} - -// WithContext returns a copy of the transaction associated with a context used -// for cancellation. When cancelled it is up to the receiver of this transaction -// to abort any attempt to deliver the transaction message. -func (t *Transaction) WithContext(ctx context.Context) *Transaction { - newT := *t - newT.ctx = ctx - return &newT -} - -// Ack returns a delivery response back through the transaction to the message -// source. A nil error indicates that delivery has been completed successfully, -// a non-nil error indicates that the message could not be delivered and should -// be retried or nacked upstream. -func (t *Transaction) Ack(ctx context.Context, err error) error { - if t.responseFunc != nil { - return t.responseFunc(ctx, err) - } - select { - case t.responseChan <- err: - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/message/util.go b/internal/message/util.go deleted file mode 100644 index 20c83c21e8..0000000000 --- a/internal/message/util.go +++ /dev/null @@ -1,152 +0,0 @@ -package message - -import ( - "bytes" - "encoding/json" - "errors" - "io" - "os" - "strconv" - "time" -) - -var useNumber = true - -func init() { - if os.Getenv("BENTHOS_USE_NUMBER") == "false" { - useNumber = false - } -} - -//------------------------------------------------------------------------------ - -// GetAllBytes returns a 2D byte slice representing the raw byte content of the -// parts of a message. -func GetAllBytes(m Batch) [][]byte { - if len(m) == 0 { - return nil - } - parts := make([][]byte, len(m)) - _ = m.Iter(func(i int, p *Part) error { - parts[i] = p.AsBytes() - return nil - }) - return parts -} - -//------------------------------------------------------------------------------ - -func decodeJSON(rawBytes []byte) (structured any, err error) { - dec := json.NewDecoder(bytes.NewReader(rawBytes)) - if useNumber { - dec.UseNumber() - } - - if err = dec.Decode(&structured); err != nil { - return - } - - var dummy json.RawMessage - if err = dec.Decode(&dummy); errors.Is(err, io.EOF) { - err = nil - return - } - - structured = nil - if err = dec.Decode(&dummy); err == nil || err == io.EOF { - err = errors.New("message contains multiple valid documents") - } - return -} - -func encodeJSON(d any) (rawBytes []byte) { - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - enc.SetEscapeHTML(false) - if err := enc.Encode(d); err != nil { - return nil - } - if buf.Len() > 1 { - rawBytes = buf.Bytes()[:buf.Len()-1] - } - return -} - -// Copy of value.IToString -func metaToString(i any) string { - switch t := i.(type) { - case string: - return t - case []byte: - return string(t) - case int64: - return strconv.FormatInt(t, 10) - case uint64: - return strconv.FormatUint(t, 10) - case float64: - return strconv.FormatFloat(t, 'g', -1, 64) - case json.Number: - return t.String() - case bool: - if t { - return "true" - } - return "false" - case time.Time: - return t.Format(time.RFC3339Nano) - case nil: - return `null` - } - // Last resort - return string(encodeJSON(i)) -} - -//------------------------------------------------------------------------------ - -func cloneMap(oldMap map[string]any) map[string]any { - newMap := make(map[string]any, len(oldMap)) - for k, v := range oldMap { - newMap[k] = cloneGeneric(v) - } - return newMap -} - -func cloneCheekyMap(oldMap map[any]any) map[any]any { - newMap := make(map[any]any, len(oldMap)) - for k, v := range oldMap { - newMap[k] = cloneGeneric(v) - } - return newMap -} - -func cloneSlice(oldSlice []any) []any { - newSlice := make([]any, len(oldSlice)) - for i, v := range oldSlice { - newSlice[i] = cloneGeneric(v) - } - return newSlice -} - -// cloneGeneric is a utility function that recursively copies a generic -// structure usually resulting from a JSON parse. -func cloneGeneric(root any) any { - switch t := root.(type) { - case map[string]any: - return cloneMap(t) - case map[any]any: - return cloneCheekyMap(t) - case []any: - return cloneSlice(t) - default: - // Oops, this means we have 'dirty' types within the object, we pass - // these through uncloned and hope that the author knows what they're - // doing. - return root - } -} - -// CopyJSON recursively creates a deep copy of a JSON structure extracted from a -// message part. -func CopyJSON(root any) any { - return cloneGeneric(root) -} diff --git a/internal/message/util_test.go b/internal/message/util_test.go deleted file mode 100644 index 9f83825da4..0000000000 --- a/internal/message/util_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package message - -import ( - "encoding/json" - "reflect" - "testing" -) - -//------------------------------------------------------------------------------ - -func TestGetAllBytes(t *testing.T) { - rawBytes := [][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - } - m := QuickBatch(rawBytes) - if exp, act := rawBytes, GetAllBytes(m); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong result: %s != %s", act, exp) - } -} - -func TestCloneGeneric(t *testing.T) { - var original any - var cloned any - - err := json.Unmarshal([]byte(`{ - "root":{ - "first":{ - "value1": 1, - "value2": 1.2, - "value3": false, - "value4": "hello world" - }, - "second": [ - 1, - 1.2, - false, - "hello world" - ] - } - }`), &original) - if err != nil { - t.Fatal(err) - } - - cloned = cloneGeneric(original) - if exp, act := original, cloned; !reflect.DeepEqual(exp, act) { - t.Fatalf("Wrong cloned contents: %v != %v", act, exp) - } - - target := cloned.(map[string]any) - target = target["root"].(map[string]any) - target = target["first"].(map[string]any) - target["value1"] = 2 - - target = original.(map[string]any) - target = target["root"].(map[string]any) - target = target["first"].(map[string]any) - if exp, act := float64(1), target["value1"].(float64); exp != act { - t.Errorf("Original value was mutated: %v != %v", act, exp) - } -} - -func TestCloneGenericYAML(t *testing.T) { - var original any = map[any]any{ - "root": map[any]any{ - "first": map[any]any{ - "value1": 1, - "value2": 1.2, - "value3": false, - "value4": "hello world", - }, - "second": []any{ - 1, 1.2, false, "hello world", - }, - }, - } - - cloned := cloneGeneric(original) - if exp, act := original, cloned; !reflect.DeepEqual(exp, act) { - t.Fatalf("Wrong cloned contents: %v != %v", act, exp) - } - - target := cloned.(map[any]any) - target = target["root"].(map[any]any) - target = target["first"].(map[any]any) - target["value1"] = 2 - - target = original.(map[any]any) - target = target["root"].(map[any]any) - target = target["first"].(map[any]any) - if exp, act := 1, target["value1"].(int); exp != act { - t.Errorf("Original value was mutated: %v != %v", act, exp) - } -} - -//------------------------------------------------------------------------------ - -var benchResult float64 - -func BenchmarkCloneGeneric(b *testing.B) { - var generic, cloned any - err := json.Unmarshal([]byte(`{ - "root":{ - "first":{ - "value1": 1, - "value2": 1.2, - "value3": false, - "value4": "hello world" - }, - "second": [ - 1, - 1.2, - false, - "hello world" - ] - } - }`), &generic) - if err != nil { - b.Fatal(err) - } - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - cloned = cloneGeneric(generic) - } - b.StopTimer() - - target := cloned.(map[string]any) - target = target["root"].(map[string]any) - target = target["first"].(map[string]any) - benchResult = target["value1"].(float64) - if exp, act := float64(1), benchResult; exp != act { - b.Errorf("Wrong result: %v != %v", act, exp) - } -} - -func BenchmarkCloneJSON(b *testing.B) { - var generic, cloned any - err := json.Unmarshal([]byte(`{ - "root":{ - "first":{ - "value1": 1, - "value2": 1.2, - "value3": false, - "value4": "hello world" - }, - "second": [ - 1, - 1.2, - false, - "hello world" - ] - } - }`), &generic) - if err != nil { - b.Fatal(err) - } - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var interBytes []byte - if interBytes, err = json.Marshal(generic); err != nil { - b.Fatal(err) - } - if err = json.Unmarshal(interBytes, &cloned); err != nil { - b.Fatal(err) - } - } - b.StopTimer() - - target := cloned.(map[string]any) - target = target["root"].(map[string]any) - target = target["first"].(map[string]any) - benchResult = target["value1"].(float64) - if exp, act := float64(1), benchResult; exp != act { - b.Errorf("Wrong result: %v != %v", act, exp) - } -} - -//------------------------------------------------------------------------------ diff --git a/internal/metadata/exclude_filter.go b/internal/metadata/exclude_filter.go deleted file mode 100644 index e8070fe5a5..0000000000 --- a/internal/metadata/exclude_filter.go +++ /dev/null @@ -1,76 +0,0 @@ -package metadata - -import ( - "strings" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// ExcludeFilterFields returns a docs spec for the fields within a metadata -// config struct. -func ExcludeFilterFields() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString("exclude_prefixes", "Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages."). - Array().HasDefault([]any{}), - } -} - -// ExcludeFilterConfig describes actions to be performed on message metadata -// before being sent to an output destination. -type ExcludeFilterConfig struct { - ExcludePrefixes []string `json:"exclude_prefixes" yaml:"exclude_prefixes"` -} - -// NewExcludeFilterConfig returns a Metadata configuration struct with default values. -func NewExcludeFilterConfig() ExcludeFilterConfig { - return ExcludeFilterConfig{ - ExcludePrefixes: []string{}, - } -} - -// Filter attempts to construct a metadata filter. -func (m ExcludeFilterConfig) Filter() (*ExcludeFilter, error) { - return &ExcludeFilter{ - excludePrefixes: m.ExcludePrefixes, - }, nil -} - -// ExcludeFilter provides a way to filter metadata keys based on a Metadata -// config. -type ExcludeFilter struct { - excludePrefixes []string -} - -// Match returns false if the provided string matches the configured filters and -// true otherwise. It also returns true if no filters are configured. -func (f *ExcludeFilter) Match(str string) bool { - for _, prefix := range f.excludePrefixes { - if strings.HasPrefix(str, prefix) { - return false - } - } - return true -} - -// Iter applies a function to each metadata key value pair that passes the -// filter. -func (f *ExcludeFilter) Iter(m *message.Part, fn func(k string, v any) error) error { - return m.MetaIterMut(func(k string, v any) error { - if !f.Match(k) { - return nil - } - return fn(k, v) - }) -} - -// IterStr applies a function to each metadata key value pair that passes the -// filter with the value serialised as a string. -func (f *ExcludeFilter) IterStr(m *message.Part, fn func(k, v string) error) error { - return m.MetaIterStr(func(k, v string) error { - if !f.Match(k) { - return nil - } - return fn(k, v) - }) -} diff --git a/internal/metadata/exclude_filter_test.go b/internal/metadata/exclude_filter_test.go deleted file mode 100644 index 42538190d2..0000000000 --- a/internal/metadata/exclude_filter_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package metadata - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestExcludeFilter(t *testing.T) { - tests := []struct { - name string - inputMeta map[string]any - outputMeta map[string]any - conf ExcludeFilterConfig - }{ - { - name: "no filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - conf: NewExcludeFilterConfig(), - }, - { - name: "foo filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "bar": "bar1", - "baz": "baz1", - }, - conf: ExcludeFilterConfig{ - ExcludePrefixes: []string{"f"}, - }, - }, - { - name: "empty filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{}, - conf: ExcludeFilterConfig{ - ExcludePrefixes: []string{""}, - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - part := message.NewPart(nil) - for k, v := range test.inputMeta { - part.MetaSetMut(k, v) - } - filter, err := test.conf.Filter() - require.NoError(t, err) - - outputMeta := map[string]any{} - require.NoError(t, filter.Iter(part, func(k string, v any) error { - outputMeta[k] = v - return nil - })) - - assert.Equal(t, test.outputMeta, outputMeta) - }) - } -} diff --git a/internal/metadata/include_filter.go b/internal/metadata/include_filter.go deleted file mode 100644 index dcf3fd6959..0000000000 --- a/internal/metadata/include_filter.go +++ /dev/null @@ -1,109 +0,0 @@ -package metadata - -import ( - "fmt" - "regexp" - "strings" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// IncludeFilterDocs returns a docs spec for a metadata filter where keys are -// ignored by default and must be explicitly included. -func IncludeFilterDocs() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString( - "include_prefixes", "Provide a list of explicit metadata key prefixes to match against.", - []string{"foo_", "bar_"}, - []string{"kafka_"}, - []string{"content-"}, - ).Array().HasDefault([]any{}), - docs.FieldString( - "include_patterns", "Provide a list of explicit metadata key regular expression (re2) patterns to match against.", - []string{".*"}, - []string{"_timestamp_unix$"}, - ).Array().HasDefault([]any{}), - } -} - -// IncludeFilterConfig contains configuration fields for a metadata filter where -// keys are ignored by default and must be explicitly included. -type IncludeFilterConfig struct { - IncludePrefixes []string `json:"include_prefixes" yaml:"include_prefixes"` - IncludePatterns []string `json:"include_patterns" yaml:"include_patterns"` -} - -// NewIncludeFilterConfig returns an IncludeFilterConfig struct with default -// values. -func NewIncludeFilterConfig() IncludeFilterConfig { - return IncludeFilterConfig{ - IncludePrefixes: []string{}, - IncludePatterns: []string{}, - } -} - -// CreateFilter attempts to construct a filter object. -func (c IncludeFilterConfig) CreateFilter() (*IncludeFilter, error) { - var includePatterns []*regexp.Regexp - for _, pattern := range c.IncludePatterns { - compiledPattern, err := regexp.Compile(pattern) - if err != nil { - return nil, fmt.Errorf("failed to compile regexp %q: %s", pattern, err) - } - includePatterns = append(includePatterns, compiledPattern) - } - return &IncludeFilter{ - includePrefixes: c.IncludePrefixes, - includePatterns: includePatterns, - }, nil -} - -// IncludeFilter provides a way to filter keys based on a Config. -type IncludeFilter struct { - includePrefixes []string - includePatterns []*regexp.Regexp -} - -// IsSet returns true if there are any rules configured for matching keys. -func (f *IncludeFilter) IsSet() bool { - return len(f.includePrefixes) > 0 || len(f.includePatterns) > 0 -} - -// Match returns true if the provided string matches the configured filters and -// false otherwise. It also returns false if no filters are configured. -func (f *IncludeFilter) Match(str string) bool { - for _, prefix := range f.includePrefixes { - if strings.HasPrefix(str, prefix) { - return true - } - } - for _, pattern := range f.includePatterns { - if matched := pattern.MatchString(str); matched { - return true - } - } - return false -} - -// Iter applies a function to each metadata key value pair that passes the -// filter. -func (f *IncludeFilter) Iter(m *message.Part, fn func(k string, v any) error) error { - return m.MetaIterMut(func(k string, v any) error { - if !f.Match(k) { - return nil - } - return fn(k, v) - }) -} - -// IterStr applies a function to each metadata key value pair that passes the -// filter with the value serialised as a string. -func (f *IncludeFilter) IterStr(m *message.Part, fn func(k, v string) error) error { - return m.MetaIterStr(func(k, v string) error { - if !f.Match(k) { - return nil - } - return fn(k, v) - }) -} diff --git a/internal/metadata/include_filter_test.go b/internal/metadata/include_filter_test.go deleted file mode 100644 index a92d3ed7d8..0000000000 --- a/internal/metadata/include_filter_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package metadata - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestMetadataFilter(t *testing.T) { - tests := []struct { - name string - inputMeta map[string]any - outputMeta map[string]any - conf IncludeFilterConfig - }{ - { - name: "no filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{}, - conf: NewIncludeFilterConfig(), - }, - { - name: "foo prefix filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "foo": "foo1", - }, - conf: IncludeFilterConfig{ - IncludePrefixes: []string{"f"}, - }, - }, - { - name: "ar$ pattern filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "bar": "bar1", - }, - conf: IncludeFilterConfig{ - IncludePatterns: []string{"ar$"}, - }, - }, - { - name: "empty prefix filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - conf: IncludeFilterConfig{ - IncludePrefixes: []string{""}, - }, - }, - { - name: "empty pattern filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - conf: IncludeFilterConfig{ - IncludePatterns: []string{""}, - }, - }, - { - name: "foo prefix filter and bar pattern filter", - inputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - "baz": "baz1", - }, - outputMeta: map[string]any{ - "foo": "foo1", - "bar": "bar1", - }, - conf: IncludeFilterConfig{ - IncludePrefixes: []string{"foo"}, - IncludePatterns: []string{"bar"}, - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - part := message.NewPart(nil) - for k, v := range test.inputMeta { - part.MetaSetMut(k, v) - } - - filter, err := test.conf.CreateFilter() - require.NoError(t, err) - - outputMeta := map[string]any{} - require.NoError(t, filter.Iter(part, func(k string, v any) error { - outputMeta[k] = v - return nil - })) - - assert.Equal(t, test.outputMeta, outputMeta) - }) - } -} diff --git a/internal/old/util/throttle/package.go b/internal/old/util/throttle/package.go deleted file mode 100644 index b81c46f65e..0000000000 --- a/internal/old/util/throttle/package.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package throttle implements throttle strategies. -package throttle diff --git a/internal/old/util/throttle/type.go b/internal/old/util/throttle/type.go deleted file mode 100644 index 86092ae436..0000000000 --- a/internal/old/util/throttle/type.go +++ /dev/null @@ -1,142 +0,0 @@ -package throttle - -import ( - "context" - "sync/atomic" - "time" -) - -//------------------------------------------------------------------------------ - -// Type is a throttle of retries to avoid endless busy loops when a message -// fails to reach its destination. -type Type struct { - // consecutiveRetries is the live count of consecutive retries. - consecutiveRetries int64 - - // throttlePeriod is the current throttle period, by default this is set to - // the baseThrottlePeriod. - throttlePeriod int64 - - // unthrottledRetries is the number of concecutive retries we are - // comfortable attempting before throttling begins. - unthrottledRetries int64 - - // maxExponentialPeriod is the maximum duration for which our throttle lasts - // when exponentially increasing. - maxExponentialPeriod int64 - - // baseThrottlePeriod is the static duration for which our throttle lasts. - baseThrottlePeriod int64 - - // closeChan can interrupt a throttle when closed. - closeChan <-chan struct{} -} - -// New creates a new throttle, which permits a static number of consecutive -// retries before throttling subsequent retries. A success will reset the count -// of consecutive retries. -func New(options ...func(*Type)) *Type { - t := &Type{ - unthrottledRetries: 3, - baseThrottlePeriod: int64(time.Second), - maxExponentialPeriod: int64(time.Minute), - closeChan: nil, - } - t.throttlePeriod = t.baseThrottlePeriod - for _, option := range options { - option(t) - } - return t -} - -//------------------------------------------------------------------------------ - -// OptMaxUnthrottledRetries sets the maximum number of consecutive retries that -// will be attempted before throttling will begin. -func OptMaxUnthrottledRetries(n int64) func(*Type) { - return func(t *Type) { - t.unthrottledRetries = n - } -} - -// OptMaxExponentPeriod sets the maximum period of time that throttles will last -// when exponentially increasing. -func OptMaxExponentPeriod(period time.Duration) func(*Type) { - return func(t *Type) { - t.maxExponentialPeriod = int64(period) - } -} - -// OptThrottlePeriod sets the static period of time that throttles will last. -func OptThrottlePeriod(period time.Duration) func(*Type) { - return func(t *Type) { - t.baseThrottlePeriod = int64(period) - t.throttlePeriod = int64(period) - } -} - -// OptCloseChan sets a read-only channel that, if closed, will interrupt a retry -// throttle early. -func OptCloseChan(c <-chan struct{}) func(*Type) { - return func(t *Type) { - t.closeChan = c - } -} - -//------------------------------------------------------------------------------ - -// Retry indicates that a retry is about to occur and, if appropriate, will -// block until either the throttle period is over and the retry may be attempted -// (returning true) or that the close channel has closed (returning false). -func (t *Type) Retry() bool { - return t.RetryWithContext(context.Background()) -} - -// RetryWithContext indicates that a retry is about to occur and, if -// appropriate, will block until either the throttle period is over and the -// retry may be attempted (returning true) or that the close channel has closed -// (returning false), or that the context was cancelled (false). -func (t *Type) RetryWithContext(ctx context.Context) bool { - if rets := atomic.AddInt64(&t.consecutiveRetries, 1); rets <= t.unthrottledRetries { - return true - } - select { - case <-time.After(time.Duration(atomic.LoadInt64(&t.throttlePeriod))): - case <-t.closeChan: - return false - case <-ctx.Done(): - return false - } - return true -} - -// ExponentialRetry is the same as Retry except also sets the throttle period to -// exponentially increase after each consecutive retry. -func (t *Type) ExponentialRetry() bool { - return t.ExponentialRetryWithContext(context.Background()) -} - -// ExponentialRetryWithContext is the same as RetryWithContext except also sets -// the throttle period to exponentially increase after each consecutive retry. -func (t *Type) ExponentialRetryWithContext(ctx context.Context) bool { - if atomic.LoadInt64(&t.consecutiveRetries) > t.unthrottledRetries { - if throtPrd := atomic.LoadInt64(&t.throttlePeriod); throtPrd < t.maxExponentialPeriod { - throtPrd *= 2 - if throtPrd > t.maxExponentialPeriod { - throtPrd = t.maxExponentialPeriod - } - atomic.StoreInt64(&t.throttlePeriod, throtPrd) - } - } - return t.RetryWithContext(ctx) -} - -// Reset clears the count of consecutive retries and resets the exponential -// backoff. -func (t *Type) Reset() { - atomic.StoreInt64(&t.consecutiveRetries, 0) - atomic.StoreInt64(&t.throttlePeriod, t.baseThrottlePeriod) -} - -//------------------------------------------------------------------------------ diff --git a/internal/old/util/throttle/type_test.go b/internal/old/util/throttle/type_test.go deleted file mode 100644 index 96ebcb0cef..0000000000 --- a/internal/old/util/throttle/type_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package throttle - -import ( - "testing" - "time" -) - -func TestBasicThrottle(t *testing.T) { - closeChan := make(chan struct{}) - close(closeChan) - - throt := New( - OptMaxUnthrottledRetries(3), - OptMaxExponentPeriod(time.Second*30), - OptThrottlePeriod(time.Second), - OptCloseChan(closeChan), - ) - - for i := 0; i < 3; i++ { - if !throt.Retry() { - t.Errorf("Throttle blocked early: %v", i) - } - } - - if throt.Retry() { - t.Errorf("Throttle didn't throttle at end") - } -} - -func TestThrottleReset(t *testing.T) { - closeChan := make(chan struct{}) - close(closeChan) - - throt := New( - OptMaxUnthrottledRetries(3), - OptMaxExponentPeriod(time.Second*30), - OptThrottlePeriod(time.Second), - OptCloseChan(closeChan), - ) - - for j := 0; j < 3; j++ { - for i := 0; i < 3; i++ { - if !throt.Retry() { - t.Errorf("Throttle blocked early: %v", i) - } - } - if throt.Retry() { - t.Errorf("Throttle didn't throttle at end") - } - throt.Reset() - } -} - -func TestThrottleLinear(t *testing.T) { - t.Skip("Tests are unpredictable on slow machines") - t.Parallel() - - throt := New( - OptMaxUnthrottledRetries(1), - OptMaxExponentPeriod(time.Millisecond*500), - OptThrottlePeriod(time.Millisecond*100), - ) - - errMargin := 0.05 - - tBefore := time.Now() - throt.Retry() - - exp := time.Duration(0) - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - for i := 0; i < 5; i++ { - tBefore = time.Now() - throt.Retry() - - exp = time.Millisecond * 100 - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - } -} - -func TestThrottleExponent(t *testing.T) { - t.Skip("Tests are unpredictable on slow machines") - t.Parallel() - - base := time.Millisecond * 50 - - throt := New( - OptMaxUnthrottledRetries(1), - OptMaxExponentPeriod(base*8), - OptThrottlePeriod(base), - ) - - errMargin := 0.05 - - tBefore := time.Now() - throt.ExponentialRetry() - - exp := time.Duration(0) - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = base - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = base * 2 - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = base * 4 - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = base * 8 - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = base * 8 - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - throt.Reset() - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = time.Duration(0) - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } - - tBefore = time.Now() - throt.ExponentialRetry() - - exp = base - if act := time.Since(tBefore); (act - exp).Seconds() > errMargin { - t.Errorf("Unexpected retry period: %v != %v", act, exp) - } -} diff --git a/internal/pipeline/config_test.go b/internal/pipeline/config_test.go deleted file mode 100644 index e04764d1db..0000000000 --- a/internal/pipeline/config_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package pipeline_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -func TestConfigParseYAML(t *testing.T) { - tests := []struct { - name string - input string - errContains string - validateFn func(t testing.TB, v pipeline.Config) - }{ - { - name: "basic config", - input: ` -threads: 123 -processors: - - label: a - mapping: 'root = "a"' - - label: b - mapping: 'root = "b"' -`, - validateFn: func(t testing.TB, v pipeline.Config) { - assert.Equal(t, 123, v.Threads) - require.Len(t, v.Processors, 2) - assert.Equal(t, "a", v.Processors[0].Label) - assert.Equal(t, "mapping", v.Processors[0].Type) - assert.Equal(t, "b", v.Processors[1].Label) - assert.Equal(t, "mapping", v.Processors[1].Type) - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - n, err := docs.UnmarshalYAML([]byte(test.input)) - require.NoError(t, err) - - conf, err := pipeline.FromAny(bundle.GlobalEnvironment, n) - if test.errContains == "" { - require.NoError(t, err) - test.validateFn(t, conf) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } - }) - } -} diff --git a/internal/pipeline/constructor.go b/internal/pipeline/constructor.go deleted file mode 100644 index dfe37dba33..0000000000 --- a/internal/pipeline/constructor.go +++ /dev/null @@ -1,126 +0,0 @@ -package pipeline - -import ( - "fmt" - "strconv" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/value" -) - -var threadsField = docs.FieldInt("threads", "The number of threads to execute processing pipelines across.").HasDefault(-1) - -func ConfigSpec() docs.FieldSpec { - return docs.FieldObject( - "pipeline", "Describes optional processing pipelines used for mutating messages.", - ).WithChildren( - threadsField, - docs.FieldProcessor("processors", "A list of processors to apply to messages.").Array().HasDefault([]any{}), - ) -} - -// Config is a configuration struct for creating parallel processing pipelines. -// The number of resuling parallel processing pipelines will match the number of -// threads specified. Processors are executed on each message in the order that -// they are written. -// -// In order to fully utilise each processing thread you must either have a -// number of parallel inputs that matches or surpasses the number of pipeline -// threads, or use a memory buffer. -type Config struct { - Threads int `json:"threads" yaml:"threads"` - Processors []processor.Config `json:"processors" yaml:"processors"` -} - -// NewConfig returns a configuration struct fully populated with default values. -func NewConfig() Config { - return Config{ - Threads: -1, - Processors: []processor.Config{}, - } -} - -//------------------------------------------------------------------------------ - -// New creates an input type based on an input configuration. -func New(conf Config, mgr bundle.NewManagement) (processor.Pipeline, error) { - processors := make([]processor.V1, len(conf.Processors)) - for j, procConf := range conf.Processors { - var err error - pMgr := mgr.IntoPath("processors", strconv.Itoa(j)) - processors[j], err = pMgr.NewProcessor(procConf) - if err != nil { - return nil, err - } - } - if conf.Threads == 1 { - return NewProcessor(processors...), nil - } - return NewPool(conf.Threads, mgr.Logger(), processors...) -} - -func FromAny(prov docs.Provider, value any) (conf Config, err error) { - switch t := value.(type) { - case Config: - return t, nil - case *yaml.Node: - return fromYAML(prov, t) - case map[string]any: - return fromMap(prov, t) - } - err = fmt.Errorf("unexpected value, expected object, got %T", value) - return -} - -func fromMap(prov docs.Provider, val map[string]any) (conf Config, err error) { - conf = NewConfig() - - if threadsV, exists := val["threads"]; exists { - var threads64 int64 - if threads64, err = value.IGetInt(threadsV); err != nil { - return - } - conf.Threads = int(threads64) - } - - if procVs, ok := val["processors"].([]any); ok { - for _, iv := range procVs { - var tmpProc processor.Config - if tmpProc, err = processor.FromAny(prov, iv); err != nil { - return - } - conf.Processors = append(conf.Processors, tmpProc) - } - } - return -} - -func fromYAML(prov docs.Provider, val *yaml.Node) (conf Config, err error) { - conf = NewConfig() - for i := 0; i < len(val.Content)-1; i += 2 { - switch val.Content[i].Value { - case "threads": - if err = val.Content[i+1].Decode(&conf.Threads); err != nil { - return - } - case "processors": - node := val.Content[i+1] - if node.Kind != yaml.SequenceNode { - err = fmt.Errorf("line %v: expected array value, got %v", node.Line, node.Kind) - return - } - for _, pNode := range node.Content { - var tmpProc processor.Config - if tmpProc, err = processor.FromAny(prov, pNode); err != nil { - return - } - conf.Processors = append(conf.Processors, tmpProc) - } - } - } - return -} diff --git a/internal/pipeline/package.go b/internal/pipeline/package.go deleted file mode 100644 index b428f36211..0000000000 --- a/internal/pipeline/package.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package pipeline contains structures that implement both the Producer and -// Consumer interfaces. They can be used as extra pipeline components for -// various utilities. -package pipeline diff --git a/internal/pipeline/pool.go b/internal/pipeline/pool.go deleted file mode 100644 index 2ad1791439..0000000000 --- a/internal/pipeline/pool.go +++ /dev/null @@ -1,167 +0,0 @@ -package pipeline - -import ( - "context" - "runtime" - "sync" - "sync/atomic" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Pool is a pool of pipelines. Each pipeline reads from a shared transaction -// channel. Inputs remain coupled to their outputs as they propagate the -// response channel in the transaction. -type Pool struct { - workers []processor.Pipeline - - log log.Modular - - messagesIn <-chan message.Transaction - messagesOut chan message.Transaction - - shutSig *shutdown.Signaller -} - -// NewPool creates a new processing pool. -func NewPool(threads int, log log.Modular, msgProcessors ...processor.V1) (*Pool, error) { - if threads <= 0 { - threads = runtime.NumCPU() - } - - p := &Pool{ - workers: make([]processor.Pipeline, threads), - log: log, - messagesOut: make(chan message.Transaction), - shutSig: shutdown.NewSignaller(), - } - - for i := range p.workers { - p.workers[i] = NewProcessor(msgProcessors...) - } - - return p, nil -} - -//------------------------------------------------------------------------------ - -// loop is the processing loop of this pipeline. -func (p *Pool) loop() { - // Note this is currently kept open as we only have our children as a - // shutdown mechanism. This puts trust in individual processor pipelines, if - // that's not realistic we can consider adding a close now to the - // TriggerCloseNow method. - closeNowCtx, cnDone := p.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - defer func() { - for _, c := range p.workers { - if err := c.WaitForClose(closeNowCtx); err != nil { - break - } - } - - close(p.messagesOut) - p.shutSig.TriggerHasStopped() - }() - - internalMessages := make(chan message.Transaction) - remainingWorkers := int64(len(p.workers)) - - var closeInternalOnce sync.Once - - for _, worker := range p.workers { - if err := worker.Consume(p.messagesIn); err != nil { - p.log.Error("Failed to start pipeline worker: %v\n", err) - atomic.AddInt64(&remainingWorkers, -1) - continue - } - go func(w processor.Pipeline) { - defer func() { - if v := atomic.AddInt64(&remainingWorkers, -1); v <= 0 { - closeInternalOnce.Do(func() { - close(internalMessages) - }) - } - }() - for { - var t message.Transaction - var open bool - select { - case t, open = <-w.TransactionChan(): - if !open { - return - } - case <-p.shutSig.HardStopChan(): - return - } - select { - case internalMessages <- t: - case <-p.shutSig.HardStopChan(): - return - } - } - }(worker) - } - - for atomic.LoadInt64(&remainingWorkers) > 0 { - select { - case t, open := <-internalMessages: - if !open { - return - } - select { - case p.messagesOut <- t: - case <-p.shutSig.HardStopChan(): - return - } - case <-p.shutSig.HardStopChan(): - return - } - } -} - -//------------------------------------------------------------------------------ - -// Consume assigns a messages channel for the pipeline to read. -func (p *Pool) Consume(msgs <-chan message.Transaction) error { - if p.messagesIn != nil { - return component.ErrAlreadyStarted - } - p.messagesIn = msgs - go p.loop() - return nil -} - -// TransactionChan returns the channel used for consuming messages from this -// pipeline. -func (p *Pool) TransactionChan() <-chan message.Transaction { - return p.messagesOut -} - -// TriggerCloseNow signals that the component should close immediately, -// messages in flight will be dropped. -func (p *Pool) TriggerCloseNow() { - for _, w := range p.workers { - w.TriggerCloseNow() - } - p.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the component has closed down or the context is -// cancelled. Closing occurs either when the input transaction channel is -// closed and messages are flushed (and acked), or when CloseNowAsync is -// called. -func (p *Pool) WaitForClose(ctx context.Context) error { - select { - case <-p.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/pipeline/pool_test.go b/internal/pipeline/pool_test.go deleted file mode 100644 index c650ed5f3f..0000000000 --- a/internal/pipeline/pool_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package pipeline_test - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/pipeline" - - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestPoolBasic(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockProc := &mockMsgProcessor{dropChan: make(chan bool)} - go func() { - mockProc.dropChan <- true - }() - - proc, err := pipeline.NewPool(1, log.Noop(), mockProc) - require.NoError(t, err) - - tChan, resChan := make(chan message.Transaction), make(chan error) - - require.NoError(t, proc.Consume(tChan)) - assert.Error(t, proc.Consume(tChan)) - - msg := message.QuickBatch([][]byte{ - []byte(`one`), - []byte(`two`), - }) - - // First message should be dropped and return immediately - select { - case tChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Fatal("Timed out") - } - select { - case _, open := <-proc.TransactionChan(): - if !open { - t.Fatal("Closed early") - } else { - t.Fatal("Message was not dropped") - } - case res, open := <-resChan: - if !open { - t.Fatal("Closed early") - } - if res != errMockProc { - t.Error(res) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - // Do not drop next message - go func() { - mockProc.dropChan <- false - }() - - // Send message - select { - case tChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - var procT message.Transaction - var open bool - - // Receive new message - select { - case procT, open = <-proc.TransactionChan(): - if !open { - t.Error("Closed early") - } - if exp, act := [][]byte{[]byte("foo"), []byte("bar")}, message.GetAllBytes(procT.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message received: %s != %s", act, exp) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - // Respond without error - go func() { - require.NoError(t, procT.Ack(ctx, nil)) - }() - - // Receive response - select { - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } - if res != nil { - t.Error(res) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - proc.TriggerCloseNow() - require.NoError(t, proc.WaitForClose(ctx)) -} - -func TestPoolMultiMsgs(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockProc := &mockSplitProcessor{} - - proc, err := pipeline.NewPool(1, log.Noop(), mockProc) - require.NoError(t, err) - - tChan, resChan := make(chan message.Transaction), make(chan error) - if err := proc.Consume(tChan); err != nil { - t.Fatal(err) - } - - for j := 0; j < 10; j++ { - expMsgs := map[string]struct{}{ - "foo test": {}, - "bar test": {}, - "baz test": {}, - } - - // Send message - select { - case tChan <- message.NewTransaction(message.Batch{ - message.NewPart([]byte(`foo`)), - message.NewPart([]byte(`bar`)), - message.NewPart([]byte(`baz`)), - }, resChan): - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - for i := 0; i < 3; i++ { - // Receive messages - var procT message.Transaction - var open bool - select { - case procT, open = <-proc.TransactionChan(): - if !open { - t.Error("Closed early") - } - act := string(procT.Payload.Get(0).AsBytes()) - if _, exists := expMsgs[act]; !exists { - t.Errorf("Unexpected result: %v", act) - } else { - delete(expMsgs, act) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - // Respond with no error - require.NoError(t, procT.Ack(ctx, nil)) - } - - // Receive response - select { - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } else if res != nil { - t.Error(res) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - if len(expMsgs) != 0 { - t.Errorf("Expected messages were not received: %v", expMsgs) - } - } - - proc.TriggerCloseNow() - require.NoError(t, proc.WaitForClose(ctx)) -} - -func TestPoolMultiThreads(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - conf := pipeline.NewConfig() - conf.Threads = 2 - conf.Processors = append(conf.Processors, processor.NewConfig()) - - proc, err := pipeline.New(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - tChan, resChan := make(chan message.Transaction), make(chan error) - if err := proc.Consume(tChan); err != nil { - t.Fatal(err) - } - - msg := message.QuickBatch([][]byte{ - []byte(`one`), - []byte(`two`), - }) - - for j := 0; j < conf.Threads; j++ { - // Send message - select { - case tChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - } - for j := 0; j < conf.Threads; j++ { - // Receive messages - var procT message.Transaction - var open bool - select { - case procT, open = <-proc.TransactionChan(): - if !open { - t.Error("Closed early") - } - if exp, act := [][]byte{[]byte("one"), []byte("two")}, message.GetAllBytes(procT.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message received: %s != %s", act, exp) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - - go func(tran message.Transaction) { - // Respond with no error - require.NoError(t, tran.Ack(ctx, nil)) - }(procT) - } - for j := 0; j < conf.Threads; j++ { - // Receive response - select { - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } else if res != nil { - t.Error(res) - } - case <-time.After(time.Second * 5): - t.Fatal("Timed out") - } - } - - proc.TriggerCloseNow() - require.NoError(t, proc.WaitForClose(ctx)) -} - -func TestPoolMultiNaturalClose(t *testing.T) { - conf := pipeline.NewConfig() - conf.Threads = 2 - conf.Processors = append(conf.Processors, processor.NewConfig()) - - proc, err := pipeline.New(conf, mock.NewManager()) - if err != nil { - t.Fatal(err) - } - - tChan := make(chan message.Transaction) - if err := proc.Consume(tChan); err != nil { - t.Fatal(err) - } - - close(tChan) - require.NoError(t, proc.WaitForClose(context.Background())) -} diff --git a/internal/pipeline/processor.go b/internal/pipeline/processor.go deleted file mode 100644 index 4303d1d109..0000000000 --- a/internal/pipeline/processor.go +++ /dev/null @@ -1,177 +0,0 @@ -package pipeline - -import ( - "context" - "sync" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Processor is a pipeline that supports both Consumer and Producer interfaces. -// The processor will read from a source, perform some processing, and then -// either propagate a new message or drop it. -type Processor struct { - msgProcessors []processor.V1 - - messagesOut chan message.Transaction - responsesIn chan error - - messagesIn <-chan message.Transaction - - shutSig *shutdown.Signaller -} - -// NewProcessor returns a new message processing pipeline. -func NewProcessor(msgProcessors ...processor.V1) *Processor { - return &Processor{ - msgProcessors: msgProcessors, - messagesOut: make(chan message.Transaction), - responsesIn: make(chan error), - shutSig: shutdown.NewSignaller(), - } -} - -//------------------------------------------------------------------------------ - -// loop is the processing loop of this pipeline. -func (p *Processor) loop() { - closeNowCtx, cnDone := p.shutSig.HardStopCtx(context.Background()) - defer cnDone() - - defer func() { - // Signal all children to close. - for _, c := range p.msgProcessors { - if err := c.Close(closeNowCtx); err != nil { - break - } - } - - close(p.messagesOut) - p.shutSig.TriggerHasStopped() - }() - - var open bool - for !p.shutSig.IsSoftStopSignalled() { - var tran message.Transaction - select { - case tran, open = <-p.messagesIn: - if !open { - return - } - case <-p.shutSig.HardStopChan(): - return - } - - sorter, sortBatch := message.NewSortGroup(tran.Payload) - - resultBatches, err := processor.ExecuteAll(closeNowCtx, p.msgProcessors, sortBatch) - if len(resultBatches) == 0 || err != nil { - if _ = tran.Ack(closeNowCtx, err); closeNowCtx.Err() != nil { - return - } - continue - } - - if len(resultBatches) == 1 { - select { - case p.messagesOut <- message.NewTransactionFunc(resultBatches[0], tran.Ack): - case <-p.shutSig.HardStopChan(): - return - } - continue - } - - var ( - errMut sync.Mutex - batchErr *batch.Error - generalErr error - batchWG sync.WaitGroup - ) - - for _, b := range resultBatches { - var wgOnce sync.Once - batchWG.Add(1) - tmpBatch := b.ShallowCopy() - - select { - case p.messagesOut <- message.NewTransactionFunc(tmpBatch, func(ctx context.Context, err error) error { - if err != nil { - errMut.Lock() - defer errMut.Unlock() - - if batchErr == nil { - batchErr = batch.NewError(sortBatch, err) - } - for _, m := range tmpBatch { - if bIndex := sorter.GetIndex(m); bIndex >= 0 { - batchErr.Failed(bIndex, err) - } else { - // We are unable to link this message with an origin - // and therefore we must provide a general - // batch-wide error instead. - generalErr = err - } - } - } - - wgOnce.Do(func() { - batchWG.Done() - }) - return nil - }): - case <-p.shutSig.HardStopChan(): - return - } - } - - batchWG.Wait() - - if generalErr != nil { - _ = tran.Ack(closeNowCtx, generalErr) - } else if batchErr != nil { - _ = tran.Ack(closeNowCtx, batchErr) - } else { - _ = tran.Ack(closeNowCtx, nil) - } - } -} - -//------------------------------------------------------------------------------ - -// Consume assigns a messages channel for the pipeline to read. -func (p *Processor) Consume(msgs <-chan message.Transaction) error { - if p.messagesIn != nil { - return component.ErrAlreadyStarted - } - p.messagesIn = msgs - go p.loop() - return nil -} - -// TransactionChan returns the channel used for consuming messages from this -// pipeline. -func (p *Processor) TransactionChan() <-chan message.Transaction { - return p.messagesOut -} - -// TriggerCloseNow signals that the processor pipeline should close immediately. -func (p *Processor) TriggerCloseNow() { - p.shutSig.TriggerHardStop() -} - -// WaitForClose blocks until the component has closed down or the context is -// cancelled. Closing occurs either when the input transaction channel is closed -// and messages are flushed (and acked), or when CloseNowAsync is called. -func (p *Processor) WaitForClose(ctx context.Context) error { - select { - case <-p.shutSig.HasStoppedChan(): - case <-ctx.Done(): - return ctx.Err() - } - return nil -} diff --git a/internal/pipeline/processor_test.go b/internal/pipeline/processor_test.go deleted file mode 100644 index 66def7d6b9..0000000000 --- a/internal/pipeline/processor_test.go +++ /dev/null @@ -1,462 +0,0 @@ -package pipeline_test - -import ( - "context" - "errors" - "fmt" - "reflect" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -var errMockProc = errors.New("this is an error from mock processor") - -type mockMsgProcessor struct { - dropChan chan bool - hasClosedAsync bool - hasWaitedForClose bool - mut sync.Mutex -} - -func (m *mockMsgProcessor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - if drop := <-m.dropChan; drop { - return nil, errMockProc - } - newMsg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - }) - msgs := [1]message.Batch{newMsg} - return msgs[:], nil -} - -func (m *mockMsgProcessor) Close(ctx context.Context) error { - m.mut.Lock() - m.hasClosedAsync = true - m.hasWaitedForClose = true - m.mut.Unlock() - return nil -} - -func TestProcessorPipeline(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockProc := &mockMsgProcessor{dropChan: make(chan bool)} - - // Drop first message - go func() { - mockProc.dropChan <- true - }() - - proc := pipeline.NewProcessor(mockProc) - - tChan, resChan := make(chan message.Transaction), make(chan error) - - if err := proc.Consume(tChan); err != nil { - t.Error(err) - } - if err := proc.Consume(tChan); err == nil { - t.Error("Expected error from dupe listening") - } - - msg := message.QuickBatch([][]byte{ - []byte(`one`), - []byte(`two`), - }) - - // First message should be dropped and return immediately - select { - case tChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case _, open := <-proc.TransactionChan(): - if !open { - t.Error("Closed early") - } else { - t.Error("Message was not dropped") - } - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } - if res != errMockProc { - t.Error(res) - } - case <-time.After(time.Second): - t.Error("Timed out") - } - - // Do not drop next message - go func() { - mockProc.dropChan <- false - }() - - // Send message - select { - case tChan <- message.NewTransaction(msg, resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - - var procT message.Transaction - var open bool - select { - case procT, open = <-proc.TransactionChan(): - if !open { - t.Error("Closed early") - } - if exp, act := [][]byte{[]byte("foo"), []byte("bar")}, message.GetAllBytes(procT.Payload); !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong message received: %s != %s", act, exp) - } - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } - if res != nil { - t.Error(res) - } else { - t.Error("Message was dropped") - } - case <-time.After(time.Second): - t.Error("Timed out") - } - - // Respond without error - go func() { - require.NoError(t, procT.Ack(ctx, nil)) - }() - - // Receive response - select { - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } else if res != nil { - t.Error(res) - } - case <-time.After(time.Second): - t.Error("Timed out") - } - - proc.TriggerCloseNow() - if err := proc.WaitForClose(ctx); err != nil { - t.Error(err) - } - if !mockProc.hasClosedAsync { - t.Error("Expected mockproc to have closed asynchronously") - } - if !mockProc.hasWaitedForClose { - t.Error("Expected mockproc to have waited for close") - } -} - -type mockSplitProcessor struct { - hasClosedAsync bool - hasWaitedForClose bool - mut sync.Mutex -} - -func (m *mockSplitProcessor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - var msgs []message.Batch - for _, p := range msg { - tmpMsg := p.ShallowCopy() - tmpMsg.SetBytes(fmt.Appendf(nil, "%s test", p.AsBytes())) - msgs = append(msgs, message.Batch{tmpMsg}) - } - return msgs, nil -} - -func (m *mockSplitProcessor) Close(ctx context.Context) error { - m.mut.Lock() - m.hasClosedAsync = true - m.hasWaitedForClose = true - m.mut.Unlock() - return nil -} - -func TestProcessorMultiMsgs(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockProc := &mockSplitProcessor{} - proc := pipeline.NewProcessor(mockProc) - - tChan, resChan := make(chan message.Transaction), make(chan error) - require.NoError(t, proc.Consume(tChan)) - - // Send message - select { - case tChan <- message.NewTransaction(message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - }, resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - - expMsgs := map[string]struct{}{ - "foo test": {}, - "bar test": {}, - "baz test": {}, - } - - resFns := []func(context.Context, error) error{} - - // Receive N messages - for i := 0; i < 3; i++ { - select { - case procT, open := <-proc.TransactionChan(): - require.True(t, open) - - act := string(procT.Payload.Get(0).AsBytes()) - if _, exists := expMsgs[act]; !exists { - t.Errorf("Unexpected result: %v", act) - } else { - delete(expMsgs, act) - } - resFns = append(resFns, procT.Ack) - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - if len(expMsgs) != 0 { - t.Errorf("Expected messages were not received: %v", expMsgs) - } - - // Respond without error N times - for i := 0; i < 3; i++ { - require.NoError(t, resFns[i](ctx, nil)) - } - - // Receive error - select { - case res, open := <-resChan: - if !open { - t.Error("Closed early") - } else if res != nil { - t.Error(res) - } - case <-time.After(time.Second): - t.Error("Timed out") - } - - proc.TriggerCloseNow() - require.NoError(t, proc.WaitForClose(ctx)) - if !mockProc.hasClosedAsync { - t.Error("Expected mockproc to have closed asynchronously") - } - if !mockProc.hasWaitedForClose { - t.Error("Expected mockproc to have waited for close") - } -} - -func TestProcessorMultiMsgsBatchError(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockProc := &mockSplitProcessor{} - proc := pipeline.NewProcessor(mockProc) - - tChan, resChan := make(chan message.Transaction), make(chan error) - - require.NoError(t, proc.Consume(tChan)) - - sortGroup, inputBatch := message.NewSortGroup(message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - }) - - // Send message - select { - case tChan <- message.NewTransaction(inputBatch, resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - - expMsgs := map[string]struct{}{ - "foo test": {}, - "bar test": {}, - "baz test": {}, - } - - resFns := []func(context.Context, error) error{} - - // Receive expected messages - for i := 0; i < 3; i++ { - select { - case procT, open := <-proc.TransactionChan(): - require.True(t, open) - - act := string(procT.Payload.Get(0).AsBytes()) - if _, exists := expMsgs[act]; !exists { - t.Errorf("Unexpected result: %v", act) - } else { - delete(expMsgs, act) - } - resFns = append(resFns, procT.Ack) - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - assert.Empty(t, expMsgs) - require.Len(t, resFns, 3) - - require.NoError(t, resFns[0](ctx, nil)) - require.NoError(t, resFns[1](ctx, errors.New("oh no"))) - require.NoError(t, resFns[2](ctx, nil)) - - // Receive overall ack - select { - case err, open := <-resChan: - require.True(t, open) - require.EqualError(t, err, "oh no") - - var batchErr *batch.Error - require.ErrorAs(t, err, &batchErr) - - indexErrs := map[int]string{} - batchErr.WalkPartsBySource(sortGroup, inputBatch, func(i int, p *message.Part, err error) bool { - if err != nil { - indexErrs[i] = err.Error() - } - return true - }) - assert.Equal(t, map[int]string{ - 1: "oh no", - }, indexErrs) - case <-time.After(time.Second): - t.Error("Timed out") - } - - proc.TriggerCloseNow() - require.NoError(t, proc.WaitForClose(ctx)) - if !mockProc.hasClosedAsync { - t.Error("Expected mockproc to have closed asynchronously") - } - if !mockProc.hasWaitedForClose { - t.Error("Expected mockproc to have waited for close") - } -} - -type mockPhantomProcessor struct { - hasClosedAsync bool - hasWaitedForClose bool - mut sync.Mutex -} - -func (m *mockPhantomProcessor) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { - var msgs []message.Batch - for _, p := range msg { - tmpMsg := p.ShallowCopy() - tmpMsg.SetBytes(fmt.Appendf(nil, "%s test", p.AsBytes())) - msgs = append(msgs, message.Batch{tmpMsg}) - } - msgs = append(msgs, message.Batch{ - message.NewPart([]byte("phantom message")), - }) - return msgs, nil -} - -func (m *mockPhantomProcessor) Close(ctx context.Context) error { - m.mut.Lock() - m.hasClosedAsync = true - m.hasWaitedForClose = true - m.mut.Unlock() - return nil -} - -func TestProcessorMultiMsgsBatchUnknownError(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - mockProc := &mockPhantomProcessor{} - proc := pipeline.NewProcessor(mockProc) - - tChan, resChan := make(chan message.Transaction), make(chan error) - - require.NoError(t, proc.Consume(tChan)) - - _, inputBatch := message.NewSortGroup(message.Batch{ - message.NewPart([]byte("foo")), - message.NewPart([]byte("bar")), - message.NewPart([]byte("baz")), - }) - - // Send message - select { - case tChan <- message.NewTransaction(inputBatch, resChan): - case <-time.After(time.Second): - t.Error("Timed out") - } - - expMsgs := map[string]struct{}{ - "foo test": {}, - "bar test": {}, - "baz test": {}, - "phantom message": {}, - } - - resFns := []func(context.Context, error) error{} - - // Receive expected messages - for i := 0; i < 4; i++ { - select { - case procT, open := <-proc.TransactionChan(): - require.True(t, open) - - act := string(procT.Payload.Get(0).AsBytes()) - if _, exists := expMsgs[act]; !exists { - t.Errorf("Unexpected result: %v", act) - } else { - delete(expMsgs, act) - } - resFns = append(resFns, procT.Ack) - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - assert.Empty(t, expMsgs) - require.Len(t, resFns, 4) - - require.NoError(t, resFns[0](ctx, nil)) - require.NoError(t, resFns[1](ctx, nil)) - require.NoError(t, resFns[2](ctx, nil)) - require.NoError(t, resFns[3](ctx, errors.New("oh no"))) - - // Receive overall ack - select { - case err, open := <-resChan: - require.True(t, open) - require.EqualError(t, err, "oh no") - - var batchErr *batch.Error - require.False(t, errors.As(err, &batchErr)) - case <-time.After(time.Second): - t.Error("Timed out") - } - - proc.TriggerCloseNow() - require.NoError(t, proc.WaitForClose(ctx)) - if !mockProc.hasClosedAsync { - t.Error("Expected mockproc to have closed asynchronously") - } - if !mockProc.hasWaitedForClose { - t.Error("Expected mockproc to have waited for close") - } -} diff --git a/internal/serverless/handler.go b/internal/serverless/handler.go deleted file mode 100644 index 5d4f3969f3..0000000000 --- a/internal/serverless/handler.go +++ /dev/null @@ -1,198 +0,0 @@ -package serverless - -import ( - "context" - "errors" - "fmt" - "os" - "time" - - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - ioutput "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/pipeline" - "github.com/benthosdev/benthos/v4/internal/transaction" -) - -// ServerlessResponseType is an output type that redirects pipeline outputs back -// to the handler. -const ServerlessResponseType = "sync_response" - -// Handler contains a live Benthos pipeline and wraps it within an invoke -// handler. -type Handler struct { - transactionChan chan message.Transaction - done func(exitTimeout time.Duration) error -} - -// Close shuts down the underlying pipeline. If the shut down takes longer than -// the specified timeout it is aborted and an error is returned. -func (h *Handler) Close(tout time.Duration) error { - return h.done(tout) -} - -// Handle is a request/response func that injects a payload into the underlying -// Benthos pipeline and returns a result. -func (h *Handler) Handle(ctx context.Context, obj any) (any, error) { - part := message.NewPart(nil) - part.SetStructuredMut(obj) - msg := message.Batch{part} - - store := transaction.NewResultStore() - transaction.AddResultStore(msg, store) - - resChan := make(chan error, 1) - - select { - case h.transactionChan <- message.NewTransaction(msg, resChan): - case <-ctx.Done(): - return nil, errors.New("request cancelled") - } - - select { - case res := <-resChan: - if res != nil { - return nil, res - } - case <-ctx.Done(): - return nil, errors.New("request cancelled") - } - - resultBatches := store.Get() - if len(resultBatches) == 0 { - return map[string]any{"message": "request successful"}, nil - } - - lambdaResults := make([][]any, len(resultBatches)) - for i, batch := range resultBatches { - batchResults := make([]any, batch.Len()) - if err := batch.Iter(func(j int, p *message.Part) error { - var merr error - if batchResults[j], merr = p.AsStructured(); merr != nil { - return fmt.Errorf("failed to marshal json response: %v", merr) - } - return nil - }); err != nil { - return nil, fmt.Errorf("failed to process result batch '%v': %v", i, err) - } - lambdaResults[i] = batchResults - } - - if len(lambdaResults) == 1 { - if len(lambdaResults[0]) == 1 { - return lambdaResults[0][0], nil - } - return lambdaResults[0], nil - } - - genBatchOfBatches := make([]any, len(lambdaResults)) - for i, b := range lambdaResults { - genBatchOfBatches[i] = b - } - return genBatchOfBatches, nil -} - -// NewHandler returns a Handler by creating a Benthos pipeline. -func NewHandler(conf config.Type) (*Handler, error) { - // Logging and stats aggregation. - logger, err := log.New(os.Stdout, ifs.OS(), conf.Logger) - if err != nil { - return nil, fmt.Errorf("failed to create logger: %v", err) - } - - // We use a temporary manager with just the logger initialised for metrics - // instantiation. Doing this means that metrics plugins will use a global - // environment for child plugins and bloblang mappings, which we might want - // to revise in future. - tmpMgr := mock.NewManager() - tmpMgr.L = logger - - // Create our metrics type. - var stats *metrics.Namespaced - if stats, err = bundle.AllMetrics.Init(conf.Metrics, tmpMgr); err != nil { - logger.Error("Failed to connect metrics aggregator: %v\n", err) - stats = metrics.NewNamespaced(metrics.Noop()) - } - - // Create our tracer type. - trac, err := bundle.AllTracers.Init(conf.Tracer, tmpMgr) - if err != nil { - logger.Error("Failed to initialise tracer: %v\n", err) - trac = noop.NewTracerProvider() - } - - // Create resource manager. - manager, err := manager.New(conf.ResourceConfig, manager.OptSetLogger(logger), manager.OptSetMetrics(stats), manager.OptSetTracer(trac)) - if err != nil { - return nil, fmt.Errorf("failed to create resource: %v", err) - } - - // Create pipeline and output layers. - var pipelineLayer processor.Pipeline - var outputLayer ioutput.Streamed - - transactionChan := make(chan message.Transaction, 1) - - pMgr := manager.IntoPath("pipeline") - if pipelineLayer, err = pipeline.New(conf.Pipeline, pMgr); err != nil { - return nil, fmt.Errorf("failed to create resource pipeline: %w", err) - } - - oMgr := manager.IntoPath("output") - if outputLayer, err = oMgr.NewOutput(conf.Output); err != nil { - return nil, fmt.Errorf("failed to create resource output: %w", err) - } - - if err = pipelineLayer.Consume(transactionChan); err != nil { - return nil, fmt.Errorf("failed to create resource: %v", err) - } - - if err = outputLayer.Consume(pipelineLayer.TransactionChan()); err != nil { - return nil, fmt.Errorf("failed to create resource: %v", err) - } - - return &Handler{ - transactionChan: transactionChan, - done: func(exitTimeout time.Duration) error { - close(transactionChan) - - ctx, done := context.WithTimeout(context.Background(), exitTimeout) - defer done() - - outputLayer.TriggerCloseNow() - if err = outputLayer.WaitForClose(ctx); err != nil { - return fmt.Errorf("failed to cleanly close output layer: %v", err) - } - if err = pipelineLayer.WaitForClose(ctx); err != nil { - return fmt.Errorf("failed to cleanly close pipeline layer: %v", err) - } - - manager.TriggerStopConsuming() - if err = manager.WaitForClose(ctx); err != nil { - return fmt.Errorf("failed to cleanly close resources: %v", err) - } - - defer func() { - if shutter, ok := trac.(interface { - Shutdown(context.Context) error - }); ok { - _ = shutter.Shutdown(context.Background()) - } - }() - - if sCloseErr := stats.Close(); sCloseErr != nil { - logger.Error("Failed to cleanly close metrics aggregator: %v\n", sCloseErr) - } - return nil - }, - }, nil -} diff --git a/internal/serverless/handler_test.go b/internal/serverless/handler_test.go deleted file mode 100644 index 9f1850f395..0000000000 --- a/internal/serverless/handler_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package serverless - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "reflect" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestHandlerAsync(t *testing.T) { - var results [][]byte - var resMut sync.Mutex - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - resMut.Lock() - defer resMut.Unlock() - - resBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - results = append(results, resBytes) - - _, _ = w.Write([]byte("success")) - })) - defer ts.Close() - - conf, err := testutil.ConfigFromYAML(fmt.Sprintf(` -output: - http_client: - url: %v -`, ts.URL)) - require.NoError(t, err) - - h, err := NewHandler(conf) - if err != nil { - t.Fatal(err) - } - - var res any - if res, err = h.Handle(context.Background(), map[string]any{"foo": "bar"}); err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"message": "request successful"}, res; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %v != %v", exp, act) - } - if exp, act := [][]byte{[]byte(`{"foo":"bar"}`)}, results; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %s != %s", exp, act) - } - - if err = h.Close(time.Second * 10); err != nil { - t.Error(err) - } -} - -func TestHandlerSync(t *testing.T) { - conf, err := testutil.ConfigFromYAML(` -output: - sync_response: {} -`) - require.NoError(t, err) - - h, err := NewHandler(conf) - if err != nil { - t.Fatal(err) - } - - var res any - if res, err = h.Handle(context.Background(), map[string]any{"foo": "bar"}); err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"foo": "bar"}, res; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %v != %v", exp, act) - } - - if err = h.Close(time.Second * 10); err != nil { - t.Error(err) - } -} - -func TestHandlerSyncBatch(t *testing.T) { - conf, err := testutil.ConfigFromYAML(` -pipeline: - processors: - - select_parts: - parts: [ 0, 0, 0 ] -output: - sync_response: {} -`) - require.NoError(t, err) - - h, err := NewHandler(conf) - if err != nil { - t.Fatal(err) - } - - var res any - if res, err = h.Handle(context.Background(), map[string]any{"foo": "bar"}); err != nil { - t.Fatal(err) - } - if exp, act := []any{ - map[string]any{"foo": "bar"}, - map[string]any{"foo": "bar"}, - map[string]any{"foo": "bar"}, - }, res; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %v != %v", exp, act) - } - - if err = h.Close(time.Second * 10); err != nil { - t.Error(err) - } -} - -func TestHandlerSyncBatches(t *testing.T) { - conf, err := testutil.ConfigFromYAML(` -pipeline: - processors: - - select_parts: - parts: [ 0, 0, 0 ] - - split: {} -output: - sync_response: {} -`) - require.NoError(t, err) - - h, err := NewHandler(conf) - if err != nil { - t.Fatal(err) - } - - var res any - if res, err = h.Handle(context.Background(), map[string]any{"foo": "bar"}); err != nil { - t.Fatal(err) - } - if exp, act := []any{ - []any{map[string]any{"foo": "bar"}}, - []any{map[string]any{"foo": "bar"}}, - []any{map[string]any{"foo": "bar"}}, - }, res; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %v != %v", exp, act) - } - - if err = h.Close(time.Second * 10); err != nil { - t.Error(err) - } -} - -func TestHandlerCombined(t *testing.T) { - var results [][]byte - var resMut sync.Mutex - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - resMut.Lock() - defer resMut.Unlock() - - resBytes, err := io.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - results = append(results, resBytes) - - _, _ = w.Write([]byte("success")) - })) - defer ts.Close() - - conf, err := testutil.ConfigFromYAML(fmt.Sprintf(` -output: - broker: - outputs: - - sync_response: {} - - http_client: - url: %v -`, ts.URL)) - require.NoError(t, err) - - h, err := NewHandler(conf) - if err != nil { - t.Fatal(err) - } - - var res any - if res, err = h.Handle(context.Background(), map[string]any{"foo": "bar"}); err != nil { - t.Fatal(err) - } - if exp, act := map[string]any{"foo": "bar"}, res; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %v != %v", exp, act) - } - if exp, act := [][]byte{[]byte(`{"foo":"bar"}`)}, results; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong sync response: %s != %s", exp, act) - } - - if err = h.Close(time.Second * 10); err != nil { - t.Error(err) - } -} diff --git a/internal/serverless/lambda/config.go b/internal/serverless/lambda/config.go deleted file mode 100644 index e1a0869aa5..0000000000 --- a/internal/serverless/lambda/config.go +++ /dev/null @@ -1,46 +0,0 @@ -package lambda - -import ( - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func DefaultConfigAndSpec() (conf config.Type, spec docs.FieldSpecs, err error) { - spec = config.Spec() - - spec.SetDefault(map[string]any{ - "none": map[string]any{}, - }, "metrics") - - spec.SetDefault("json", "logger", "format") - - spec.SetDefault(map[string]any{ - "switch": map[string]any{ - "retry_until_success": false, - "cases": []any{ - map[string]any{ - "check": "errored()", - "output": map[string]any{ - "reject": "processing failed due to: ${! error() }", - }, - }, - map[string]any{ - "output": map[string]any{ - "sync_response": map[string]any{}, - }, - }, - }, - }, - }, "output") - - var pConf *docs.ParsedConfig - if pConf, err = spec.ParsedConfigFromAny(map[string]any{}); err != nil { - return - } - - if conf, err = config.FromParsed(bundle.GlobalEnvironment, pConf, nil); err != nil { - return - } - return -} diff --git a/internal/serverless/lambda/config_test.go b/internal/serverless/lambda/config_test.go deleted file mode 100644 index 149b704cdb..0000000000 --- a/internal/serverless/lambda/config_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package lambda_test - -import ( - "testing" - - "github.com/benthosdev/benthos/v4/internal/serverless/lambda" - _ "github.com/benthosdev/benthos/v4/public/components/all" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetConfig(t *testing.T) { - conf, _, err := lambda.DefaultConfigAndSpec() - require.NoError(t, err) - - assert.Equal(t, "none", conf.Metrics.Type) - assert.Equal(t, "json", conf.Logger.Format) -} diff --git a/internal/serverless/lambda/lambda.go b/internal/serverless/lambda/lambda.go deleted file mode 100644 index 6d431919a3..0000000000 --- a/internal/serverless/lambda/lambda.go +++ /dev/null @@ -1,96 +0,0 @@ -package lambda - -import ( - "errors" - "fmt" - "os" - "time" - - "github.com/aws/aws-lambda-go/lambda" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/serverless" -) - -var handler *serverless.Handler - -// Run executes Benthos as an AWS Lambda function. Configuration can be stored -// within the environment variable BENTHOS_CONFIG. -func Run() { - // A list of default config paths to check for if not explicitly defined - defaultPaths := []string{ - "./benthos.yaml", - "./config.yaml", - "/benthos.yaml", - "/etc/benthos/config.yaml", - "/etc/benthos.yaml", - } - if path := os.Getenv("BENTHOS_CONFIG_PATH"); path != "" { - defaultPaths = append([]string{path}, defaultPaths...) - } - - conf, confSpec, err := DefaultConfigAndSpec() - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file create error: %v\n", err) - os.Exit(1) - } - - if confStr := os.Getenv("BENTHOS_CONFIG"); confStr != "" { - confBytes, err := config.ReplaceEnvVariables([]byte(confStr), os.LookupEnv) - if err != nil { - // TODO: Make this configurable somehow maybe, along with linting - // errors. - var errEnvMissing *config.ErrMissingEnvVars - if errors.As(err, &errEnvMissing) { - confBytes = errEnvMissing.BestAttempt - } else { - fmt.Fprintf(os.Stderr, "Configuration file read error: %v\n", err) - os.Exit(1) - } - } - - confNode, err := docs.UnmarshalYAML(confBytes) - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file parse error: %v\n", err) - os.Exit(1) - } - - pConf, err := confSpec.ParsedConfigFromAny(confNode) - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file parse error: %v\n", err) - os.Exit(1) - } - - conf, err = config.FromParsed(bundle.GlobalEnvironment, pConf, nil) - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file read error: %v\n", err) - os.Exit(1) - } - } else { - // Iterate default config paths - for _, path := range defaultPaths { - if _, err := ifs.OS().Stat(path); err == nil { - conf, _, err = config.ReadYAMLFileLinted(ifs.OS(), confSpec, path, false, docs.NewLintConfig(bundle.GlobalEnvironment)) - if err != nil { - fmt.Fprintf(os.Stderr, "Configuration file read error: %v\n", err) - os.Exit(1) - } - break - } - } - } - - if handler, err = serverless.NewHandler(conf); err != nil { - fmt.Fprintf(os.Stderr, "Initialisation error: %v\n", err) - os.Exit(1) - } - - lambda.Start(handler.Handle) - if err = handler.Close(time.Second * 30); err != nil { - fmt.Fprintf(os.Stderr, "Shut down error: %v\n", err) - os.Exit(1) - } -} diff --git a/internal/serverless/lambda/package.go b/internal/serverless/lambda/package.go deleted file mode 100644 index 9c2479adc3..0000000000 --- a/internal/serverless/lambda/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package lambda contains the execution logic for running Benthos as an AWS -// lambda function. -package lambda diff --git a/internal/serverless/package.go b/internal/serverless/package.go deleted file mode 100644 index bb1350945b..0000000000 --- a/internal/serverless/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package serverless contains shared components for serverless distributions of -// Benthos. -package serverless diff --git a/internal/stream/config.go b/internal/stream/config.go deleted file mode 100644 index bae08f9434..0000000000 --- a/internal/stream/config.go +++ /dev/null @@ -1,64 +0,0 @@ -package stream - -import ( - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -const ( - fieldInput = "input" - fieldBuffer = "buffer" - fieldPipeline = "pipeline" - fieldOutput = "output" -) - -// Config is a configuration struct representing all four layers of a Benthos -// stream. -type Config struct { - Input input.Config `yaml:"input"` - Buffer buffer.Config `yaml:"buffer"` - Pipeline pipeline.Config `yaml:"pipeline"` - Output output.Config `yaml:"output"` - - rawSource any -} - -func (c *Config) GetRawSource() any { - return c.rawSource -} - -func FromParsed(prov docs.Provider, pConf *docs.ParsedConfig, rawSource any) (conf Config, err error) { - conf.rawSource = rawSource - var v any - if v, err = pConf.FieldAny(fieldInput); err != nil { - return - } - if conf.Input, err = input.FromAny(prov, v); err != nil { - return - } - - if v, err = pConf.FieldAny(fieldBuffer); err != nil { - return - } - if conf.Buffer, err = buffer.FromAny(prov, v); err != nil { - return - } - - if v, err = pConf.FieldAny(fieldPipeline); err != nil { - return - } - if conf.Pipeline, err = pipeline.FromAny(prov, v); err != nil { - return - } - - if v, err = pConf.FieldAny(fieldOutput); err != nil { - return - } - if conf.Output, err = output.FromAny(prov, v); err != nil { - return - } - return -} diff --git a/internal/stream/config_test.go b/internal/stream/config_test.go deleted file mode 100644 index 83619877e9..0000000000 --- a/internal/stream/config_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package stream_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -func TestConfigParseYAML(t *testing.T) { - tests := []struct { - name string - input string - errContains string - validateFn func(t testing.TB, v stream.Config) - }{ - { - name: "one of everything", - input: ` -input: - label: a - generate: - count: 1 - mapping: 'root.id = "a"' - interval: 1s - -buffer: - memory: - limit: 456 - -pipeline: - threads: 123 - -output: - label: c - reject: "c rejected" -`, - validateFn: func(t testing.TB, v stream.Config) { - assert.Equal(t, "a", v.Input.Label) - assert.Equal(t, "generate", v.Input.Type) - assert.Equal(t, "memory", v.Buffer.Type) - assert.Equal(t, 123, v.Pipeline.Threads) - assert.Equal(t, "c", v.Output.Label) - assert.Equal(t, "reject", v.Output.Type) - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - conf, err := testutil.StreamFromYAML(test.input) - if test.errContains == "" { - require.NoError(t, err) - test.validateFn(t, conf) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } - }) - } -} diff --git a/internal/stream/docs.go b/internal/stream/docs.go deleted file mode 100644 index 720dd4a7f2..0000000000 --- a/internal/stream/docs.go +++ /dev/null @@ -1,33 +0,0 @@ -package stream - -import ( - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -// Spec returns a docs.FieldSpec for a stream configuration. -func Spec() docs.FieldSpecs { - defaultInput := map[string]any{"inproc": ""} - if _, exists := bundle.GlobalEnvironment.GetDocs("stdin", docs.TypeInput); exists { - defaultInput = map[string]any{ - "stdin": map[string]any{}, - } - } - - defaultOutput := map[string]any{"inproc": ""} - if _, exists := bundle.GlobalEnvironment.GetDocs("stdout", docs.TypeOutput); exists { - defaultOutput = map[string]any{ - "stdout": map[string]any{}, - } - } - - return docs.FieldSpecs{ - docs.FieldInput(fieldInput, "An input to source messages from.").HasDefault(defaultInput), - docs.FieldBuffer(fieldBuffer, "An optional buffer to store messages during transit.").HasDefault(map[string]any{ - "none": map[string]any{}, - }), - pipeline.ConfigSpec(), - docs.FieldOutput(fieldOutput, "An output to sink messages to.").HasDefault(defaultOutput), - } -} diff --git a/internal/stream/manager/api.go b/internal/stream/manager/api.go deleted file mode 100644 index 33a1949432..0000000000 --- a/internal/stream/manager/api.go +++ /dev/null @@ -1,654 +0,0 @@ -package manager - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "strings" - "sync" - - "github.com/Jeffail/gabs/v2" - "github.com/gorilla/mux" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/stream" - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func (m *Type) registerEndpoints(enableCrud bool) { - m.manager.RegisterEndpoint( - "/ready", - "Returns 200 OK if the inputs and outputs of all running streams are connected, otherwise a 503 is returned. If there are no active streams 200 is returned.", - m.HandleStreamReady, - ) - if !enableCrud { - return - } - m.manager.RegisterEndpoint( - "/resources/{type}/{id}", - "POST: Create or replace a given resource configuration of a specified type. Types supported are `cache`, `input`, `output`, `processor` and `rate_limit`.", - m.HandleResourceCRUD, - ) - m.manager.RegisterEndpoint( - "/streams/{id}/stats", - "GET a structured JSON object containing metrics for the stream.", - m.HandleStreamStats, - ) - m.manager.RegisterEndpoint( - "/streams/{id}", - "Perform CRUD operations on streams, supporting POST (Create),"+ - " GET (Read), PUT (Update), PATCH (Patch update)"+ - " and DELETE (Delete).", - m.HandleStreamCRUD, - ) - m.manager.RegisterEndpoint( - "/streams", - "GET: List all streams along with their status and uptimes."+ - " POST: Post an object of stream ids to stream configs, all"+ - " streams will be replaced by this new set.", - m.HandleStreamsCRUD, - ) -} - -type lintErrors struct { - LintErrs []string `json:"lint_errors"` -} - -func (m *Type) lintCtx() docs.LintContext { - lConf := docs.NewLintConfig(m.manager.Environment()) - lConf.BloblangEnv = bloblang.XWrapEnvironment(m.manager.BloblEnvironment()).Deactivated() - return docs.NewLintContext(lConf) -} - -func (m *Type) lintStreamConfigNode(node *yaml.Node) (lints []string) { - for _, dLint := range stream.Spec().LintYAML(m.lintCtx(), node) { - lints = append(lints, dLint.Error()) - } - return -} - -// HandleStreamsCRUD is an http.HandleFunc for returning maps of active benthos -// streams by their id, status and uptime or overwriting the entire set of -// streams. -func (m *Type) HandleStreamsCRUD(w http.ResponseWriter, r *http.Request) { - var serverErr, requestErr error - defer func() { - if r.Body != nil { - r.Body.Close() - } - if serverErr != nil { - m.manager.Logger().Error("Streams CRUD Error: %v\n", serverErr) - http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway) - return - } - if requestErr != nil { - m.manager.Logger().Debug("Streams request CRUD Error: %v\n", requestErr) - http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest) - return - } - }() - - type confInfo struct { - Active bool `json:"active"` - Uptime float64 `json:"uptime"` - UptimeStr string `json:"uptime_str"` - } - infos := map[string]confInfo{} - - m.lock.Lock() - for id, strInfo := range m.streams { - infos[id] = confInfo{ - Active: strInfo.IsRunning(), - Uptime: strInfo.Uptime().Seconds(), - UptimeStr: strInfo.Uptime().String(), - } - } - m.lock.Unlock() - - switch r.Method { - case "GET": - var resBytes []byte - if resBytes, serverErr = json.Marshal(infos); serverErr == nil { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(resBytes) - } - return - case "POST": - default: - requestErr = errors.New("method not supported") - return - } - - var setBytes []byte - if setBytes, requestErr = io.ReadAll(r.Body); requestErr != nil { - return - } - - nodeSet := map[string]yaml.Node{} - if requestErr = yaml.Unmarshal(setBytes, &nodeSet); requestErr != nil { - return - } - - if r.URL.Query().Get("chilled") != "true" { - var lints []string - for k, n := range nodeSet { - for _, l := range m.lintStreamConfigNode(&n) { - keyLint := fmt.Sprintf("stream '%v': %v", k, l) - lints = append(lints, keyLint) - m.manager.Logger().Debug("Streams request linting error: %v\n", keyLint) - } - } - if len(lints) > 0 { - errBytes, _ := json.Marshal(lintErrors{ - LintErrs: lints, - }) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write(errBytes) - return - } - } - - toDelete := []string{} - toUpdate := map[string]stream.Config{} - toCreate := map[string]stream.Config{} - - spec := stream.Spec() - - for id := range infos { - newConf, exists := nodeSet[id] - if !exists { - toDelete = append(toDelete, id) - } else { - var rawSource any - if requestErr = newConf.Decode(&rawSource); requestErr != nil { - return - } - var pConf *docs.ParsedConfig - if pConf, requestErr = spec.ParsedConfigFromAny(&newConf); requestErr != nil { - return - } - if toUpdate[id], requestErr = stream.FromParsed(m.manager.Environment(), pConf, rawSource); requestErr != nil { - return - } - } - } - for id, conf := range nodeSet { - if _, exists := infos[id]; !exists { - var rawSource any - if requestErr = conf.Decode(&rawSource); requestErr != nil { - return - } - var pConf *docs.ParsedConfig - if pConf, requestErr = spec.ParsedConfigFromAny(&conf); requestErr != nil { - return - } - if toCreate[id], requestErr = stream.FromParsed(m.manager.Environment(), pConf, rawSource); requestErr != nil { - return - } - } - } - - wg := sync.WaitGroup{} - wg.Add(len(toDelete)) - wg.Add(len(toUpdate)) - wg.Add(len(toCreate)) - - errDelete := make([]error, len(toDelete)) - errUpdate := make([]error, len(toUpdate)) - errCreate := make([]error, len(toCreate)) - - for i, id := range toDelete { - go func(sid string, j int) { - errDelete[j] = m.Delete(r.Context(), sid) - wg.Done() - }(id, i) - } - i := 0 - for id, conf := range toUpdate { - newConf := conf - go func(sid string, sconf *stream.Config, j int) { - errUpdate[j] = m.Update(r.Context(), sid, *sconf) - wg.Done() - }(id, &newConf, i) - i++ - } - i = 0 - for id, conf := range toCreate { - newConf := conf - go func(sid string, sconf *stream.Config, j int) { - errCreate[j] = m.Create(sid, *sconf) - wg.Done() - }(id, &newConf, i) - i++ - } - - wg.Wait() - - errs := []string{} - for _, err := range errDelete { - if err != nil { - errs = append(errs, fmt.Sprintf("failed to delete stream: %v", err)) - } - } - for _, err := range errUpdate { - if err != nil { - errs = append(errs, fmt.Sprintf("failed to update stream: %v", err)) - } - } - for _, err := range errCreate { - if err != nil { - errs = append(errs, fmt.Sprintf("failed to create stream: %v", err)) - } - } - - if len(errs) > 0 { - requestErr = errors.New(strings.Join(errs, "\n")) - } -} - -// HandleStreamCRUD is an http.HandleFunc for performing CRUD operations on -// individual streams. -func (m *Type) HandleStreamCRUD(w http.ResponseWriter, r *http.Request) { - var serverErr, requestErr error - defer func() { - if r.Body != nil { - r.Body.Close() - } - if serverErr != nil { - m.manager.Logger().Error("Streams CRUD Error: %v\n", serverErr) - http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway) - return - } - if requestErr != nil { - m.manager.Logger().Debug("Streams request CRUD Error: %v\n", requestErr) - http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest) - return - } - }() - - id := mux.Vars(r)["id"] - if id == "" { - http.Error(w, "Var `id` must be set", http.StatusBadRequest) - return - } - - readConfig := func() (confOut stream.Config, lints []string, err error) { - var confBytes []byte - if confBytes, err = io.ReadAll(r.Body); err != nil { - return - } - - ignoreLints := r.URL.Query().Get("chilled") == "true" - - if confBytes, err = config.ReplaceEnvVariables(confBytes, os.LookupEnv); err != nil { - var errEnvMissing *config.ErrMissingEnvVars - if ignoreLints && errors.As(err, &errEnvMissing) { - confBytes = errEnvMissing.BestAttempt - } else { - return - } - } - - var node *yaml.Node - if node, err = docs.UnmarshalYAML(confBytes); err != nil { - return - } - - if !ignoreLints { - lints = m.lintStreamConfigNode(node) - for _, l := range lints { - m.manager.Logger().Info("Stream '%v' config: %v\n", id, l) - } - } - - var rawSource any - _ = node.Decode(&rawSource) - - var pConf *docs.ParsedConfig - if pConf, err = stream.Spec().ParsedConfigFromAny(node); err != nil { - return - } - confOut, err = stream.FromParsed(m.manager.Environment(), pConf, rawSource) - return - } - patchConfig := func(confIn stream.Config) (confOut stream.Config, err error) { - var patchBytes []byte - if patchBytes, err = io.ReadAll(r.Body); err != nil { - return - } - - cRoot := value.IClone(confIn.GetRawSource()) - - var pRoot any - if err = yaml.Unmarshal(patchBytes, &pRoot); err != nil { - return - } - - gObj := gabs.Wrap(cRoot) - if err = gObj.MergeFn(gabs.Wrap(pRoot), func(destination, source any) any { - return source - }); err != nil { - return - } - - var confNode yaml.Node - if err = confNode.Encode(gObj.Data()); err != nil { - return - } - - var pConf *docs.ParsedConfig - if pConf, err = stream.Spec().ParsedConfigFromAny(&confNode); err != nil { - return - } - confOut, err = stream.FromParsed(m.manager.Environment(), pConf, gObj.Data()) - return - } - - var conf stream.Config - var lints []string - switch r.Method { - case "POST": - if conf, lints, requestErr = readConfig(); requestErr != nil { - return - } - if len(lints) > 0 { - errBytes, _ := json.Marshal(lintErrors{ - LintErrs: lints, - }) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write(errBytes) - return - } - serverErr = m.Create(id, conf) - case "GET": - var info *StreamStatus - if info, serverErr = m.Read(id); serverErr == nil { - conf := info.Config() - sanit := conf.GetRawSource() - - var bodyBytes []byte - if bodyBytes, serverErr = json.Marshal(struct { - Active bool `json:"active"` - Uptime float64 `json:"uptime"` - UptimeStr string `json:"uptime_str"` - Config any `json:"config"` - }{ - Active: info.IsRunning(), - Uptime: info.Uptime().Seconds(), - UptimeStr: info.Uptime().String(), - Config: sanit, - }); serverErr != nil { - return - } - - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(bodyBytes) - } - case "PUT": - if conf, lints, requestErr = readConfig(); requestErr != nil { - return - } - if len(lints) > 0 { - errBytes, _ := json.Marshal(lintErrors{ - LintErrs: lints, - }) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write(errBytes) - return - } - serverErr = m.Update(r.Context(), id, conf) - case "DELETE": - serverErr = m.Delete(r.Context(), id) - case "PATCH": - var info *StreamStatus - if info, serverErr = m.Read(id); serverErr == nil { - if conf, requestErr = patchConfig(info.Config()); requestErr != nil { - return - } - serverErr = m.Update(r.Context(), id, conf) - } - default: - requestErr = fmt.Errorf("verb not supported: %v", r.Method) - } - - if serverErr == ErrStreamDoesNotExist { - serverErr = nil - http.Error(w, "Stream not found", http.StatusNotFound) - return - } - if serverErr == ErrStreamExists { - serverErr = nil - http.Error(w, "Stream already exists", http.StatusBadRequest) - return - } -} - -// HandleResourceCRUD is an http.HandleFunc for performing CRUD operations on -// resource components. -func (m *Type) HandleResourceCRUD(w http.ResponseWriter, r *http.Request) { - var serverErr, requestErr error - defer func() { - if r.Body != nil { - r.Body.Close() - } - if serverErr != nil { - m.manager.Logger().Error("Resource CRUD Error: %v\n", serverErr) - http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway) - return - } - if requestErr != nil { - m.manager.Logger().Debug("Resource request CRUD Error: %v\n", requestErr) - http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest) - return - } - }() - - if r.Method != "POST" { - requestErr = fmt.Errorf("verb not supported: %v", r.Method) - return - } - - id := mux.Vars(r)["id"] - if id == "" { - http.Error(w, "Var `id` must be set", http.StatusBadRequest) - return - } - - ctx := r.Context() - - var storeFn func(*yaml.Node) - - docType := docs.Type(mux.Vars(r)["type"]) - switch docType { - case docs.TypeCache: - storeFn = func(n *yaml.Node) { - var cacheConf cache.Config - if cacheConf, requestErr = cache.FromAny(m.manager.Environment(), n); requestErr != nil { - return - } - serverErr = m.manager.StoreCache(ctx, id, cacheConf) - } - case docs.TypeInput: - storeFn = func(n *yaml.Node) { - var inputConf input.Config - if inputConf, requestErr = input.FromAny(m.manager.Environment(), n); requestErr != nil { - return - } - serverErr = m.manager.StoreInput(ctx, id, inputConf) - } - case docs.TypeOutput: - storeFn = func(n *yaml.Node) { - var outputConf output.Config - if outputConf, requestErr = output.FromAny(m.manager.Environment(), n); requestErr != nil { - return - } - serverErr = m.manager.StoreOutput(ctx, id, outputConf) - } - case docs.TypeProcessor: - storeFn = func(n *yaml.Node) { - var procConf processor.Config - if procConf, requestErr = processor.FromAny(m.manager.Environment(), n); requestErr != nil { - return - } - serverErr = m.manager.StoreProcessor(ctx, id, procConf) - } - case docs.TypeRateLimit: - storeFn = func(n *yaml.Node) { - var rlConf ratelimit.Config - if rlConf, requestErr = ratelimit.FromAny(m.manager.Environment(), n); requestErr != nil { - return - } - serverErr = m.manager.StoreRateLimit(ctx, id, rlConf) - } - default: - http.Error(w, "Var `type` must be set to one of `cache`, `input`, `output`, `processor` or `rate_limit`", http.StatusBadRequest) - return - } - - var confNode *yaml.Node - var lints []string - { - var confBytes []byte - if confBytes, requestErr = io.ReadAll(r.Body); requestErr != nil { - return - } - - ignoreLints := r.URL.Query().Get("chilled") == "true" - - if confBytes, requestErr = config.ReplaceEnvVariables(confBytes, os.LookupEnv); requestErr != nil { - var errEnvMissing *config.ErrMissingEnvVars - if ignoreLints && errors.As(requestErr, &errEnvMissing) { - confBytes = errEnvMissing.BestAttempt - requestErr = nil - } else { - return - } - } - - var node yaml.Node - if requestErr = yaml.Unmarshal(confBytes, &node); requestErr != nil { - return - } - confNode = &node - - if !ignoreLints { - for _, l := range docs.LintYAML(m.lintCtx(), docType, &node) { - lints = append(lints, l.Error()) - m.manager.Logger().Info("Resource '%v' config: %v\n", id, l) - } - } - } - if len(lints) > 0 { - errBytes, _ := json.Marshal(lintErrors{ - LintErrs: lints, - }) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write(errBytes) - return - } - - storeFn(confNode) -} - -// HandleStreamStats is an http.HandleFunc for obtaining metrics for a stream. -func (m *Type) HandleStreamStats(w http.ResponseWriter, r *http.Request) { - var serverErr, requestErr error - defer func() { - if r.Body != nil { - r.Body.Close() - } - if serverErr != nil { - m.manager.Logger().Error("Stream stats Error: %v\n", serverErr) - http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway) - return - } - if requestErr != nil { - m.manager.Logger().Debug("Stream request stats Error: %v\n", requestErr) - http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest) - return - } - }() - - id := mux.Vars(r)["id"] - if id == "" { - http.Error(w, "Var `id` must be set", http.StatusBadRequest) - return - } - - switch r.Method { - case "GET": - var info *StreamStatus - if info, serverErr = m.Read(id); serverErr == nil { - values := map[string]any{} - for k, v := range info.metrics.GetCounters() { - values[k] = v - } - for k, v := range info.metrics.GetTimings() { - ps := v.Percentiles([]float64{0.5, 0.9, 0.99}) - values[k] = struct { - P50 float64 `json:"p50"` - P90 float64 `json:"p90"` - P99 float64 `json:"p99"` - }{ - P50: ps[0], - P90: ps[1], - P99: ps[2], - } - } - values["uptime_ns"] = info.Uptime().Nanoseconds() - - jBytes, err := json.Marshal(values) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(jBytes) - } - default: - requestErr = fmt.Errorf("verb not supported: %v", r.Method) - } - if serverErr == ErrStreamDoesNotExist { - serverErr = nil - http.Error(w, "Stream not found", http.StatusNotFound) - return - } -} - -// HandleStreamReady is an http.HandleFunc for providing a ready check across -// all streams. -func (m *Type) HandleStreamReady(w http.ResponseWriter, r *http.Request) { - var notReady []string - - m.lock.Lock() - for k, v := range m.streams { - if !v.IsReady() && v.IsRunning() { - notReady = append(notReady, k) - } - } - m.lock.Unlock() - - if len(notReady) == 0 { - _, _ = w.Write([]byte("OK")) - return - } - - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "streams %v are not connected\n", strings.Join(notReady, ", ")) -} diff --git a/internal/stream/manager/api_test.go b/internal/stream/manager/api_test.go deleted file mode 100644 index f6423d615e..0000000000 --- a/internal/stream/manager/api_test.go +++ /dev/null @@ -1,1013 +0,0 @@ -package manager_test - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - "time" - - "github.com/Jeffail/gabs/v2" - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - yaml "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - bmanager "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/stream/manager" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func router(m *manager.Type) *mux.Router { - router := mux.NewRouter() - router.HandleFunc("/ready", m.HandleStreamReady) - router.HandleFunc("/streams", m.HandleStreamsCRUD) - router.HandleFunc("/streams/{id}", m.HandleStreamCRUD) - router.HandleFunc("/streams/{id}/stats", m.HandleStreamStats) - router.HandleFunc("/resources/{type}/{id}", m.HandleResourceCRUD) - return router -} - -func genRequest(verb, url string, payload any) *http.Request { - var body io.Reader - - if payload != nil { - if s, ok := payload.(string); ok { - body = strings.NewReader(s) - } else { - bodyBytes, err := json.Marshal(payload) - if err != nil { - panic(err) - } - body = bytes.NewReader(bodyBytes) - } - } - - req, err := http.NewRequest(verb, url, body) - if err != nil { - panic(err) - } - - return req -} - -func genYAMLRequest(verb, url string, payload any) *http.Request { - var body io.Reader - - if payload != nil { - if s, ok := payload.(string); ok { - body = bytes.NewReader([]byte(s)) - } else { - bodyBytes, err := yaml.Marshal(payload) - if err != nil { - panic(err) - } - body = bytes.NewReader(bodyBytes) - } - } - - req, err := http.NewRequest(verb, url, body) - if err != nil { - panic(err) - } - - return req -} - -type listItemBody struct { - Active bool `json:"active"` - Uptime float64 `json:"uptime"` - UptimeStr string `json:"uptime_str"` -} - -type listBody map[string]listItemBody - -func parseListBody(data *bytes.Buffer) listBody { - result := listBody{} - if err := json.Unmarshal(data.Bytes(), &result); err != nil { - panic(err) - } - return result -} - -type getBody struct { - Active bool `json:"active"` - Uptime float64 `json:"uptime"` - UptimeStr string `json:"uptime_str"` - Config any `json:"config"` -} - -func parseGetBody(t *testing.T, data *bytes.Buffer) getBody { - t.Helper() - result := getBody{} - if err := yaml.Unmarshal(data.Bytes(), &result); err != nil { - t.Fatal(err) - } - return result -} - -type endpointReg struct { - endpoints map[string]http.HandlerFunc -} - -func (f *endpointReg) RegisterEndpoint(path, desc string, h http.HandlerFunc) { - f.endpoints[path] = h -} - -func TestTypeAPIDisabled(t *testing.T) { - r := &endpointReg{endpoints: map[string]http.HandlerFunc{}} - rMgr, err := bmanager.New(bmanager.NewResourceConfig(), bmanager.OptSetAPIReg(r)) - require.NoError(t, err) - - _ = manager.New(rMgr, - manager.OptAPIEnabled(true), - ) - assert.Greater(t, len(r.endpoints), 1) - - r = &endpointReg{endpoints: map[string]http.HandlerFunc{}} - rMgr, err = bmanager.New(bmanager.NewResourceConfig(), bmanager.OptSetAPIReg(r)) - require.NoError(t, err) - - _ = manager.New(rMgr, - manager.OptAPIEnabled(false), - ) - assert.Len(t, r.endpoints, 1) - assert.Contains(t, r.endpoints, "/ready") -} - -func TestTypeAPIBadMethods(t *testing.T) { - mgr := manager.New(mock.NewManager()) - - r := router(mgr) - - request := genRequest("DELETE", "/streams", nil) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusBadRequest, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } - - request = genRequest("DERP", "/streams/foo", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusBadRequest, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } -} - -func harmlessConf() any { - return map[string]any{ - "input": map[string]any{ - "generate": map[string]any{ - "mapping": "root = deleted()", - }, - }, - "output": map[string]any{ - "drop": map[string]any{}, - }, - } -} - -func TestTypeAPIBasicOperations(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - conf := harmlessConf() - - request := genRequest("PUT", "/streams/foo", conf) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - require.Equal(t, http.StatusNotFound, response.Code, response.Body.String()) - - request = genRequest("GET", "/streams/foo", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) - - request = genRequest("POST", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - request = genRequest("POST", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusBadRequest, response.Code) - - assert.Eventually(t, func() bool { - request = genRequest("GET", "/ready", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - return response.Code == http.StatusOK - }, time.Second*10, time.Millisecond*50) - - request = genRequest("GET", "/streams/bar", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) - - request = genRequest("GET", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - info := parseGetBody(t, response.Body) - assert.True(t, info.Active) - - assert.Equal(t, "root = deleted()", gabs.Wrap(info.Config).S("input", "generate", "mapping").Data()) - - newConf := harmlessConf() - _, _ = gabs.Wrap(newConf).Set("memory", "buffer", "type") - - request = genRequest("PUT", "/streams/foo", newConf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - request = genRequest("GET", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - info = parseGetBody(t, response.Body) - assert.True(t, info.Active) - - assert.Equal(t, "memory", gabs.Wrap(info.Config).S("buffer", "type").Data()) - - request = genRequest("DELETE", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - request = genRequest("DELETE", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code, response.Body.String()) - - testVar := "__TEST_INPUT_MAPPING" - - t.Setenv(testVar, `root.meow = 5`) - - request = genRequest("POST", "/streams/fooEnv?chilled=true", map[string]any{ - "input": map[string]any{ - "generate": map[string]any{ - "mapping": "${__TEST_INPUT_MAPPING}", - }, - }, - "output": map[string]any{ - "type": "drop", - }, - }) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - request = genRequest("GET", "/streams/fooEnv", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - info = parseGetBody(t, response.Body) - - assert.True(t, info.Active) - assert.Equal(t, `root.meow = 5`, gabs.Wrap(info.Config).S("input", "generate", "mapping").Data()) - - request = genRequest("DELETE", "/streams/fooEnv", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) -} - -func TestTypeAPIPatch(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - conf := harmlessConf() - - request := genRequest("PATCH", "/streams/foo", conf) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusNotFound, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } - - request = genRequest("POST", "/streams/foo?chilled=true", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - require.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - patchConf := map[string]any{ - "input": map[string]any{ - "generate": map[string]any{ - "interval": "2s", - }, - }, - } - request = genRequest("PATCH", "/streams/foo", patchConf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v: %v", act, exp, response.Body.String()) - } - - request = genRequest("GET", "/streams/foo", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v: %v", act, exp, response.Body.String()) - } - info := parseGetBody(t, response.Body) - if !info.Active { - t.Fatal("Stream not active") - } - - assert.Equal(t, "2s", gabs.Wrap(info.Config).S("input", "generate", "interval").Data()) -} - -func TestTypeAPIBasicOperationsYAML(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - conf := harmlessConf() - - request := genYAMLRequest("PUT", "/streams/foo?chilled=true", conf) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) - - request = genYAMLRequest("GET", "/streams/foo", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) - - request = genYAMLRequest("POST", "/streams/foo?chilled=true", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - request = genYAMLRequest("POST", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusBadRequest, response.Code) - - request = genYAMLRequest("GET", "/streams/bar", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) - - request = genYAMLRequest("GET", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - info := parseGetBody(t, response.Body) - require.True(t, info.Active) - assert.Equal(t, "root = deleted()", gabs.Wrap(info.Config).S("input", "generate", "mapping").Data()) - - newConf := harmlessConf() - _, _ = gabs.Wrap(newConf).Set("memory", "buffer", "type") - - request = genYAMLRequest("PUT", "/streams/foo?chilled=true", newConf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - request = genYAMLRequest("GET", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - info = parseGetBody(t, response.Body) - require.True(t, info.Active) - assert.Equal(t, "memory", gabs.Wrap(info.Config).S("buffer", "type").Data()) - - request = genYAMLRequest("DELETE", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - request = genYAMLRequest("DELETE", "/streams/foo", conf) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) -} - -func TestTypeAPIList(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - request := genRequest("GET", "/streams", nil) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } - info := parseListBody(response.Body) - if exp, act := (listBody{}), info; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong list response: %v != %v", act, exp) - } - - conf, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = deleted()' -output: - drop: {} -`) - require.NoError(t, err) - - if err := mgr.Create("foo", conf); err != nil { - t.Fatal(err) - } - - request = genRequest("GET", "/streams", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - if exp, act := http.StatusOK, response.Code; exp != act { - t.Errorf("Unexpected result: %v != %v", act, exp) - } - info = parseListBody(response.Body) - if exp, act := true, info["foo"].Active; !reflect.DeepEqual(exp, act) { - t.Errorf("Wrong list response: %v != %v", act, exp) - } -} - -func TestTypeAPISetStreams(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - origConf, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = deleted()' -output: - drop: {} -`) - require.NoError(t, err) - - require.NoError(t, mgr.Create("foo", origConf)) - require.NoError(t, mgr.Create("bar", origConf)) - - request := genRequest("GET", "/streams", nil) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - info := parseListBody(response.Body) - assert.True(t, info["foo"].Active) - assert.True(t, info["bar"].Active) - - barConf := harmlessConf() - _, _ = gabs.Wrap(barConf).Set("root = this.BAR_ONE", "input", "generate", "mapping") - bar2Conf := harmlessConf() - _, _ = gabs.Wrap(bar2Conf).Set("root = this.BAR_TWO", "input", "generate", "mapping") - bazConf := harmlessConf() - _, _ = gabs.Wrap(bazConf).Set("root = this.BAZ_ONE", "input", "generate", "mapping") - - streamsBody := map[string]any{} - streamsBody["bar"] = barConf - streamsBody["bar2"] = bar2Conf - streamsBody["baz"] = bazConf - - request = genRequest("POST", "/streams", streamsBody) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - request = genRequest("GET", "/streams", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - info = parseListBody(response.Body) - assert.NotContains(t, info, "foo") - assert.Contains(t, info, "bar") - assert.Contains(t, info, "baz") - - request = genRequest("GET", "/streams/bar", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - conf := parseGetBody(t, response.Body) - assert.Equal(t, "root = this.BAR_ONE", gabs.Wrap(conf.Config).S("input", "generate", "mapping").Data()) - - request = genRequest("GET", "/streams/bar2", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - conf = parseGetBody(t, response.Body) - assert.Equal(t, "root = this.BAR_TWO", gabs.Wrap(conf.Config).S("input", "generate", "mapping").Data()) - - request = genRequest("GET", "/streams/baz", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - - conf = parseGetBody(t, response.Body) - assert.Equal(t, "root = this.BAZ_ONE", gabs.Wrap(conf.Config).S("input", "generate", "mapping").Data()) -} - -func testConfToAny(t testing.TB, conf any) any { - var node yaml.Node - err := node.Encode(conf) - require.NoError(t, err) - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - err = config.Spec().SanitiseYAML(&node, sanitConf) - require.NoError(t, err) - - var v any - require.NoError(t, node.Decode(&v)) - return v -} - -func TestTypeAPIStreamsDefaultConf(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - body := []byte(`{ - "foo": { - "input": { - "generate": { - "mapping": "root = deleted()" - } - }, - "output": { - "drop": {} - } - } -}`) - - request, err := http.NewRequest("POST", "/streams", bytes.NewReader(body)) - require.NoError(t, err) - - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - status, err := mgr.Read("foo") - require.NoError(t, err) - - v := testConfToAny(t, status.Config()) - - assert.Nil(t, gabs.Wrap(v).S("input", "generate", "interval").Data()) -} - -func TestTypeAPIStreamsLinting(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - body := []byte(`{ - "foo": { - "input": { - "generate": { - "mapping": "root = deleted()" - } - }, - "output": { - "type":"drop", - "inproc": "meow" - } - }, - "bar": { - "input": { - "generate": { - "mapping": "root = deleted()" - }, - "type": "inproc" - }, - "output": { - "drop": {} - } - } -}`) - - request, err := http.NewRequest("POST", "/streams", bytes.NewReader(body)) - require.NoError(t, err) - - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusBadRequest, response.Code) - assert.Equal(t, "application/json", response.Result().Header.Get("Content-Type")) - - expLints := []string{ - "stream 'foo': (10,1) field inproc is invalid when the component type is drop (output)", - "stream 'bar': (15,1) field generate is invalid when the component type is inproc (input)", - } - var actLints struct { - LintErrors []string `json:"lint_errors"` - } - require.NoError(t, json.Unmarshal(response.Body.Bytes(), &actLints)) - assert.ElementsMatch(t, expLints, actLints.LintErrors) - - request, err = http.NewRequest("POST", "/streams?chilled=true", bytes.NewReader(body)) - require.NoError(t, err) - - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) -} - -func TestTypeAPIDefaultConf(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - body := []byte(`{ - "input": { - "generate": { - "mapping": "root = deleted()" - } - }, - "output": { - "drop": {} - } -}`) - - request, err := http.NewRequest("POST", "/streams/foo", bytes.NewReader(body)) - require.NoError(t, err) - - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - status, err := mgr.Read("foo") - require.NoError(t, err) - - v := testConfToAny(t, status.Config()) - assert.Nil(t, gabs.Wrap(v).S("input", "generate", "interval").Data()) -} - -func TestTypeAPILinting(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - body := []byte(`{ - "input": { - "generate": { - "mapping": "root = deleted()" - } - }, - "output": { - "type":"drop", - "inproc": "meow" - }, - "cache_resources": [ - {"label":"not_interested","memory":{}} - ] -}`) - - request, err := http.NewRequest("POST", "/streams/foo", bytes.NewReader(body)) - require.NoError(t, err) - - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusBadRequest, response.Code) - assert.Equal(t, "application/json", response.Result().Header.Get("Content-Type")) - - expLints := `{"lint_errors":["(9,1) field inproc is invalid when the component type is drop (output)","(11,1) field cache_resources not recognised"]}` - assert.Equal(t, expLints, response.Body.String()) - - request, err = http.NewRequest("POST", "/streams/foo?chilled=true", bytes.NewReader(body)) - require.NoError(t, err) - - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) -} - -func TestResourceAPILinting(t *testing.T) { - tests := []struct { - name string - ctype string - config string - lints []string - }{ - { - name: "cache bad", - ctype: "cache", - config: `memory: - default_ttl: 123s - nope: nah - compaction_interval: 1s`, - lints: []string{ - "(3,1) field nope not recognised", - }, - }, - { - name: "input bad", - ctype: "input", - config: `generate: - mapping: root = deleted() - nope: nah`, - lints: []string{ - "(3,1) field nope not recognised", - }, - }, - { - name: "output bad", - ctype: "output", - config: `retry: - output: - drop: {} - nope: nah`, - lints: []string{ - "(4,1) field nope not recognised", - }, - }, - { - name: "processor bad", - ctype: "processor", - config: `split: - size: 10 - nope: nah`, - lints: []string{ - "(3,1) field nope not recognised", - }, - }, - { - name: "rate limit bad", - ctype: "rate_limit", - config: `local: - count: 10 - nope: nah`, - lints: []string{ - "(3,1) field nope not recognised", - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - bmgr, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(bmgr) - - r := router(mgr) - - url := fmt.Sprintf("/resources/%v/foo", test.ctype) - body := []byte(test.config) - - request, err := http.NewRequest("POST", url, bytes.NewReader(body)) - require.NoError(t, err) - - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusBadRequest, response.Code) - assert.Equal(t, "application/json", response.Result().Header.Get("Content-Type")) - - expLints, err := json.Marshal(struct { - LintErrors []string `json:"lint_errors"` - }{ - LintErrors: test.lints, - }) - require.NoError(t, err) - - assert.Equal(t, string(expLints), response.Body.String()) - - request, err = http.NewRequest("POST", url+"?chilled=true", bytes.NewReader(body)) - require.NoError(t, err) - - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code, response.Body.String()) - }) - } -} - -func TestTypeAPIGetStats(t *testing.T) { - mgr, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - smgr := manager.New(mgr) - - r := router(smgr) - - origConf, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = deleted()' -output: - drop: {} -`) - require.NoError(t, err) - - err = smgr.Create("foo", origConf) - require.NoError(t, err) - - <-time.After(time.Millisecond * 100) - - request := genRequest("GET", "/streams/not_exist/stats", nil) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusNotFound, response.Code) - - request = genRequest("POST", "/streams/foo/stats", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusBadRequest, response.Code) - - request = genRequest("GET", "/streams/foo/stats", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - stats, err := gabs.ParseJSON(response.Body.Bytes()) - require.NoError(t, err) - - assert.NotEmpty(t, stats.ChildrenMap(), response.Body.String()) -} - -func TestTypeAPISetResources(t *testing.T) { - bmgr, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - tChan := make(chan message.Transaction) - bmgr.SetPipe("feed_in", tChan) - - mgr := manager.New(bmgr) - - tmpDir := t.TempDir() - - dir1 := filepath.Join(tmpDir, "dir1") - require.NoError(t, os.MkdirAll(dir1, 0o750)) - - dir2 := filepath.Join(tmpDir, "dir2") - require.NoError(t, os.MkdirAll(dir2, 0o750)) - - r := router(mgr) - - request := genYAMLRequest("POST", "/resources/cache/foocache?chilled=true", fmt.Sprintf(` -file: - directory: %v -`, dir1)) - hResponse := httptest.NewRecorder() - r.ServeHTTP(hResponse, request) - assert.Equal(t, http.StatusOK, hResponse.Code, hResponse.Body.String()) - - streamConf, err := testutil.StreamFromYAML(` -input: - inproc: feed_in -output: - cache: - key: '${! json("id") }' - target: foocache -`) - require.NoError(t, err) - - request = genYAMLRequest("POST", "/streams/foo?chilled=true", streamConf) - hResponse = httptest.NewRecorder() - r.ServeHTTP(hResponse, request) - assert.Equal(t, http.StatusOK, hResponse.Code, hResponse.Body.String()) - - resChan := make(chan error) - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(`{"id":"first","content":"hello world"}`)}), resChan): - case <-time.After(time.Second * 5): - t.Fatal("timed out") - } - select { - case <-resChan: - case <-time.After(time.Second * 5): - t.Fatal("timed out") - } - - request = genYAMLRequest("POST", "/resources/cache/foocache?chilled=true", fmt.Sprintf(` -file: - directory: %v -`, dir2)) - hResponse = httptest.NewRecorder() - r.ServeHTTP(hResponse, request) - assert.Equal(t, http.StatusOK, hResponse.Code, hResponse.Body.String()) - - select { - case tChan <- message.NewTransaction(message.QuickBatch([][]byte{[]byte(`{"id":"second","content":"hello world 2"}`)}), resChan): - case <-time.After(time.Second * 5): - t.Fatal("timed out") - } - select { - case <-resChan: - case <-time.After(time.Second * 5): - t.Fatal("timed out") - } - - files, err := os.ReadDir(dir1) - require.NoError(t, err) - assert.Len(t, files, 1) - - file1Bytes, err := os.ReadFile(filepath.Join(dir1, "first")) - require.NoError(t, err) - assert.Equal(t, `{"id":"first","content":"hello world"}`, string(file1Bytes)) - - files, err = os.ReadDir(dir2) - require.NoError(t, err) - assert.Len(t, files, 1) - - file2Bytes, err := os.ReadFile(filepath.Join(dir2, "second")) - require.NoError(t, err) - assert.Equal(t, `{"id":"second","content":"hello world 2"}`, string(file2Bytes)) -} - -func TestAPIReady(t *testing.T) { - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - r := router(mgr) - - request := genRequest("POST", "/streams/foo", ` -input: - generate: - count: 1 - mapping: 'root = {}' - interval: "" - -output: - drop: {} -`) - response := httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - assert.Eventually(t, func() bool { - request = genRequest("GET", "/ready", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - return response.Code == http.StatusOK - }, time.Second*10, time.Millisecond*50) - - request = genRequest("POST", "/streams/bar", ` -input: - generate: - count: 1 - mapping: 'root = {}' - interval: "" - -output: - websocket: - url: not**a**valid**url -`) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - assert.Equal(t, http.StatusOK, response.Code) - - assert.Eventually(t, func() bool { - request = genRequest("GET", "/ready", nil) - response = httptest.NewRecorder() - r.ServeHTTP(response, request) - return response.Code == http.StatusServiceUnavailable - }, time.Second*10, time.Millisecond*50) -} diff --git a/internal/stream/manager/package.go b/internal/stream/manager/package.go deleted file mode 100644 index a837e48abb..0000000000 --- a/internal/stream/manager/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package manager creates and manages multiple streams, providing an API for -// performing CRUD operations. -package manager diff --git a/internal/stream/manager/type.go b/internal/stream/manager/type.go deleted file mode 100644 index 847c42b7d9..0000000000 --- a/internal/stream/manager/type.go +++ /dev/null @@ -1,263 +0,0 @@ -package manager - -import ( - "context" - "errors" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -// StreamStatus tracks a stream along with information regarding its internals. -type StreamStatus struct { - stoppedAfter int64 - config stream.Config - strm *stream.Type - metrics *metrics.Local - createdAt time.Time -} - -func newStreamStatus(conf stream.Config, stats *metrics.Local) *StreamStatus { - return &StreamStatus{ - config: conf, - metrics: stats, - createdAt: time.Now(), - } -} - -func (s *StreamStatus) setStream(strm *stream.Type) { - s.strm = strm -} - -// IsRunning returns a boolean indicating whether the stream is currently -// running. -func (s *StreamStatus) IsRunning() bool { - return atomic.LoadInt64(&s.stoppedAfter) == 0 -} - -// IsReady returns a boolean indicating whether the stream is connected at both -// the input and output level. -func (s *StreamStatus) IsReady() bool { - return s.strm.IsReady() -} - -// Uptime returns a time.Duration indicating the current uptime of the stream. -func (s *StreamStatus) Uptime() time.Duration { - if stoppedAfter := atomic.LoadInt64(&s.stoppedAfter); stoppedAfter > 0 { - return time.Duration(stoppedAfter) - } - return time.Since(s.createdAt) -} - -// Config returns the configuration of the stream. -func (s *StreamStatus) Config() stream.Config { - return s.config -} - -// Metrics returns a metrics aggregator of the stream. -func (s *StreamStatus) Metrics() *metrics.Local { - return s.metrics -} - -// setClosed sets the flag indicating that the stream is closed. -func (s *StreamStatus) setClosed() { - atomic.SwapInt64(&s.stoppedAfter, int64(time.Since(s.createdAt))) -} - -//------------------------------------------------------------------------------ - -// StreamProcConstructorFunc is a closure type that constructs a processor type -// for new streams, where the id of the stream is provided as an argument. -type StreamProcConstructorFunc func(streamID string) (processor.V1, error) - -//------------------------------------------------------------------------------ - -// Type manages a collection of streams, providing APIs for CRUD operations on -// the streams. -type Type struct { - closed bool - streams map[string]*StreamStatus - - manager bundle.NewManagement - apiEnabled bool - - lock sync.Mutex -} - -// New creates a new stream manager.Type. -func New(mgr bundle.NewManagement, opts ...func(*Type)) *Type { - t := &Type{ - streams: map[string]*StreamStatus{}, - apiEnabled: true, - manager: mgr, - } - for _, opt := range opts { - opt(t) - } - t.registerEndpoints(t.apiEnabled) - return t -} - -//------------------------------------------------------------------------------ - -// OptAPIEnabled sets whether the stream manager registers API endpoints for -// CRUD operations on streams. This is enabled by default. -func OptAPIEnabled(b bool) func(*Type) { - return func(t *Type) { - t.apiEnabled = b - } -} - -//------------------------------------------------------------------------------ - -// Errors specifically returned by a stream manager. -var ( - ErrStreamExists = errors.New("stream already exists") - ErrStreamDoesNotExist = errors.New("stream does not exist") -) - -//------------------------------------------------------------------------------ - -// Create attempts to construct and run a new stream under a unique ID. If the -// ID already exists an error is returned. -func (m *Type) Create(id string, conf stream.Config) error { - m.lock.Lock() - defer m.lock.Unlock() - - if m.closed { - return component.ErrTypeClosed - } - - if _, exists := m.streams[id]; exists { - return ErrStreamExists - } - - strmFlatMetrics := metrics.NewLocal() - sMgr := m.manager.ForStream(id).WithAddedMetrics(strmFlatMetrics) - - // Note we initialise the status without a stream pointer, this is okay as - // long as we do not add it to m.streams without one set. - // - // This seems a bit wonky but we can't rule out a race condition between - // the stream terminating and setClosed and actually initialising a status. - wrapper := newStreamStatus(conf, strmFlatMetrics) - strm, err := stream.New(conf, sMgr, stream.OptOnClose(func() { - wrapper.setClosed() - })) - if err != nil { - return err - } - - wrapper.setStream(strm) - m.streams[id] = wrapper - return nil -} - -// Read attempts to obtain the status of a managed stream. Returns an error if -// the stream does not exist. -func (m *Type) Read(id string) (*StreamStatus, error) { - m.lock.Lock() - defer m.lock.Unlock() - - if m.closed { - return nil, component.ErrTypeClosed - } - - wrapper, exists := m.streams[id] - if !exists { - return nil, ErrStreamDoesNotExist - } - - return wrapper, nil -} - -// Update attempts to stop an existing stream and replace it with a new version -// of the same stream. -func (m *Type) Update(ctx context.Context, id string, conf stream.Config) error { - m.lock.Lock() - _, exists := m.streams[id] - closed := m.closed - m.lock.Unlock() - - if closed { - return component.ErrTypeClosed - } - if !exists { - return ErrStreamDoesNotExist - } - - if err := m.Delete(ctx, id); err != nil { - return err - } - return m.Create(id, conf) -} - -// Delete attempts to stop and remove a stream by its ID. Returns an error if -// the stream was not found, or if clean shutdown fails in the specified period -// of time. -func (m *Type) Delete(ctx context.Context, id string) error { - m.lock.Lock() - if m.closed { - m.lock.Unlock() - return component.ErrTypeClosed - } - - wrapper, exists := m.streams[id] - m.lock.Unlock() - if !exists { - return ErrStreamDoesNotExist - } - - if err := wrapper.strm.Stop(ctx); err != nil { - return err - } - - m.lock.Lock() - delete(m.streams, id) - m.lock.Unlock() - - return nil -} - -//------------------------------------------------------------------------------ - -// Stop attempts to gracefully shut down all active streams and close the -// stream manager. -func (m *Type) Stop(ctx context.Context) error { - m.lock.Lock() - defer m.lock.Unlock() - - resultChan := make(chan string) - - for k, v := range m.streams { - go func(id string, strm *StreamStatus) { - if err := strm.strm.Stop(ctx); err != nil { - resultChan <- id - } else { - resultChan <- "" - } - }(k, v) - } - - failedStreams := []string{} - for i := 0; i < len(m.streams); i++ { - if failedStrm := <-resultChan; failedStrm != "" { - failedStreams = append(failedStreams, failedStrm) - } - } - - m.streams = map[string]*StreamStatus{} - m.closed = true - - if len(failedStreams) > 0 { - return fmt.Errorf("failed to gracefully stop the following streams: %v", failedStreams) - } - return nil -} diff --git a/internal/stream/manager/type_stress_test.go b/internal/stream/manager/type_stress_test.go deleted file mode 100644 index 07e7ce83ca..0000000000 --- a/internal/stream/manager/type_stress_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package manager_test - -import ( - "context" - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/testutil" - bmanager "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream/manager" - - // Import pure components for tests. - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" -) - -func TestTypeUnderStress(t *testing.T) { - t.Skip("Skipping long running stress test") - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := manager.New(res) - - conf, err := testutil.StreamFromYAML(` -input: - generate: - count: 3 - interval: 1us - mapping: 'root.id = uuid_v4()' -output: - drop: {} -`) - require.NoError(t, err) - - wg := sync.WaitGroup{} - for j := 0; j < 1000; j++ { - wg.Add(1) - go func(threadID int) { - defer wg.Done() - for i := 0; i < 100; i++ { - streamID := fmt.Sprintf("foo-%v-%v", threadID, i) - require.NoError(t, mgr.Create(streamID, conf)) - - assert.Eventually(t, func() bool { - details, err := mgr.Read(streamID) - return err == nil && !details.IsRunning() - }, time.Second, time.Millisecond*50) - - require.NoError(t, mgr.Delete(ctx, streamID)) - } - }(j) - } - - wg.Wait() - require.NoError(t, mgr.Stop(ctx)) -} diff --git a/internal/stream/manager/type_test.go b/internal/stream/manager/type_test.go deleted file mode 100644 index ee745da713..0000000000 --- a/internal/stream/manager/type_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package manager - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - bmanager "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -func harmlessConf(t testing.TB) stream.Config { - t.Helper() - - c, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = deleted()' -output: - drop: {} -`) - require.NoError(t, err) - - return c -} - -func TestTypeBasicOperations(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := New(res) - - if err := mgr.Update(ctx, "foo", harmlessConf(t)); err == nil { - t.Error("Expected error on empty update") - } - if _, err := mgr.Read("foo"); err == nil { - t.Error("Expected error on empty read") - } - - if err := mgr.Create("foo", harmlessConf(t)); err != nil { - t.Fatal(err) - } - if err := mgr.Create("foo", harmlessConf(t)); err == nil { - t.Error("Expected error on duplicate create") - } - - if info, err := mgr.Read("foo"); err != nil { - t.Error(err) - } else if !info.IsRunning() { - t.Error("Stream not active") - } else if act, exp := info.Config(), harmlessConf(t); !reflect.DeepEqual(act, exp) { - t.Errorf("Unexpected config: %v != %v", act, exp) - } - - newConf := harmlessConf(t) - newConf.Buffer.Type = "memory" - - if err := mgr.Update(ctx, "foo", newConf); err != nil { - t.Error(err) - } - - if info, err := mgr.Read("foo"); err != nil { - t.Error(err) - } else if !info.IsRunning() { - t.Error("Stream not active") - } else if act, exp := info.Config(), newConf; !reflect.DeepEqual(act, exp) { - t.Errorf("Unexpected config: %v != %v", act, exp) - } - - if err := mgr.Delete(ctx, "foo"); err != nil { - t.Fatal(err) - } - if err := mgr.Delete(ctx, "foo"); err == nil { - t.Error("Expected error on duplicate delete") - } - - if err := mgr.Stop(ctx); err != nil { - t.Error(err) - } - - if exp, act := component.ErrTypeClosed, mgr.Create("foo", harmlessConf(t)); act != exp { - t.Errorf("Unexpected error: %v != %v", act, exp) - } -} - -func TestTypeBasicClose(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - res, err := bmanager.New(bmanager.NewResourceConfig()) - require.NoError(t, err) - - mgr := New(res) - - conf := harmlessConf(t) - if err := mgr.Create("foo", conf); err != nil { - t.Fatal(err) - } - - if err := mgr.Stop(ctx); err != nil { - t.Error(err) - } - - if exp, act := component.ErrTypeClosed, mgr.Create("foo", harmlessConf(t)); act != exp { - t.Errorf("Unexpected error: %v != %v", act, exp) - } -} diff --git a/internal/stream/type.go b/internal/stream/type.go deleted file mode 100644 index 7811ffb25e..0000000000 --- a/internal/stream/type.go +++ /dev/null @@ -1,273 +0,0 @@ -package stream - -import ( - "bytes" - "context" - "errors" - "net/http" - "runtime/pprof" - "sync/atomic" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/pipeline" -) - -// Type creates and manages the lifetime of a Benthos stream. -type Type struct { - conf Config - - inputLayer input.Streamed - bufferLayer buffer.Streamed - pipelineLayer processor.Pipeline - outputLayer output.Streamed - - manager bundle.NewManagement - - onClose func() - closed uint32 -} - -// New creates a new stream.Type. -func New(conf Config, mgr bundle.NewManagement, opts ...func(*Type)) (*Type, error) { - t := &Type{ - conf: conf, - manager: mgr, - onClose: func() {}, - closed: 0, - } - for _, opt := range opts { - opt(t) - } - if err := t.start(); err != nil { - return nil, err - } - - healthCheck := func(w http.ResponseWriter, r *http.Request) { - inputConnected := t.inputLayer.Connected() - outputConnected := t.outputLayer.Connected() - - if atomic.LoadUint32(&t.closed) == 1 { - http.Error(w, "Stream terminated", http.StatusNotFound) - return - } - - if inputConnected && outputConnected { - _, _ = w.Write([]byte("OK")) - return - } - - w.WriteHeader(http.StatusServiceUnavailable) - if !inputConnected { - _, _ = w.Write([]byte("input not connected\n")) - } - if !outputConnected { - _, _ = w.Write([]byte("output not connected\n")) - } - } - t.manager.RegisterEndpoint( - "/ready", - "Returns 200 OK if all inputs and outputs are connected, otherwise a 503 is returned.", - healthCheck, - ) - return t, nil -} - -//------------------------------------------------------------------------------ - -// OptOnClose sets a closure to be called when the stream closes. -func OptOnClose(onClose func()) func(*Type) { - return func(t *Type) { - t.onClose = onClose - } -} - -//------------------------------------------------------------------------------ - -// IsReady returns a boolean indicating whether both the input and output layers -// of the stream are connected. -func (t *Type) IsReady() bool { - return t.inputLayer.Connected() && t.outputLayer.Connected() -} - -func (t *Type) start() (err error) { - // Constructors - iMgr := t.manager.IntoPath("input") - if t.inputLayer, err = iMgr.NewInput(t.conf.Input); err != nil { - return - } - if t.conf.Buffer.Type != "none" { - bMgr := t.manager.IntoPath("buffer") - if t.bufferLayer, err = bMgr.NewBuffer(t.conf.Buffer); err != nil { - return - } - } - if tLen := len(t.conf.Pipeline.Processors); tLen > 0 { - pMgr := t.manager.IntoPath("pipeline") - if t.pipelineLayer, err = pipeline.New(t.conf.Pipeline, pMgr); err != nil { - return - } - } - oMgr := t.manager.IntoPath("output") - if t.outputLayer, err = oMgr.NewOutput(t.conf.Output); err != nil { - return - } - - // Start chaining components - var nextTranChan <-chan message.Transaction - - nextTranChan = t.inputLayer.TransactionChan() - if t.bufferLayer != nil { - if err = t.bufferLayer.Consume(nextTranChan); err != nil { - return - } - nextTranChan = t.bufferLayer.TransactionChan() - } - if t.pipelineLayer != nil { - if err = t.pipelineLayer.Consume(nextTranChan); err != nil { - return - } - nextTranChan = t.pipelineLayer.TransactionChan() - } - if err = t.outputLayer.Consume(nextTranChan); err != nil { - return - } - - go func(out output.Streamed) { - for { - if err := out.WaitForClose(context.Background()); err == nil { - t.onClose() - atomic.StoreUint32(&t.closed, 1) - return - } - } - }(t.outputLayer) - - return nil -} - -// StopGracefully attempts to close the stream in the most graceful way by only -// closing the input layer and waiting for all other layers to terminate by -// proxy. This should guarantee that all in-flight and buffered data is resolved -// before shutting down. -func (t *Type) StopGracefully(ctx context.Context) (err error) { - t.inputLayer.TriggerStopConsuming() - if err = t.inputLayer.WaitForClose(ctx); err != nil { - return - } - - // If we have a buffer then wait right here. We want to try and allow the - // buffer to empty out before prompting the other layers to shut down. - if t.bufferLayer != nil { - t.bufferLayer.TriggerStopConsuming() - if err = t.bufferLayer.WaitForClose(ctx); err != nil { - return - } - } - - // After this point we can start closing the remaining components. - if t.pipelineLayer != nil { - if err = t.pipelineLayer.WaitForClose(ctx); err != nil { - return - } - } - - if err = t.outputLayer.WaitForClose(ctx); err != nil { - return - } - return nil -} - -// StopUnordered attempts to close all components in parallel without allowing -// the stream to gracefully wind down in the order of component layers. This -// should only be attempted if both stopGracefully and stopOrdered failed. -func (t *Type) StopUnordered(ctx context.Context) (err error) { - t.inputLayer.TriggerCloseNow() - if t.bufferLayer != nil { - t.bufferLayer.TriggerCloseNow() - } - if t.pipelineLayer != nil { - t.pipelineLayer.TriggerCloseNow() - } - t.outputLayer.TriggerCloseNow() - - if err = t.inputLayer.WaitForClose(ctx); err != nil { - return - } - - if t.bufferLayer != nil { - if err = t.bufferLayer.WaitForClose(ctx); err != nil { - return - } - } - - if t.pipelineLayer != nil { - if err = t.pipelineLayer.WaitForClose(ctx); err != nil { - return - } - } - - if err = t.outputLayer.WaitForClose(ctx); err != nil { - return - } - return nil -} - -// Stop attempts to close the stream within the specified timeout period. -// Initially the attempt is graceful, but if the context contains a deadline and -// it draws near the attempt becomes progressively less graceful. -// -// If the context is cancelled an error is returned _after_ asynchronously -// instructing the remaining stream components to terminate ungracefully. -func (t *Type) Stop(ctx context.Context) error { - ctxCloseGraceful := ctx - - // If the provided context has a known deadline then we calculate a period - // of time whereby it would be appropriate to abandon graceful termination - // and attempt ungraceful termination within that deadline. - if deadline, ok := ctx.Deadline(); ok { - // The calculated time we're willing to wait for graceful termination is - // three quarters of the overall deadline. - tUntil := time.Until(deadline) - tUntil -= (tUntil / 4) - - if tUntil > time.Second { - var gDone func() - ctxCloseGraceful, gDone = context.WithTimeout(ctx, tUntil) - defer gDone() - } - } - - // Attempt graceful termination by instructing the input to stop consuming - // and for all downstream components to finish. - err := t.StopGracefully(ctxCloseGraceful) - if err == nil { - return nil - } - if !(errors.Is(err, context.Canceled) && errors.Is(err, context.DeadlineExceeded)) { - t.manager.Logger().Error("Encountered error whilst attempting to shut down gracefully: %v\n", err) - } - - // If graceful termination failed then call unordered termination, if the - // overall ctx is already cancelled this will still trigger asynchronous - // clean up of resources, which is a best attempt. - if err = t.StopUnordered(ctx); err == nil { - return nil - } - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - t.manager.Logger().Info("Some components prevented forced termination as they were either blocked from delivering data or from acknowledging delivered data within the shutdown timeout. This could potentially cause duplicate messages to be delivered on the next run.") - - dumpBuf := bytes.NewBuffer(nil) - _ = pprof.Lookup("goroutine").WriteTo(dumpBuf, 1) - - t.manager.Logger().Debug(dumpBuf.String()) - } else { - t.manager.Logger().Error("Encountered error whilst forcefully shutting down: %v\n", err) - } - return err -} diff --git a/internal/stream/type_test.go b/internal/stream/type_test.go deleted file mode 100644 index 328a18a219..0000000000 --- a/internal/stream/type_test.go +++ /dev/null @@ -1,233 +0,0 @@ -package stream_test - -import ( - "context" - "io" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/stream" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestTypeConstruction(t *testing.T) { - conf, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = {}' -buffer: - memory: {} -output: - drop: {} -`) - require.NoError(t, err) - - newMgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - strm, err := stream.New(conf, newMgr) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - assert.NoError(t, strm.Stop(ctx)) - - newMgr, err = manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - strm, err = stream.New(conf, newMgr) - require.NoError(t, err) - - require.NoError(t, strm.Stop(ctx)) -} - -func TestStreamCloseUngraceful(t *testing.T) { - t.Parallel() - - conf, err := testutil.StreamFromYAML(` -input: - generate: - interval: "" - mapping: 'root = "hello world"' -output: - inproc: foo -`) - require.NoError(t, err) - - newMgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - strm, err := stream.New(conf, newMgr) - require.NoError(t, err) - - tChan, err := newMgr.GetPipe("foo") - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - var tTmp message.Transaction - select { - case tTmp = <-tChan: - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } - require.Len(t, tTmp.Payload, 1) - - pBytes := tTmp.Payload[0].AsBytes() - assert.Equal(t, "hello world", string(pBytes)) - - assert.Error(t, strm.Stop(ctx)) -} - -func TestTypeCloseGracefully(t *testing.T) { - conf, err := testutil.StreamFromYAML(` -input: - generate: - interval: "" - mapping: 'root = {}' -buffer: - memory: {} -output: - drop: {} -`) - require.NoError(t, err) - - newMgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - strm, err := stream.New(conf, newMgr) - require.NoError(t, err) - assert.NoError(t, strm.StopGracefully(ctx)) - - strm, err = stream.New(conf, newMgr) - require.NoError(t, err) - assert.NoError(t, strm.StopGracefully(ctx)) - - conf.Pipeline.Processors = []processor.Config{ - processor.NewConfig(), - } - - strm, err = stream.New(conf, newMgr) - require.NoError(t, err) - assert.NoError(t, strm.StopGracefully(ctx)) -} - -func TestTypeCloseUnordered(t *testing.T) { - conf, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = {}' -buffer: - memory: {} -output: - drop: {} -`) - require.NoError(t, err) - - newMgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - strm, err := stream.New(conf, newMgr) - require.NoError(t, err) - assert.NoError(t, strm.StopUnordered(ctx)) - - strm, err = stream.New(conf, newMgr) - require.NoError(t, err) - assert.NoError(t, strm.StopUnordered(ctx)) - - conf.Pipeline.Processors = []processor.Config{ - processor.NewConfig(), - } - - strm, err = stream.New(conf, newMgr) - require.NoError(t, err) - assert.NoError(t, strm.StopUnordered(ctx)) -} - -type mockAPIReg struct { - server *httptest.Server -} - -func (ar mockAPIReg) RegisterEndpoint(path, desc string, h http.HandlerFunc) { - ar.server.Config.Handler = h -} - -func (ar mockAPIReg) Close() { - ar.server.Close() -} - -func newMockAPIReg() mockAPIReg { - return mockAPIReg{ - server: httptest.NewServer(nil), - } -} - -func validateHealthCheckResponse(t *testing.T, serverURL, expectedResponse string) { - t.Helper() - - res, err := http.Get(serverURL + "/ready") - require.NoError(t, err) - defer res.Body.Close() - - data, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, expectedResponse, string(data)) -} - -func TestHealthCheck(t *testing.T) { - conf, err := testutil.StreamFromYAML(` -input: - generate: - mapping: 'root = {}' - -output: - drop: {} -`) - require.NoError(t, err) - - mockAPIReg := newMockAPIReg() - defer mockAPIReg.Close() - - newMgr, err := manager.New(manager.NewResourceConfig(), manager.OptSetAPIReg(&mockAPIReg)) - require.NoError(t, err) - - strm, err := stream.New(conf, newMgr) - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer done() - for !strm.IsReady() { - select { - case <-ctx.Done(): - t.Fatalf("Failed to start stream") - case <-time.After(10 * time.Millisecond): - } - } - - validateHealthCheckResponse(t, mockAPIReg.server.URL, "OK") - - stopCtx, stopDone := context.WithTimeout(context.Background(), time.Minute) - defer stopDone() - - assert.NoError(t, strm.StopUnordered(stopCtx)) - - validateHealthCheckResponse(t, mockAPIReg.server.URL, "Stream terminated\n") -} diff --git a/internal/template/config.go b/internal/template/config.go deleted file mode 100644 index c89e1c7e5e..0000000000 --- a/internal/template/config.go +++ /dev/null @@ -1,276 +0,0 @@ -package template - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/fatih/color" - "github.com/nsf/jsondiff" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// FieldConfig describes a configuration field used in the template. -type FieldConfig struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - Type *string `yaml:"type,omitempty"` - Kind *string `yaml:"kind,omitempty"` - Default *any `yaml:"default,omitempty"` - Advanced bool `yaml:"advanced"` -} - -// TestConfig defines a unit test for the template. -type TestConfig struct { - Name string `yaml:"name"` - Config yaml.Node `yaml:"config"` - Expected yaml.Node `yaml:"expected,omitempty"` -} - -// Config describes a Benthos component template. -type Config struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - Status string `yaml:"status"` - Categories []string `yaml:"categories"` - Summary string `yaml:"summary"` - Description string `yaml:"description"` - Fields []FieldConfig `yaml:"fields"` - Mapping string `yaml:"mapping"` - MetricsMapping string `yaml:"metrics_mapping"` - Tests []TestConfig `yaml:"tests"` -} - -// FieldSpec creates a documentation field spec from a template field config. -func (c FieldConfig) FieldSpec() (docs.FieldSpec, error) { - f := docs.FieldAnything(c.Name, c.Description) - f.IsAdvanced = c.Advanced - if c.Default != nil { - f = f.HasDefault(*c.Default) - } - if c.Type == nil { - return f, errors.New("missing type field") - } - f = f.HasType(docs.FieldType(*c.Type)) - if c.Kind != nil { - switch *c.Kind { - case "map": - f = f.Map() - case "list": - f = f.Array() - case "scalar": - default: - return f, fmt.Errorf("unrecognised scalar type: %v", *c.Kind) - } - } - return f, nil -} - -// ComponentSpec creates a documentation component spec from a template config. -func (c Config) ComponentSpec() (docs.ComponentSpec, error) { - fields := make([]docs.FieldSpec, len(c.Fields)) - for i, fieldConf := range c.Fields { - var err error - if fields[i], err = fieldConf.FieldSpec(); err != nil { - return docs.ComponentSpec{}, fmt.Errorf("field %v: %w", i, err) - } - } - config := docs.FieldComponent().WithChildren(fields...) - - status := docs.StatusStable - if c.Status != "" { - status = docs.Status(c.Status) - } - - return docs.ComponentSpec{ - Name: c.Name, - Type: docs.Type(c.Type), - Status: status, - Plugin: true, - Categories: c.Categories, - Summary: c.Summary, - Description: c.Description, - Config: config, - }, nil -} - -func (c Config) compile() (*compiled, error) { - spec, err := c.ComponentSpec() - if err != nil { - return nil, err - } - mapping, err := bloblang.GlobalEnvironment().NewMapping(c.Mapping) - if err != nil { - var perr *parser.Error - if errors.As(err, &perr) { - return nil, fmt.Errorf("parse mapping: %v", perr.ErrorAtPositionStructured("", []rune(c.Mapping))) - } - return nil, fmt.Errorf("parse mapping: %w", err) - } - var metricsMapping *metrics.Mapping - if c.MetricsMapping != "" { - if metricsMapping, err = metrics.NewMapping(c.MetricsMapping, log.Noop()); err != nil { - return nil, fmt.Errorf("parse metrics mapping: %w", err) - } - } - return &compiled{spec: spec, mapping: mapping, metricsMapping: metricsMapping}, nil -} - -func diffYAMLNodesAsJSON(expNode *yaml.Node, actNode any) (string, error) { - var iexp any - if err := expNode.Decode(&iexp); err != nil { - return "", fmt.Errorf("failed to marshal expected %w", err) - } - - expBytes, err := json.Marshal(iexp) - if err != nil { - return "", fmt.Errorf("failed to marshal expected %w", err) - } - actBytes, err := json.Marshal(actNode) - if err != nil { - return "", fmt.Errorf("failed to marshal actual %w", err) - } - - jdopts := jsondiff.DefaultConsoleOptions() - diff, explanation := jsondiff.Compare(expBytes, actBytes, &jdopts) - if diff != jsondiff.FullMatch { - return explanation, nil - } - return "", nil -} - -// Test ensures that the template compiles, and executes any unit test -// definitions within the config. -func (c Config) Test() ([]string, error) { - compiled, err := c.compile() - if err != nil { - return nil, err - } - - var failures []string - for _, test := range c.Tests { - outConf, err := compiled.Render(&test.Config) - if err != nil { - return nil, fmt.Errorf("test '%v': %w", test.Name, err) - } - - var yNode yaml.Node - if err := yNode.Encode(outConf); err == nil { - for _, lint := range docs.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), docs.Type(c.Type), &yNode) { - failures = append(failures, fmt.Sprintf("test '%v': lint error in resulting config: %v", test.Name, lint.Error())) - } - } else { - failures = append(failures, fmt.Sprintf("test '%v': failed to encode resulting config as YAML: %v", test.Name, err.Error())) - } - if len(test.Expected.Content) > 0 { - diff, err := diffYAMLNodesAsJSON(&test.Expected, outConf) - if err != nil { - return nil, fmt.Errorf("test '%v': %w", test.Name, err) - } - if diff != "" { - diff = color.New(color.Reset).SprintFunc()(diff) - return nil, fmt.Errorf("test '%v': mismatch between expected and actual resulting config: %v", test.Name, diff) - } - } - } - return failures, nil -} - -// ReadConfigYAML attempts to read a YAML byte slice as a template configuration -// file. -func ReadConfigYAML(templateBytes []byte) (conf Config, lints []docs.Lint, err error) { - if err = yaml.Unmarshal(templateBytes, &conf); err != nil { - return - } - - var node yaml.Node - if err = yaml.Unmarshal(templateBytes, &node); err != nil { - return - } - - lints = ConfigSpec().LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), &node) - return -} - -// ReadConfigFile attempts to read a template configuration file. -func ReadConfigFile(path string) (conf Config, lints []docs.Lint, err error) { - var templateBytes []byte - if templateBytes, err = ifs.ReadFile(ifs.OS(), path); err != nil { - return - } - return ReadConfigYAML(templateBytes) -} - -//------------------------------------------------------------------------------ - -// FieldConfigSpec returns a configuration spec for a field of a template. -func FieldConfigSpec() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString("name", "The name of the field."), - docs.FieldString("description", "A description of the field.").HasDefault(""), - docs.FieldString("type", "The scalar type of the field.").HasAnnotatedOptions( - "string", "standard string type", - "int", "standard integer type", - "float", "standard float type", - "bool", "a boolean true/false", - "unknown", "allows for nesting arbitrary configuration inside of a field", - ), - docs.FieldString("kind", "The kind of the field.").HasOptions( - "scalar", "map", "list", - ).HasDefault("scalar"), - docs.FieldAnything("default", "An optional default value for the field. If a default value is not specified then a configuration without the field is considered incorrect.").Optional(), - docs.FieldBool("advanced", "Whether this field is considered advanced.").HasDefault(false), - } -} - -func templateMetricsMappingDocs() docs.FieldSpec { - f := docs.MetricsMappingFieldSpec("metrics_mapping") - f.Description += ` - -Invocations of this mapping are able to reference a variable $label in order to obtain the value of the label provided to the template config. This allows you to match labels with the root of the config.` - return f -} - -// ConfigSpec returns a configuration spec for a template. -func ConfigSpec() docs.FieldSpecs { - return docs.FieldSpecs{ - docs.FieldString("name", "The name of the component this template will create."), - docs.FieldString( - "type", "The type of the component this template will create.", - ).HasOptions( - "cache", "input", "output", "processor", "rate_limit", - ), - docs.FieldString( - "status", "The stability of the template describing the likelihood that the configuration spec of the template, or it's behavior, will change.", - ).HasAnnotatedOptions( - "stable", "This template is stable and will therefore not change in a breaking way outside of major version releases.", - "beta", "This template is beta and will therefore not change in a breaking way unless a major problem is found.", - "experimental", "This template is experimental and therefore subject to breaking changes outside of major version releases.", - ).HasDefault("stable"), - docs.FieldString( - "categories", "An optional list of tags, which are used for arbitrarily grouping components in documentation.", - ).Array().HasDefault([]any{}), - docs.FieldString("summary", "A short summary of the component.").HasDefault(""), - docs.FieldString("description", "A longer form description of the component and how to use it.").HasDefault(""), - docs.FieldObject("fields", "The configuration fields of the template, fields specified here will be parsed from a Benthos config and will be accessible from the template mapping.").Array().WithChildren(FieldConfigSpec()...), - docs.FieldBloblang( - "mapping", "A xref:guides:bloblang/about.adoc[Bloblang] mapping that translates the fields of the template into a valid Benthos configuration for the target component type.", - ), - templateMetricsMappingDocs(), - docs.FieldObject( - "tests", "Optional unit test definitions for the template that verify certain configurations produce valid configs. These tests are executed with the command `benthos template lint`.", - ).Array().WithChildren( - docs.FieldString("name", "A name to identify the test."), - docs.FieldObject("config", "A configuration to run this test with, the config resulting from applying the template with this config will be linted."), - docs.FieldObject("expected", "An optional configuration describing the expected result of applying the template, when specified the result will be diffed and any mismatching fields will be reported as a test error.").Optional(), - ).HasDefault([]any{}), - } -} diff --git a/internal/template/docs.adoc b/internal/template/docs.adoc deleted file mode 100644 index 8511332c15..0000000000 --- a/internal/template/docs.adoc +++ /dev/null @@ -1,109 +0,0 @@ -= Templating -:description: Learn how templates work. - - -//// - THIS FILE IS AUTOGENERATED! - - To make changes please edit the contents of: - internal/template/docs.adoc -//// - -[WARNING] -.Experimental -==== -Templates are an experimental feature and therefore subject to change outside of major version releases. -==== - -Templates are a way to define new {page-component-title} components (similar to plugins) that are implemented by generating a {page-component-title} config snippet from pre-defined parameter fields. This is useful when a common pattern of {page-component-title} configuration is used but with varying parameters each time. - -A template is defined in a YAML file that can be imported when {page-component-title} runs using the flag `-t`: - -[source,bash] ----- -benthos -t "./templates/*.yaml" -c ./config.yaml ----- - -The template describes the type of the component and configuration fields that can be used to customize it, followed by a xref:guides:bloblang/about.adoc[Bloblang mapping] that translates an object containing those fields into a benthos config structure. This allows you to use logic to generate more complex configurations: - -[tabs] -====== -Template:: -+ --- - -[source,yaml] ----- -name: aws_sqs_list -type: input - -fields: - - name: urls - type: string - kind: list - - name: region - type: string - default: us-east-1 - -mapping: | - root.broker.inputs = this.urls.map_each(url -> { - "aws_sqs": { - "url": url, - "region": this.region, - } - }) ----- --- -Config:: -+ --- - -[source,yaml] ----- -input: - aws_sqs_list: - urls: - - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 - - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 - -pipeline: - processors: - - mapping: | - root.id = uuid_v4() - root.foo = this.inner.foo - root.body = this.outer ----- --- -Result:: -+ --- - -[source,yaml] ----- -input: - broker: - inputs: - - aws_sqs: - url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 - region: us-east-1 - - aws_sqs: - url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 - region: us-east-1 - -pipeline: - processors: - - mapping: | - root.id = uuid_v4() - root.foo = this.inner.foo - root.body = this.outer ----- --- -====== - -You can see more examples of templates at https://github.com/benthosdev/benthos/tree/main/config/template_examples^. - -== Fields - -The schema of a template file is as follows: - -{{template "field_docs" . -}} diff --git a/internal/template/docs.go b/internal/template/docs.go deleted file mode 100644 index c32e81bb97..0000000000 --- a/internal/template/docs.go +++ /dev/null @@ -1,29 +0,0 @@ -package template - -import ( - "bytes" - "text/template" - - "github.com/benthosdev/benthos/v4/internal/docs" - - _ "embed" -) - -//go:embed docs.adoc -var templateDocs string - -type templateContext struct { - Fields []docs.FieldSpecCtx -} - -// DocsMarkdown returns a markdown document for the templates documentation. -func DocsMarkdown() ([]byte, error) { - templateDocsTemplate := docs.FieldsTemplate(false) + templateDocs - - var buf bytes.Buffer - err := template.Must(template.New("templates").Parse(templateDocsTemplate)).Execute(&buf, templateContext{ - Fields: docs.FieldObject("", "").WithChildren(ConfigSpec()...).FlattenChildrenForDocs(), - }) - - return buf.Bytes(), err -} diff --git a/internal/template/template.go b/internal/template/template.go deleted file mode 100644 index 3228f01093..0000000000 --- a/internal/template/template.go +++ /dev/null @@ -1,239 +0,0 @@ -package template - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// InitTemplates parses and registers native templates, as well as templates -// at paths provided, and returns any linting errors that occur. -func InitTemplates(templatesPaths ...string) ([]string, error) { - var lints []string - for _, tPath := range templatesPaths { - tmplConf, tLints, err := ReadConfigFile(tPath) - if err != nil { - return nil, fmt.Errorf("template %v: %w", tPath, err) - } - for _, l := range tLints { - lints = append(lints, fmt.Sprintf("template file %v: %v", tPath, l)) - } - - tmpl, err := tmplConf.compile() - if err != nil { - return nil, fmt.Errorf("template %v: %w", tPath, err) - } - - if err := registerTemplate(bundle.GlobalEnvironment, tmpl); err != nil { - return nil, fmt.Errorf("template %v: %w", tPath, err) - } - } - return lints, nil -} - -//------------------------------------------------------------------------------ - -// Compiled is a template that has been compiled from a config. -type compiled struct { - spec docs.ComponentSpec - mapping *mapping.Executor - metricsMapping *metrics.Mapping -} - -// Render a compiled template by providing a generic config. -func (c *compiled) Render(node any) (any, error) { - var genericConf any - var err error - switch t := node.(type) { - case *yaml.Node: - genericConf, err = c.spec.Config.Children.YAMLToMap(t, docs.ToValueConfig{}) - default: - genericConf, err = c.spec.Config.Children.AnyToMap(t, docs.ToValueConfig{}) - } - if err != nil { - return nil, fmt.Errorf("invalid config for template component: %w", err) - } - - part := message.NewPart(nil) - part.SetStructuredMut(genericConf) - msg := message.Batch{part} - - newPart, err := c.mapping.MapPart(0, msg) - if err != nil { - return nil, fmt.Errorf("mapping failed for template component: %w", err) - } - - resultGeneric, err := newPart.AsStructured() - if err != nil { - return nil, fmt.Errorf("mapping for template component resulted in invalid config: %w", err) - } - return resultGeneric, nil -} - -//------------------------------------------------------------------------------ - -// RegisterTemplateYAML attempts to register a new template component to the -// specified environment. -func RegisterTemplateYAML(env *bundle.Environment, template []byte) error { - tmplConf, _, err := ReadConfigYAML(template) - if err != nil { - return err - } - - tmpl, err := tmplConf.compile() - if err != nil { - return err - } - - return registerTemplate(env, tmpl) -} - -// RegisterTemplate attempts to add a template component to the global list of -// component types. -func registerTemplate(env *bundle.Environment, tmpl *compiled) error { - switch tmpl.spec.Type { - case docs.TypeCache: - return registerCacheTemplate(tmpl, env) - case docs.TypeInput: - return registerInputTemplate(tmpl, env) - case docs.TypeOutput: - return registerOutputTemplate(tmpl, env) - case docs.TypeProcessor: - return registerProcessorTemplate(tmpl, env) - case docs.TypeRateLimit: - return registerRateLimitTemplate(tmpl, env) - } - return fmt.Errorf("unable to register template for component type %v", tmpl.spec.Type) -} - -// WithMetricsMapping attempts to wrap the metrics of a manager with a metrics -// mapping. -func WithMetricsMapping(nm bundle.NewManagement, m *metrics.Mapping) bundle.NewManagement { - if t, ok := nm.(*manager.Type); ok { - return t.WithMetricsMapping(m) - } - return nm -} - -func registerCacheTemplate(tmpl *compiled, env *bundle.Environment) error { - return env.CacheAdd(func(c cache.Config, nm bundle.NewManagement) (cache.V1, error) { - newConf, err := tmpl.Render(c.Plugin) - if err != nil { - return nil, err - } - - conf, err := cache.FromAny(env, newConf) - if err != nil { - return nil, err - } - - if tmpl.metricsMapping != nil { - nm = WithMetricsMapping(nm, tmpl.metricsMapping.WithStaticVars(map[string]any{ - "label": c.Label, - })) - } - return nm.NewCache(conf) - }, tmpl.spec) -} - -func registerInputTemplate(tmpl *compiled, env *bundle.Environment) error { - return env.InputAdd(func(c input.Config, nm bundle.NewManagement) (input.Streamed, error) { - newConf, err := tmpl.Render(c.Plugin) - if err != nil { - return nil, err - } - - conf, err := input.FromAny(env, newConf) - if err != nil { - return nil, err - } - - // Template processors inserted _before_ configured processors. - conf.Processors = append(conf.Processors, c.Processors...) - - if tmpl.metricsMapping != nil { - nm = WithMetricsMapping(nm, tmpl.metricsMapping.WithStaticVars(map[string]any{ - "label": c.Label, - })) - } - return nm.NewInput(conf) - }, tmpl.spec) -} - -func registerOutputTemplate(tmpl *compiled, env *bundle.Environment) error { - return env.OutputAdd(func(c output.Config, nm bundle.NewManagement, pcf ...processor.PipelineConstructorFunc) (output.Streamed, error) { - newConf, err := tmpl.Render(c.Plugin) - if err != nil { - return nil, err - } - - conf, err := output.FromAny(env, newConf) - if err != nil { - return nil, err - } - - // Template processors inserted _after_ configured processors. - conf.Processors = append(c.Processors, conf.Processors...) - - if tmpl.metricsMapping != nil { - nm = WithMetricsMapping(nm, tmpl.metricsMapping.WithStaticVars(map[string]any{ - "label": c.Label, - })) - } - return nm.NewOutput(conf, pcf...) - }, tmpl.spec) -} - -func registerProcessorTemplate(tmpl *compiled, env *bundle.Environment) error { - return env.ProcessorAdd(func(c processor.Config, nm bundle.NewManagement) (processor.V1, error) { - newConf, err := tmpl.Render(c.Plugin) - if err != nil { - return nil, err - } - - conf, err := processor.FromAny(env, newConf) - if err != nil { - return nil, err - } - - if tmpl.metricsMapping != nil { - nm = WithMetricsMapping(nm, tmpl.metricsMapping.WithStaticVars(map[string]any{ - "label": c.Label, - })) - } - return nm.NewProcessor(conf) - }, tmpl.spec) -} - -func registerRateLimitTemplate(tmpl *compiled, env *bundle.Environment) error { - return env.RateLimitAdd(func(c ratelimit.Config, nm bundle.NewManagement) (ratelimit.V1, error) { - newConf, err := tmpl.Render(c.Plugin) - if err != nil { - return nil, err - } - - conf, err := ratelimit.FromAny(env, newConf) - if err != nil { - return nil, err - } - - if tmpl.metricsMapping != nil { - nm = WithMetricsMapping(nm, tmpl.metricsMapping.WithStaticVars(map[string]any{ - "label": c.Label, - })) - } - return nm.NewRateLimit(conf) - }, tmpl.spec) -} diff --git a/internal/template/template_test.go b/internal/template/template_test.go deleted file mode 100644 index 2ec0fc4b28..0000000000 --- a/internal/template/template_test.go +++ /dev/null @@ -1,301 +0,0 @@ -package template_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/template" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestCacheTemplate(t *testing.T) { - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - require.NoError(t, template.RegisterTemplateYAML(mgr.Environment(), []byte(` -name: foo_memory -type: cache - -fields: - - name: foovalue - type: string - -mapping: | - root.memory.init_values.foo = this.foovalue -`))) - - conf, err := cache.FromAny(mgr, map[string]any{ - "foo_memory": map[string]any{ - "foovalue": "meow", - }, - }) - require.NoError(t, err) - - c, err := mgr.NewCache(conf) - require.NoError(t, err) - - res, err := c.Get(context.Background(), "foo") - require.NoError(t, err) - - assert.Equal(t, "meow", string(res)) -} - -func TestInputTemplate(t *testing.T) { - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - require.NoError(t, template.RegisterTemplateYAML(mgr.Environment(), []byte(` -name: generate_a_foo -type: input - -fields: - - name: name - type: string - -mapping: | - root.generate.count = 1 - root.generate.interval = "1ms" - root.generate.mapping = """root.foo = "%v" """.format(this.name) - root.processors = [ - { - "mutation": """root.bar = "and this too" """, - }, - ] -`))) - - conf, err := input.FromAny(mgr, map[string]any{ - "generate_a_foo": map[string]any{ - "name": "meow", - }, - "processors": []any{ - map[string]any{ - "mutation": "root.bar = this.bar.uppercase()", - }, - }, - }) - require.NoError(t, err) - - strm, err := mgr.NewInput(conf) - require.NoError(t, err) - - var tran message.Transaction - var open bool - select { - case tran, open = <-strm.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - require.True(t, open) - - require.Len(t, tran.Payload, 1) - assert.Equal(t, `{"bar":"AND THIS TOO","foo":"meow"}`, string(tran.Payload[0].AsBytes())) - - require.NoError(t, tran.Ack(context.Background(), nil)) - - select { - case _, open = <-strm.TransactionChan(): - case <-time.After(time.Second): - t.Fatal("timed out") - } - require.False(t, open) -} - -func TestOutputTemplate(t *testing.T) { - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - require.NoError(t, template.RegisterTemplateYAML(mgr.Environment(), []byte(` -name: write_inproc -type: output - -fields: - - name: name - type: string - -mapping: | - root.inproc = this.name - root.processors = [ - { - "mapping": "root = content().uppercase()", - }, - ] -`))) - - conf, err := output.FromAny(mgr, map[string]any{ - "write_inproc": map[string]any{ - "name": "foos", - }, - "processors": []any{ - map[string]any{ - "mapping": `root = content() + " woof"`, - }, - }, - }) - require.NoError(t, err) - - strm, err := mgr.NewOutput(conf) - require.NoError(t, err) - - tInChan := make(chan message.Transaction) - require.NoError(t, strm.Consume(tInChan)) - - tOutChan, err := mgr.GetPipe("foos") - require.NoError(t, err) - - select { - case tInChan <- message.NewTransactionFunc(message.Batch{ - message.NewPart([]byte("meow")), - }, func(ctx context.Context, err error) error { - return nil - }): - case <-time.After(time.Second): - t.Fatal("timed out") - } - - var tran message.Transaction - var open bool - select { - case tran, open = <-tOutChan: - case <-time.After(time.Second): - t.Fatal("timed out") - } - require.True(t, open) - - require.Len(t, tran.Payload, 1) - assert.Equal(t, `MEOW WOOF`, string(tran.Payload[0].AsBytes())) - - require.NoError(t, tran.Ack(context.Background(), nil)) - - close(tInChan) - strm.TriggerCloseNow() - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - require.NoError(t, strm.WaitForClose(ctx)) -} - -func TestProcessorTemplate(t *testing.T) { - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - require.NoError(t, template.RegisterTemplateYAML(mgr.Environment(), []byte(` -name: append_foo -type: processor - -fields: - - name: foo - type: string - -mapping: | - root.mapping = """root = content() + "%v" """.format(this.foo) -`))) - - conf, err := processor.FromAny(mgr, map[string]any{ - "append_foo": map[string]any{ - "foo": " meow", - }, - }) - require.NoError(t, err) - - p, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - res, err := p.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte("woof")), - }) - require.NoError(t, err) - require.Len(t, res, 1) - require.Len(t, res[0], 1) - assert.Equal(t, `woof meow`, string(res[0][0].AsBytes())) -} - -func TestProcessorTemplateOddIndentation(t *testing.T) { - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - require.NoError(t, template.RegisterTemplateYAML(mgr.Environment(), []byte(` -name: meow -type: processor - -mapping: | - map switch_if { - root.check = "this.go == true" - - root.processors = [ - { - "mutation": """ - root.id = this.id.uppercase() - """ - }, - ] - } - root.switch = [ - this.apply("switch_if") - ] -`))) - - conf, err := processor.FromAny(mgr, map[string]any{ - "meow": map[string]any{}, - }) - require.NoError(t, err) - - p, err := mgr.NewProcessor(conf) - require.NoError(t, err) - - res, err := p.ProcessBatch(context.Background(), message.Batch{ - message.NewPart([]byte(`{"go":true,"id":"aaa"}`)), - }) - require.NoError(t, err) - require.Len(t, res, 1) - require.Len(t, res[0], 1) - assert.Equal(t, `{"go":true,"id":"AAA"}`, string(res[0][0].AsBytes())) -} - -func TestRateLimitTemplate(t *testing.T) { - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - require.NoError(t, template.RegisterTemplateYAML(mgr.Environment(), []byte(` -name: foo -type: rate_limit - -fields: - - name: i - type: string - -mapping: | - root.local.count = 1 - root.local.interval = this.i -`))) - - conf, err := ratelimit.FromAny(mgr, map[string]any{ - "foo": map[string]any{ - "i": "1h", - }, - }) - require.NoError(t, err) - - r, err := mgr.NewRateLimit(conf) - require.NoError(t, err) - - d, err := r.Access(context.Background()) - require.NoError(t, err) - assert.Equal(t, d, time.Duration(0)) - - d, err = r.Access(context.Background()) - require.NoError(t, err) - assert.Greater(t, d, time.Hour-time.Minute) - assert.Less(t, d, time.Hour+time.Minute) -} diff --git a/internal/tls/docs.go b/internal/tls/docs.go deleted file mode 100644 index b59f0e88e9..0000000000 --- a/internal/tls/docs.go +++ /dev/null @@ -1,52 +0,0 @@ -package tls - -import "github.com/benthosdev/benthos/v4/internal/docs" - -// FieldSpec returns a spec for a common TLS field. -func FieldSpec() docs.FieldSpec { - return docs.FieldObject( - "tls", "Custom TLS settings can be used to override system defaults.", - ).WithChildren( - docs.FieldBool( - "enabled", "Whether custom TLS settings are enabled.", - ).HasDefault(false), - - docs.FieldBool( - "skip_cert_verify", "Whether to skip server side certificate verification.", - ).HasDefault(false), - - docs.FieldBool( - "enable_renegotiation", "Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`.", - ).AtVersion("3.45.0").Advanced().HasDefault(false), - - docs.FieldString( - "root_cas", "An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate.", "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", - ).HasDefault("").Secret(), - - docs.FieldString( - "root_cas_file", "An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate.", "./root_cas.pem", - ).HasDefault(""), - - docs.FieldObject( - "client_certs", "A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both.", - []any{ - map[string]any{ - "cert": "foo", - "key": "bar", - }, - }, - []any{ - map[string]any{ - "cert_file": "./example.pem", - "key_file": "./example.key", - }, - }, - ).Array().WithChildren( - docs.FieldString("cert", "A plain text certificate to use.").HasDefault(""), - docs.FieldString("key", "A plain text certificate key to use.").HasDefault("").Secret(), - docs.FieldString("cert_file", "The path of a certificate to use.").HasDefault(""), - docs.FieldString("key_file", "The path of a certificate key to use.").HasDefault(""), - docs.FieldString("password", "A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext.", "foo", "${KEY_PASSWORD}").HasDefault("").Secret(), - ).HasDefault([]any{}), - ).Advanced() -} diff --git a/internal/tls/package.go b/internal/tls/package.go deleted file mode 100644 index ff24476338..0000000000 --- a/internal/tls/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package tls provides Benthos configuration fields and wrappers for a -// crypto/tls config. -package tls diff --git a/internal/tls/type.go b/internal/tls/type.go deleted file mode 100644 index 46719351c8..0000000000 --- a/internal/tls/type.go +++ /dev/null @@ -1,214 +0,0 @@ -package tls - -import ( - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - - "github.com/youmark/pkcs8" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -// ClientCertConfig contains config fields for a client certificate. -type ClientCertConfig struct { - CertFile string `json:"cert_file" yaml:"cert_file"` - KeyFile string `json:"key_file" yaml:"key_file"` - Cert string `json:"cert" yaml:"cert"` - Key string `json:"key" yaml:"key"` - Password string `json:"password" yaml:"password"` -} - -// Config contains configuration params for TLS. -type Config struct { - Enabled bool `json:"enabled" yaml:"enabled"` - RootCAs string `json:"root_cas" yaml:"root_cas"` - RootCAsFile string `json:"root_cas_file" yaml:"root_cas_file"` - InsecureSkipVerify bool `json:"skip_cert_verify" yaml:"skip_cert_verify"` - ClientCertificates []ClientCertConfig `json:"client_certs" yaml:"client_certs"` - EnableRenegotiation bool `json:"enable_renegotiation" yaml:"enable_renegotiation"` -} - -// NewConfig creates a new Config with default values. -func NewConfig() Config { - return Config{ - Enabled: false, - RootCAs: "", - RootCAsFile: "", - InsecureSkipVerify: false, - ClientCertificates: []ClientCertConfig{}, - EnableRenegotiation: false, - } -} - -//------------------------------------------------------------------------------ - -func defaultTLSConfig() *tls.Config { - return &tls.Config{ - MinVersion: tls.VersionTLS12, - } -} - -// GetNonToggled returns a valid *tls.Config based on the configuration values -// of Config. If none of the config fields are set then a nil config is -// returned. -func (c *Config) GetNonToggled(f ifs.FS) (*tls.Config, error) { - var tlsConf *tls.Config - initConf := func() { - if tlsConf != nil { - return - } - tlsConf = defaultTLSConfig() - } - - if c.RootCAs != "" && c.RootCAsFile != "" { - return nil, errors.New("only one field between root_cas and root_cas_file can be specified") - } - - if c.RootCAsFile != "" { - caCert, err := ifs.ReadFile(f, c.RootCAsFile) - if err != nil { - return nil, err - } - initConf() - tlsConf.RootCAs = x509.NewCertPool() - tlsConf.RootCAs.AppendCertsFromPEM(caCert) - } - - if c.RootCAs != "" { - initConf() - tlsConf.RootCAs = x509.NewCertPool() - tlsConf.RootCAs.AppendCertsFromPEM([]byte(c.RootCAs)) - } - - for _, conf := range c.ClientCertificates { - cert, err := conf.Load(f) - if err != nil { - return nil, err - } - initConf() - tlsConf.Certificates = append(tlsConf.Certificates, cert) - } - - if c.EnableRenegotiation { - initConf() - tlsConf.Renegotiation = tls.RenegotiateFreelyAsClient - } - - if c.InsecureSkipVerify { - initConf() - tlsConf.InsecureSkipVerify = true - } - - return tlsConf, nil -} - -// Get returns a valid *tls.Config based on the configuration values of Config, -// or nil if tls is not enabled. -func (c *Config) Get(f ifs.FS) (*tls.Config, error) { - if !c.Enabled { - return nil, nil - } - tConf, err := c.GetNonToggled(f) - if err != nil { - return nil, err - } - if tConf == nil { - tConf = defaultTLSConfig() - } - return tConf, nil -} - -func getKeyPair(cert []byte, keyType string, keyBytes []byte) (tls.Certificate, error) { - return tls.X509KeyPair(cert, pem.EncodeToMemory(&pem.Block{Type: keyType, Bytes: keyBytes})) -} - -func loadKeyPair(cert, key []byte, password string) (tls.Certificate, error) { - keyPem, _ := pem.Decode(key) - if keyPem == nil { - return tls.Certificate{}, errors.New("failed to decode private key") - } - - var err error - //nolint:staticcheck // SA1019 Disable linting for deprecated x509.IsEncryptedPEMBlock call - if x509.IsEncryptedPEMBlock(keyPem) { - if password == "" { - return tls.Certificate{}, errors.New("missing password for PKCS#1 encrypted private key") - } - - var decryptedKey []byte - //nolint:staticcheck // SA1019 Disable linting for deprecated x509.DecryptPEMBlock call - if decryptedKey, err = x509.DecryptPEMBlock(keyPem, []byte(password)); err != nil { - return tls.Certificate{}, fmt.Errorf("failed to parse encrypted PKCS#1 private key: %s", err) - } - - // x509.DecryptPEMBlock() can sometimes fail to detect invalid passwords and will return a nil error, so we - // should validate the decrypted key. Otherwise, tls.X509KeyPair() will return an error anyway, but it - // wouldn't be clear why. Details here: https://github.com/golang/go/issues/10171 - // and here https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/crypto/tls/tls.go;l=339 - validKey := false - if _, err = x509.ParsePKCS1PrivateKey(decryptedKey); err == nil { - validKey = true - } - if _, err = x509.ParsePKCS8PrivateKey(decryptedKey); err == nil { - validKey = true - } - if _, err := x509.ParseECPrivateKey(decryptedKey); err == nil { - validKey = true - } - if !validKey { - return tls.Certificate{}, fmt.Errorf("failed to decrypt PKCS#1 key: %s", x509.IncorrectPasswordError) - } - - return getKeyPair(cert, keyPem.Type, decryptedKey) - } else if keyPem.Type == "ENCRYPTED PRIVATE KEY" { - if password == "" { - return tls.Certificate{}, errors.New("missing password for PKCS#8 encrypted private key") - } - - var decryptedKey *rsa.PrivateKey - if decryptedKey, err = pkcs8.ParsePKCS8PrivateKeyRSA(keyPem.Bytes, []byte(password)); err != nil { - return tls.Certificate{}, fmt.Errorf("failed to parse encrypted PKCS#8 private key: %s", err) - } - return getKeyPair(cert, keyPem.Type, x509.MarshalPKCS1PrivateKey(decryptedKey)) - } - - return tls.X509KeyPair(cert, key) -} - -// Load returns a TLS certificate, based on either file paths in the -// config or the raw certs as strings. -func (c *ClientCertConfig) Load(f ifs.FS) (tls.Certificate, error) { - if c.CertFile != "" || c.KeyFile != "" { - if c.CertFile == "" { - return tls.Certificate{}, errors.New("missing cert_file field in client certificate config") - } - if c.KeyFile == "" { - return tls.Certificate{}, errors.New("missing key_file field in client certificate config") - } - - cert, err := ifs.ReadFile(f, c.CertFile) - if err != nil { - return tls.Certificate{}, err - } - - key, err := ifs.ReadFile(f, c.KeyFile) - if err != nil { - return tls.Certificate{}, err - } - - return loadKeyPair(cert, key, c.Password) - } - - if c.Cert == "" { - return tls.Certificate{}, errors.New("missing cert field in client certificate config") - } - if c.Key == "" { - return tls.Certificate{}, errors.New("missing key field in client certificate config") - } - - return loadKeyPair([]byte(c.Cert), []byte(c.Key), c.Password) -} diff --git a/internal/tls/type_test.go b/internal/tls/type_test.go deleted file mode 100644 index af326dfbe1..0000000000 --- a/internal/tls/type_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package tls - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "log" - "math/big" - "os" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/youmark/pkcs8" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -func createCertificates() (certPem, keyPem []byte) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - - priv := x509.MarshalPKCS1PrivateKey(key) - - tml := x509.Certificate{ - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(5, 0, 0), - SerialNumber: big.NewInt(123123), - Subject: pkix.Name{ - CommonName: "Benthos", - Organization: []string{"Benthos"}, - }, - BasicConstraintsValid: true, - } - - cert, err := x509.CreateCertificate(rand.Reader, &tml, &tml, &key.PublicKey, key) - if err != nil { - log.Fatal("Certificate cannot be created.", err.Error()) - } - - certPem = pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert, - }) - - keyPem = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: priv}) - - return certPem, keyPem -} - -type keyPair struct { - cert []byte - key []byte -} - -func createCertificatesWithEncryptedPKCS1Key(t *testing.T, password string) keyPair { - t.Helper() - - certPem, keyPem := createCertificates() - decodedKey, _ := pem.Decode(keyPem) - - //nolint:staticcheck // SA1019 Disable linting for deprecated x509.EncryptPEMBlock call - block, err := x509.EncryptPEMBlock(rand.Reader, decodedKey.Type, decodedKey.Bytes, []byte(password), x509.PEMCipher3DES) - require.NoError(t, err) - - keyPem = pem.EncodeToMemory( - block, - ) - return keyPair{cert: certPem, key: keyPem} -} - -func createCertificatesWithEncryptedPKCS8Key(t *testing.T, password string) keyPair { - t.Helper() - - certPem, keyPem := createCertificates() - pemBlock, _ := pem.Decode(keyPem) - decodedKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) - require.NoError(t, err) - - keyBytes, err := pkcs8.ConvertPrivateKeyToPKCS8(decodedKey, []byte(password)) - require.NoError(t, err) - - return keyPair{cert: certPem, key: pem.EncodeToMemory(&pem.Block{Type: "ENCRYPTED PRIVATE KEY", Bytes: keyBytes})} -} - -func TestCertificateFileWithEncryptedKey(t *testing.T) { - tests := []struct { - name string - kp keyPair - }{ - { - name: "PKCS#1", - kp: createCertificatesWithEncryptedPKCS1Key(t, "benthos"), - }, - { - name: "PKCS#8", - kp: createCertificatesWithEncryptedPKCS8Key(t, "benthos"), - }, - } - - tmpDir := t.TempDir() - for _, test := range tests { - fCert, _ := os.CreateTemp(tmpDir, "cert.pem") - _, _ = fCert.Write(test.kp.cert) - fCert.Close() - - fKey, _ := os.CreateTemp(tmpDir, "key.pem") - _, _ = fKey.Write(test.kp.key) - fKey.Close() - - c := ClientCertConfig{ - KeyFile: fKey.Name(), - CertFile: fCert.Name(), - Password: "benthos", - } - - _, err := c.Load(ifs.OS()) - if err != nil { - t.Errorf("Failed to load %s certificate: %s", test.name, err) - } - } -} - -func TestCertificateWithEncryptedKey(t *testing.T) { - tests := []struct { - name string - kp keyPair - }{ - { - name: "PKCS#1", - kp: createCertificatesWithEncryptedPKCS1Key(t, "benthos"), - }, - { - name: "PKCS#8", - kp: createCertificatesWithEncryptedPKCS8Key(t, "benthos"), - }, - } - - for _, test := range tests { - c := ClientCertConfig{ - Cert: string(test.kp.cert), - Key: string(test.kp.key), - Password: "benthos", - } - - _, err := c.Load(ifs.OS()) - if err != nil { - t.Errorf("Failed to load %s certificate: %s", test.name, err) - } - } -} - -func TestCertificateFileWithEncryptedKeyAndWrongPassword(t *testing.T) { - tests := []struct { - name string - kp keyPair - err string - }{ - { - name: "PKCS#1", - kp: createCertificatesWithEncryptedPKCS1Key(t, "benthos"), - err: "x509: decryption password incorrect", - }, - { - name: "PKCS#8", - kp: createCertificatesWithEncryptedPKCS8Key(t, "benthos"), - err: "pkcs8: incorrect password", - }, - } - - tmpDir := t.TempDir() - for _, test := range tests { - fCert, _ := os.CreateTemp(tmpDir, "cert.pem") - _, _ = fCert.Write(test.kp.cert) - fCert.Close() - - fKey, _ := os.CreateTemp(tmpDir, "key.pem") - _, _ = fKey.Write(test.kp.key) - fKey.Close() - - c := ClientCertConfig{ - KeyFile: fKey.Name(), - CertFile: fCert.Name(), - Password: "not_bentho", - } - - _, err := c.Load(ifs.OS()) - require.ErrorContains(t, err, test.err, test.name) - } -} - -func TestEncryptedKeyWithWrongPassword(t *testing.T) { - tests := []struct { - name string - kp keyPair - err string - }{ - { - name: "PKCS#1", - kp: createCertificatesWithEncryptedPKCS1Key(t, "benthos"), - err: "x509: decryption password incorrect", - }, - { - name: "PKCS#8", - kp: createCertificatesWithEncryptedPKCS8Key(t, "benthos"), - err: "pkcs8: incorrect password", - }, - } - - for _, test := range tests { - c := ClientCertConfig{ - Cert: string(test.kp.cert), - Key: string(test.kp.key), - Password: "not_bentho", - } - - _, err := c.Load(ifs.OS()) - require.ErrorContains(t, err, test.err, test.name) - } -} - -func TestCertificateFileWithNoEncryption(t *testing.T) { - cert, key := createCertificates() - - tmpDir := t.TempDir() - - fCert, _ := os.CreateTemp(tmpDir, "cert.pem") - _, _ = fCert.Write(cert) - defer fCert.Close() - - fKey, _ := os.CreateTemp(tmpDir, "key.pem") - _, _ = fKey.Write(key) - defer fKey.Close() - - c := ClientCertConfig{ - KeyFile: fKey.Name(), - CertFile: fCert.Name(), - } - - _, err := c.Load(ifs.OS()) - if err != nil { - t.Errorf("Failed to load certificate %s", err) - } -} - -func TestCertificateWithNoEncryption(t *testing.T) { - cert, key := createCertificates() - - c := ClientCertConfig{ - Key: string(key), - Cert: string(cert), - } - - _, err := c.Load(ifs.OS()) - if err != nil { - t.Errorf("Failed to load certificate %s", err) - } -} diff --git a/internal/tracing/otel.go b/internal/tracing/otel.go deleted file mode 100644 index adf3c22f14..0000000000 --- a/internal/tracing/otel.go +++ /dev/null @@ -1,158 +0,0 @@ -package tracing - -import ( - "context" - "strings" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -const ( - name = "benthos" -) - -// GetSpan returns a span attached to a message part. Returns nil if the part -// doesn't have a span attached. -func GetSpan(p *message.Part) *Span { - ctx := message.GetContext(p) - return GetSpanFromContext(ctx) -} - -// GetSpan returns a span within a context. Returns nil if the context doesn't -// have a span attached. -func GetSpanFromContext(ctx context.Context) *Span { - t := trace.SpanFromContext(ctx) - return OtelSpan(ctx, t) -} - -// GetActiveSpan returns a span attached to a message part. Returns nil if the -// part doesn't have a span attached or it is inactive. -func GetActiveSpan(p *message.Part) *Span { - ctx := message.GetContext(p) - t := trace.SpanFromContext(ctx) - if !t.IsRecording() { - return nil - } - return OtelSpan(ctx, t) -} - -// GetTraceID returns the traceID from a span attached to a message part. Returns a zeroed traceID if the part -// doesn't have a span attached. -func GetTraceID(p *message.Part) string { - ctx := message.GetContext(p) - span := trace.SpanFromContext(ctx) - return span.SpanContext().TraceID().String() -} - -// WithChildSpan takes a message, extracts a span, creates a new child span, -// and returns a new message with that span embedded. The original message is -// unchanged. -func WithChildSpan(prov trace.TracerProvider, operationName string, part *message.Part) (*message.Part, *Span) { - span := GetActiveSpan(part) - if span == nil { - ctx, t := prov.Tracer(name).Start(part.GetContext(), operationName) - span = OtelSpan(ctx, t) - part = part.WithContext(ctx) - } else { - ctx, t := prov.Tracer(name).Start(span.ctx, operationName) - span = OtelSpan(ctx, t) - part = part.WithContext(ctx) - } - return part, span -} - -// WithChildSpans takes a message, extracts spans per message part, creates new -// child spans, and returns a new message with those spans embedded. The -// original message is unchanged. -func WithChildSpans(prov trace.TracerProvider, operationName string, batch message.Batch) (message.Batch, []*Span) { - spans := make([]*Span, 0, len(batch)) - newParts := make(message.Batch, len(batch)) - for i, part := range batch { - if part == nil { - continue - } - var otSpan *Span - newParts[i], otSpan = WithChildSpan(prov, operationName, part) - spans = append(spans, otSpan) - } - return newParts, spans -} - -// WithSiblingSpans takes a message, extracts spans per message part, creates -// new sibling spans, and returns a new message with those spans embedded. The -// original message is unchanged. -func WithSiblingSpans(prov trace.TracerProvider, operationName string, batch message.Batch) (message.Batch, []*Span) { - spans := make([]*Span, 0, len(batch)) - newParts := make([]*message.Part, batch.Len()) - for i, part := range batch { - if part == nil { - continue - } - otSpan := GetActiveSpan(part) - if otSpan == nil { - ctx, t := prov.Tracer(name).Start(part.GetContext(), operationName) - otSpan = OtelSpan(ctx, t) - } else { - ctx, t := prov.Tracer(name).Start( - part.GetContext(), operationName, - trace.WithLinks(trace.LinkFromContext(otSpan.ctx)), - ) - otSpan = OtelSpan(ctx, t) - } - newParts[i] = message.WithContext(otSpan.ctx, part) - spans = append(spans, otSpan) - } - return newParts, spans -} - -//------------------------------------------------------------------------------ - -// InitSpans sets up OpenTracing spans on each message part if one does not -// already exist. -func InitSpans(prov trace.TracerProvider, operationName string, batch message.Batch) { - for i, p := range batch { - batch[i] = InitSpan(prov, operationName, p) - } -} - -// InitSpan sets up an OpenTracing span on a message part if one does not -// already exist. -func InitSpan(prov trace.TracerProvider, operationName string, part *message.Part) *message.Part { - if GetActiveSpan(part) != nil { - return part - } - ctx, _ := prov.Tracer(name).Start(part.GetContext(), operationName) - return message.WithContext(ctx, part) -} - -// InitSpansFromParentTextMap obtains a span parent reference from a text map -// and creates child spans for each message. -func InitSpansFromParentTextMap(prov trace.TracerProvider, operationName string, textMapGeneric map[string]any, batch message.Batch) error { - c := propagation.MapCarrier{} - for k, v := range textMapGeneric { - if vStr, ok := v.(string); ok { - c[strings.ToLower(k)] = vStr - } - } - - textProp := otel.GetTextMapPropagator() - for i, p := range batch { - ctx := textProp.Extract(p.GetContext(), c) - pCtx, _ := prov.Tracer(name).Start(ctx, operationName) - batch[i] = message.WithContext(pCtx, p) - } - return nil -} - -// FinishSpans calls Finish on all message parts containing a span. -func FinishSpans(batch message.Batch) { - for _, p := range batch { - if span := GetActiveSpan(p); span != nil { - span.unwrap().End() - } - } -} diff --git a/internal/tracing/otel_test.go b/internal/tracing/otel_test.go deleted file mode 100644 index bc4ba214ca..0000000000 --- a/internal/tracing/otel_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package tracing - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestInitSpansFromParentTextMap(t *testing.T) { - t.Run("it will update the context for each message in the batch", func(t *testing.T) { - textMap := map[string]any{ - "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - } - - msgOne := message.NewPart([]byte("hello")) - msgTwo := message.NewPart([]byte("world")) - - batch := message.Batch([]*message.Part{msgOne, msgTwo}) - - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) - tp := noop.NewTracerProvider() - - err := InitSpansFromParentTextMap(tp, "test", textMap, batch) - assert.NoError(t, err) - - spanOne := trace.SpanFromContext(batch[0].GetContext()) - assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", spanOne.SpanContext().TraceID().String()) - assert.Equal(t, "00f067aa0ba902b7", spanOne.SpanContext().SpanID().String()) - - spanTwo := trace.SpanFromContext(batch[1].GetContext()) - assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", spanTwo.SpanContext().TraceID().String()) - assert.Equal(t, "00f067aa0ba902b7", spanTwo.SpanContext().SpanID().String()) - }) -} diff --git a/internal/tracing/package.go b/internal/tracing/package.go deleted file mode 100644 index 1ef918a95d..0000000000 --- a/internal/tracing/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package tracing implements utility functions for interacting with a global -// tracing system. Currently this system uses the opentelemetry APIs. -package tracing diff --git a/internal/tracing/span.go b/internal/tracing/span.go deleted file mode 100644 index c6c35976cd..0000000000 --- a/internal/tracing/span.go +++ /dev/null @@ -1,75 +0,0 @@ -package tracing - -import ( - "context" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" -) - -// Span abstracts the span type of our global tracing system in order to allow -// it to be replaced in future. -type Span struct { - ctx context.Context - w trace.Span -} - -// OtelSpan creates a common span from the open telemetry package. -func OtelSpan(ctx context.Context, s trace.Span) *Span { - if s == nil { - return nil - } - return &Span{ctx: ctx, w: s} -} - -func (s *Span) unwrap() trace.Span { - if s == nil { - return nil - } - return s.w -} - -// LogKV adds log key/value pairs to the span. -func (s *Span) LogKV(name string, kv ...string) { - if s == nil { - return - } - var attrs []attribute.KeyValue - for i := 0; i < len(kv)-1; i += 2 { - attrs = append(attrs, attribute.String(kv[i], kv[i+1])) - } - s.w.AddEvent(name, trace.WithAttributes(attrs...)) -} - -// SetTag sets a given tag to a value. -func (s *Span) SetTag(key, value string) { - if s == nil { - return - } - s.w.SetAttributes(attribute.String(key, value)) -} - -// Finish the span. -func (s *Span) Finish() { - if s == nil { - return - } - s.w.End() -} - -// TextMap attempts to inject a span into a map object in text map format. -func (s *Span) TextMap() (map[string]any, error) { - if s == nil { - return nil, nil - } - c := propagation.MapCarrier{} - otel.GetTextMapPropagator().Inject(s.ctx, c) - - spanMapGeneric := make(map[string]any, len(c)) - for k, v := range c { - spanMapGeneric[k] = v - } - return spanMapGeneric, nil -} diff --git a/internal/tracing/v2/otel.go b/internal/tracing/v2/otel.go deleted file mode 100644 index 7ecfdf7b40..0000000000 --- a/internal/tracing/v2/otel.go +++ /dev/null @@ -1,155 +0,0 @@ -package tracing - -import ( - "context" - "strings" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - name = "benthos" -) - -// GetSpan returns a span attached to a message part. Returns nil if the part -// doesn't have a span attached. -func GetSpan(p *service.Message) *Span { - return GetSpanFromContext(p.Context()) -} - -// GetSpan returns a span within a context. Returns nil if the context doesn't -// have a span attached. -func GetSpanFromContext(ctx context.Context) *Span { - t := trace.SpanFromContext(ctx) - return OtelSpan(ctx, t) -} - -// GetActiveSpan returns a span attached to a message part. Returns nil if the -// part doesn't have a span attached or it is inactive. -func GetActiveSpan(p *service.Message) *Span { - t := trace.SpanFromContext(p.Context()) - if !t.IsRecording() { - return nil - } - return OtelSpan(p.Context(), t) -} - -// GetTraceID returns the traceID from a span attached to a message part. Returns a zeroed traceID if the part -// doesn't have a span attached. -func GetTraceID(p *service.Message) string { - span := trace.SpanFromContext(p.Context()) - return span.SpanContext().TraceID().String() -} - -// WithChildSpan takes a message, extracts a span, creates a new child span, -// and returns a new message with that span embedded. The original message is -// unchanged. -func WithChildSpan(prov trace.TracerProvider, operationName string, part *service.Message) (*service.Message, *Span) { - span := GetActiveSpan(part) - if span == nil { - ctx, t := prov.Tracer(name).Start(part.Context(), operationName) - span = OtelSpan(ctx, t) - part = part.WithContext(ctx) - } else { - ctx, t := prov.Tracer(name).Start(span.ctx, operationName) - span = OtelSpan(ctx, t) - part = part.WithContext(ctx) - } - return part, span -} - -// WithChildSpans takes a message, extracts spans per message part, creates new -// child spans, and returns a new message with those spans embedded. The -// original message is unchanged. -func WithChildSpans(prov trace.TracerProvider, operationName string, batch service.MessageBatch) (service.MessageBatch, []*Span) { - spans := make([]*Span, 0, len(batch)) - newParts := make(service.MessageBatch, len(batch)) - for i, part := range batch { - if part == nil { - continue - } - var otSpan *Span - newParts[i], otSpan = WithChildSpan(prov, operationName, part) - spans = append(spans, otSpan) - } - return newParts, spans -} - -// WithSiblingSpans takes a message, extracts spans per message part, creates -// new sibling spans, and returns a new message with those spans embedded. The -// original message is unchanged. -func WithSiblingSpans(prov trace.TracerProvider, operationName string, batch service.MessageBatch) (service.MessageBatch, []*Span) { - spans := make([]*Span, 0, len(batch)) - newParts := make([]*service.Message, len(batch)) - for i, part := range batch { - if part == nil { - continue - } - otSpan := GetActiveSpan(part) - if otSpan == nil { - ctx, t := prov.Tracer(name).Start(part.Context(), operationName) - otSpan = OtelSpan(ctx, t) - } else { - ctx, t := prov.Tracer(name).Start( - part.Context(), operationName, - trace.WithLinks(trace.LinkFromContext(otSpan.ctx)), - ) - otSpan = OtelSpan(ctx, t) - } - newParts[i] = part.WithContext(otSpan.ctx) - spans = append(spans, otSpan) - } - return newParts, spans -} - -//------------------------------------------------------------------------------ - -// InitSpans sets up OpenTracing spans on each message part if one does not -// already exist. -func InitSpans(prov trace.TracerProvider, operationName string, batch service.MessageBatch) { - for i, p := range batch { - batch[i] = InitSpan(prov, operationName, p) - } -} - -// InitSpan sets up an OpenTracing span on a message part if one does not -// already exist. -func InitSpan(prov trace.TracerProvider, operationName string, part *service.Message) *service.Message { - if GetActiveSpan(part) != nil { - return part - } - ctx, _ := prov.Tracer(name).Start(part.Context(), operationName) - return part.WithContext(ctx) -} - -// InitSpansFromParentTextMap obtains a span parent reference from a text map -// and creates child spans for each message. -func InitSpansFromParentTextMap(prov trace.TracerProvider, operationName string, textMapGeneric map[string]any, batch service.MessageBatch) error { - c := propagation.MapCarrier{} - for k, v := range textMapGeneric { - if vStr, ok := v.(string); ok { - c[strings.ToLower(k)] = vStr - } - } - - textProp := otel.GetTextMapPropagator() - for i, p := range batch { - ctx := textProp.Extract(p.Context(), c) - pCtx, _ := prov.Tracer(name).Start(ctx, operationName) - batch[i] = p.WithContext(pCtx) - } - return nil -} - -// FinishSpans calls Finish on all message parts containing a span. -func FinishSpans(batch service.MessageBatch) { - for _, p := range batch { - if span := GetActiveSpan(p); span != nil { - span.unwrap().End() - } - } -} diff --git a/internal/tracing/v2/otel_test.go b/internal/tracing/v2/otel_test.go deleted file mode 100644 index d3d6816394..0000000000 --- a/internal/tracing/v2/otel_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package tracing - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestInitSpansFromParentTextMap(t *testing.T) { - t.Run("it will update the context for each message in the batch", func(t *testing.T) { - textMap := map[string]any{ - "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - } - - msgOne := service.NewMessage([]byte("hello")) - msgTwo := service.NewMessage([]byte("world")) - - batch := service.MessageBatch{msgOne, msgTwo} - - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) - tp := noop.NewTracerProvider() - - err := InitSpansFromParentTextMap(tp, "test", textMap, batch) - assert.NoError(t, err) - - spanOne := trace.SpanFromContext(batch[0].Context()) - assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", spanOne.SpanContext().TraceID().String()) - assert.Equal(t, "00f067aa0ba902b7", spanOne.SpanContext().SpanID().String()) - - spanTwo := trace.SpanFromContext(batch[1].Context()) - assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", spanTwo.SpanContext().TraceID().String()) - assert.Equal(t, "00f067aa0ba902b7", spanTwo.SpanContext().SpanID().String()) - }) -} diff --git a/internal/tracing/v2/package.go b/internal/tracing/v2/package.go deleted file mode 100644 index 1ef918a95d..0000000000 --- a/internal/tracing/v2/package.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package tracing implements utility functions for interacting with a global -// tracing system. Currently this system uses the opentelemetry APIs. -package tracing diff --git a/internal/tracing/v2/span.go b/internal/tracing/v2/span.go deleted file mode 100644 index c6c35976cd..0000000000 --- a/internal/tracing/v2/span.go +++ /dev/null @@ -1,75 +0,0 @@ -package tracing - -import ( - "context" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" -) - -// Span abstracts the span type of our global tracing system in order to allow -// it to be replaced in future. -type Span struct { - ctx context.Context - w trace.Span -} - -// OtelSpan creates a common span from the open telemetry package. -func OtelSpan(ctx context.Context, s trace.Span) *Span { - if s == nil { - return nil - } - return &Span{ctx: ctx, w: s} -} - -func (s *Span) unwrap() trace.Span { - if s == nil { - return nil - } - return s.w -} - -// LogKV adds log key/value pairs to the span. -func (s *Span) LogKV(name string, kv ...string) { - if s == nil { - return - } - var attrs []attribute.KeyValue - for i := 0; i < len(kv)-1; i += 2 { - attrs = append(attrs, attribute.String(kv[i], kv[i+1])) - } - s.w.AddEvent(name, trace.WithAttributes(attrs...)) -} - -// SetTag sets a given tag to a value. -func (s *Span) SetTag(key, value string) { - if s == nil { - return - } - s.w.SetAttributes(attribute.String(key, value)) -} - -// Finish the span. -func (s *Span) Finish() { - if s == nil { - return - } - s.w.End() -} - -// TextMap attempts to inject a span into a map object in text map format. -func (s *Span) TextMap() (map[string]any, error) { - if s == nil { - return nil, nil - } - c := propagation.MapCarrier{} - otel.GetTextMapPropagator().Inject(s.ctx, c) - - spanMapGeneric := make(map[string]any, len(c)) - for k, v := range c { - spanMapGeneric[k] = v - } - return spanMapGeneric, nil -} diff --git a/internal/transaction/benchmarks_test.go b/internal/transaction/benchmarks_test.go deleted file mode 100644 index b3cdd869ba..0000000000 --- a/internal/transaction/benchmarks_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package transaction - -import ( - "context" - "fmt" - "sync" - "testing" - - "github.com/stretchr/testify/require" -) - -func BenchmarkTransactionsChannelN10(b *testing.B) { - benchmarkTransactionsChannelBased(b, 10) -} - -func BenchmarkTransactionsChannelN100(b *testing.B) { - benchmarkTransactionsChannelBased(b, 100) -} - -func BenchmarkTransactionsChannelN1000(b *testing.B) { - benchmarkTransactionsChannelBased(b, 1000) -} - -func BenchmarkTransactionsChannelN10000(b *testing.B) { - benchmarkTransactionsChannelBased(b, 10000) -} - -func BenchmarkTransactionsFuncN10(b *testing.B) { - benchmarkTransactionsFuncBased(b, 10) -} - -func BenchmarkTransactionsFuncN100(b *testing.B) { - benchmarkTransactionsFuncBased(b, 100) -} - -func BenchmarkTransactionsFuncN1000(b *testing.B) { - benchmarkTransactionsFuncBased(b, 1000) -} - -func BenchmarkTransactionsFuncN10000(b *testing.B) { - benchmarkTransactionsFuncBased(b, 10000) -} - -type messageDumb struct { - raw []byte -} - -type transactionChanRes struct { - m messageDumb - resChan chan<- error -} - -type transactionFnRes struct { - m messageDumb - resFn func(context.Context, error) error -} - -func benchmarkTransactionsChannelBased(b *testing.B, buffered int) { - tChan := make(chan transactionChanRes) - - b.ReportAllocs() - - var wg sync.WaitGroup - go func() { - for i := 0; i < b.N; i++ { - wg.Add(1) - - rChan := make(chan error) - tChan <- transactionChanRes{ - m: messageDumb{raw: []byte(fmt.Sprintf("hello world %v", i))}, - resChan: rChan, - } - - go func() { - defer wg.Done() - <-rChan - }() - } - close(tChan) - }() - - rChans := make([]chan<- error, buffered) - ackAll := func() { - for j := 0; j < buffered; j++ { - if rChans[j] == nil { - return - } - rChans[j] <- nil - } - } - for { - for j := 0; j < buffered; j++ { - rChans[j] = nil - } - for j := 0; j < buffered; j++ { - tran, open := <-tChan - if !open { - ackAll() - wg.Wait() - return - } - rChans[j] = tran.resChan - } - ackAll() - } -} - -func benchmarkTransactionsFuncBased(b *testing.B, buffered int) { - tChan := make(chan transactionFnRes) - - b.ReportAllocs() - - var wg sync.WaitGroup - go func() { - for i := 0; i < b.N; i++ { - wg.Add(1) - tChan <- transactionFnRes{ - m: messageDumb{raw: []byte(fmt.Sprintf("hello world %v", i))}, - resFn: func(c context.Context, e error) error { - wg.Done() - return nil - }, - } - } - close(tChan) - }() - - rFns := make([]func(context.Context, error) error, buffered) - ackAll := func() { - for j := 0; j < buffered; j++ { - if rFns[j] == nil { - return - } - require.NoError(b, rFns[j](context.Background(), nil)) - } - } - for { - for j := 0; j < buffered; j++ { - rFns[j] = nil - } - for j := 0; j < buffered; j++ { - tran, open := <-tChan - if !open { - ackAll() - wg.Wait() - return - } - rFns[j] = tran.resFn - } - ackAll() - } -} diff --git a/internal/transaction/result_store.go b/internal/transaction/result_store.go deleted file mode 100644 index dd4685d948..0000000000 --- a/internal/transaction/result_store.go +++ /dev/null @@ -1,102 +0,0 @@ -package transaction - -import ( - "context" - "errors" - "sync" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// ErrNoStore is an error returned by components attempting to write a message -// batch to a ResultStore but are unable to locate the store within the batch -// context. -var ErrNoStore = errors.New("result store not found within batch context") - -// ResultStoreKeyType is the recommended type of a context key for adding -// ResultStores to a message context. -type ResultStoreKeyType int - -// ResultStoreKey is the recommended key value for adding ResultStores to a -// message context. -const ResultStoreKey ResultStoreKeyType = iota - -// ResultStore is a type designed to be propagated along with a message as a way -// for an output destination to store the final version of the message payload -// as it saw it. -// -// It is intended that this structure is placed within a message via an attached -// context, usually under the key 'result_store'. -type ResultStore interface { - // Add a message to the store. The message will be deep copied and have its - // context wiped before storing, and is therefore safe to add even when - // ownership of the message is about to be yielded. - Add(msg message.Batch) - - // Get the stored slice of messages. - Get() []message.Batch - - // Clear any currently stored messages. - Clear() -} - -//------------------------------------------------------------------------------ - -type resultStoreImpl struct { - payloads []message.Batch - sync.RWMutex -} - -func (r *resultStoreImpl) Add(msg message.Batch) { - newBatch := make(message.Batch, len(msg)) - for i, p := range msg { - newBatch[i] = message.WithContext(context.Background(), p.DeepCopy()) - } - r.Lock() - r.payloads = append(r.payloads, newBatch) - r.Unlock() -} - -func (r *resultStoreImpl) Get() []message.Batch { - r.RLock() - defer r.RUnlock() - return r.payloads -} - -func (r *resultStoreImpl) Clear() { - r.Lock() - r.payloads = nil - r.Unlock() -} - -//------------------------------------------------------------------------------ - -// NewResultStore returns an implementation of ResultStore. -func NewResultStore() ResultStore { - return &resultStoreImpl{} -} - -//------------------------------------------------------------------------------ - -// AddResultStore sets a result store within the context of the provided message -// that allows a roundtrip.Writer or any other component to propagate a -// resulting message back to the origin. -func AddResultStore(msg message.Batch, store ResultStore) { - for i, p := range msg { - ctx := message.GetContext(p) - msg[i] = message.WithContext(context.WithValue(ctx, ResultStoreKey, store), p) - } -} - -// SetAsResponse takes a mutated message and stores it as a response message, -// this action fails if the message does not contain a valid ResultStore within -// its context. -func SetAsResponse(msg message.Batch) error { - ctx := message.GetContext(msg.Get(0)) - store, ok := ctx.Value(ResultStoreKey).(ResultStore) - if !ok { - return ErrNoStore - } - store.Add(msg) - return nil -} diff --git a/internal/transaction/result_store_test.go b/internal/transaction/result_store_test.go deleted file mode 100644 index 3a37ecfff6..0000000000 --- a/internal/transaction/result_store_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package transaction - -import ( - "context" - "testing" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestResultStore(t *testing.T) { - impl := &resultStoreImpl{} - ctx := context.WithValue(context.Background(), ResultStoreKey, impl) - msg := message.Batch{ - message.WithContext(ctx, message.NewPart([]byte("foo"))), - message.NewPart([]byte("bar")), - } - - impl.Add(msg) - results := impl.Get() - if len(results) != 1 { - t.Fatalf("Wrong count of result batches: %v", len(results)) - } - if results[0].Len() != 2 { - t.Fatalf("Wrong count of messages: %v", results[0].Len()) - } - if exp, act := "foo", string(results[0].Get(0).AsBytes()); exp != act { - t.Errorf("Wrong message contents: %v != %v", act, exp) - } - if exp, act := "bar", string(results[0].Get(1).AsBytes()); exp != act { - t.Errorf("Wrong message contents: %v != %v", act, exp) - } - if store := message.GetContext(results[0].Get(0)).Value(ResultStoreKey); store != nil { - t.Error("Unexpected nested result store") - } - - impl.Clear() - if exp, act := len(impl.Get()), 0; exp != act { - t.Errorf("Unexpected count of stored messages: %v != %v", act, exp) - } -} diff --git a/internal/transaction/tracked.go b/internal/transaction/tracked.go deleted file mode 100644 index 9e624f286a..0000000000 --- a/internal/transaction/tracked.go +++ /dev/null @@ -1,85 +0,0 @@ -package transaction - -import ( - "context" - "errors" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Tracked is a transaction type that adds identifying tags to messages such -// that an error returned resulting from multiple transaction messages can be -// reduced. -type Tracked struct { - msg message.Batch - group *message.SortGroup - ackFn func(context.Context, error) error -} - -// NewTracked creates a transaction from a message batch and a response channel. -// The message is tagged with an identifier for the transaction, and if an error -// is returned from a downstream component that merged messages from other -// transactions the tag can be used in order to determine whether the message -// owned by this transaction succeeded. -func NewTracked(msg message.Batch, ackFn func(context.Context, error) error) *Tracked { - group, trackedMsg := message.NewSortGroup(msg) - return &Tracked{ - msg: trackedMsg, - group: group, - ackFn: ackFn, - } -} - -// Message returns the message owned by this transaction. -func (t *Tracked) Message() message.Batch { - return t.msg -} - -func (t *Tracked) getResFromGroup(walkable *batch.Error) error { - // We're faced with a batch error. However, there's a chance that the - // messages failed in the batch were not sourced from this transaction due - // to batching, archiving, etc. - // - // Therefore, we can be cheeky, poke around, and if we both can verify that - // all of our messages are represented in this batch error and also that - // they are all nil within it then we can safely say that the parts making - // up this transaction are safe to acknowledge. - remainingIndexes := make(map[int]struct{}, t.msg.Len()) - for i := 0; i < t.msg.Len(); i++ { - remainingIndexes[i] = struct{}{} - } - - var res error - walkable.WalkPartsBySource(t.group, t.msg, func(index int, p *message.Part, err error) bool { - if err != nil { - res = err - return false - } - delete(remainingIndexes, index) - return len(remainingIndexes) != 0 - }) - if res != nil { - return res - } - - if len(remainingIndexes) > 0 { - return errors.Unwrap(walkable) - } - return nil -} - -func (t *Tracked) resFromError(err error) error { - if err != nil { - var walkable *batch.Error - if errors.As(err, &walkable) { - err = t.getResFromGroup(walkable) - } - } - return err -} - -// Ack provides a response to the upstream service from an error. -func (t *Tracked) Ack(ctx context.Context, err error) error { - return t.ackFn(ctx, t.resFromError(err)) -} diff --git a/internal/transaction/tracked_test.go b/internal/transaction/tracked_test.go deleted file mode 100644 index f8925fdf71..0000000000 --- a/internal/transaction/tracked_test.go +++ /dev/null @@ -1,270 +0,0 @@ -package transaction - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/message" -) - -func TestTaggingErrorsSinglePart(t *testing.T) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - errTest3 := errors.New("test err 3") - - tran := NewTracked(msg, nil) - - // No error - assert.NoError(t, tran.resFromError(nil)) - - // Static error - assert.Equal(t, errTest1, tran.resFromError(errTest1)) - - // Create batch error with single part - batchErr := batch.NewError(tran.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest2, tran.resFromError(batchErr)) - - // Create new message, no common part, and create batch error - newMsg := message.QuickBatch([][]byte{[]byte("bar")}) - batchErr = batch.NewError(newMsg, errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest1, tran.resFromError(batchErr)) - - // Add tran part to new message and create batch error with error on non-tran part - newMsg = append(newMsg, tran.Message().Get(0)) - batchErr = batch.NewError(newMsg, errTest1) - batchErr.Failed(0, errTest2) - - assert.NoError(t, tran.resFromError(batchErr)) - - // Create batch error for tran part - batchErr.Failed(1, errTest3) - - assert.Equal(t, errTest3, tran.resFromError(batchErr)) -} - -func TestTaggingErrorsMultiplePart(t *testing.T) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - errTest3 := errors.New("test err 3") - - tran := NewTracked(msg, nil) - - // No error - assert.NoError(t, tran.resFromError(nil)) - - // Static error - assert.Equal(t, errTest1, tran.resFromError(errTest1)) - - // Create batch error with single part - batchErr := batch.NewError(tran.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest2, tran.resFromError(batchErr)) - - // Create new message, no common part, and create batch error - newMsg := message.QuickBatch([][]byte{[]byte("baz")}) - batchErr = batch.NewError(newMsg, errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest1, tran.resFromError(batchErr)) - - // Add tran part to new message, still returning general error due to - // missing part - newMsg = append(newMsg, tran.Message().Get(0)) - batchErr = batch.NewError(newMsg, errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest1, tran.resFromError(batchErr)) - - // Add next tran part to new message, and return ack now - newMsg = append(newMsg, tran.Message().Get(1)) - batchErr = batch.NewError(newMsg, errTest1) - batchErr.Failed(0, errTest2) - - assert.NoError(t, tran.resFromError(batchErr)) - - // Create batch error with error on non-tran part - batchErr = batch.NewError(newMsg, errTest1) - batchErr.Failed(0, errTest2) - - assert.NoError(t, tran.resFromError(batchErr)) - - // Create batch error for tran part - batchErr.Failed(1, errTest3) - assert.Equal(t, errTest3, tran.resFromError(batchErr)) -} - -func TestTaggingErrorsNestedOverlap(t *testing.T) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - - tranOne := NewTracked(msg, nil) - - msgTwo := message.QuickBatch(nil) - msgTwo = append(msgTwo, tranOne.Message().Get(1), tranOne.Message().Get(0)) - tranTwo := NewTracked(msgTwo, nil) - - // No error - assert.NoError(t, tranOne.resFromError(nil)) - assert.NoError(t, tranTwo.resFromError(nil)) - - // Static error - assert.Equal(t, errTest1, tranOne.resFromError(errTest1)) - assert.Equal(t, errTest1, tranTwo.resFromError(errTest1)) - - // Create batch error with single part - batchErr := batch.NewError(tranTwo.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest2, tranOne.resFromError(batchErr)) - assert.Equal(t, errTest2, tranTwo.resFromError(batchErr)) - - // And if the batch error only touches the first message, only see error in - // first transaction - batchErr = batch.NewError(tranOne.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest2, tranOne.resFromError(batchErr)) - assert.Equal(t, errTest1, tranTwo.resFromError(batchErr)) -} - -func TestTaggingErrorsNestedSerial(t *testing.T) { - msgOne := message.QuickBatch([][]byte{ - []byte("foo"), - }) - msgTwo := message.QuickBatch([][]byte{ - []byte("bar"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - - tranOne := NewTracked(msgOne, nil) - tranTwo := NewTracked(msgTwo, nil) - - msg := message.Batch{ - tranOne.Message().Get(0), - tranTwo.Message().Get(0), - } - - // No error - assert.NoError(t, tranOne.resFromError(nil)) - assert.NoError(t, tranTwo.resFromError(nil)) - - // Static error - assert.Equal(t, errTest1, tranOne.resFromError(errTest1)) - assert.Equal(t, errTest1, tranTwo.resFromError(errTest1)) - - // Create batch error with single part - batchErr := batch.NewError(msg, errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(t, errTest2, tranOne.resFromError(batchErr)) - assert.NoError(t, tranTwo.resFromError(batchErr)) -} - -func BenchmarkErrorWithTagging(b *testing.B) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - - for i := 0; i < b.N; i++ { - tran := NewTracked(msg, nil) - - batchErr := batch.NewError(tran.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(b, errTest2, tran.resFromError(batchErr)) - } -} - -func BenchmarkErrorWithTaggingN3(b *testing.B) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - - for i := 0; i < b.N; i++ { - tran := NewTracked(msg, nil) - tranTwo := NewTracked(tran.Message(), nil) - tranThree := NewTracked(tranTwo.Message(), nil) - - batchErr := batch.NewError(tranThree.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(b, errTest2, tran.resFromError(batchErr)) - assert.Equal(b, errTest2, tranTwo.resFromError(batchErr)) - assert.Equal(b, errTest2, tranThree.resFromError(batchErr)) - } -} - -func BenchmarkErrorWithTaggingN2(b *testing.B) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - }) - - errTest1 := errors.New("test err 1") - errTest2 := errors.New("test err 2") - - for i := 0; i < b.N; i++ { - tran := NewTracked(msg, nil) - tranTwo := NewTracked(tran.Message(), nil) - - batchErr := batch.NewError(tranTwo.Message(), errTest1) - batchErr.Failed(0, errTest2) - - assert.Equal(b, errTest2, tran.resFromError(batchErr)) - assert.Equal(b, errTest2, tranTwo.resFromError(batchErr)) - } -} - -func BenchmarkErrorNoTagging(b *testing.B) { - msg := message.QuickBatch([][]byte{ - []byte("foo"), - []byte("bar"), - []byte("baz"), - []byte("buz"), - }) - - errTest1 := errors.New("test err 1") - - for i := 0; i < b.N; i++ { - tran := NewTracked(msg, nil) - assert.Equal(b, errTest1, tran.resFromError(errTest1)) - } -} diff --git a/internal/value/errors.go b/internal/value/errors.go deleted file mode 100644 index addf56ef01..0000000000 --- a/internal/value/errors.go +++ /dev/null @@ -1,72 +0,0 @@ -package value - -import ( - "bytes" - "fmt" -) - -// TypeError represents an error where a value of a type was required for a -// function, method or operator but instead a different type was found. -type TypeError struct { - From string - Expected []Type - Actual Type - Value string -} - -// Error implements the standard error interface for TypeError. -func (t *TypeError) Error() string { - var errStr bytes.Buffer - if len(t.Expected) > 0 { - errStr.WriteString("expected ") - for i, exp := range t.Expected { - if i > 0 { - if len(t.Expected) > 2 && i < (len(t.Expected)-1) { - errStr.WriteString(", ") - } else { - errStr.WriteString(" or ") - } - } - errStr.WriteString(string(exp)) - } - errStr.WriteString(" value") - } else { - errStr.WriteString("unexpected value") - } - - fmt.Fprintf(&errStr, ", got %v", t.Actual) - - if t.From != "" { - fmt.Fprintf(&errStr, " from %v", t.From) - } - - if t.Value != "" { - fmt.Fprintf(&errStr, " (%v)", t.Value) - } - - return errStr.String() -} - -// NewTypeError creates a new type error. -func NewTypeError(value any, exp ...Type) *TypeError { - return NewTypeErrorFrom("", value, exp...) -} - -// NewTypeErrorFrom creates a new type error with an annotation of the query -// that provided the wrong type. -func NewTypeErrorFrom(from string, value any, exp ...Type) *TypeError { - valueStr := "" - valueType := ITypeOf(value) - switch valueType { - case TString: - valueStr = fmt.Sprintf(`"%v"`, value) - case TBool, TNumber: - valueStr = fmt.Sprintf("%v", value) - } - return &TypeError{ - From: from, - Expected: exp, - Actual: valueType, - Value: valueStr, - } -} diff --git a/internal/value/type_helpers.go b/internal/value/type_helpers.go deleted file mode 100644 index a1d08cb490..0000000000 --- a/internal/value/type_helpers.go +++ /dev/null @@ -1,901 +0,0 @@ -package value - -import ( - "encoding/json" - "errors" - "fmt" - "math" - "strconv" - "time" - - "github.com/Jeffail/gabs/v2" -) - -// Type represents a discrete value type supported by Bloblang queries. -type Type string - -// ValueType variants. -var ( - TString Type = "string" - TBytes Type = "bytes" - TNumber Type = "number" - TBool Type = "bool" - TTimestamp Type = "timestamp" - TArray Type = "array" - TObject Type = "object" - TNull Type = "null" - TDelete Type = "delete" - TNothing Type = "nothing" - TQuery Type = "query expression" - TUnknown Type = "unknown" - - // Specialised and not generally known over ValueNumber. - TInt Type = "integer" - TFloat Type = "float" -) - -// ITypeOf returns the type of a boxed value as a discrete ValueType. If the -// type of the value is unknown then ValueUnknown is returned. -func ITypeOf(i any) Type { - switch i.(type) { - case string: - return TString - case []byte: - return TBytes - case int, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, json.Number: - return TNumber - case bool: - return TBool - case time.Time: - return TTimestamp - case []any: - return TArray - case map[string]any: - return TObject - case Delete: - return TDelete - case Nothing: - return TNothing - case nil: - return TNull - } - if _, isDyn := i.(interface { - Annotation() string - }); isDyn { - return TQuery - } - return TUnknown -} - -//------------------------------------------------------------------------------ - -// Delete is a special type that serializes to `null` when forced but indicates -// a target should be deleted. -type Delete *struct{} - -// Nothing is a special type that serializes to `null` when forced but indicates -// a query should be disregarded (and not mapped). -type Nothing *struct{} - -// IGetNumber takes a boxed value and attempts to extract a number (float64) -// from it. -func IGetNumber(v any) (float64, error) { - switch t := v.(type) { - case int: - return float64(t), nil - case int8: - return float64(t), nil - case int16: - return float64(t), nil - case int32: - return float64(t), nil - case int64: - return float64(t), nil - case uint: - return float64(t), nil - case uint8: - return float64(t), nil - case uint16: - return float64(t), nil - case uint32: - return float64(t), nil - case uint64: - return float64(t), nil - case float32: - return float64(t), nil - case float64: - return t, nil - case json.Number: - return t.Float64() - } - return 0, NewTypeError(v, TNumber) -} - -// IGetFloat32 takes a boxed value and attempts to extract a number (float32) -// from it. -func IGetFloat32(v any) (float32, error) { - switch t := v.(type) { - case int: - return float32(t), nil - case int8: - return float32(t), nil - case int16: - return float32(t), nil - case int32: - return float32(t), nil - case int64: - return float32(t), nil - case uint: - return float32(t), nil - case uint8: - return float32(t), nil - case uint16: - return float32(t), nil - case uint32: - return float32(t), nil - case uint64: - return float32(t), nil - case float32: - return t, nil - case float64: - return float32(t), nil - case json.Number: - v, e := t.Float64() - return float32(v), e - } - return 0, NewTypeError(v, TNumber) -} - -// IGetInt takes a boxed value and attempts to extract an integer (int64) from -// it. -func IGetInt(v any) (int64, error) { - switch t := v.(type) { - case int: - return int64(t), nil - case int8: - return int64(t), nil - case int16: - return int64(t), nil - case int32: - return int64(t), nil - case int64: - return t, nil - case uint: - return int64(t), nil - case uint8: - return int64(t), nil - case uint16: - return int64(t), nil - case uint32: - return int64(t), nil - case uint64: - return int64(t), nil - case float32: - return int64(t), nil - case float64: - return int64(t), nil - case json.Number: - i, err := t.Int64() - if err == nil { - return i, nil - } - if f, ferr := t.Float64(); ferr == nil { - return int64(f), nil - } - return 0, err - } - return 0, NewTypeError(v, TNumber) -} - -// IGetUInt takes a boxed value and attempts to extract an unsigned integer -// (uint64) from it. -func IGetUInt(v any) (uint64, error) { - switch v.(type) { - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, json.Number: - // We're passing through here because it handles out of bounds issues. - return IToUint(v) - } - return 0, NewTypeError(v, TNumber) -} - -// IGetBool takes a boxed value and attempts to extract a boolean from it. -func IGetBool(v any) (bool, error) { - switch t := v.(type) { - case bool: - return t, nil - case int: - return t != 0, nil - case int8: - return t != 0, nil - case int16: - return t != 0, nil - case int32: - return t != 0, nil - case int64: - return t != 0, nil - case uint: - return t != 0, nil - case uint8: - return t != 0, nil - case uint16: - return t != 0, nil - case uint32: - return t != 0, nil - case uint64: - return t != 0, nil - case float32: - return t != 0, nil - case float64: - return t != 0, nil - case json.Number: - return t.String() != "0", nil - } - return false, NewTypeError(v, TBool) -} - -// IGetString takes a boxed value and attempts to return a string value. Returns -// an error if the value is not a string or byte slice. -func IGetString(v any) (string, error) { - switch t := v.(type) { - case string: - return t, nil - case []byte: - return string(t), nil - case time.Time: - return t.Format(time.RFC3339Nano), nil - } - return "", NewTypeError(v, TString) -} - -// IGetBytes takes a boxed value and attempts to return a byte slice value. -// Returns an error if the value is not a string or byte slice. -func IGetBytes(v any) ([]byte, error) { - switch t := v.(type) { - case string: - return []byte(t), nil - case []byte: - return t, nil - case time.Time: - return t.AppendFormat(nil, time.RFC3339Nano), nil - } - return nil, NewTypeError(v, TBytes) -} - -// IGetTimestamp takes a boxed value and attempts to coerce it into a timestamp, -// either by interpretting a numerical value as a unix timestamp, or by parsing -// a string value as RFC3339Nano. -func IGetTimestamp(v any) (time.Time, error) { - if tVal, ok := v.(time.Time); ok { - return tVal, nil - } - switch t := ISanitize(v).(type) { - case int64: - return time.Unix(t, 0), nil - case uint64: - return time.Unix(int64(t), 0), nil - case float64: - fint := math.Trunc(t) - fdec := t - fint - return time.Unix(int64(fint), int64(fdec*1e9)), nil - case json.Number: - if i, err := t.Int64(); err == nil { - return time.Unix(i, 0), nil - } else if f, err := t.Float64(); err == nil { - fint := math.Trunc(f) - fdec := f - fint - return time.Unix(int64(fint), int64(fdec*1e9)), nil - } else { - return time.Time{}, fmt.Errorf("failed to parse value '%v' as number", v) - } - case []byte: - return time.Parse(time.RFC3339Nano, string(t)) - case string: - return time.Parse(time.RFC3339Nano, t) - } - return time.Time{}, NewTypeError(v, TNumber, TString) -} - -// IIsNull returns whether a bloblang type is null, this includes Delete and -// Nothing types. -func IIsNull(i any) bool { - if i == nil { - return true - } - switch i.(type) { - case Delete, Nothing: - return true - } - return false -} - -func RestrictForComparison(v any) any { - v = ISanitize(v) - switch t := v.(type) { - case int64: - return float64(t) - case uint64: - return float64(t) - case json.Number: - if f, err := IGetNumber(t); err == nil { - return f - } - case []byte: - return string(t) - } - return v -} - -// ISanitize takes a boxed value of any type and attempts to convert it into one -// of the following types: string, []byte, int64, uint64, float64, bool, -// []interface{}, map[string]interface{}, Delete, Nothing. -func ISanitize(i any) any { - switch t := i.(type) { - case string, []byte, int64, uint64, float64, bool, []any, map[string]any, Delete, Nothing: - return i - case json.RawMessage: - return []byte(t) - case json.Number: - if i, err := t.Int64(); err == nil { - return i - } - if f, err := t.Float64(); err == nil { - return f - } - return t.String() - case time.Time: - return t.Format(time.RFC3339Nano) - case int: - return int64(t) - case int8: - return int64(t) - case int16: - return int64(t) - case int32: - return int64(t) - case uint: - return uint64(t) - case uint8: - return uint64(t) - case uint16: - return uint64(t) - case uint32: - return uint64(t) - case float32: - return float64(t) - } - // Do NOT support unknown types (for now). - return nil -} - -// IToBytes takes a boxed value of any type and attempts to convert it into a -// byte slice. -func IToBytes(i any) []byte { - switch t := i.(type) { - case string: - return []byte(t) - case []byte: - return t - case json.Number: - return []byte(t.String()) - case int64: - return strconv.AppendInt(nil, t, 10) - case uint64: - return strconv.AppendUint(nil, t, 10) - case float64: - return strconv.AppendFloat(nil, t, 'g', -1, 64) - case bool: - if t { - return []byte("true") - } - return []byte("false") - case time.Time: - return t.AppendFormat(nil, time.RFC3339Nano) - case nil: - return []byte(`null`) - } - // Last resort - return gabs.Wrap(i).Bytes() -} - -// IToString takes a boxed value of any type and attempts to convert it into a -// string. -func IToString(i any) string { - switch t := i.(type) { - case string: - return t - case []byte: - return string(t) - case int64: - return strconv.FormatInt(t, 10) - case uint64: - return strconv.FormatUint(t, 10) - case float64: - return strconv.FormatFloat(t, 'g', -1, 64) - case json.Number: - return t.String() - case bool: - if t { - return "true" - } - return "false" - case time.Time: - return t.Format(time.RFC3339Nano) - case nil: - return `null` - } - // Last resort - return gabs.Wrap(i).String() -} - -// IToNumber takes a boxed value and attempts to extract a number (float64) -// from it or parse one. -func IToNumber(v any) (float64, error) { - return IToFloat64(v) -} - -// IToFloat64 takes a boxed value and attempts to extract a number (float64) -// from it or parse one. -func IToFloat64(v any) (float64, error) { - switch t := v.(type) { - case int: - return float64(t), nil - case int8: - return float64(t), nil - case int16: - return float64(t), nil - case int32: - return float64(t), nil - case int64: - return float64(t), nil - case uint: - return float64(t), nil - case uint8: - return float64(t), nil - case uint16: - return float64(t), nil - case uint32: - return float64(t), nil - case uint64: - return float64(t), nil - case float64: - return t, nil - case float32: - return float64(t), nil - case json.Number: - return t.Float64() - case []byte: - return strconv.ParseFloat(string(t), 64) - case string: - return strconv.ParseFloat(t, 64) - } - return 0, NewTypeError(v, TNumber) -} - -// IToFloat32 takes a boxed value and attempts to extract a number (float32) -// from it or parse one. -func IToFloat32(v any) (float32, error) { - switch t := v.(type) { - case int: - return float32(t), nil - case int8: - return float32(t), nil - case int16: - return float32(t), nil - case int32: - return float32(t), nil - case int64: - return float32(t), nil - case uint: - return float32(t), nil - case uint8: - return float32(t), nil - case uint16: - return float32(t), nil - case uint32: - return float32(t), nil - case uint64: - return float32(t), nil - case float64: - return float32(t), nil - case json.Number: - f64, err := strconv.ParseFloat(string(t), 32) - if err != nil { - return 0, err - } - return float32(f64), nil - case []byte: - f64, err := strconv.ParseFloat(string(t), 32) - if err != nil { - return 0, err - } - return float32(f64), nil - case string: - f64, err := strconv.ParseFloat(t, 32) - if err != nil { - return 0, err - } - return float32(f64), nil - } - return 0, NewTypeError(v, TNumber) -} - -const ( - maxUint = ^uint64(0) - maxUint32 = ^uint32(0) - maxUint16 = ^uint16(0) - maxUint8 = ^uint8(0) - MaxInt = maxUint >> 1 - maxInt32 = maxUint32 >> 1 - maxInt16 = maxUint16 >> 1 - maxInt8 = maxUint8 >> 1 - MinInt = ^int64(MaxInt) - minInt32 = ^int32(maxInt32) - minInt16 = ^int16(maxInt16) - minInt8 = ^int8(maxInt8) -) - -// IToInt takes a boxed value and attempts to extract a number (int64) from it -// or parse one. -func IToInt(v any) (int64, error) { - switch t := v.(type) { - case int: - return int64(t), nil - case int8: - return int64(t), nil - case int16: - return int64(t), nil - case int32: - return int64(t), nil - case int64: - return t, nil - case uint: - return int64(t), nil - case uint8: - return int64(t), nil - case uint16: - return int64(t), nil - case uint32: - return int64(t), nil - case uint64: - if t > MaxInt { - return 0, errors.New("unsigned integer value is too large to be cast as a signed integer") - } - return int64(t), nil - case float32: - return IToInt(float64(t)) - case float64: - if math.IsInf(t, 0) { - return 0, errors.New("cannot convert +/-INF to an integer") - } - if math.IsNaN(t) { - return 0, errors.New("cannot convert NAN to an integer") - } - if t > float64(MaxInt) { - return 0, errors.New("float value is too large to be cast as a signed integer") - } - if t < float64(MinInt) { - return 0, errors.New("float value is too small to be cast as a signed integer") - } - if t-float64(int64(t)) != 0 { - return 0, errors.New("float value contains decimals and therefore cannot be cast as a signed integer, if you intend to round the value then call `.round()` explicitly before this cast") - } - return int64(t), nil - case json.Number: - return t.Int64() - case []byte: - return strconv.ParseInt(string(t), 0, 64) - case string: - return strconv.ParseInt(t, 0, 64) - } - return 0, NewTypeError(v, TNumber) -} - -// IToInt32 takes a boxed value and attempts to extract a number (int32) from -// it or parse one. -func IToInt32(v any) (int32, error) { - if v, ok := v.(int32); ok { - return v, nil - } - i64, err := IToInt(v) - if err != nil { - return 0, err - } - if i64 > int64(maxInt32) { - return 0, errors.New("value is too large to be cast as a 32-bit signed integer") - } - if i64 < int64(minInt32) { - return 0, errors.New("value is too small to be cast as a 32-bit signed integer") - } - return int32(i64), nil -} - -// IToInt16 takes a boxed value and attempts to extract a number (int64) from -// it or parse one. -func IToInt16(v any) (int16, error) { - if v, ok := v.(int16); ok { - return v, nil - } - i64, err := IToInt(v) - if err != nil { - return 0, err - } - if i64 > int64(maxInt16) { - return 0, errors.New("value is too large to be cast as a 16-bit signed integer") - } - if i64 < int64(minInt16) { - return 0, errors.New("value is too small to be cast as a 16-bit signed integer") - } - return int16(i64), nil -} - -// IToInt8 takes a boxed value and attempts to extract a number (int8) from -// it or parse one. -func IToInt8(v any) (int8, error) { - if v, ok := v.(int8); ok { - return v, nil - } - i64, err := IToInt(v) - if err != nil { - return 0, err - } - if i64 > int64(maxInt8) { - return 0, errors.New("value is too large to be cast as an 8-bit signed integer") - } - if i64 < int64(minInt8) { - return 0, errors.New("value is too small to be cast as an 8-bit signed integer") - } - return int8(i64), nil -} - -// IToUint takes a boxed value and attempts to extract a number (uint64) from it -// or parse one. -func IToUint(v any) (uint64, error) { - switch t := v.(type) { - case uint: - return uint64(t), nil - case uint8: - return uint64(t), nil - case uint16: - return uint64(t), nil - case uint32: - return uint64(t), nil - case uint64: - return t, nil - case int: - return IToUint(int64(t)) - case int8: - return IToUint(int64(t)) - case int16: - return IToUint(int64(t)) - case int32: - return IToUint(int64(t)) - case int64: - if t < 0 { - return 0, errors.New("signed integer value is negative and cannot be cast as an unsigned integer") - } - return uint64(t), nil - case float32: - return IToUint(float64(t)) - case float64: - if t < 0 { - return 0, errors.New("float value is negative and cannot be cast as an unsigned integer") - } - if math.IsInf(t, 0) { - return 0, errors.New("cannot convert +/-INF to an unsigned integer") - } - if math.IsNaN(t) { - return 0, errors.New("cannot convert NAN to an unsigned integer") - } - if t > float64(maxUint) { - return 0, errors.New("float value is too large to be cast as an unsigned integer") - } - if t-float64(uint64(t)) != 0 { - return 0, errors.New("float value contains decimals and therefore cannot be cast as an unsigned integer, if you intend to round the value then call `.round()` explicitly before this cast") - } - return uint64(t), nil - case json.Number: - i, err := t.Int64() - if err != nil { - return 0, err - } - if i < 0 { - return 0, errors.New("signed integer value is negative and cannot be cast as an unsigned integer") - } - return uint64(i), nil - case []byte: - return strconv.ParseUint(string(t), 0, 64) - case string: - return strconv.ParseUint(t, 0, 64) - } - return 0, NewTypeError(v, TNumber) -} - -// IToUint32 takes a boxed value and attempts to extract a number (uint32) from -// it or parse one. -func IToUint32(v any) (uint32, error) { - if v, ok := v.(uint32); ok { - return v, nil - } - u64, err := IToUint(v) - if err != nil { - return 0, err - } - if u64 > uint64(maxUint32) { - return 0, errors.New("value is too large to be cast as a 32-bit unsigned integer") - } - return uint32(u64), nil -} - -// IToUint16 takes a boxed value and attempts to extract a number (uint16) from -// it or parse one. -func IToUint16(v any) (uint16, error) { - if v, ok := v.(uint16); ok { - return v, nil - } - u64, err := IToUint(v) - if err != nil { - return 0, err - } - if u64 > uint64(maxUint16) { - return 0, errors.New("value is too large to be cast as a 16-bit unsigned integer") - } - return uint16(u64), nil -} - -// IToUint8 takes a boxed value and attempts to extract a number (uint8) from -// it or parse one. -func IToUint8(v any) (uint8, error) { - if v, ok := v.(uint8); ok { - return v, nil - } - u64, err := IToUint(v) - if err != nil { - return 0, err - } - if u64 > uint64(maxUint8) { - return 0, errors.New("value is too large to be cast as an 8-bit unsigned integer") - } - return uint8(u64), nil -} - -// IToBool takes a boxed value and attempts to extract a boolean from it or -// parse it into a bool. -func IToBool(v any) (bool, error) { - switch t := v.(type) { - case bool: - return t, nil - case int: - return t != 0, nil - case int8: - return t != 0, nil - case int16: - return t != 0, nil - case int32: - return t != 0, nil - case int64: - return t != 0, nil - case uint: - return t != 0, nil - case uint8: - return t != 0, nil - case uint16: - return t != 0, nil - case uint32: - return t != 0, nil - case uint64: - return t != 0, nil - case float32: - return t != 0, nil - case float64: - return t != 0, nil - case json.Number: - return t.String() != "0", nil - case []byte: - if v, err := strconv.ParseBool(string(t)); err == nil { - return v, nil - } - case string: - if v, err := strconv.ParseBool(t); err == nil { - return v, nil - } - } - return false, NewTypeError(v, TBool) -} - -// IClone performs a deep copy of a generic value. -func IClone(root any) any { - switch t := root.(type) { - case map[string]any: - newMap := make(map[string]any, len(t)) - for k, v := range t { - newMap[k] = IClone(v) - } - return newMap - case []any: - newSlice := make([]any, len(t)) - for i, v := range t { - newSlice[i] = IClone(v) - } - return newSlice - } - return root -} - -// ICompare returns true if both the left and right are equal according to one -// of the following conditions: -// -// - The types exactly match and have the same value -// - The types are both either a string or byte slice and the underlying data is the same -// - The types are both numerical and have the same value -// - Both types are a matching slice or map containing values matching these same conditions. -func ICompare(left, right any) bool { - if left == nil && right == nil { - return true - } - switch lhs := RestrictForComparison(left).(type) { - case string: - rhs, err := IGetString(right) - if err != nil { - return false - } - return lhs == rhs - case float64: - rhs, err := IGetNumber(right) - if err != nil { - return false - } - return lhs == rhs - case bool: - rhs, err := IGetBool(right) - if err != nil { - return false - } - return lhs == rhs - case []any: - rhs, matches := right.([]any) - if !matches { - return false - } - if len(lhs) != len(rhs) { - return false - } - for i, vl := range lhs { - if !ICompare(vl, rhs[i]) { - return false - } - } - return true - case map[string]any: - rhs, matches := right.(map[string]any) - if !matches { - return false - } - if len(lhs) != len(rhs) { - return false - } - for k, vl := range lhs { - if !ICompare(vl, rhs[k]) { - return false - } - } - return true - } - return false -} - -func IGetStringMap(v any) (map[string]string, error) { - iMap, ok := v.(map[string]any) - if !ok { - if sMap, ok := v.(map[string]string); ok { - return sMap, nil - } - return nil, NewTypeError(v, TObject) - } - sMap := make(map[string]string, len(iMap)) - for k, ev := range iMap { - if sMap[k], ok = ev.(string); !ok { - return nil, NewTypeError(ev, TString) - } - } - return sMap, nil -} diff --git a/internal/value/type_helpers_test.go b/internal/value/type_helpers_test.go deleted file mode 100644 index 51b419723e..0000000000 --- a/internal/value/type_helpers_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package value - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestIToBytes(t *testing.T) { - vb := IToBytes(uint64(12345)) - assert.Equal(t, "12345", string(vb)) - - vb = IToBytes(true) - assert.Equal(t, "true", string(vb)) - - vb = IToBytes(float64(1.2345)) - assert.Equal(t, "1.2345", string(vb)) - - vb = IToBytes(float64(1.234567891234567891234567)) - assert.Equal(t, "1.234567891234568", string(vb)) - - vb = IToBytes(float64(1.23 * 4.567 * 1_000_000_000)) - assert.Equal(t, "5.61741e+09", string(vb)) -} - -func TestIToInt(t *testing.T) { - for _, test := range []struct { - in any - out int64 - errContains string - }{ - { - in: 123.0, - out: 123, - }, - { - in: 123.456, - errContains: "contains decimals and therefore cannot be cast as a", - }, - { - in: "123", - out: 123, - }, - { - in: MaxInt, - out: int64(MaxInt), - }, - { - in: MinInt, - out: MinInt, - }, - { - in: float64(MaxInt) + 10000, - errContains: "value is too large to be cast as a", - }, - { - in: float64(MinInt) - 10000, - errContains: "value is too small to be cast as a", - }, - } { - i, err := IToInt(test.in) - if test.errContains != "" { - require.Error(t, err, "value: %v", i) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - assert.Equal(t, test.out, i) - } - } -} diff --git a/public/bloblang/arguments.go b/public/bloblang/arguments.go deleted file mode 100644 index 84615651aa..0000000000 --- a/public/bloblang/arguments.go +++ /dev/null @@ -1,167 +0,0 @@ -package bloblang - -import ( - "fmt" - "reflect" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// ArgSpec provides an API for validating and extracting function or method -// arguments by registering them with pointer receivers. -type ArgSpec struct { - n int - validators []func(args []any) error -} - -// NewArgSpec creates an argument parser/validator. -func NewArgSpec() *ArgSpec { - return &ArgSpec{} -} - -// Extract the specified typed arguments from a slice of generic arguments. -// Returns an error if the number of arguments does not match the spec, and -// returns an *ArgError if the type of an argument is mismatched. -func (a *ArgSpec) Extract(args []any) error { - if len(args) != a.n { - return fmt.Errorf("expected %v arguments, received %v", a.n, len(args)) - } - for _, v := range a.validators { - if err := v(args); err != nil { - return err - } - } - return nil -} - -// IntVar creates an int argument to follow the previously created argument. -func (a *ArgSpec) IntVar(i *int) *ArgSpec { - index := a.n - a.n++ - - a.validators = append(a.validators, func(args []any) error { - v, err := value.IGetInt(args[index]) - if err != nil { - return newArgError(index, reflect.Int, args[index]) - } - *i = int(v) - return nil - }) - - return a -} - -// Int64Var creates an int64 argument to follow the previously created argument. -func (a *ArgSpec) Int64Var(i *int64) *ArgSpec { - index := a.n - a.n++ - - a.validators = append(a.validators, func(args []any) error { - v, err := value.IGetInt(args[index]) - if err != nil { - return newArgError(index, reflect.Int64, args[index]) - } - *i = v - return nil - }) - - return a -} - -// Float64Var creates a Float64 argument to follow the previously created -// argument. -func (a *ArgSpec) Float64Var(f *float64) *ArgSpec { - index := a.n - a.n++ - - a.validators = append(a.validators, func(args []any) error { - v, err := value.IGetNumber(args[index]) - if err != nil { - return newArgError(index, reflect.Float64, args[index]) - } - *f = v - return nil - }) - - return a -} - -// BoolVar creates a boolean argument to follow the previously created argument. -func (a *ArgSpec) BoolVar(b *bool) *ArgSpec { - index := a.n - a.n++ - - a.validators = append(a.validators, func(args []any) error { - v, err := value.IGetBool(args[index]) - if err != nil { - return newArgError(index, reflect.Bool, args[index]) - } - *b = v - return nil - }) - - return a -} - -// StringVar creates a string argument to follow the previously created -// argument. -func (a *ArgSpec) StringVar(s *string) *ArgSpec { - index := a.n - a.n++ - - a.validators = append(a.validators, func(args []any) error { - v, err := value.IGetString(args[index]) - if err != nil { - return newArgError(index, reflect.String, args[index]) - } - *s = v - return nil - }) - - return a -} - -// AnyVar creates an argument to follow the previously created argument that can -// have any value. -func (a *ArgSpec) AnyVar(i *any) *ArgSpec { - index := a.n - a.n++ - - a.validators = append(a.validators, func(args []any) error { - *i = args[index] - return nil - }) - - return a -} - -//------------------------------------------------------------------------------ - -// ArgError represents an error encountered when parsing a function or method -// argument. -type ArgError struct { - // The argument index - Index int - - // The expected argument type - ExpectedKind reflect.Kind - - // The actual type provided - ActualKind reflect.Kind - - // The value of the argument - Value any -} - -func (a *ArgError) Error() string { - return fmt.Sprintf("bad argument %v: expected %v value, got %v (%v)", a.Index, a.ExpectedKind.String(), a.ActualKind.String(), a.Value) -} - -func newArgError(index int, expected reflect.Kind, actual any) error { - return &ArgError{ - Index: index, - ExpectedKind: expected, - ActualKind: reflect.TypeOf(actual).Kind(), - Value: actual, - } -} diff --git a/public/bloblang/arguments_test.go b/public/bloblang/arguments_test.go deleted file mode 100644 index 2292f81926..0000000000 --- a/public/bloblang/arguments_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package bloblang - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestArgumentsLength(t *testing.T) { - var a, b, c int - set := NewArgSpec().IntVar(&a).IntVar(&b).IntVar(&c) - - assert.EqualError(t, set.Extract([]any{0, 1}), "expected 3 arguments, received 2") - assert.EqualError(t, set.Extract([]any{0, 1, 2, 3}), "expected 3 arguments, received 4") - assert.NoError(t, set.Extract([]any{0, 1, 2})) -} - -func TestArgumentTypes(t *testing.T) { - var a int - var b int64 - var c float64 - var d bool - var e string - var f any - set := NewArgSpec(). - IntVar(&a). - Int64Var(&b). - Float64Var(&c). - BoolVar(&d). - StringVar(&e). - AnyVar(&f) - - testCases := []struct { - name string - args []any - exp []any - err string - }{ - { - name: "bad int", - args: []any{ - "nope", int64(2), 3.0, true, "hello world", "and this", - }, - err: `bad argument 0: expected int value, got string (nope)`, - }, - { - name: "bad int64", - args: []any{ - int64(1), "nope", 3.0, true, "hello world", "and this", - }, - err: `bad argument 1: expected int64 value, got string (nope)`, - }, - { - name: "bad float64", - args: []any{ - int64(1), int64(2), "nope", true, "hello world", "and this", - }, - err: `bad argument 2: expected float64 value, got string (nope)`, - }, - { - name: "bad bool", - args: []any{ - int64(1), int64(2), 3.0, "nope", "hello world", "and this", - }, - err: `bad argument 3: expected bool value, got string (nope)`, - }, - { - name: "bad string", - args: []any{ - int64(1), int64(2), 3.0, true, 30, "and this", - }, - err: "bad argument 4: expected string value, got int (30)", - }, - { - name: "good values", - args: []any{ - int64(1), int64(2), 3.0, true, "hello world", "and this", - }, - exp: []any{ - 1, int64(2), 3.0, true, "hello world", "and this", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - err := set.Extract(test.args) - if test.err != "" { - assert.EqualError(t, err, test.err) - } else { - assert.NoError(t, err) - assert.Equal(t, test.exp, []any{ - a, b, c, d, e, f, - }) - } - }) - } -} diff --git a/public/bloblang/context.go b/public/bloblang/context.go deleted file mode 100644 index af2172db93..0000000000 --- a/public/bloblang/context.go +++ /dev/null @@ -1,93 +0,0 @@ -package bloblang - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/value" -) - -// ExecContext is an optional context provided to advanced functions and methods -// that contains information about a given mapping at the time the -// function/method is executed. The vast majority of Bloblang plugins would not -// require this context and it is omitted by default. -type ExecContext struct { - ctx query.FunctionContext -} - -func newExecContext(ctx query.FunctionContext) *ExecContext { - return &ExecContext{ - ctx: ctx, - } -} - -type ( - // ExecResultNothing represents a value yielded from an executed function - // where the mapping resulted in nothing being enacted. For example, this - // could mean the mapping was an if-statement that resolved to `false` and - // had no alternative branches to execute. - // - // When nothing is returned it is up to the caller of the function to - // interpret what this means. Usually it means the input data should be left - // as it was. - // - // THERE IS NO CIRCUMSTANCE WHERE IT IS APPROPRIATE FOR PLUGIN AUTHORS TO - // RETURN THIS VALUE. - ExecResultNothing *struct{} - - // ExecResultDelete represents a value yielded from an executed function - // where the mapping resulted in an instruction to delete the root entity. - // - // When a delete is returned it is up to the caller of the function to - // interpret what this means. Usually it means the input data should be - // deleted entirely. - // - // THERE IS NO CIRCUMSTANCE WHERE IT IS APPROPRIATE FOR PLUGIN AUTHORS TO - // RETURN THIS VALUE. - ExecResultDelete *struct{} -) - -// Exec attempts to execute a provided ExecFunction, returning either a value or -// an error. Values returned depend on the function but typically fall within -// the standard scalar, any-map and any-slice values seen by most plugins. -// -// However, two exceptions exist which should be noted by Exec callers: -// ExecResultNothing and ExecResultDelete, as both of these values could be -// yielded and must be handled differently to typical values. -func (e *ExecContext) Exec(fn *ExecFunction) (any, error) { - v, err := fn.fn.Exec(e.ctx) - if err != nil { - return nil, err - } - switch v.(type) { - case value.Delete: - return ExecResultDelete(nil), nil - case value.Nothing: - return ExecResultNothing(nil), nil - } - return v, nil -} - -// ExecToInt64 attempts to execute a provided ExecFunction, returning either an -// integer value or an error if the execution failed or the value returned was -// not a valid integer. -func (e *ExecContext) ExecToInt64(fn *ExecFunction) (int64, error) { - v, err := fn.fn.Exec(e.ctx) - if err != nil { - return 0, err - } - return value.IToInt(v) -} - -// ExecFunction represents an active Bloblang function that can be executed by -// providing it an ExecContext. This is only relevant for advanced functions and -// methods that may wish to exhibit fully customised behaviours and/or mutate -// the contextual state of parameters and/or method targets. -// -// Most plugin authors will not have a need for interacting with any -// ExecFunction. -type ExecFunction struct { - fn query.Function -} - -func newExecFunction(fn query.Function) *ExecFunction { - return &ExecFunction{fn: fn} -} diff --git a/public/bloblang/context_test.go b/public/bloblang/context_test.go deleted file mode 100644 index fd09b5e15a..0000000000 --- a/public/bloblang/context_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package bloblang_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestFunctionContext(t *testing.T) { - env := bloblang.NewEnvironment() - - require.NoError(t, env.RegisterAdvancedFunction( - "foo", bloblang.NewPluginSpec(). - Param(bloblang.NewQueryParam("thing", true)), - - func(args *bloblang.ParsedParams) (bloblang.AdvancedFunction, error) { - thingFn, err := args.GetQuery("thing") - if err != nil { - return nil, err - } - - return func(ctx *bloblang.ExecContext) (any, error) { - v, err := ctx.Exec(thingFn) - if err != nil { - return "Meow: " + err.Error(), nil - } - return v, nil - }, nil - })) - - require.NoError(t, env.RegisterAdvancedMethod( - "bar", bloblang.NewPluginSpec(). - Param(bloblang.NewQueryParam("thing", true).Optional()), - - func(args *bloblang.ParsedParams) (bloblang.AdvancedMethod, error) { - thingFn, err := args.GetOptionalQuery("thing") - if err != nil { - return nil, err - } - - return func(ctx *bloblang.ExecContext, target *bloblang.ExecFunction) (any, error) { - v, err := ctx.Exec(target) - if err != nil && thingFn != nil { - v, err = ctx.Exec(thingFn) - } - if err != nil { - return "Meow: " + err.Error(), nil - } - return v, nil - }, nil - })) - - exec, err := env.Parse(` -root.a = foo(this.a + " must be a string") -root.b = (this.a + " must be a string").bar() -root.c = (this.a + " must be a string").bar("%v wasnt a string".format(this.a)) -`) - require.NoError(t, err) - - res, err := exec.Query(map[string]any{ - "a": 1, - }) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "a": "Meow: cannot add types number (from field `this.a`) and string (from string literal)", - "b": "Meow: cannot add types number (from field `this.a`) and string (from string literal)", - "c": "1 wasnt a string", - }, res) - - res, err = exec.Query(map[string]any{ - "a": "woof", - }) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "a": "woof must be a string", - "b": "woof must be a string", - "c": "woof must be a string", - }, res) -} diff --git a/public/bloblang/environment.go b/public/bloblang/environment.go deleted file mode 100644 index 08dcb14bf7..0000000000 --- a/public/bloblang/environment.go +++ /dev/null @@ -1,438 +0,0 @@ -package bloblang - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -// Environment provides an isolated Bloblang environment where the available -// features, functions and methods can be modified. -type Environment struct { - env *bloblang.Environment -} - -// GlobalEnvironment returns the global default environment. Modifying this -// environment will impact all Bloblang parses that aren't initialized with an -// isolated environment, as well as any new environments initialized after the -// changes. -func GlobalEnvironment() *Environment { - return &Environment{ - env: bloblang.GlobalEnvironment(), - } -} - -// NewEnvironment creates a fresh Bloblang environment, starting with the full -// range of globally defined features (functions and methods), and provides APIs -// for expanding or contracting the features available to this environment. -// -// It's worth using an environment when you need to restrict the access or -// capabilities that certain bloblang mappings have versus others. -// -// For example, an environment could be created that removes any functions for -// accessing environment variables or reading data from the host disk, which -// could be used in certain situations without removing those functions globally -// for all mappings. -func NewEnvironment() *Environment { - return GlobalEnvironment().Clone() -} - -// NewEmptyEnvironment creates a fresh Bloblang environment starting completely -// empty, where no functions or methods are initially available. -func NewEmptyEnvironment() *Environment { - return &Environment{ - env: bloblang.NewEmptyEnvironment(), - } -} - -// Clone an environment in order to register functions and methods without -// modifying the existing environment. -func (e *Environment) Clone() *Environment { - return e.WithoutFunctions().WithoutMethods() -} - -// Parse a Bloblang mapping using the Environment to determine the features -// (functions and methods) available to the mapping. -// -// When a parsing error occurs the error will be the type *ParseError, which -// gives access to the line and column where the error occurred, as well as a -// method for creating a well formatted error message. -func (e *Environment) Parse(blobl string) (*Executor, error) { - exec, err := e.env.NewMapping(blobl) - if err != nil { - if pErr, ok := err.(*parser.Error); ok { - return nil, internalToPublicParserError([]rune(blobl), pErr) - } - return nil, err - } - return newExecutor(exec), nil -} - -// CheckInterpolatedString attempts to parse a Bloblang interpolated string -// using the Environment to determine the features (functions and methods) -// available to it. -// -// When a parsing error occurs the error will be the type *ParseError, which -// gives access to the line and column where the error occurred, as well as a -// method for creating a well formatted error message. -func (e *Environment) CheckInterpolatedString(str string) error { - _, err := e.env.NewField(str) - if err != nil { - if pErr, ok := err.(*parser.Error); ok { - return internalToPublicParserError([]rune(str), pErr) - } - return err - } - return nil -} - -// RegisterMethod adds a new Bloblang method to the environment. All method -// names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake -// case). -func (e *Environment) RegisterMethod(name string, ctor MethodConstructor) error { - spec := query.NewMethodSpec(name, "").InCategory(query.MethodCategoryPlugin, "") - spec.Params = query.VariadicParams() - return e.env.RegisterMethod(spec, func(target query.Function, args *query.ParsedParams) (query.Function, error) { - fn, err := ctor(args.Raw()...) - if err != nil { - return nil, err - } - return query.ClosureFunction("method "+name, func(ctx query.FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - return fn(v) - }, target.QueryTargets), nil - }) -} - -func methodSpecFromPublic(name string, spec *PluginSpec) query.MethodSpec { - category := spec.category - if category == "" { - category = query.MethodCategoryPlugin - } - // Deprecated overrides all others - if spec.status == query.StatusDeprecated { - category = query.MethodCategoryDeprecated - } - var examples []query.ExampleSpec - for _, e := range spec.examples { - var res []string - for _, inputOutput := range e.inputOutputs { - res = append(res, inputOutput[0], inputOutput[1]) - } - if e.skipTesting { - examples = append(examples, query.NewNotTestedExampleSpec(e.summary, e.mapping, res...)) - } else { - examples = append(examples, query.NewExampleSpec(e.summary, e.mapping, res...)) - } - } - iSpec := query.NewMethodSpec(name, spec.description).InCategory(category, "", examples...).AtVersion(spec.version) - if spec.status != "" { - iSpec.Status = spec.status - } - if spec.impure { - iSpec = iSpec.MarkImpure() - } - iSpec.Params = spec.params - return iSpec -} - -// RegisterMethodV2 adds a new Bloblang method to the environment using a -// provided ParamsSpec to define the name of the method and its parameters. -// -// Plugin names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func (e *Environment) RegisterMethodV2(name string, spec *PluginSpec, ctor MethodConstructorV2) error { - return e.env.RegisterMethod(methodSpecFromPublic(name, spec), func(target query.Function, args *query.ParsedParams) (query.Function, error) { - parsedParams := newParsedParams(args, e) - - fn, err := ctor(parsedParams) - if err != nil { - return nil, err - } - - if spec.isStaticFn(parsedParams) { - if sTarget, isLiteral := target.(*query.Literal); isLiteral { - v, err := fn(sTarget.Value) - if err != nil { - return nil, err - } - return query.NewLiteralFunction("method "+name, v), nil - } - } - - return query.ClosureFunction("method "+name, func(ctx query.FunctionContext) (any, error) { - v, err := target.Exec(ctx) - if err != nil { - return nil, err - } - return fn(v) - }, target.QueryTargets), nil - }) -} - -// RegisterAdvancedMethod adds a new Bloblang method to the environment using a -// provided ParamsSpec to define the name of the method and its parameters. -// Advanced methods are provided extra context during invocation. -// -// Plugin names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func (e *Environment) RegisterAdvancedMethod(name string, spec *PluginSpec, ctor AdvancedMethodConstructor) error { - return e.env.RegisterMethod(methodSpecFromPublic(name, spec), func(target query.Function, args *query.ParsedParams) (query.Function, error) { - parsedParams := newParsedParams(args, e) - - fn, err := ctor(parsedParams) - if err != nil { - return nil, err - } - - return query.ClosureFunction("method "+name, func(ctx query.FunctionContext) (any, error) { - return fn(newExecContext(ctx), newExecFunction(target)) - }, target.QueryTargets), nil - }) -} - -// RegisterFunction adds a new Bloblang function to the environment. All -// function names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func (e *Environment) RegisterFunction(name string, ctor FunctionConstructor) error { - spec := query.NewFunctionSpec(query.FunctionCategoryPlugin, name, "") - spec.Params = query.VariadicParams() - return e.env.RegisterFunction(spec, func(args *query.ParsedParams) (query.Function, error) { - fn, err := ctor(args.Raw()...) - if err != nil { - return nil, err - } - return query.ClosureFunction("function "+name, func(ctx query.FunctionContext) (any, error) { - return fn() - }, nil), nil - }) -} - -func functionSpecFromPublic(name string, spec *PluginSpec) query.FunctionSpec { - category := spec.category - if category == "" { - category = query.FunctionCategoryPlugin - } - // Deprecated overrides all others - if spec.status == query.StatusDeprecated { - category = query.FunctionCategoryDeprecated - } - var examples []query.ExampleSpec - for _, e := range spec.examples { - var res []string - for _, inputOutput := range e.inputOutputs { - res = append(res, inputOutput[0], inputOutput[1]) - } - if e.skipTesting { - examples = append(examples, query.NewNotTestedExampleSpec(e.summary, e.mapping, res...)) - } else { - examples = append(examples, query.NewExampleSpec(e.summary, e.mapping, res...)) - } - } - iSpec := query.NewFunctionSpec(category, name, spec.description, examples...).AtVersion(spec.version) - if spec.status != "" { - iSpec.Status = spec.status - } - if spec.impure { - iSpec = iSpec.MarkImpure() - } - iSpec.Params = spec.params - return iSpec -} - -// RegisterFunctionV2 adds a new Bloblang function to the environment using a -// provided ParamsSpec to define the name of the function and its parameters. -// -// Plugin names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func (e *Environment) RegisterFunctionV2(name string, spec *PluginSpec, ctor FunctionConstructorV2) error { - return e.env.RegisterFunction(functionSpecFromPublic(name, spec), func(args *query.ParsedParams) (query.Function, error) { - parsedParams := newParsedParams(args, e) - - fn, err := ctor(parsedParams) - if err != nil { - return nil, err - } - - if spec.isStaticFn(parsedParams) { - v, err := fn() - if err != nil { - return nil, err - } - return query.NewLiteralFunction("function "+name, v), nil - } - - return query.ClosureFunction("function "+name, func(ctx query.FunctionContext) (any, error) { - return fn() - }, nil), nil - }) -} - -// RegisterAdvancedFunction adds a new Bloblang function to the environment -// using a provided ParamsSpec to define the name of the function and its -// parameters. Advanced functions are provided extra context during invocation. -// -// Plugin names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func (e *Environment) RegisterAdvancedFunction(name string, spec *PluginSpec, ctor AdvancedFunctionConstructor) error { - return e.env.RegisterFunction(functionSpecFromPublic(name, spec), func(args *query.ParsedParams) (query.Function, error) { - parsedParams := newParsedParams(args, e) - - fn, err := ctor(parsedParams) - if err != nil { - return nil, err - } - - return query.ClosureFunction("function "+name, func(ctx query.FunctionContext) (any, error) { - return fn(newExecContext(ctx)) - }, nil), nil - }) -} - -// WithoutMethods returns a copy of the environment but with a variadic list of -// method names removed. Instantiation of these removed methods within a mapping -// will cause errors at parse time. -func (e *Environment) WithoutMethods(names ...string) *Environment { - return &Environment{ - env: e.env.WithoutMethods(names...), - } -} - -// WithoutFunctions returns a copy of the environment but with a variadic list -// of function names removed. Instantiation of these removed functions within a -// mapping will cause errors at parse time. -func (e *Environment) WithoutFunctions(names ...string) *Environment { - return &Environment{ - env: e.env.WithoutFunctions(names...), - } -} - -// WithDisabledImports returns a copy of the environment where imports within -// mappings are disabled. -func (e *Environment) WithDisabledImports() *Environment { - return &Environment{ - env: e.env.WithDisabledImports(), - } -} - -// WithCustomImporter returns a copy of the environment where imports from -// mappings are done via a provided closure function. -func (e *Environment) WithCustomImporter(fn func(name string) ([]byte, error)) *Environment { - return &Environment{ - env: e.env.WithCustomImporter(fn), - } -} - -// WithMaxMapRecursion returns a copy of the environment where the maximum -// recursion allowed for maps is set to a given value. If the execution of a -// mapping from this environment matches this number of recursive map calls the -// mapping will error out. -func (e *Environment) WithMaxMapRecursion(n int) *Environment { - return &Environment{ - env: e.env.WithMaxMapRecursion(n), - } -} - -// OnlyPure removes any methods and functions that have been registered but are -// marked as impure. Impure in this context means the method/function is able to -// mutate global state or access machine state (read environment variables, -// files, etc). Note that methods/functions that access the machine clock are -// not marked as pure, so timestamp functions will still work. -func (e *Environment) OnlyPure() *Environment { - return &Environment{ - env: e.env.OnlyPure(), - } -} - -// Deactivated returns a version of the environment where constructors are -// disabled for all functions and methods, allowing mappings to be parsed and -// validated but not executed. -// -// The underlying register of functions and methods is shared with the target -// environment, and therefore functions/methods registered to this set will also -// be added to the still activated environment. Use Clone in order to avoid -// this. -func (e *Environment) Deactivated() *Environment { - return &Environment{ - env: e.env.Deactivated(), - } -} - -//------------------------------------------------------------------------------ - -// Parse a Bloblang mapping allowing the use of the globally accessible range of -// features (functions and methods). -// -// When a parsing error occurs the error will be the type *ParseError, which -// gives access to the line and column where the error occurred, as well as a -// method for creating a well formatted error message. -func Parse(blobl string) (*Executor, error) { - exec, err := parser.ParseMapping(parser.GlobalContext(), blobl) - if err != nil { - return nil, internalToPublicParserError([]rune(blobl), err) - } - return newExecutor(exec), nil -} - -// RegisterMethod adds a new Bloblang method to the global environment. All -// method names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func RegisterMethod(name string, ctor MethodConstructor) error { - return GlobalEnvironment().RegisterMethod(name, ctor) -} - -// RegisterMethodV2 adds a new Bloblang method to the global environment. All -// method names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func RegisterMethodV2(name string, spec *PluginSpec, ctor MethodConstructorV2) error { - return GlobalEnvironment().RegisterMethodV2(name, spec, ctor) -} - -// RegisterAdvancedMethod adds a new advanced Bloblang method to the global -// environment. All method names must match the regular expression -// /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func RegisterAdvancedMethod(name string, spec *PluginSpec, ctor AdvancedMethodConstructor) error { - return GlobalEnvironment().RegisterAdvancedMethod(name, spec, ctor) -} - -// RegisterFunction adds a new Bloblang function to the global environment. All -// function names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func RegisterFunction(name string, ctor FunctionConstructor) error { - return GlobalEnvironment().RegisterFunction(name, ctor) -} - -// RegisterFunctionV2 adds a new Bloblang function to the global environment. -// All function names must match the regular expression -// /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func RegisterFunctionV2(name string, spec *PluginSpec, ctor FunctionConstructorV2) error { - return GlobalEnvironment().RegisterFunctionV2(name, spec, ctor) -} - -// RegisterAdvancedFunction adds a new advanced Bloblang function to the global -// environment. All function names must match the regular expression -// /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func RegisterAdvancedFunction(name string, spec *PluginSpec, ctor AdvancedFunctionConstructor) error { - return GlobalEnvironment().RegisterAdvancedFunction(name, spec, ctor) -} - -// WalkFunctions executes a provided function argument for every function that -// has been registered to the environment. -func (e *Environment) WalkFunctions(fn func(name string, spec *FunctionView)) { - e.env.WalkFunctions(func(name string, spec query.FunctionSpec) { - v := &FunctionView{spec: spec} - fn(name, v) - }) -} - -// WalkMethods executes a provided function argument for every method that has -// been registered to the environment. -func (e *Environment) WalkMethods(fn func(name string, spec *MethodView)) { - e.env.WalkMethods(func(name string, spec query.MethodSpec) { - v := &MethodView{spec: spec} - fn(name, v) - }) -} diff --git a/public/bloblang/environment_test.go b/public/bloblang/environment_test.go deleted file mode 100644 index 7981af8d70..0000000000 --- a/public/bloblang/environment_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package bloblang - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEnvironment(t *testing.T) { - env1, env2 := NewEnvironment(), NewEnvironment() - - require.NoError(t, env1.RegisterMethod("foo", func(_ ...any) (Method, error) { - return StringMethod(func(s string) (any, error) { - return "foo:" + s, nil - }), nil - })) - - require.NoError(t, env2.RegisterFunction("bar", func(_ ...any) (Function, error) { - return func() (any, error) { - return "bar", nil - }, nil - })) - - _, err := env1.Parse(`root = bar()`) - assert.EqualError(t, err, "unrecognised function 'bar'") - - exe, err := env1.Parse(`root = "bar".foo()`) - require.NoError(t, err) - - v, err := exe.Query(nil) - require.NoError(t, err) - assert.Equal(t, "foo:bar", v) - - _, err = env2.Parse(`root = "bar".foo()`) - assert.EqualError(t, err, "unrecognised method 'foo'") - - exe, err = env2.Parse(`root = bar()`) - require.NoError(t, err) - - v, err = exe.Query(nil) - require.NoError(t, err) - assert.Equal(t, "bar", v) -} - -func TestEnvironmentV2(t *testing.T) { - env1, env2 := NewEnvironment(), NewEnvironment() - - require.NoError(t, env1.RegisterMethodV2("foo", NewPluginSpec(), func(_ *ParsedParams) (Method, error) { - return StringMethod(func(s string) (any, error) { - return "foo:" + s, nil - }), nil - })) - - require.NoError(t, env2.RegisterFunctionV2("bar", NewPluginSpec(), func(_ *ParsedParams) (Function, error) { - return func() (any, error) { - return "bar", nil - }, nil - })) - - _, err := env1.Parse(`root = bar()`) - assert.EqualError(t, err, "unrecognised function 'bar'") - - exe, err := env1.Parse(`root = "bar".foo()`) - require.NoError(t, err) - - v, err := exe.Query(nil) - require.NoError(t, err) - assert.Equal(t, "foo:bar", v) - - _, err = env2.Parse(`root = "bar".foo()`) - assert.EqualError(t, err, "unrecognised method 'foo'") - - exe, err = env2.Parse(`root = bar()`) - require.NoError(t, err) - - v, err = exe.Query(nil) - require.NoError(t, err) - assert.Equal(t, "bar", v) -} - -func TestEmptyEnvironment(t *testing.T) { - env := NewEmptyEnvironment() - - require.NoError(t, env.RegisterMethod("foo", func(_ ...any) (Method, error) { - return StringMethod(func(s string) (any, error) { - return "foo:" + s, nil - }), nil - })) - - _, err := env.Parse(`root = now()`) - assert.EqualError(t, err, "unrecognised function 'now'") - - exe, err := env.Parse(`root = "hello world".foo()`) - require.NoError(t, err) - - v, err := exe.Query(nil) - require.NoError(t, err) - assert.Equal(t, "foo:hello world", v) -} - -func TestEnvironmentDisabledImports(t *testing.T) { - env := NewEmptyEnvironment().WithDisabledImports() - - _, err := env.Parse(`from "/tmp/foo.blobl"`) - require.Error(t, err) - assert.Contains(t, err.Error(), "imports are disabled in this context") -} diff --git a/public/bloblang/environment_unwrapper.go b/public/bloblang/environment_unwrapper.go deleted file mode 100644 index 56e1af59d6..0000000000 --- a/public/bloblang/environment_unwrapper.go +++ /dev/null @@ -1,23 +0,0 @@ -package bloblang - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang" -) - -type environmentUnwrapper struct { - child *bloblang.Environment -} - -func (e environmentUnwrapper) Unwrap() *bloblang.Environment { - return e.child -} - -// XUnwrapper is for internal use only, do not use this. -func (e *Environment) XUnwrapper() any { - return environmentUnwrapper{child: e.env} -} - -// XWrapEnvironment is for internal use only, do not use this. -func XWrapEnvironment(v *bloblang.Environment) *Environment { - return &Environment{env: v} -} diff --git a/public/bloblang/example_plugins_v2_test.go b/public/bloblang/example_plugins_v2_test.go deleted file mode 100644 index 5aaae29d25..0000000000 --- a/public/bloblang/example_plugins_v2_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package bloblang_test - -import ( - "encoding/json" - "fmt" - "math/rand" - "strings" - - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// This example demonstrates how to create Bloblang methods and functions and -// execute them with a Bloblang mapping using the new V2 methods, which adds -// support to our functions and methods for optional named parameters. -func Example_bloblangFunctionPluginV2() { - multiplyWrongSpec := bloblang.NewPluginSpec(). - Description("Multiplies two numbers together but gets it slightly wrong. Whoops."). - Param(bloblang.NewFloat64Param("left").Description("The first of two numbers to multiply.")). - Param(bloblang.NewFloat64Param("right").Description("The second of two numbers to multiply.")) - - if err := bloblang.RegisterFunctionV2( - "multiply_but_always_slightly_wrong", multiplyWrongSpec, - func(args *bloblang.ParsedParams) (bloblang.Function, error) { - left, err := args.GetFloat64("left") - if err != nil { - return nil, err - } - - right, err := args.GetFloat64("right") - if err != nil { - return nil, err - } - - return func() (any, error) { - return left*right + 0.02, nil - }, nil - }); err != nil { - panic(err) - } - - // Our function now optionally supports named parameters, when a function is - // instantiated with unamed parameters they must follow the order in which - // the parameters are registered. - mapping := ` -root.num_ab = multiply_but_always_slightly_wrong(left: this.a, right: this.b) -root.num_cd = multiply_but_always_slightly_wrong(this.c, this.d) -` - - exe, err := bloblang.Parse(mapping) - if err != nil { - panic(err) - } - - res, err := exe.Query(map[string]any{ - "a": 1.2, "b": 2.6, "c": 5.3, "d": 8.2, - }) - if err != nil { - panic(err) - } - - jsonBytes, err := json.Marshal(res) - if err != nil { - panic(err) - } - - fmt.Println(string(jsonBytes)) - // Output: {"num_ab":3.14,"num_cd":43.48} -} - -// This example demonstrates how to create Bloblang methods and functions and -// execute them with a Bloblang mapping using the new V2 methods, which adds -// support to our functions and methods for optional named parameters. -func Example_bloblangMethodPluginV2() { - hugStringSpec := bloblang.NewPluginSpec(). - Description("Wraps a string with a prefix and suffix."). - Param(bloblang.NewStringParam("prefix").Description("The prefix to insert.")). - Param(bloblang.NewStringParam("suffix").Description("The suffix to append.")) - - if err := bloblang.RegisterMethodV2("hug_string", hugStringSpec, func(args *bloblang.ParsedParams) (bloblang.Method, error) { - prefix, err := args.GetString("prefix") - if err != nil { - return nil, err - } - - suffix, err := args.GetString("suffix") - if err != nil { - return nil, err - } - - return bloblang.StringMethod(func(s string) (any, error) { - return prefix + s + suffix, nil - }), nil - }); err != nil { - panic(err) - } - - reverseSpec := bloblang.NewPluginSpec(). - Description("Reverses the order of an array target, but sometimes it randomly doesn't. Whoops.") - - if err := bloblang.RegisterMethodV2("sometimes_reverse", reverseSpec, func(*bloblang.ParsedParams) (bloblang.Method, error) { - rand := rand.New(rand.NewSource(0)) - return bloblang.ArrayMethod(func(in []any) (any, error) { - if rand.Int()%3 == 0 { - // Whoopsie - return in, nil - } - out := make([]any, len(in)) - copy(out, in) - for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { - out[i], out[j] = out[j], out[i] - } - return out, nil - }), nil - }); err != nil { - panic(err) - } - - // Our methods now optionally support named parameters, when a method is - // instantiated with unamed parameters they must follow the order in which - // the parameters are registered. - mapping := ` -root.new_summary = this.summary.hug_string(prefix: "meow", suffix: "woof") -root.reversed = this.names.sometimes_reverse() -` - - exe, err := bloblang.Parse(mapping) - if err != nil { - panic(err) - } - - res, err := exe.Query(map[string]any{ - "summary": "quack", - "names": []any{"denny", "pixie", "olaf", "jen", "spuz"}, - }) - if err != nil { - panic(err) - } - - jsonBytes, err := json.Marshal(res) - if err != nil { - panic(err) - } - - fmt.Println(string(jsonBytes)) - // Output: {"new_summary":"meowquackwoof","reversed":["spuz","jen","olaf","pixie","denny"]} -} - -// This example demonstrates how to create and use an isolated Bloblang -// environment with some standard functions removed. -func Example_bloblangRestrictedEnvironment() { - env := bloblang.NewEnvironment().WithoutFunctions("env", "file") - - customThingSpec := bloblang.NewPluginSpec(). - Description("Uppercases some stuff or something") - - if err := env.RegisterMethodV2("custom_thing", customThingSpec, func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return bloblang.StringMethod(func(s string) (any, error) { - return strings.ToUpper(s), nil - }), nil - }); err != nil { - panic(err) - } - - mapping := ` -root.foo = this.foo.string() -root.bar = this.bar + this.baz -root.buz = this.buz.content.custom_thing() -` - - exe, err := env.Parse(mapping) - if err != nil { - panic(err) - } - - res, err := exe.Query(map[string]any{ - "foo": 50.0, - "bar": "first bit ", - "baz": "second bit", - "buz": map[string]any{ - "id": "XXXX", - "content": "some nested content", - }, - }) - if err != nil { - panic(err) - } - - jsonBytes, err := json.Marshal(res) - if err != nil { - panic(err) - } - - fmt.Println(string(jsonBytes)) - // Output: {"bar":"first bit second bit","buz":"SOME NESTED CONTENT","foo":"50"} -} diff --git a/public/bloblang/executor.go b/public/bloblang/executor.go deleted file mode 100644 index 01dab63ec6..0000000000 --- a/public/bloblang/executor.go +++ /dev/null @@ -1,87 +0,0 @@ -package bloblang - -import ( - "errors" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/value" -) - -// Executor stores a parsed Bloblang mapping and provides APIs for executing it. -type Executor struct { - exec *mapping.Executor - emptyQueryMessage message.Batch -} - -func newExecutor(exec *mapping.Executor) *Executor { - return &Executor{ - exec: exec, - emptyQueryMessage: message.QuickBatch(nil), - } -} - -// ErrRootDeleted is returned by a Bloblang query when the mapping results in -// the root being deleted. It might be considered correct to do this in -// situations where filtering is allowed or expected. -var ErrRootDeleted = errors.New("root was deleted") - -// Query executes a Bloblang mapping against a value and returns the result. The -// argument and return values can be structured using the same -// map[string]interface{} and []interface{} types as would be returned by the Go -// standard json package unmarshaler. -// -// If the mapping results in the root of the new document being deleted then -// ErrRootDeleted is returned, which can be used as a signal to filter rather -// than fail the mapping. -func (e *Executor) Query(val any) (any, error) { - res, err := e.exec.Exec(query.FunctionContext{ - Maps: e.exec.Maps(), - Vars: map[string]any{}, - Index: 0, - MsgBatch: e.emptyQueryMessage, - }.WithValue(val)) - if err != nil { - return nil, err - } - - switch res.(type) { - case value.Delete: - return nil, ErrRootDeleted - case value.Nothing: - return val, nil - } - return res, nil -} - -// Overlay executes a Bloblang mapping against a value, where assignments are -// overlayed onto an existing structure. -// -// If the mapping results in the root of the new document being deleted then -// ErrRootDeleted is returned, which can be used as a signal to filter rather -// than fail the mapping. -func (e *Executor) Overlay(val any, onto *any) error { - vars := map[string]any{} - - if err := e.exec.ExecOnto(query.FunctionContext{ - Maps: e.exec.Maps(), - Vars: vars, - Index: 0, - MsgBatch: e.emptyQueryMessage, - NewValue: onto, - }.WithValue(val), mapping.AssignmentContext{ - Vars: vars, - Value: onto, - }); err != nil { - return err - } - - switch (*onto).(type) { - case value.Delete: - return ErrRootDeleted - case value.Nothing: - *onto = nil - } - return nil -} diff --git a/public/bloblang/executor_test.go b/public/bloblang/executor_test.go deleted file mode 100644 index e9051b9599..0000000000 --- a/public/bloblang/executor_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package bloblang - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestExecutorQuery(t *testing.T) { - tests := []struct { - name string - mapping string - input any - output any - errContains string - }{ - { - name: "no metadata get", - mapping: `root = meta("foo")`, - output: nil, - }, - { - name: "no metadata set", - mapping: `meta foo = "hello"`, - errContains: "unable to assign metadata in the current context", - }, - { - name: "variable get and set", - mapping: `let foo = "foo value" -root = $foo`, - output: "foo value", - }, - { - name: "not mapped", - mapping: `root = if false { "not this" }`, - input: map[string]any{ - "hello": "world", - }, - output: map[string]any{ - "hello": "world", - }, - }, - { - name: "delete root for some reason", - mapping: `root = deleted()`, - errContains: "root was deleted", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - m, err := NewEnvironment().Parse(test.mapping) - require.NoError(t, err) - - res, err := m.Query(test.input) - if test.errContains == "" { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } - }) - } -} - -func TestExecutorOverlay(t *testing.T) { - tests := []struct { - name string - mapping string - overlay any - input any - output any - errContains string - }{ - { - name: "no metadata get", - mapping: `root = meta("foo")`, - output: nil, - }, - { - name: "no metadata set", - mapping: `meta foo = "hello"`, - errContains: "unable to assign metadata in the current context", - }, - { - name: "variable get and set", - mapping: `let foo = "foo value" -root = $foo`, - output: "foo value", - }, - { - name: "set nested field from nil", - mapping: `root.foo.bar = "hello world"`, - output: map[string]any{ - "foo": map[string]any{ - "bar": "hello world", - }, - }, - }, - { - name: "set nested field from value", - mapping: `root.foo.bar = "hello world"`, - overlay: "value type", - errContains: "the root was a non-object type", - }, - { - name: "set nested field from object", - mapping: `root.foo.bar = "hello world"`, - overlay: map[string]any{ - "baz": "started with this", - }, - output: map[string]any{ - "foo": map[string]any{ - "bar": "hello world", - }, - "baz": "started with this", - }, - }, - { - name: "not mapped", - mapping: `root = if false { "not this" }`, - overlay: map[string]any{ - "hello": "world", - }, - output: map[string]any{ - "hello": "world", - }, - }, - { - name: "delete root for some reason", - mapping: `root = deleted()`, - errContains: "root was deleted", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - m, err := NewEnvironment().Parse(test.mapping) - require.NoError(t, err) - - res := test.overlay - err = m.Overlay(test.input, &res) - if test.errContains == "" { - require.NoError(t, err) - assert.Equal(t, test.output, res) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } - }) - } -} - -func TestExecutorQueryVarAllocation(t *testing.T) { - m, err := NewEnvironment().Parse(` -root.foo = $meow | "not init" -let meow = "meow meow" -root.bar = $meow | "not init" -root.baz = this.input - `) - require.NoError(t, err) - - expected := map[string]any{ - "foo": "not init", - "bar": "meow meow", - "baz": "from input", - } - - res, err := m.Query(map[string]any{ - "input": "from input", - }) - require.NoError(t, err) - assert.Equal(t, expected, res) - - // Run it again and make sure our variables were reset. - res, err = m.Query(map[string]any{ - "input": "from input 2", - }) - expected["baz"] = "from input 2" - require.NoError(t, err) - assert.Equal(t, expected, res) -} - -func TestExecutorOverlayVarAllocation(t *testing.T) { - m, err := NewEnvironment().Parse(` -root.foo = $meow | "not init" -let meow = "meow meow" -root.bar = $meow | "not init" -root.baz = this.input - `) - require.NoError(t, err) - - expected := map[string]any{ - "started": "with this", - "foo": "not init", - "bar": "meow meow", - "baz": "from input", - } - - var onto any = map[string]any{ - "started": "with this", - } - - err = m.Overlay(map[string]any{ - "input": "from input", - }, &onto) - require.NoError(t, err) - assert.Equal(t, expected, onto) - - // Run it again and make sure our variables were reset. - err = m.Overlay(map[string]any{ - "input": "from input 2", - }, &onto) - require.NoError(t, err) - expected["baz"] = "from input 2" - assert.Equal(t, expected, onto) -} diff --git a/public/bloblang/executor_unwrapper.go b/public/bloblang/executor_unwrapper.go deleted file mode 100644 index 63e4d66c2d..0000000000 --- a/public/bloblang/executor_unwrapper.go +++ /dev/null @@ -1,16 +0,0 @@ -package bloblang - -import "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - -type executorUnwrapper struct { - child *mapping.Executor -} - -func (e executorUnwrapper) Unwrap() *mapping.Executor { - return e.child -} - -// XUnwrapper is for internal use only, do not use this. -func (e *Executor) XUnwrapper() any { - return executorUnwrapper{child: e.exec} -} diff --git a/public/bloblang/function.go b/public/bloblang/function.go deleted file mode 100644 index 3bdb251268..0000000000 --- a/public/bloblang/function.go +++ /dev/null @@ -1,41 +0,0 @@ -package bloblang - -// Function defines a Bloblang function, arguments are provided to the -// constructor, allowing the implementation of this function to resolve them -// statically when possible. -type Function func() (any, error) - -// FunctionConstructor defines a constructor for a Bloblang function, where a -// variadic list of arguments are provided. -// -// When a function is parsed from a mapping with static arguments the -// constructor will be called only once at parse time. When a function is parsed -// with dynamic arguments, such as a value derived from the mapping input, the -// constructor will be called on each invocation of the mapping with the derived -// arguments. -// -// For a convenient way to perform type checking and coercion on the arguments -// use an ArgSpec. -type FunctionConstructor func(args ...any) (Function, error) - -// FunctionConstructorV2 defines a constructor for a Bloblang function where -// parameters are parsed using a ParamsSpec provided when registering the -// function. -// -// When a function is parsed from a mapping with static arguments the -// constructor will be called only once at parse time. When a function is parsed -// with dynamic arguments, such as a value derived from the mapping input, the -// constructor will be called on each invocation of the mapping with the derived -// arguments. -type FunctionConstructorV2 func(args *ParsedParams) (Function, error) - -//------------------------------------------------------------------------------ - -// AdvancedFunction defines a Bloblang function that accesses the execution -// context of the mapping during invocation. -type AdvancedFunction func(ctx *ExecContext) (any, error) - -// AdvancedFunctionConstructor defines a constructor for a Bloblang function -// where parameters are parsed using a ParamsSpec provided when registering the -// function, and the constructed function is provided an ExecContext. -type AdvancedFunctionConstructor func(args *ParsedParams) (AdvancedFunction, error) diff --git a/public/bloblang/method.go b/public/bloblang/method.go deleted file mode 100644 index 3a3cd3845e..0000000000 --- a/public/bloblang/method.go +++ /dev/null @@ -1,148 +0,0 @@ -package bloblang - -import ( - "time" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// Method defines a Bloblang function that executes on a value. Arguments are -// provided to the constructor, allowing the implementation of this method to -// resolve them statically when possible. -// -// In order to avoid type checking the value use one of the typed variants such -// as StringMethod. -type Method func(v any) (any, error) - -// MethodConstructor defines a constructor for a Bloblang method, where a -// variadic list of arguments are provided. -// -// When a method is parsed from a mapping with static arguments the constructor -// will be called only once at parse time. When a method is parsed with dynamic -// arguments, such as a value derived from the mapping input, the constructor -// will be called on each invocation of the mapping with the derived arguments. -// -// For a convenient way to perform type checking and coercion on the arguments -// use an ArgSpec. -type MethodConstructor func(args ...any) (Method, error) - -// MethodConstructorV2 defines a constructor for a Bloblang method where -// parameters are parsed using a ParamsSpec provided when registering the -// method. -// -// When a method is parsed from a mapping with static arguments the constructor -// will be called only once at parse time. When a method is parsed with dynamic -// arguments, such as a value derived from the mapping input, the constructor -// will be called on each invocation of the mapping with the derived arguments. -type MethodConstructorV2 func(args *ParsedParams) (Method, error) - -//------------------------------------------------------------------------------ - -// AdvancedMethod defines a Bloblang method that accesses the execution context -// of the mapping during invocation. Advanced methods are responsible for -// calling Exec upon the ExecFunction they target, and have the capability to -// mutate or modify the execution context of that target. -type AdvancedMethod func(ctx *ExecContext, fn *ExecFunction) (any, error) - -// AdvancedMethodConstructor defines a constructor for a Bloblang method -// where parameters are parsed using a ParamsSpec provided when registering the -// method, and the constructed method is provided an ExecContext. -type AdvancedMethodConstructor func(args *ParsedParams) (AdvancedMethod, error) - -//------------------------------------------------------------------------------ - -// StringMethod creates a general method signature from a string method by -// performing type checking on the method target. -func StringMethod(methodFn func(string) (any, error)) Method { - return func(v any) (any, error) { - str, err := value.IGetString(v) - if err != nil { - return nil, err - } - return methodFn(str) - } -} - -// BytesMethod creates a general method signature from a byte slice method by -// performing type checking on the method target. -func BytesMethod(methodFn func([]byte) (any, error)) Method { - return func(v any) (any, error) { - b, err := value.IGetBytes(v) - if err != nil { - return nil, err - } - return methodFn(b) - } -} - -// TimestampMethod creates a general method signature from a timestamp method by -// performing type checking on the method target. -func TimestampMethod(methodFn func(time.Time) (any, error)) Method { - return func(v any) (any, error) { - t, err := value.IGetTimestamp(v) - if err != nil { - return nil, err - } - return methodFn(t) - } -} - -// ArrayMethod creates a general method signature from an array method by -// performing type checking on the method target. -func ArrayMethod(methodFn func([]any) (any, error)) Method { - return func(v any) (any, error) { - arr, ok := v.([]any) - if !ok { - return nil, value.NewTypeError(v, value.TArray) - } - return methodFn(arr) - } -} - -// BoolMethod creates a general method signature from a bool method by -// performing type checking on the method target. -func BoolMethod(methodFn func(bool) (any, error)) Method { - return func(v any) (any, error) { - b, err := value.IGetBool(v) - if err != nil { - return nil, err - } - return methodFn(b) - } -} - -// Int64Method creates a general method signature from an int method by -// performing type checking on the method target. -func Int64Method(methodFn func(int64) (any, error)) Method { - return func(v any) (any, error) { - i, err := value.IGetInt(v) - if err != nil { - return nil, err - } - return methodFn(i) - } -} - -// Float64Method creates a general method signature from a float method by -// performing type checking on the method target. -func Float64Method(methodFn func(float64) (any, error)) Method { - return func(v any) (any, error) { - f, err := value.IGetNumber(v) - if err != nil { - return nil, err - } - return methodFn(f) - } -} - -// ObjectMethod creates a general method signature from an object method by -// performing type checking on the method target. -func ObjectMethod(methodFn func(obj map[string]any) (any, error)) Method { - return func(v any) (any, error) { - obj, ok := v.(map[string]any) - if !ok { - return nil, value.NewTypeError(v, value.TObject) - } - return methodFn(obj) - } -} diff --git a/public/bloblang/method_test.go b/public/bloblang/method_test.go deleted file mode 100644 index 8d722fb51c..0000000000 --- a/public/bloblang/method_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package bloblang - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTypedMethods(t *testing.T) { - testCases := []struct { - name string - fn Method - in any - exp any - err string - }{ - { - name: "bad int64", - fn: Int64Method(func(i int64) (any, error) { - return i, nil - }), - in: "not an int", - err: `expected number value, got string ("not an int")`, - }, - { - name: "good int64", - fn: Int64Method(func(i int64) (any, error) { - return i * 2, nil - }), - in: 5, - exp: int64(10), - }, - { - name: "bad float64", - fn: Float64Method(func(f float64) (any, error) { - return f, nil - }), - in: "not a float", - err: `expected number value, got string ("not a float")`, - }, - { - name: "good float64", - fn: Float64Method(func(f float64) (any, error) { - return f * 2, nil - }), - in: 5.0, - exp: 10.0, - }, - { - name: "bad string", - fn: StringMethod(func(s string) (any, error) { - return s, nil - }), - in: 5, - err: "expected string value, got number (5)", - }, - { - name: "good string", - fn: StringMethod(func(s string) (any, error) { - return "yep: " + s, nil - }), - in: "hey", - exp: "yep: hey", - }, - { - name: "bad bytes", - fn: BytesMethod(func(s []byte) (any, error) { - return s, nil - }), - in: 5, - err: "expected bytes value, got number (5)", - }, - { - name: "good bytes", - fn: BytesMethod(func(s []byte) (any, error) { - return append([]byte("yep: "), s...), nil - }), - in: []byte("hey"), - exp: []byte("yep: hey"), - }, - { - name: "bad bool", - fn: BoolMethod(func(b bool) (any, error) { - return b, nil - }), - in: "nope", - err: `expected bool value, got string ("nope")`, - }, - { - name: "good bool", - fn: BoolMethod(func(b bool) (any, error) { - return !b, nil - }), - in: true, - exp: false, - }, - { - name: "bad object", - fn: ObjectMethod(func(o map[string]any) (any, error) { - return o, nil - }), - in: 5, - err: "expected object value, got number (5)", - }, - { - name: "bad array", - fn: ArrayMethod(func(a []any) (any, error) { - return a, nil - }), - in: 5, - err: "expected array value, got number (5)", - }, - { - name: "bad timestamp", - fn: TimestampMethod(func(t time.Time) (any, error) { - return t, nil - }), - in: "not a timestamp", - err: "parsing time \"not a timestamp\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"not a timestamp\" as \"2006\"", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.name, func(t *testing.T) { - out, err := test.fn(test.in) - if test.err != "" { - assert.EqualError(t, err, test.err) - } else { - require.NoError(t, err) - assert.Equal(t, test.exp, out) - } - }) - } -} diff --git a/public/bloblang/package.go b/public/bloblang/package.go deleted file mode 100644 index 852dc7f7e0..0000000000 --- a/public/bloblang/package.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package bloblang provides high level APIs for registering custom Bloblang -// plugins, as well as for parsing and executing Bloblang mappings. -// -// For a video guide on Benthos plugins check out: https://youtu.be/uH6mKw-Ly0g -// And an example repo containing component plugins and tests can be found at: -// https://github.com/benthosdev/benthos-plugin-example -// -// Plugins can either be registered globally, and will be accessible to any -// component parsing Bloblang expressions in the executable, or they can be -// registered as part of an isolated environment. -package bloblang diff --git a/public/bloblang/parse_error.go b/public/bloblang/parse_error.go deleted file mode 100644 index 34fc8ee5db..0000000000 --- a/public/bloblang/parse_error.go +++ /dev/null @@ -1,34 +0,0 @@ -package bloblang - -import "github.com/benthosdev/benthos/v4/internal/bloblang/parser" - -// ParseError is a structured error type for Bloblang parser errors that -// provides access to information such as the line and column where the error -// occurred. -type ParseError struct { - Line int - Column int - - input []rune - iErr *parser.Error -} - -// Error returns a single line error string. -func (p *ParseError) Error() string { - return p.iErr.Error() -} - -// ErrorMultiline returns an error string spanning multiple lines that provides -// a cleaner view of the specific error. -func (p *ParseError) ErrorMultiline() string { - return p.iErr.ErrorAtPositionStructured("", p.input) -} - -func internalToPublicParserError(input []rune, p *parser.Error) *ParseError { - pErr := &ParseError{ - input: input, - iErr: p, - } - pErr.Line, pErr.Column = parser.LineAndColOf(input, p.Input) - return pErr -} diff --git a/public/bloblang/parse_error_test.go b/public/bloblang/parse_error_test.go deleted file mode 100644 index 9ed689d71b..0000000000 --- a/public/bloblang/parse_error_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package bloblang - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParseErrors(t *testing.T) { - tests := []struct { - name string - mapping string - expLine int - expCol int - errContains string - }{ - { - name: "Bad assignment error", - mapping: ` -root = -# there wasn't a value! -`, - expLine: 2, - expCol: 8, - errContains: "expected query", - }, - { - name: "Bad function args error", - mapping: ` -root.foo = this.foo -root.bar = this.bar.uppercase().replace("something)`, - expLine: 3, - expCol: 52, - errContains: "expected end quote", - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - _, err := Parse(test.mapping) - require.Error(t, err) - - pErr, ok := err.(*ParseError) - require.True(t, ok) - - assert.Equal(t, test.expLine, pErr.Line) - assert.Equal(t, test.expCol, pErr.Column) - assert.Contains(t, pErr.ErrorMultiline(), test.errContains, pErr.ErrorMultiline()) - }) - } -} diff --git a/public/bloblang/spec.go b/public/bloblang/spec.go deleted file mode 100644 index bf881e3605..0000000000 --- a/public/bloblang/spec.go +++ /dev/null @@ -1,397 +0,0 @@ -package bloblang - -import ( - "encoding/json" - "time" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -// ParamDefinition describes a single parameter for a function or method. -type ParamDefinition struct { - def query.ParamDefinition -} - -// NewStringParam creates a new string typed parameter. Parameter names must -// match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func NewStringParam(name string) ParamDefinition { - return ParamDefinition{ - def: query.ParamString(name, ""), - } -} - -// NewTimestampParam creates a new timestamp typed parameter. Parameter names -// must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func NewTimestampParam(name string) ParamDefinition { - return ParamDefinition{ - def: query.ParamTimestamp(name, ""), - } -} - -// NewInt64Param creates a new 64-bit integer typed parameter. Parameter names -// must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func NewInt64Param(name string) ParamDefinition { - return ParamDefinition{ - def: query.ParamInt64(name, ""), - } -} - -// NewFloat64Param creates a new float64 typed parameter. Parameter names must -// match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func NewFloat64Param(name string) ParamDefinition { - return ParamDefinition{ - def: query.ParamFloat(name, ""), - } -} - -// NewBoolParam creates a new bool typed parameter. Parameter names must match -// the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func NewBoolParam(name string) ParamDefinition { - return ParamDefinition{ - def: query.ParamBool(name, ""), - } -} - -// NewAnyParam creates a new parameter that can be any type. Parameter names -// must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ (snake case). -func NewAnyParam(name string) ParamDefinition { - return ParamDefinition{ - def: query.ParamAny(name, ""), - } -} - -// NewQueryParam creates a new advanced parameter that can yield any value and -// is encapsulated as an ExecFunction. This is important for advanced functions -// and methods that need greater control over how the parameters are resolved. -// The allowScalars parameter determines whether scalar values are valid for -// this parameter, when `false` all parameter arguments must be dynamic -// expressions. -// -// However, most plugins will not benefit from query parameters, and they can -// only be resolved via the ExecContext provided to functions and methods -// registered with RegisterAdvancedFunction and RegisterAdvancedMethod -// respectively. -// -// Parameter names must match the regular expression /^[a-z0-9]+(_[a-z0-9]+)*$/ -// (snake case). -func NewQueryParam(name string, allowScalars bool) ParamDefinition { - return ParamDefinition{ - def: query.ParamQuery(name, "", allowScalars), - } -} - -// Description adds an optional description to the parameter definition, this is -// used when generating documentation for the parameter to describe what the -// parameter is for. -func (d ParamDefinition) Description(str string) ParamDefinition { - d.def.Description = str - return d -} - -// Optional marks the parameter as optional. -func (d ParamDefinition) Optional() ParamDefinition { - d.def = d.def.Optional() - return d -} - -// Default adds a default value to a parameter, also making it implicitly -// optional. -func (d ParamDefinition) Default(v any) ParamDefinition { - d.def = d.def.Default(v) - return d -} - -//------------------------------------------------------------------------------ - -// PluginSpec documents and defines the parameters of a function or method and -// the way in which it should be used. -// -// Using a plugin spec with explicit parameters means that instantiations of the -// plugin can be done using either classic argument types (foo, bar, baz), -// following the order in which the parameters are added, or named style -// (c: baz, a: foo). -type PluginSpec struct { - status query.Status - category string - description string - impure bool - isStaticFn func(params *ParsedParams) bool - params query.Params - examples []pluginExample - version string -} - -type pluginExample struct { - summary string - mapping string - inputOutputs [][2]string - skipTesting bool -} - -// NewPluginSpec creates a new plugin definition for a function or method -// plugin that describes the arguments that the plugin expects. -func NewPluginSpec() *PluginSpec { - return &PluginSpec{ - params: query.NewParams(), - isStaticFn: func(params *ParsedParams) bool { - return false - }, - } -} - -// Experimental flags the plugin as an experimental component. -func (p *PluginSpec) Experimental() *PluginSpec { - p.status = query.StatusExperimental - return p -} - -// Beta flags the plugin as a beta component. -func (p *PluginSpec) Beta() *PluginSpec { - p.status = query.StatusBeta - return p -} - -// Deprecated flags the plugin as a deprecated component, it will still be valid -// in mappings but won't appear prominently in documentation. -func (p *PluginSpec) Deprecated() *PluginSpec { - p.status = query.StatusDeprecated - return p -} - -// Category adds an optional category string to the plugin spec, this is used -// when generating documentation for the plugin. -func (p *PluginSpec) Category(str string) *PluginSpec { - p.category = str - return p -} - -// Description adds an optional description to the plugin spec, this is used -// when generating documentation for the plugin. -func (p *PluginSpec) Description(str string) *PluginSpec { - p.description = str - return p -} - -// Version specifies that this plugin was introduced in a given version. -func (p *PluginSpec) Version(v string) *PluginSpec { - p.version = v - return p -} - -// Example adds an optional example to the plugin spec, this is used when -// generating documentation for the plugin. An example consists of a short -// summary, a mapping demonstrating the plugin, and one or more input/output -// combinations. When generating documentation the project will also run these -// examples and ensure they produce the documented results, in order to skip -// these checks use ExampleNotTested. -func (p *PluginSpec) Example(summary, mapping string, inputOutputs ...[2]string) *PluginSpec { - p.examples = append(p.examples, pluginExample{ - summary: summary, - mapping: mapping, - inputOutputs: inputOutputs, - }) - return p -} - -// ExampleNotTested adds an optional example to the plugin spec, this is used -// when generating documentation for the plugin. An example consists of a short -// summary, a mapping demonstrating the plugin, and one or more input/output -// combinations. -// -// The implementation of the plugin is expected to be correct, but the -// input/output combinations are not tested to be accurate at any stage. This is -// particularly useful in cases where the example input/output combinations are -// redacted or non-deterministic. -func (p *PluginSpec) ExampleNotTested(summary, mapping string, inputOutputs ...[2]string) *PluginSpec { - p.examples = append(p.examples, pluginExample{ - summary: summary, - mapping: mapping, - inputOutputs: inputOutputs, - skipTesting: true, - }) - return p -} - -// Variadic marks this plugin as having variadic parameters, which means any -// number of arguments can be provided and they are unnamed. It is invalid to -// combine variadic with named parameters. -// -// A variadic method is able to extract arguments from a *ParsedParams object -// via the AsSlice method. -func (p *PluginSpec) Variadic() *PluginSpec { - p.params.Variadic = true - return p -} - -// Param adds a parameter to the spec. Instantiations of the plugin with -// nameless arguments (foo, bar, baz) must follow the order in which fields are -// added to the spec. -func (p *PluginSpec) Param(def ParamDefinition) *PluginSpec { - p.params = p.params.Add(def.def) - return p -} - -// Impure marks the plugin as "impure", meaning it either reads from or -// interacts with state outside of the boundaries of a single mapping -// invocation. This usually means reading state from the machine. Impure plugins -// are excluded from some bloblang environments. -func (p *PluginSpec) Impure() *PluginSpec { - p.impure = true - return p -} - -// Static marks the plugin as a statically evaluated function or method. This is -// a guarantee that given the same parameters this plugin will always yield the -// same value. -// -// Marking a function or method as static has the advantage that it can -// sometimes be optimistically evaluated at mapping parse time when given static -// arguments. -func (p *PluginSpec) Static() *PluginSpec { - p.isStaticFn = func(params *ParsedParams) bool { - return true - } - return p -} - -// StaticWithFunc marks the plugin as a potentially statically evaluated -// function or method, but only given certain parameters as determined by the -// provided closure function. This is a guarantee that given the same parameters -// this plugin will always yield the same value. -// -// Marking a function or method as static has the advantage that it can -// sometimes be optimistically evaluated at mapping parse time when given static -// arguments. -func (p *PluginSpec) StaticWithFunc(fn func(params *ParsedParams) bool) *PluginSpec { - p.isStaticFn = fn - return p -} - -// EncodeJSON attempts to parse a JSON object as a byte slice and uses it to -// populate the configuration spec. The schema of this method is undocumented -// and is not intended for general use. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func (p *PluginSpec) EncodeJSON(v []byte) error { - def := struct { - Description string `json:"description"` - Params query.Params `json:"params"` - }{} - if err := json.Unmarshal(v, &def); err != nil { - return err - } - p.description = def.Description - p.params = def.Params - return nil -} - -//------------------------------------------------------------------------------ - -// ParsedParams is a reference to the arguments of a method or function -// instantiation. -type ParsedParams struct { - par *query.ParsedParams - e *Environment -} - -func newParsedParams(p *query.ParsedParams, e *Environment) *ParsedParams { - return &ParsedParams{ - par: p, - e: e, - } -} - -// AsSlice returns a slice of raw argument values. -func (p *ParsedParams) AsSlice() []any { - return p.par.Raw() -} - -// Get an argument value with a given name and return it boxed within an empty -// interface. -func (p *ParsedParams) Get(name string) (any, error) { - return p.par.Field(name) -} - -// GetString returns a string argument value with a given name. -func (p *ParsedParams) GetString(name string) (string, error) { - return p.par.FieldString(name) -} - -// GetOptionalString returns a string argument value with a given name if it -// was defined, otherwise nil. -func (p *ParsedParams) GetOptionalString(name string) (*string, error) { - return p.par.FieldOptionalString(name) -} - -// GetTimestamp returns a timestamp argument value with a given name. -func (p *ParsedParams) GetTimestamp(name string) (time.Time, error) { - return p.par.FieldTimestamp(name) -} - -// GetOptionalTimestamp returns a timestamp argument value with a given name if -// it was defined, otherwise nil. -func (p *ParsedParams) GetOptionalTimestamp(name string) (*time.Time, error) { - return p.par.FieldOptionalTimestamp(name) -} - -// GetInt64 returns an integer argument value with a given name. -func (p *ParsedParams) GetInt64(name string) (int64, error) { - return p.par.FieldInt64(name) -} - -// GetOptionalInt64 returns an int argument value with a given name if it was -// defined, otherwise nil. -func (p *ParsedParams) GetOptionalInt64(name string) (*int64, error) { - return p.par.FieldOptionalInt64(name) -} - -// GetFloat64 returns a float argument value with a given name. -func (p *ParsedParams) GetFloat64(name string) (float64, error) { - return p.par.FieldFloat(name) -} - -// GetOptionalFloat64 returns a float argument value with a given name if it -// was defined, otherwise nil. -func (p *ParsedParams) GetOptionalFloat64(name string) (*float64, error) { - return p.par.FieldOptionalFloat(name) -} - -// GetBool returns a bool argument value with a given name. -func (p *ParsedParams) GetBool(name string) (bool, error) { - return p.par.FieldBool(name) -} - -// GetOptionalBool returns a bool argument value with a given name if it was -// defined, otherwise nil. -func (p *ParsedParams) GetOptionalBool(name string) (*bool, error) { - return p.par.FieldOptionalBool(name) -} - -// GetQuery returns an ExecFunction from a parameter defined as a NewQueryParam. -func (p *ParsedParams) GetQuery(name string) (*ExecFunction, error) { - fn, err := p.par.FieldQuery(name) - if err != nil { - return nil, err - } - return newExecFunction(fn), nil -} - -// GetOptionalQuery returns an ExecFunction from a parameter defined as a -// NewQueryParam if it was defined, otherwise nil. -func (p *ParsedParams) GetOptionalQuery(name string) (*ExecFunction, error) { - fn, err := p.par.FieldOptionalQuery(name) - if err != nil { - return nil, err - } - if fn == nil { - return nil, nil - } - return newExecFunction(fn), nil -} - -// ImportFile attempts to read a file via the underlying environment importer. -// Relative paths will be resolved from the path of the file being imported. -func (p *ParsedParams) ImportFile(name string) ([]byte, error) { - return p.e.env.ImportFile(name) -} diff --git a/public/bloblang/spec_test.go b/public/bloblang/spec_test.go deleted file mode 100644 index bfd614a09c..0000000000 --- a/public/bloblang/spec_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package bloblang - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParsedParamsNameless(t *testing.T) { - params := NewPluginSpec(). - Param(NewStringParam("first")). - Param(NewInt64Param("second").Default(5)). - Param(NewBoolParam("third")) - - parsedInternal, err := params.params.PopulateNameless("foo", 9, true) - require.NoError(t, err) - - assert.Equal(t, []any{ - "foo", int64(9), true, - }, parsedInternal.Raw()) - - parsed := &ParsedParams{par: parsedInternal} - - v, err := parsed.Get("first") - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, err = parsed.Get("second") - require.NoError(t, err) - assert.Equal(t, int64(9), v) - - v, err = parsed.Get("third") - require.NoError(t, err) - assert.Equal(t, true, v) - - _, err = parsed.Get("fourth") - require.Error(t, err) -} - -func TestParsedParamsNamed(t *testing.T) { - params := NewPluginSpec(). - Param(NewStringParam("first")). - Param(NewInt64Param("second").Default(5)). - Param(NewBoolParam("third")) - - parsedInternal, err := params.params.PopulateNamed(map[string]any{ - "first": "foo", - "second": 9, - "third": true, - }) - require.NoError(t, err) - - assert.Equal(t, []any{ - "foo", int64(9), true, - }, parsedInternal.Raw()) - - parsed := &ParsedParams{par: parsedInternal} - - v, err := parsed.Get("first") - require.NoError(t, err) - assert.Equal(t, "foo", v) - - v, err = parsed.Get("second") - require.NoError(t, err) - assert.Equal(t, int64(9), v) - - v, err = parsed.Get("third") - require.NoError(t, err) - assert.Equal(t, true, v) - - _, err = parsed.Get("fourth") - require.Error(t, err) -} - -func TestParsedParams(t *testing.T) { - params := NewPluginSpec(). - Param(NewStringParam("first").Optional()). - Param(NewInt64Param("second").Optional()). - Param(NewFloat64Param("third").Optional()). - Param(NewBoolParam("fourth").Optional()). - Param(NewTimestampParam("fifth").Optional()) - - parsedInternal, err := params.params.PopulateNameless("one", 2, 3.0, true, "2023-10-13T08:39:28+00:00") - require.NoError(t, err) - - parsed := &ParsedParams{par: parsedInternal} - - s, err := parsed.GetString("first") - require.NoError(t, err) - assert.Equal(t, "one", s) - - i, err := parsed.GetInt64("second") - require.NoError(t, err) - assert.Equal(t, int64(2), i) - - f, err := parsed.GetFloat64("third") - require.NoError(t, err) - assert.Equal(t, 3.0, f) - - b, err := parsed.GetBool("fourth") - require.NoError(t, err) - assert.True(t, b) - - ts, err := parsed.GetTimestamp("fifth") - require.NoError(t, err) - assert.Equal(t, ts.UTC(), time.Unix(1697186368, 0).UTC()) -} - -func TestParsedParamsOptional(t *testing.T) { - params := NewPluginSpec(). - Param(NewStringParam("first").Optional()). - Param(NewInt64Param("second").Optional()). - Param(NewFloat64Param("third").Optional()). - Param(NewBoolParam("fourth").Optional()). - Param(NewTimestampParam("fifth").Optional()) - - parsedInternal, err := params.params.PopulateNameless("one", 2, 3.0, true, "2023-10-13T08:39:28+00:00") - require.NoError(t, err) - - parsed := &ParsedParams{par: parsedInternal} - - s, err := parsed.GetOptionalString("first") - require.NoError(t, err) - require.NotNil(t, s) - assert.Equal(t, "one", *s) - - i, err := parsed.GetOptionalInt64("second") - require.NoError(t, err) - require.NotNil(t, i) - assert.Equal(t, int64(2), *i) - - f, err := parsed.GetOptionalFloat64("third") - require.NoError(t, err) - require.NotNil(t, f) - assert.Equal(t, 3.0, *f) - - b, err := parsed.GetOptionalBool("fourth") - require.NoError(t, err) - require.NotNil(t, b) - assert.True(t, *b) - - ts, err := parsed.GetOptionalTimestamp("fifth") - require.NoError(t, err) - require.NotNil(t, ts) - assert.Equal(t, ts.UTC(), time.Unix(1697186368, 0).UTC()) - - // Without any args - parsedInternal, err = params.params.PopulateNameless() - require.NoError(t, err) - - parsed = &ParsedParams{par: parsedInternal} - - s, err = parsed.GetOptionalString("first") - require.NoError(t, err) - assert.Nil(t, s) - - i, err = parsed.GetOptionalInt64("second") - require.NoError(t, err) - assert.Nil(t, i) - - f, err = parsed.GetOptionalFloat64("third") - require.NoError(t, err) - assert.Nil(t, f) - - b, err = parsed.GetOptionalBool("fourth") - require.NoError(t, err) - assert.Nil(t, b) - - ts, err = parsed.GetOptionalTimestamp("fifth") - require.NoError(t, err) - assert.Nil(t, ts) -} diff --git a/public/bloblang/util.go b/public/bloblang/util.go deleted file mode 100644 index 429c992b6b..0000000000 --- a/public/bloblang/util.go +++ /dev/null @@ -1,50 +0,0 @@ -package bloblang - -import ( - "time" - - "github.com/benthosdev/benthos/v4/internal/value" -) - -// ValueToString converts any value into a string according to the same rules -// that other native benthos components including bloblang would follow, where -// simple value types are stringified, but complex types are converted into JSON -// marshalled as a string. -func ValueToString(v any) string { - return value.IToString(v) -} - -// ValueAsBytes takes a boxed value and attempts to return a byte slice value. -// Returns an error if the value is not a string or byte slice. -func ValueAsBytes(v any) ([]byte, error) { - return value.IGetBytes(v) -} - -// ValueAsTimestamp takes a boxed value and attempts to coerce it into a -// timestamp, either by interpretting a numerical value as a unix timestamp, or -// by parsing a string value as RFC3339Nano. -func ValueAsTimestamp(v any) (time.Time, error) { - return value.IGetTimestamp(v) -} - -// ValueAsInt64 takes a boxed value and attempts to extract a number from it. -func ValueAsInt64(v any) (int64, error) { - return value.IGetInt(v) -} - -// ValueAsFloat64 takes a boxed value and attempts to extract a number from it. -func ValueAsFloat64(v any) (float64, error) { - return value.IGetNumber(v) -} - -// ValueAsFloat32 takes a boxed value and attempts to extract a number from it. -func ValueAsFloat32(v any) (float32, error) { - return value.IGetFloat32(v) -} - -// ValueSanitized takes a boxed value of any type and attempts to convert it -// into one of the following types: string, []byte, int64, uint64, float64, -// bool, []interface{}, map[string]interface{}, Delete, Nothing. -func ValueSanitized(i any) any { - return value.ISanitize(i) -} diff --git a/public/bloblang/view.go b/public/bloblang/view.go deleted file mode 100644 index bb7cbbd7cd..0000000000 --- a/public/bloblang/view.go +++ /dev/null @@ -1,48 +0,0 @@ -package bloblang - -import ( - "encoding/json" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" -) - -// FunctionView describes a particular function belonging to a Bloblang -// environment. -type FunctionView struct { - spec query.FunctionSpec -} - -// Description provides an overview of the function. -func (v *FunctionView) Description() string { - return v.spec.Description -} - -// FormatJSON returns a byte slice of the function configuration formatted as a -// JSON object. The schema of this method is undocumented and is not intended -// for general use. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func (v *FunctionView) FormatJSON() ([]byte, error) { - return json.Marshal(v.spec) -} - -// MethodView describes a particular method belonging to a Bloblang environment. -type MethodView struct { - spec query.MethodSpec -} - -// Description provides an overview of the method. -func (v *MethodView) Description() string { - return v.spec.Description -} - -// FormatJSON returns a byte slice of the method configuration formatted as a -// JSON object. The schema of this method is undocumented and is not intended -// for general use. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func (v *MethodView) FormatJSON() ([]byte, error) { - return json.Marshal(v.spec) -} diff --git a/public/components/all/package.go b/public/components/all/package.go index 920245c8ab..9fc4bd3a22 100644 --- a/public/components/all/package.go +++ b/public/components/all/package.go @@ -5,50 +5,50 @@ package all import ( // Import all public sub-categories. - _ "github.com/benthosdev/benthos/v4/public/components/amqp09" - _ "github.com/benthosdev/benthos/v4/public/components/amqp1" - _ "github.com/benthosdev/benthos/v4/public/components/avro" - _ "github.com/benthosdev/benthos/v4/public/components/aws" - _ "github.com/benthosdev/benthos/v4/public/components/azure" - _ "github.com/benthosdev/benthos/v4/public/components/beanstalkd" - _ "github.com/benthosdev/benthos/v4/public/components/cassandra" - _ "github.com/benthosdev/benthos/v4/public/components/changelog" - _ "github.com/benthosdev/benthos/v4/public/components/cockroachdb" - _ "github.com/benthosdev/benthos/v4/public/components/confluent" - _ "github.com/benthosdev/benthos/v4/public/components/couchbase" - _ "github.com/benthosdev/benthos/v4/public/components/crypto" - _ "github.com/benthosdev/benthos/v4/public/components/dgraph" - _ "github.com/benthosdev/benthos/v4/public/components/discord" - _ "github.com/benthosdev/benthos/v4/public/components/elasticsearch" - _ "github.com/benthosdev/benthos/v4/public/components/gcp" - _ "github.com/benthosdev/benthos/v4/public/components/hdfs" - _ "github.com/benthosdev/benthos/v4/public/components/influxdb" - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/jaeger" - _ "github.com/benthosdev/benthos/v4/public/components/javascript" - _ "github.com/benthosdev/benthos/v4/public/components/kafka" - _ "github.com/benthosdev/benthos/v4/public/components/maxmind" - _ "github.com/benthosdev/benthos/v4/public/components/memcached" - _ "github.com/benthosdev/benthos/v4/public/components/mongodb" - _ "github.com/benthosdev/benthos/v4/public/components/mqtt" - _ "github.com/benthosdev/benthos/v4/public/components/msgpack" - _ "github.com/benthosdev/benthos/v4/public/components/nanomsg" - _ "github.com/benthosdev/benthos/v4/public/components/nats" - _ "github.com/benthosdev/benthos/v4/public/components/nsq" - _ "github.com/benthosdev/benthos/v4/public/components/opensearch" - _ "github.com/benthosdev/benthos/v4/public/components/otlp" - _ "github.com/benthosdev/benthos/v4/public/components/prometheus" - _ "github.com/benthosdev/benthos/v4/public/components/pulsar" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - _ "github.com/benthosdev/benthos/v4/public/components/pure/extended" - _ "github.com/benthosdev/benthos/v4/public/components/pusher" - _ "github.com/benthosdev/benthos/v4/public/components/redis" - _ "github.com/benthosdev/benthos/v4/public/components/sentry" - _ "github.com/benthosdev/benthos/v4/public/components/sftp" - _ "github.com/benthosdev/benthos/v4/public/components/snowflake" - _ "github.com/benthosdev/benthos/v4/public/components/splunk" - _ "github.com/benthosdev/benthos/v4/public/components/sql" - _ "github.com/benthosdev/benthos/v4/public/components/statsd" - _ "github.com/benthosdev/benthos/v4/public/components/twitter" - _ "github.com/benthosdev/benthos/v4/public/components/wasm" + _ "github.com/redpanda-data/connect/v4/public/components/amqp09" + _ "github.com/redpanda-data/connect/v4/public/components/amqp1" + _ "github.com/redpanda-data/connect/v4/public/components/avro" + _ "github.com/redpanda-data/connect/v4/public/components/aws" + _ "github.com/redpanda-data/connect/v4/public/components/azure" + _ "github.com/redpanda-data/connect/v4/public/components/beanstalkd" + _ "github.com/redpanda-data/connect/v4/public/components/cassandra" + _ "github.com/redpanda-data/connect/v4/public/components/changelog" + _ "github.com/redpanda-data/connect/v4/public/components/cockroachdb" + _ "github.com/redpanda-data/connect/v4/public/components/confluent" + _ "github.com/redpanda-data/connect/v4/public/components/couchbase" + _ "github.com/redpanda-data/connect/v4/public/components/crypto" + _ "github.com/redpanda-data/connect/v4/public/components/dgraph" + _ "github.com/redpanda-data/connect/v4/public/components/discord" + _ "github.com/redpanda-data/connect/v4/public/components/elasticsearch" + _ "github.com/redpanda-data/connect/v4/public/components/gcp" + _ "github.com/redpanda-data/connect/v4/public/components/hdfs" + _ "github.com/redpanda-data/connect/v4/public/components/influxdb" + _ "github.com/redpanda-data/connect/v4/public/components/io" + _ "github.com/redpanda-data/connect/v4/public/components/jaeger" + _ "github.com/redpanda-data/connect/v4/public/components/javascript" + _ "github.com/redpanda-data/connect/v4/public/components/kafka" + _ "github.com/redpanda-data/connect/v4/public/components/maxmind" + _ "github.com/redpanda-data/connect/v4/public/components/memcached" + _ "github.com/redpanda-data/connect/v4/public/components/mongodb" + _ "github.com/redpanda-data/connect/v4/public/components/mqtt" + _ "github.com/redpanda-data/connect/v4/public/components/msgpack" + _ "github.com/redpanda-data/connect/v4/public/components/nanomsg" + _ "github.com/redpanda-data/connect/v4/public/components/nats" + _ "github.com/redpanda-data/connect/v4/public/components/nsq" + _ "github.com/redpanda-data/connect/v4/public/components/opensearch" + _ "github.com/redpanda-data/connect/v4/public/components/otlp" + _ "github.com/redpanda-data/connect/v4/public/components/prometheus" + _ "github.com/redpanda-data/connect/v4/public/components/pulsar" + _ "github.com/redpanda-data/connect/v4/public/components/pure" + _ "github.com/redpanda-data/connect/v4/public/components/pure/extended" + _ "github.com/redpanda-data/connect/v4/public/components/pusher" + _ "github.com/redpanda-data/connect/v4/public/components/redis" + _ "github.com/redpanda-data/connect/v4/public/components/sentry" + _ "github.com/redpanda-data/connect/v4/public/components/sftp" + _ "github.com/redpanda-data/connect/v4/public/components/snowflake" + _ "github.com/redpanda-data/connect/v4/public/components/splunk" + _ "github.com/redpanda-data/connect/v4/public/components/sql" + _ "github.com/redpanda-data/connect/v4/public/components/statsd" + _ "github.com/redpanda-data/connect/v4/public/components/twitter" + _ "github.com/redpanda-data/connect/v4/public/components/wasm" ) diff --git a/public/components/all/x_benthos_extra.go b/public/components/all/x_benthos_extra.go index f07e3a81ef..6437b8b927 100644 --- a/public/components/all/x_benthos_extra.go +++ b/public/components/all/x_benthos_extra.go @@ -5,5 +5,5 @@ package all import ( // Import extra packages, these are packages only imported with the tag // x_benthos_extra, which is normally reserved for -cgo suffixed builds - _ "github.com/benthosdev/benthos/v4/internal/impl/zeromq" + _ "github.com/redpanda-data/connect/v4/internal/impl/zeromq" ) diff --git a/public/components/amqp09/package.go b/public/components/amqp09/package.go index 5dec0546cf..a51c27dfda 100644 --- a/public/components/amqp09/package.go +++ b/public/components/amqp09/package.go @@ -2,5 +2,5 @@ package amqp09 import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/amqp09" + _ "github.com/redpanda-data/connect/v4/internal/impl/amqp09" ) diff --git a/public/components/amqp1/package.go b/public/components/amqp1/package.go index e0903ff139..e86ca126ac 100644 --- a/public/components/amqp1/package.go +++ b/public/components/amqp1/package.go @@ -2,5 +2,5 @@ package amqp1 import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/amqp1" + _ "github.com/redpanda-data/connect/v4/internal/impl/amqp1" ) diff --git a/public/components/avro/package.go b/public/components/avro/package.go index 4325dc5ecc..90b842d559 100644 --- a/public/components/avro/package.go +++ b/public/components/avro/package.go @@ -2,5 +2,5 @@ package avro import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/avro" + _ "github.com/redpanda-data/connect/v4/internal/impl/avro" ) diff --git a/public/components/aws/package.go b/public/components/aws/package.go index 625f66915f..b962810558 100644 --- a/public/components/aws/package.go +++ b/public/components/aws/package.go @@ -2,8 +2,8 @@ package aws import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/aws" - _ "github.com/benthosdev/benthos/v4/internal/impl/elasticsearch/aws" - _ "github.com/benthosdev/benthos/v4/internal/impl/kafka/aws" - _ "github.com/benthosdev/benthos/v4/internal/impl/opensearch/aws" + _ "github.com/redpanda-data/connect/v4/internal/impl/aws" + _ "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch/aws" + _ "github.com/redpanda-data/connect/v4/internal/impl/kafka/aws" + _ "github.com/redpanda-data/connect/v4/internal/impl/opensearch/aws" ) diff --git a/public/components/aws/serverless.go b/public/components/aws/serverless.go deleted file mode 100644 index 1a8fde67b3..0000000000 --- a/public/components/aws/serverless.go +++ /dev/null @@ -1,13 +0,0 @@ -package aws - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/serverless/lambda" -) - -// RunLambda executes Benthos as an AWS Lambda function. Configuration can be -// stored within the environment variable BENTHOS_CONFIG. -func RunLambda(ctx context.Context) { - lambda.Run() -} diff --git a/public/components/azure/package.go b/public/components/azure/package.go index 29e2613099..1fd3de8c75 100644 --- a/public/components/azure/package.go +++ b/public/components/azure/package.go @@ -2,5 +2,5 @@ package azure import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/azure" + _ "github.com/redpanda-data/connect/v4/internal/impl/azure" ) diff --git a/public/components/beanstalkd/package.go b/public/components/beanstalkd/package.go index e55295bfa2..cf11d6d76e 100644 --- a/public/components/beanstalkd/package.go +++ b/public/components/beanstalkd/package.go @@ -2,5 +2,5 @@ package beanstalkd import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/beanstalkd" + _ "github.com/redpanda-data/connect/v4/internal/impl/beanstalkd" ) diff --git a/public/components/cassandra/package.go b/public/components/cassandra/package.go index 1bbba04fe1..77decc92b6 100644 --- a/public/components/cassandra/package.go +++ b/public/components/cassandra/package.go @@ -2,5 +2,5 @@ package cassandra import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/cassandra" + _ "github.com/redpanda-data/connect/v4/internal/impl/cassandra" ) diff --git a/public/components/changelog/package.go b/public/components/changelog/package.go index e6938de628..2b82c7788b 100644 --- a/public/components/changelog/package.go +++ b/public/components/changelog/package.go @@ -2,5 +2,5 @@ package changelog import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/changelog" + _ "github.com/redpanda-data/connect/v4/internal/impl/changelog" ) diff --git a/public/components/cockroachdb/package.go b/public/components/cockroachdb/package.go index bfac69107b..23b64a37e5 100644 --- a/public/components/cockroachdb/package.go +++ b/public/components/cockroachdb/package.go @@ -2,5 +2,5 @@ package cockroachdb import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/cockroachdb" + _ "github.com/redpanda-data/connect/v4/internal/impl/cockroachdb" ) diff --git a/public/components/confluent/package.go b/public/components/confluent/package.go index 9a768a4d18..bed6484004 100644 --- a/public/components/confluent/package.go +++ b/public/components/confluent/package.go @@ -2,5 +2,5 @@ package confluent import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/confluent" + _ "github.com/redpanda-data/connect/v4/internal/impl/confluent" ) diff --git a/public/components/couchbase/package.go b/public/components/couchbase/package.go index 901f8de909..8f286206bf 100644 --- a/public/components/couchbase/package.go +++ b/public/components/couchbase/package.go @@ -4,5 +4,5 @@ package couchbase import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/couchbase" + _ "github.com/redpanda-data/connect/v4/internal/impl/couchbase" ) diff --git a/public/components/crypto/package.go b/public/components/crypto/package.go index da47315009..cfa6b7c8a2 100644 --- a/public/components/crypto/package.go +++ b/public/components/crypto/package.go @@ -2,5 +2,5 @@ package crypto import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/crypto" + _ "github.com/redpanda-data/connect/v4/internal/impl/crypto" ) diff --git a/public/components/dgraph/package.go b/public/components/dgraph/package.go index 3e1729543d..535ae754ee 100644 --- a/public/components/dgraph/package.go +++ b/public/components/dgraph/package.go @@ -2,5 +2,5 @@ package dgraph import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/dgraph" + _ "github.com/redpanda-data/connect/v4/internal/impl/dgraph" ) diff --git a/public/components/discord/package.go b/public/components/discord/package.go index 3e00035df7..f9dd947f75 100644 --- a/public/components/discord/package.go +++ b/public/components/discord/package.go @@ -2,5 +2,5 @@ package discord import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/discord" + _ "github.com/redpanda-data/connect/v4/internal/impl/discord" ) diff --git a/public/components/elasticsearch/package.go b/public/components/elasticsearch/package.go index d14ab16b13..cdeb932fb9 100644 --- a/public/components/elasticsearch/package.go +++ b/public/components/elasticsearch/package.go @@ -2,5 +2,5 @@ package elasticsearch import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/elasticsearch" + _ "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" ) diff --git a/public/components/gcp/package.go b/public/components/gcp/package.go index 12b9990506..3e323408c3 100644 --- a/public/components/gcp/package.go +++ b/public/components/gcp/package.go @@ -2,5 +2,5 @@ package gcp import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/gcp" + _ "github.com/redpanda-data/connect/v4/internal/impl/gcp" ) diff --git a/public/components/hdfs/package.go b/public/components/hdfs/package.go index 945985702d..af1fff9969 100644 --- a/public/components/hdfs/package.go +++ b/public/components/hdfs/package.go @@ -2,5 +2,5 @@ package hdfs import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/hdfs" + _ "github.com/redpanda-data/connect/v4/internal/impl/hdfs" ) diff --git a/public/components/influxdb/package.go b/public/components/influxdb/package.go index 6c2835ddba..08fe3c7bb0 100644 --- a/public/components/influxdb/package.go +++ b/public/components/influxdb/package.go @@ -2,5 +2,5 @@ package influxdb import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/influxdb" + _ "github.com/redpanda-data/connect/v4/internal/impl/influxdb" ) diff --git a/public/components/io/package.go b/public/components/io/package.go index 4aeb7e0955..11d0887867 100644 --- a/public/components/io/package.go +++ b/public/components/io/package.go @@ -10,5 +10,5 @@ package io import ( // Import only io packages. - _ "github.com/benthosdev/benthos/v4/internal/impl/io" + _ "github.com/benthosdev/benthos/v4/public/components/io" ) diff --git a/public/components/jaeger/package.go b/public/components/jaeger/package.go index 32a0bc98b4..c4c3272074 100644 --- a/public/components/jaeger/package.go +++ b/public/components/jaeger/package.go @@ -2,5 +2,5 @@ package jaeger import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/jaeger" + _ "github.com/redpanda-data/connect/v4/internal/impl/jaeger" ) diff --git a/public/components/javascript/package.go b/public/components/javascript/package.go index 098c5ade05..ee86269f54 100644 --- a/public/components/javascript/package.go +++ b/public/components/javascript/package.go @@ -2,5 +2,5 @@ package couchbase import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/javascript" + _ "github.com/redpanda-data/connect/v4/internal/impl/javascript" ) diff --git a/public/components/kafka/package.go b/public/components/kafka/package.go index ec84ed1cbe..d619bda35a 100644 --- a/public/components/kafka/package.go +++ b/public/components/kafka/package.go @@ -2,5 +2,5 @@ package kafka import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/kafka" + _ "github.com/redpanda-data/connect/v4/internal/impl/kafka" ) diff --git a/public/components/maxmind/package.go b/public/components/maxmind/package.go index 5b9653477e..8eac956029 100644 --- a/public/components/maxmind/package.go +++ b/public/components/maxmind/package.go @@ -2,5 +2,5 @@ package maxmind import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/maxmind" + _ "github.com/redpanda-data/connect/v4/internal/impl/maxmind" ) diff --git a/public/components/memcached/package.go b/public/components/memcached/package.go index 9a59b2b536..ffe7f9432d 100644 --- a/public/components/memcached/package.go +++ b/public/components/memcached/package.go @@ -2,5 +2,5 @@ package memcached import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/memcached" + _ "github.com/redpanda-data/connect/v4/internal/impl/memcached" ) diff --git a/public/components/mongodb/package.go b/public/components/mongodb/package.go index 2f2fc1fc8d..ea2509ee4c 100644 --- a/public/components/mongodb/package.go +++ b/public/components/mongodb/package.go @@ -2,5 +2,5 @@ package mongodb import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/mongodb" + _ "github.com/redpanda-data/connect/v4/internal/impl/mongodb" ) diff --git a/public/components/mqtt/package.go b/public/components/mqtt/package.go index 43156b0673..623bc92474 100644 --- a/public/components/mqtt/package.go +++ b/public/components/mqtt/package.go @@ -2,5 +2,5 @@ package mqtt import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/mqtt" + _ "github.com/redpanda-data/connect/v4/internal/impl/mqtt" ) diff --git a/public/components/msgpack/package.go b/public/components/msgpack/package.go index cc9b695fea..63c57f955e 100644 --- a/public/components/msgpack/package.go +++ b/public/components/msgpack/package.go @@ -2,5 +2,5 @@ package msgpack import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/msgpack" + _ "github.com/redpanda-data/connect/v4/internal/impl/msgpack" ) diff --git a/public/components/nanomsg/package.go b/public/components/nanomsg/package.go index fe6fdb6f96..e1bb3e4d84 100644 --- a/public/components/nanomsg/package.go +++ b/public/components/nanomsg/package.go @@ -2,5 +2,5 @@ package nanomsg import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/nanomsg" + _ "github.com/redpanda-data/connect/v4/internal/impl/nanomsg" ) diff --git a/public/components/nats/package.go b/public/components/nats/package.go index 1deb07d5f3..12d5d7aa31 100644 --- a/public/components/nats/package.go +++ b/public/components/nats/package.go @@ -2,5 +2,5 @@ package nats import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/nats" + _ "github.com/redpanda-data/connect/v4/internal/impl/nats" ) diff --git a/public/components/nsq/package.go b/public/components/nsq/package.go index 34aedb4af3..cb963e7767 100644 --- a/public/components/nsq/package.go +++ b/public/components/nsq/package.go @@ -2,5 +2,5 @@ package nsq import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/nsq" + _ "github.com/redpanda-data/connect/v4/internal/impl/nsq" ) diff --git a/public/components/opensearch/package.go b/public/components/opensearch/package.go index 642aecb188..de6807681d 100644 --- a/public/components/opensearch/package.go +++ b/public/components/opensearch/package.go @@ -2,5 +2,5 @@ package opensearch import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/opensearch" + _ "github.com/redpanda-data/connect/v4/internal/impl/opensearch" ) diff --git a/public/components/otlp/package.go b/public/components/otlp/package.go index aaca151bb7..b22bdb0d1c 100644 --- a/public/components/otlp/package.go +++ b/public/components/otlp/package.go @@ -2,5 +2,5 @@ package otlp import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/otlp" + _ "github.com/redpanda-data/connect/v4/internal/impl/otlp" ) diff --git a/public/components/prometheus/package.go b/public/components/prometheus/package.go index 474ecd36f2..1031fb8971 100644 --- a/public/components/prometheus/package.go +++ b/public/components/prometheus/package.go @@ -2,5 +2,5 @@ package prometheus import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/prometheus" + _ "github.com/redpanda-data/connect/v4/internal/impl/prometheus" ) diff --git a/public/components/pulsar/package.go b/public/components/pulsar/package.go index c7e73736e5..68ed7f7f73 100644 --- a/public/components/pulsar/package.go +++ b/public/components/pulsar/package.go @@ -2,5 +2,5 @@ package pulsar import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/pulsar" + _ "github.com/redpanda-data/connect/v4/internal/impl/pulsar" ) diff --git a/public/components/pure/extended/package.go b/public/components/pure/extended/package.go index 1e116653f9..6328d992d3 100644 --- a/public/components/pure/extended/package.go +++ b/public/components/pure/extended/package.go @@ -10,12 +10,13 @@ package extended import ( // Import pure but larger packages. - _ "github.com/benthosdev/benthos/v4/internal/impl/awk" - _ "github.com/benthosdev/benthos/v4/internal/impl/jsonpath" - _ "github.com/benthosdev/benthos/v4/internal/impl/lang" - _ "github.com/benthosdev/benthos/v4/internal/impl/msgpack" - _ "github.com/benthosdev/benthos/v4/internal/impl/parquet" - _ "github.com/benthosdev/benthos/v4/internal/impl/protobuf" - _ "github.com/benthosdev/benthos/v4/internal/impl/pure/extended" - _ "github.com/benthosdev/benthos/v4/internal/impl/xml" + _ "github.com/benthosdev/benthos/v4/public/components/pure/extended" + + _ "github.com/redpanda-data/connect/v4/internal/impl/awk" + _ "github.com/redpanda-data/connect/v4/internal/impl/jsonpath" + _ "github.com/redpanda-data/connect/v4/internal/impl/lang" + _ "github.com/redpanda-data/connect/v4/internal/impl/msgpack" + _ "github.com/redpanda-data/connect/v4/internal/impl/parquet" + _ "github.com/redpanda-data/connect/v4/internal/impl/protobuf" + _ "github.com/redpanda-data/connect/v4/internal/impl/xml" ) diff --git a/public/components/pure/package.go b/public/components/pure/package.go index f74269b113..8aada7e0be 100644 --- a/public/components/pure/package.go +++ b/public/components/pure/package.go @@ -10,5 +10,5 @@ package pure import ( // Import only pure packages. - _ "github.com/benthosdev/benthos/v4/internal/impl/pure" + _ "github.com/benthosdev/benthos/v4/public/components/pure" ) diff --git a/public/components/pusher/package.go b/public/components/pusher/package.go index 1f3c1ed447..51de55ef40 100644 --- a/public/components/pusher/package.go +++ b/public/components/pusher/package.go @@ -2,5 +2,5 @@ package pusher import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/pusher" + _ "github.com/redpanda-data/connect/v4/internal/impl/pusher" ) diff --git a/public/components/redis/package.go b/public/components/redis/package.go index 77762cf10d..ce1f2150a9 100644 --- a/public/components/redis/package.go +++ b/public/components/redis/package.go @@ -2,5 +2,5 @@ package redis import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/redis" + _ "github.com/redpanda-data/connect/v4/internal/impl/redis" ) diff --git a/public/components/sentry/package.go b/public/components/sentry/package.go index adb3fd77b1..309633c5b6 100644 --- a/public/components/sentry/package.go +++ b/public/components/sentry/package.go @@ -2,5 +2,5 @@ package sentry import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/sentry" + _ "github.com/redpanda-data/connect/v4/internal/impl/sentry" ) diff --git a/public/components/sftp/package.go b/public/components/sftp/package.go index eac31e3be4..023cb4e790 100644 --- a/public/components/sftp/package.go +++ b/public/components/sftp/package.go @@ -2,5 +2,5 @@ package sftp import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/sftp" + _ "github.com/redpanda-data/connect/v4/internal/impl/sftp" ) diff --git a/public/components/snowflake/package.go b/public/components/snowflake/package.go index ae818badb7..0008efb901 100644 --- a/public/components/snowflake/package.go +++ b/public/components/snowflake/package.go @@ -2,5 +2,5 @@ package snowflake import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/snowflake" + _ "github.com/redpanda-data/connect/v4/internal/impl/snowflake" ) diff --git a/public/components/splunk/package.go b/public/components/splunk/package.go index a82f538d42..508cd76f17 100644 --- a/public/components/splunk/package.go +++ b/public/components/splunk/package.go @@ -2,5 +2,5 @@ package splunk import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/splunk" + _ "github.com/redpanda-data/connect/v4/internal/impl/splunk" ) diff --git a/public/components/sql/base/package.go b/public/components/sql/base/package.go index da7ba5ef90..02e0543327 100644 --- a/public/components/sql/base/package.go +++ b/public/components/sql/base/package.go @@ -4,5 +4,5 @@ package base import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/sql" + _ "github.com/redpanda-data/connect/v4/internal/impl/sql" ) diff --git a/public/components/statsd/package.go b/public/components/statsd/package.go index 3e1f0df7ea..f74c65bb12 100644 --- a/public/components/statsd/package.go +++ b/public/components/statsd/package.go @@ -2,5 +2,5 @@ package statsd import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/statsd" + _ "github.com/redpanda-data/connect/v4/internal/impl/statsd" ) diff --git a/public/components/twitter/package.go b/public/components/twitter/package.go index 7adb0ea6c9..d21f670047 100644 --- a/public/components/twitter/package.go +++ b/public/components/twitter/package.go @@ -2,5 +2,5 @@ package twitter import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/twitter" + _ "github.com/redpanda-data/connect/v4/internal/impl/twitter" ) diff --git a/public/components/wasm/package.go b/public/components/wasm/package.go index 49783f6c1a..478932bd6a 100644 --- a/public/components/wasm/package.go +++ b/public/components/wasm/package.go @@ -2,5 +2,5 @@ package wasm import ( // Bring in the internal plugin definitions. - _ "github.com/benthosdev/benthos/v4/internal/impl/wasm" + _ "github.com/redpanda-data/connect/v4/internal/impl/wasm" ) diff --git a/public/service/benchmark_test.go b/public/service/benchmark_test.go deleted file mode 100644 index 809718aea7..0000000000 --- a/public/service/benchmark_test.go +++ /dev/null @@ -1,451 +0,0 @@ -package service_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/internal/impl/lang" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func BenchmarkStreamPipelines(b *testing.B) { - for _, test := range []struct { - name string - confFn func(iterations, batchSize int) string - }{ - { - name: "basic pipeline", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "pipeline processors chained", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - -pipeline: - processors: - - jq: - query: '{id: .id, name: .name, mobile: .mobile, site: .site}' - - jq: - query: '{id: .id, name: .name, mobile: .mobile, site: .site}' - - jq: - query: '{id: .id, name: .name, mobile: .mobile, site: .site}' - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic mapping", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - -pipeline: - processors: - - mapping: | - root = this - root.loud_name = this.name.uppercase() - root.good_friends = this.friends.filter(f -> f.lowercase().contains("a")) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic mapping inline", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - -pipeline: - processors: - - mutation: | - root.loud_name = this.name.uppercase() - root.good_friends = this.friends.filter(f -> f.lowercase().contains("a")) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic mapping as input proc", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - processors: - - mapping: | - root = this - root.loud_name = this.name.uppercase() - root.good_friends = this.friends.filter(f -> f.lowercase().contains("a")) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic mapping inline split with input proc", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - processors: - - mutation: | - root.loud_name = this.name.uppercase() - -pipeline: - processors: - - mutation: | - root.good_friends = this.friends.filter(f -> f.lowercase().contains("a")) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic mapping as branch", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - -pipeline: - processors: - - branch: - processors: [ noop: {} ] - result_map: | - root.loud_name = this.name.uppercase() - root.good_friends = this.friends.filter(f -> f.lowercase().contains("a")) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic multiplexing", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - -output: - switch: - cases: - - check: this.id.contains("a") - output: - drop: {} - - check: this.id.contains("b") - output: - drop: {} - - check: this.id.contains("c") - output: - drop: {} - - output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic switch processor", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - -pipeline: - processors: - - switch: - - check: this.id.contains("a") - processors: - - mapping: 'root = content().uppercase()' - - check: this.id.contains("b") - processors: - - mapping: 'root = content().uppercase()' - - check: this.id.contains("c") - processors: - - mapping: 'root = content().uppercase()' - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "convoluted data generation", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - root.meows = range(0, (random_int() %% 10) + 1).fold({}, item -> item.tally.merge({ - nanoid(): fake("name") - })) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "large data mapping", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":"bar value"} - root.id = uuid_v4() - root.name = fake("name") - root.mobile = fake("phone_number") - root.site = fake("url") - root.email = fake("email") - root.friends = range(0, (random_int() %% 10) + 1).map_each(fake("name")) - root.meows = { - nanoid(): fake("name"), - nanoid(): fake("name"), - nanoid(): fake("name"), - nanoid(): fake("name"), - nanoid(): fake("name"), - } - -pipeline: - processors: - - mapping: | - root = this - root.loud_name = this.name.uppercase() - root.good_friends = this.friends.filter(f -> f.lowercase().contains("a")) - root.meows = this.meows.map_each_key(key -> key.uppercase()) - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic branch processors", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":(random_int()%%10).string()} - root.id = uuid_v4() - root.name = fake("name") - root.email = fake("email") - -pipeline: - processors: - - branch: - request_map: | - root.foo = meta("foo") - root.email = this.email - processors: - - mapping: root = content().uppercase() - result_map: | - root.foo_stuff = content().string() - - branch: - request_map: | - root.bar = meta("bar") - root.name = this.name - processors: - - mapping: root = content().uppercase() - result_map: | - root.bar_stuff = content().string() - -output: - drop: {} -`, iterations, batchSize) - }, - }, - { - name: "basic workflow processors", - confFn: func(iterations, batchSize int) string { - return fmt.Sprintf(` -input: - generate: - count: %v - batch_size: %v - interval: "" - mapping: | - meta = {"foo":"foo value","bar":(random_int()%%10).string()} - root.id = uuid_v4() - root.name = fake("name") - root.email = fake("email") - -pipeline: - processors: - - workflow: - branches: - foo_stuff: - request_map: | - root.foo = meta("foo") - root.email = this.email - processors: - - mapping: root = content().uppercase() - result_map: | - root.foo_stuff = content().string() - bar_stuff: - request_map: | - root.bar = meta("bar") - root.name = this.name - processors: - - mapping: root = content().uppercase() - result_map: | - root.bar_stuff = content().string() - -output: - drop: {} -`, iterations, batchSize) - }, - }, - } { - test := test - for _, batchSize := range []int{1, 10, 50} { - batchSize := batchSize - b.Run(fmt.Sprintf("%v/%v", test.name, batchSize), func(b *testing.B) { - iterations := b.N / batchSize - if iterations < 1 { - iterations = 1 - } - - builder := service.NewStreamBuilder() - require.NoError(b, builder.SetYAML(test.confFn(iterations, batchSize))) - require.NoError(b, builder.SetLoggerYAML(`level: none`)) - - strm, err := builder.Build() - require.NoError(b, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - b.ReportAllocs() - b.ResetTimer() - - require.NoError(b, strm.Run(ctx)) - }) - } - } -} diff --git a/public/service/buffer.go b/public/service/buffer.go deleted file mode 100644 index 45feb9764b..0000000000 --- a/public/service/buffer.go +++ /dev/null @@ -1,113 +0,0 @@ -package service - -import ( - "context" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// BatchBuffer is an interface implemented by Buffers able to read and write -// message batches. Buffers are a component type that are placed after inputs, -// and decouples the acknowledgement system of the inputs from the rest of the -// pipeline. -// -// Buffers are useful when implementing buffers intended to relieve back -// pressure from upstream components, or when implementing message aggregators -// where the concept of discrete messages running through a pipeline no longer -// applies (such as with windowing algorithms). -// -// Buffers are advanced component types that weaken delivery guarantees of a -// Benthos pipeline. Therefore, if you aren't absolutely sure that a component -// you wish to build should be a buffer type then it likely shouldn't be. -type BatchBuffer interface { - // Write a batch of messages to the buffer, the batch is accompanied with an - // acknowledge function. A non-nil error should be returned if it is not - // possible to store the given message batch in the buffer. - // - // If a nil error is returned the buffer assumes responsibility for calling - // the acknowledge function at least once during the lifetime of the - // message. - // - // This could be at the point where the message is written to the buffer, - // which weakens delivery guarantees but can be useful for decoupling the - // input from downstream components. Alternatively, this could be when the - // associated batch has been read from the buffer and acknowledged - // downstream, which preserves delivery guarantees. - WriteBatch(context.Context, MessageBatch, AckFunc) error - - // Read a batch of messages from the buffer. This call should block until - // either a batch is ready to consume, the provided context is cancelled or - // EndOfInput has been called which indicates that the buffer is no longer - // being populated with new messages. - // - // The returned acknowledge function will be called when a consumed message - // batch has been processed and sent downstream. It is up to the buffer - // implementation whether the ack function is used, it might be used in - // order to "commit" the removal of a message from the buffer in cases where - // the buffer is a persisted storage solution, or in cases where the output - // of the buffer is temporal (a windowing algorithm, etc) it might be - // considered correct to simply drop message batches that are not acked. - // - // When the buffer is closed (EndOfInput has been called and no more - // messages are available) this method should return an ErrEndOfBuffer in - // order to indicate the end of the buffered stream. - // - // It is valid to return a batch of only one message. - ReadBatch(context.Context) (MessageBatch, AckFunc, error) - - // EndOfInput indicates to the buffer that the input has ended and that once - // the buffer is depleted it should return ErrEndOfBuffer from ReadBatch in - // order to gracefully shut down the pipeline. - // - // EndOfInput should be idempotent as it may be called more than once. - EndOfInput() - - Closer -} - -//------------------------------------------------------------------------------ - -// Implements buffer.ReaderWriter. -type airGapBatchBuffer struct { - b BatchBuffer - sig *shutdown.Signaller -} - -func newAirGapBatchBuffer(b BatchBuffer) buffer.ReaderWriter { - return &airGapBatchBuffer{b: b, sig: shutdown.NewSignaller()} -} - -func (a *airGapBatchBuffer) Write(ctx context.Context, msg message.Batch, aFn buffer.AckFunc) error { - parts := make([]*Message, msg.Len()) - _ = msg.Iter(func(i int, part *message.Part) error { - parts[i] = NewInternalMessage(part) - return nil - }) - return a.b.WriteBatch(ctx, parts, AckFunc(aFn)) -} - -func (a *airGapBatchBuffer) Read(ctx context.Context) (message.Batch, buffer.AckFunc, error) { - batch, ackFn, err := a.b.ReadBatch(ctx) - if err != nil { - return nil, nil, publicToInternalErr(err) - } - - mBatch := make(message.Batch, len(batch)) - for i, p := range batch { - mBatch[i] = p.part - } - return mBatch, func(c context.Context, aerr error) error { - return ackFn(c, aerr) - }, nil -} - -func (a *airGapBatchBuffer) EndOfInput() { - a.b.EndOfInput() -} - -func (a *airGapBatchBuffer) Close(ctx context.Context) error { - return a.b.Close(ctx) -} diff --git a/public/service/buffer_test.go b/public/service/buffer_test.go deleted file mode 100644 index 8b8e0e3621..0000000000 --- a/public/service/buffer_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package service - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type memoryBuffer struct { - messages chan MessageBatch - endOfInputChan chan struct{} - closeOnce sync.Once -} - -func newMemoryBuffer(n int) *memoryBuffer { - return &memoryBuffer{ - messages: make(chan MessageBatch, n), - endOfInputChan: make(chan struct{}), - } -} - -func (m *memoryBuffer) WriteBatch(ctx context.Context, batch MessageBatch, aFn AckFunc) error { - select { - case m.messages <- batch: - case <-ctx.Done(): - return ctx.Err() - } - return aFn(context.Background(), nil) -} - -func yoloIgnoreNacks(context.Context, error) error { - // YOLO: Drop messages that are nacked - return nil -} - -func (m *memoryBuffer) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - select { - case msg := <-m.messages: - return msg, yoloIgnoreNacks, nil - case <-ctx.Done(): - return nil, nil, ctx.Err() - case <-m.endOfInputChan: - // Input has ended, so return ErrEndOfBuffer if our buffer is empty. - select { - case msg := <-m.messages: - return msg, yoloIgnoreNacks, nil - default: - return nil, nil, ErrEndOfBuffer - } - } -} - -func (m *memoryBuffer) EndOfInput() { - m.closeOnce.Do(func() { - close(m.endOfInputChan) - }) -} - -func (m *memoryBuffer) Close(ctx context.Context) error { - // Nothing to clean up - return nil -} - -func TestStreamMemoryBuffer(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - var incr, total uint8 = 100, 50 - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - b := buffer.NewStream("meow", newAirGapBatchBuffer(newMemoryBuffer(int(total))), mock.NewManager()) - require.NoError(t, b.Consume(tChan)) - - var i uint8 - - // Check correct flow no blocking - for ; i < total; i++ { - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)) - msgBytes[0][0] = i - - select { - // Send to buffer - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for unbuffered message %v send", i) - } - - // Instant response from buffer - select { - case res := <-resChan: - require.NoError(t, res) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for unbuffered message %v response", i) - } - - // Receive on output - var outTr message.Transaction - select { - case outTr = <-b.TransactionChan(): - assert.Equal(t, i, outTr.Payload.Get(0).AsBytes()[0]) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for unbuffered message %v read", i) - } - - // Response from output - require.NoError(t, outTr.Ack(ctx, nil)) - } - - for i = 0; i <= total; i++ { - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)) - msgBytes[0][0] = i - - select { - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v send", i) - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v response", i) - } - } - - // Should have reached limit here - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)+1) - - select { - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message send") - } - - // Response should block until buffer is relieved - select { - case res := <-resChan: - if res != nil { - t.Fatal(res) - } else { - t.Fatalf("Overflowed response returned before timeout") - } - case <-time.After(100 * time.Millisecond): - } - - var outTr message.Transaction - - // Extract last message - select { - case outTr = <-b.TransactionChan(): - assert.Equal(t, byte(0), outTr.Payload.Get(0).AsBytes()[0]) - require.NoError(t, outTr.Ack(ctx, nil)) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message read") - } - - // Response from the last attempt should no longer be blocking - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(100 * time.Millisecond): - t.Errorf("Final buffered response blocked") - } - - // Extract all other messages - for i = 1; i <= total; i++ { - select { - case outTr = <-b.TransactionChan(): - assert.Equal(t, i, outTr.Payload.Get(0).AsBytes()[0]) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v read", i) - } - require.NoError(t, outTr.Ack(ctx, nil)) - } - - // Get final message - select { - case outTr = <-b.TransactionChan(): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v read", i) - } - require.NoError(t, outTr.Ack(ctx, nil)) - - b.TriggerCloseNow() - require.NoError(t, b.WaitForClose(ctx)) - - close(resChan) - close(tChan) -} - -func TestStreamBufferClosing(t *testing.T) { - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - var incr, total uint8 = 100, 5 - - tChan := make(chan message.Transaction) - resChan := make(chan error) - - b := buffer.NewStream("meow", newAirGapBatchBuffer(newMemoryBuffer(int(total))), mock.NewManager()) - require.NoError(t, b.Consume(tChan)) - - var i uint8 - - // Populate buffer with some messages - for i = 0; i < total; i++ { - msgBytes := make([][]byte, 1) - msgBytes[0] = make([]byte, int(incr)) - msgBytes[0][0] = i - - select { - case tChan <- message.NewTransaction(message.QuickBatch(msgBytes), resChan): - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v send", i) - } - select { - case res := <-resChan: - assert.NoError(t, res) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for buffered message %v response", i) - } - } - - // Close input, this should prompt the stack buffer to Flush(). - close(tChan) - - // Receive all of those messages from the buffer - for i = 0; i < total; i++ { - select { - case val := <-b.TransactionChan(): - assert.Equal(t, i, val.Payload.Get(0).AsBytes()[0]) - require.NoError(t, val.Ack(ctx, nil)) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message read") - } - } - - // The buffer should now be closed, therefore so should our read channel. - select { - case _, open := <-b.TransactionChan(): - assert.False(t, open) - case <-time.After(time.Second): - t.Fatalf("Timed out waiting for final buffered message read") - } - - // Should already be shut down. - assert.NoError(t, b.WaitForClose(ctx)) -} diff --git a/public/service/cache.go b/public/service/cache.go deleted file mode 100644 index d97098c458..0000000000 --- a/public/service/cache.go +++ /dev/null @@ -1,156 +0,0 @@ -package service - -import ( - "context" - "errors" - "time" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -// Errors returned by cache types. -var ( - ErrKeyAlreadyExists = errors.New("key already exists") - ErrKeyNotFound = errors.New("key does not exist") -) - -// Cache is an interface implemented by Benthos caches. -type Cache interface { - // Get a cache item. - Get(ctx context.Context, key string) ([]byte, error) - - // Set a cache item, specifying an optional TTL. It is okay for caches to - // ignore the ttl parameter if it isn't possible to implement. - Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error - - // Add is the same operation as Set except that it returns an error if the - // key already exists. It is okay for caches to return nil on duplicates if - // it isn't possible to implement. - Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error - - // Delete attempts to remove a key. If the key does not exist then it is - // considered correct to return an error, however, for cache implementations - // where it is difficult to determine this then it is acceptable to return - // nil. - Delete(ctx context.Context, key string) error - - Closer -} - -// CacheItem represents an individual cache item. -type CacheItem struct { - Key string - Value []byte - TTL *time.Duration -} - -// batchedCache represents a cache where the underlying implementation is able -// to benefit from batched set requests. This interface is optional for caches -// and when implemented will automatically be utilised where possible. -type batchedCache interface { - // SetMulti attempts to set multiple cache items in as few requests as - // possible. - SetMulti(ctx context.Context, keyValues ...CacheItem) error -} - -//------------------------------------------------------------------------------ - -// Implements types.Cache. -type airGapCache struct { - c Cache - cm batchedCache -} - -func newAirGapCache(c Cache, stats metrics.Type) cache.V1 { - ag := &airGapCache{c: c, cm: nil} - ag.cm, _ = c.(batchedCache) - return cache.MetricsForCache(ag, stats) -} - -func (a *airGapCache) Get(ctx context.Context, key string) ([]byte, error) { - b, err := a.c.Get(ctx, key) - if errors.Is(err, ErrKeyNotFound) { - err = component.ErrKeyNotFound - } - return b, err -} - -func (a *airGapCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - return a.c.Set(ctx, key, value, ttl) -} - -func (a *airGapCache) SetMulti(ctx context.Context, keyValues map[string]cache.TTLItem) error { - if a.cm != nil { - items := make([]CacheItem, 0, len(keyValues)) - for k, v := range keyValues { - items = append(items, CacheItem{ - Key: k, - Value: v.Value, - TTL: v.TTL, - }) - } - return a.cm.SetMulti(ctx, items...) - } - for k, v := range keyValues { - if err := a.c.Set(ctx, k, v.Value, v.TTL); err != nil { - return err - } - } - return nil -} - -func (a *airGapCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - err := a.c.Add(ctx, key, value, ttl) - if errors.Is(err, ErrKeyAlreadyExists) { - err = component.ErrKeyAlreadyExists - } - return err -} - -func (a *airGapCache) Delete(ctx context.Context, key string) error { - return a.c.Delete(ctx, key) -} - -func (a *airGapCache) Close(ctx context.Context) error { - return a.c.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// Implements Cache around a types.Cache. -type reverseAirGapCache struct { - c cache.V1 -} - -func newReverseAirGapCache(c cache.V1) *reverseAirGapCache { - return &reverseAirGapCache{c} -} - -func (r *reverseAirGapCache) Get(ctx context.Context, key string) ([]byte, error) { - b, err := r.c.Get(ctx, key) - if errors.Is(err, component.ErrKeyNotFound) { - err = ErrKeyNotFound - } - return b, err -} - -func (r *reverseAirGapCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - return r.c.Set(ctx, key, value, ttl) -} - -func (r *reverseAirGapCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) (err error) { - if err = r.c.Add(ctx, key, value, ttl); errors.Is(err, component.ErrKeyAlreadyExists) { - err = ErrKeyAlreadyExists - } - return -} - -func (r *reverseAirGapCache) Delete(ctx context.Context, key string) error { - return r.c.Delete(ctx, key) -} - -func (r *reverseAirGapCache) Close(ctx context.Context) error { - return r.c.Close(ctx) -} diff --git a/public/service/cache_test.go b/public/service/cache_test.go deleted file mode 100644 index b7c72c174b..0000000000 --- a/public/service/cache_test.go +++ /dev/null @@ -1,492 +0,0 @@ -package service - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -type testCacheItem struct { - b []byte - ttl *time.Duration -} - -type closableCache struct { - m map[string]testCacheItem - err error - closed bool -} - -func (c *closableCache) Get(ctx context.Context, key string) ([]byte, error) { - if c.err != nil { - return nil, c.err - } - i, ok := c.m[key] - if !ok { - return nil, component.ErrKeyNotFound - } - return i.b, nil -} - -func (c *closableCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if c.err != nil { - return c.err - } - c.m[key] = testCacheItem{ - b: value, ttl: ttl, - } - return nil -} - -func (c *closableCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if c.err != nil { - return c.err - } - if _, ok := c.m[key]; ok { - return component.ErrKeyAlreadyExists - } - c.m[key] = testCacheItem{ - b: value, ttl: ttl, - } - return nil -} - -func (c *closableCache) Delete(ctx context.Context, key string) error { - if c.err != nil { - return c.err - } - delete(c.m, key) - return nil -} - -func (c *closableCache) Close(ctx context.Context) error { - c.closed = true - return nil -} - -type closableCacheMulti struct { - *closableCache - - multiItems map[string]testCacheItem -} - -func (c *closableCacheMulti) SetMulti(ctx context.Context, keyValues ...CacheItem) error { - if c.closableCache.err != nil { - return c.closableCache.err - } - for _, kv := range keyValues { - c.multiItems[kv.Key] = testCacheItem{ - b: kv.Value, - ttl: kv.TTL, - } - } - return nil -} - -func TestCacheAirGapShutdown(t *testing.T) { - rl := &closableCache{} - agrl := newAirGapCache(rl, metrics.Noop()) - - err := agrl.Close(context.Background()) - assert.NoError(t, err) - assert.True(t, rl.closed) -} - -func TestCacheAirGapGet(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - }, - }, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - b, err := agrl.Get(ctx, "foo") - assert.NoError(t, err) - assert.Equal(t, "bar", string(b)) - - _, err = agrl.Get(ctx, "not exist") - assert.Equal(t, err, ErrKeyNotFound) - assert.EqualError(t, err, "key does not exist") -} - -func TestCacheAirGapSet(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - err := agrl.Set(ctx, "foo", []byte("bar"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: nil, - }, - }, rl.m) - - err = agrl.Set(ctx, "foo", []byte("baz"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("baz"), - ttl: nil, - }, - }, rl.m) -} - -func TestCacheAirGapSetMultiWithTTL(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - ttl1, ttl2 := time.Second, time.Millisecond - - err := agrl.SetMulti(ctx, map[string]cache.TTLItem{ - "first": { - Value: []byte("bar"), - TTL: &ttl1, - }, - "second": { - Value: []byte("baz"), - TTL: &ttl2, - }, - }) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "first": { - b: []byte("bar"), - ttl: &ttl1, - }, - "second": { - b: []byte("baz"), - ttl: &ttl2, - }, - }, rl.m) -} - -func TestCacheAirGapSetMultiWithTTLPassthrough(t *testing.T) { - ctx := context.Background() - rl := &closableCacheMulti{ - closableCache: &closableCache{ - m: map[string]testCacheItem{}, - }, - multiItems: map[string]testCacheItem{}, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - ttl1, ttl2 := time.Second, time.Millisecond - - err := agrl.SetMulti(ctx, map[string]cache.TTLItem{ - "first": { - Value: []byte("bar"), - TTL: &ttl1, - }, - "second": { - Value: []byte("baz"), - TTL: &ttl2, - }, - }) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{}, rl.m) - assert.Equal(t, map[string]testCacheItem{ - "first": { - b: []byte("bar"), - ttl: &ttl1, - }, - "second": { - b: []byte("baz"), - ttl: &ttl2, - }, - }, rl.multiItems) -} - -func TestCacheAirGapSetWithTTL(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - ttl1, ttl2 := time.Second, time.Millisecond - err := agrl.Set(ctx, "foo", []byte("bar"), &ttl1) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: &ttl1, - }, - }, rl.m) - - err = agrl.Set(ctx, "foo", []byte("baz"), &ttl2) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("baz"), - ttl: &ttl2, - }, - }, rl.m) -} - -func TestCacheAirGapAdd(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - err := agrl.Add(ctx, "foo", []byte("bar"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: nil, - }, - }, rl.m) - - err = agrl.Add(ctx, "foo", []byte("baz"), nil) - assert.Equal(t, err, ErrKeyAlreadyExists) - assert.EqualError(t, err, "key already exists") -} - -func TestCacheAirGapAddWithTTL(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{}, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - ttl := time.Second - err := agrl.Add(ctx, "foo", []byte("bar"), &ttl) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: &ttl, - }, - }, rl.m) - - err = agrl.Add(ctx, "foo", []byte("baz"), nil) - assert.Equal(t, err, ErrKeyAlreadyExists) - assert.EqualError(t, err, "key already exists") -} - -func TestCacheAirGapDelete(t *testing.T) { - ctx := context.Background() - rl := &closableCache{ - m: map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - }, - }, - } - agrl := newAirGapCache(rl, metrics.Noop()) - - err := agrl.Delete(ctx, "foo") - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{}, rl.m) -} - -type closableCacheType struct { - m map[string]testCacheItem - err error - closed bool -} - -func (c *closableCacheType) Get(ctx context.Context, key string) ([]byte, error) { - if c.err != nil { - return nil, c.err - } - i, ok := c.m[key] - if !ok { - return nil, component.ErrKeyNotFound - } - return i.b, nil -} - -func (c *closableCacheType) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if c.err != nil { - return c.err - } - c.m[key] = testCacheItem{ - b: value, ttl: ttl, - } - return nil -} - -func (c *closableCacheType) SetMulti(ctx context.Context, items map[string]cache.TTLItem) error { - return errors.New("not implemented") -} - -func (c *closableCacheType) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if c.err != nil { - return c.err - } - if _, ok := c.m[key]; ok { - return component.ErrKeyAlreadyExists - } - c.m[key] = testCacheItem{ - b: value, ttl: ttl, - } - return nil -} - -func (c *closableCacheType) Delete(ctx context.Context, key string) error { - if c.err != nil { - return c.err - } - delete(c.m, key) - return nil -} - -func (c *closableCacheType) Close(ctx context.Context) error { - c.closed = true - return nil -} - -func TestCacheReverseAirGapShutdown(t *testing.T) { - rl := &closableCacheType{} - agrl := newReverseAirGapCache(rl) - - err := agrl.Close(context.Background()) - assert.NoError(t, err) - assert.True(t, rl.closed) -} - -func TestCacheReverseAirGapGet(t *testing.T) { - rl := &closableCacheType{ - m: map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - }, - }, - } - agrl := newReverseAirGapCache(rl) - - b, err := agrl.Get(context.Background(), "foo") - assert.NoError(t, err) - assert.Equal(t, "bar", string(b)) - - _, err = agrl.Get(context.Background(), "not exist") - assert.Equal(t, err, ErrKeyNotFound) - assert.EqualError(t, err, "key does not exist") -} - -func TestCacheReverseAirGapSet(t *testing.T) { - rl := &closableCacheType{ - m: map[string]testCacheItem{}, - } - agrl := newReverseAirGapCache(rl) - - err := agrl.Set(context.Background(), "foo", []byte("bar"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: nil, - }, - }, rl.m) - - err = agrl.Set(context.Background(), "foo", []byte("baz"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("baz"), - ttl: nil, - }, - }, rl.m) -} - -func TestCacheReverseAirGapSetWithTTL(t *testing.T) { - rl := &closableCacheType{ - m: map[string]testCacheItem{}, - } - agrl := newReverseAirGapCache(rl) - - ttl1, ttl2 := time.Second, time.Millisecond - err := agrl.Set(context.Background(), "foo", []byte("bar"), &ttl1) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: &ttl1, - }, - }, rl.m) - - err = agrl.Set(context.Background(), "foo", []byte("baz"), &ttl2) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("baz"), - ttl: &ttl2, - }, - }, rl.m) -} - -func TestCacheReverseAirGapAdd(t *testing.T) { - rl := &closableCacheType{ - m: map[string]testCacheItem{}, - } - agrl := newReverseAirGapCache(rl) - - err := agrl.Add(context.Background(), "foo", []byte("bar"), nil) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: nil, - }, - }, rl.m) - - err = agrl.Add(context.Background(), "foo", []byte("baz"), nil) - assert.Equal(t, err, ErrKeyAlreadyExists) - assert.EqualError(t, err, "key already exists") -} - -func TestCacheReverseAirGapAddWithTTL(t *testing.T) { - rl := &closableCacheType{ - m: map[string]testCacheItem{}, - } - agrl := newReverseAirGapCache(rl) - - ttl := time.Second - err := agrl.Add(context.Background(), "foo", []byte("bar"), &ttl) - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - ttl: &ttl, - }, - }, rl.m) - - err = agrl.Add(context.Background(), "foo", []byte("baz"), nil) - assert.Equal(t, err, ErrKeyAlreadyExists) - assert.EqualError(t, err, "key already exists") -} - -func TestCacheReverseAirGapDelete(t *testing.T) { - rl := &closableCacheType{ - m: map[string]testCacheItem{ - "foo": { - b: []byte("bar"), - }, - }, - } - agrl := newReverseAirGapCache(rl) - - err := agrl.Delete(context.Background(), "foo") - assert.NoError(t, err) - assert.Equal(t, map[string]testCacheItem{}, rl.m) -} diff --git a/public/service/chaos_test.go b/public/service/chaos_test.go deleted file mode 100644 index 848dfed4b5..0000000000 --- a/public/service/chaos_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package service_test - -import ( - "context" - "errors" - "fmt" - "math/rand" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type chaosOutput struct { - t *testing.T - expected string - eRate float64 - seen int64 -} - -func (c *chaosOutput) Connect(ctx context.Context) error { - return nil -} - -func (c *chaosOutput) Write(ctx context.Context, m *service.Message) error { - mBytes, err := m.AsBytes() - require.NoError(c.t, err) - assert.Equal(c.t, c.expected, string(mBytes)) - - _ = atomic.AddInt64(&c.seen, 1) - - // Whether or not we acknowledge is random - if f := rand.Float64(); f <= c.eRate { - return errors.New("chaos output chose you") - } - return nil -} - -func (c *chaosOutput) Close(ctx context.Context) error { - assert.Greater(c.t, atomic.LoadInt64(&c.seen), int64(0), c.expected) - return nil -} - -func TestChaosConfig(t *testing.T) { - env := service.NewEnvironment() - - require.NoError(t, env.RegisterOutput("chaos", service.NewConfigSpec(). - Field(service.NewStringField("expected")). - Field(service.NewFloatField("error_rate").Description("A number [0.0,1.0) representing the rate of errors.").Default(0.1)), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - exp, err := conf.FieldString("expected") - if err != nil { - return nil, 0, err - } - eRate, err := conf.FieldFloat("error_rate") - if err != nil { - return nil, 0, err - } - if eRate >= 1.0 || eRate < 0 { - return nil, 0, fmt.Errorf("error_rate must be >=0 and <1, got %v", eRate) - } - return &chaosOutput{ - t: t, - expected: exp, - eRate: eRate, - }, 10, nil - })) - - strmBuilder := env.NewStreamBuilder() - require.NoError(t, strmBuilder.SetYAML(` -logger: - level: NONE - -input: - generate: - count: 1_000 - interval: "" - mapping: 'root.seen = []' - processors: - - mutation: 'root.seen = this.seen.append("a")' - - mutation: 'root.seen = this.seen.append("b")' - -pipeline: - processors: - - mutation: 'root.seen = this.seen.append("c")' - -output: - processors: - - mutation: 'root.seen = this.seen.append("d")' - fallback: - - processors: - - mutation: 'root.seen = this.seen.append("e1")' - chaos: - expected: '{"seen":["a","b","c","d","e1"]}' - error_rate: 0.25 - - - processors: - - mutation: 'root.seen = this.seen.append("e2")' - switch: - retry_until_success: false - cases: - - output: - chaos: - expected: '{"seen":["a","b","c","d","e2","f1"]}' - error_rate: 0.10 - processors: - - mutation: 'root.seen = this.seen.append("f1")' - continue: true - - output: - chaos: - expected: '{"seen":["a","b","c","d","e2","f2"]}' - error_rate: 0.10 - processors: - - mutation: 'root.seen = this.seen.append("f2")' - continue: true - - - processors: - - mutation: 'root.seen = this.seen.append("e3")' - broker: - pattern: fan_out - outputs: - - processors: - - mutation: 'root.seen = this.seen.append("f3")' - chaos: - expected: '{"seen":["a","b","c","d","e3","f3"]}' - error_rate: 0.01 - - processors: - - mutation: 'root.seen = this.seen.append("f4")' - chaos: - expected: '{"seen":["a","b","c","d","e3","f4"]}' - error_rate: 0.01 -`)) - - strm, err := strmBuilder.Build() - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - assert.NoError(t, strm.Run(ctx)) -} diff --git a/public/service/codec/scanner.go b/public/service/codec/scanner.go deleted file mode 100644 index d93bec06af..0000000000 --- a/public/service/codec/scanner.go +++ /dev/null @@ -1,126 +0,0 @@ -package codec - -import ( - "context" - "io" - - "github.com/benthosdev/benthos/v4/internal/codec" - "github.com/benthosdev/benthos/v4/public/service" -) - -const ( - fieldCodecFromString = "codec" - crFieldCodec = "scanner" - crFieldMaxBuffer = "max_buffer" -) - -// DeprecatedCodecFields contain definitions for deprecated codec fields that -// allow backwards compatible migration towards the new scanner field. -// -// New plugins should instead use the new scanner fields. -func DeprecatedCodecFields(defaultScanner string) []*service.ConfigField { - return []*service.ConfigField{ - service.NewInternalField(codec.NewReaderDocs(fieldCodecFromString)).Deprecated().Optional(), - service.NewIntField(crFieldMaxBuffer).Deprecated().Default(1000000), - service.NewScannerField(crFieldCodec). - Description("The xref:components:scanners/about.adoc[scanner] by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once."). - Default(map[string]any{defaultScanner: map[string]any{}}). - Version("4.25.0"). - Optional(), - } -} - -// DeprecatedFallbackCodec provides a common interface that abstracts either an -// old codec implementation or a new scanner. -type DeprecatedFallbackCodec interface { - Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (DeprecatedFallbackStream, error) - Close(context.Context) error -} - -// DeprecatedFallbackStream provides a common interface that abstracts either an -// old codec implementation or a new scanner. -type DeprecatedFallbackStream interface { - NextBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) - Close(context.Context) error -} - -// DeprecatedCodecFromParsed attempts to create a deprecated fallback codec from -// a parsed config. -func DeprecatedCodecFromParsed(conf *service.ParsedConfig) (DeprecatedFallbackCodec, error) { - if conf.Contains(fieldCodecFromString) { - codecName, err := conf.FieldString(fieldCodecFromString) - if err != nil { - return nil, err - } - - maxBuffer, _ := conf.FieldInt(crFieldMaxBuffer) - if maxBuffer == 0 { - maxBuffer = 1000000 - } - - oldCtor, err := codec.GetReader(codecName, codec.ReaderConfig{ - MaxScanTokenSize: maxBuffer, - }) - if err != nil { - return nil, err - } - return &codecRInternal{oldCtor}, nil - } - - ownedCodec, err := conf.FieldScanner(crFieldCodec) - if err != nil { - return nil, err - } - return &codecRPublic{newCtor: ownedCodec}, nil -} - -type codecRInternal struct { - oldCtor codec.ReaderConstructor -} - -func (r *codecRInternal) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (DeprecatedFallbackStream, error) { - oldR, err := r.oldCtor(details.Name(), rdr, codec.ReaderAckFn(aFn)) - if err != nil { - return nil, err - } - return &streamRInternal{oldR}, nil -} - -func (r *codecRInternal) Close(ctx context.Context) error { - return nil -} - -type streamRInternal struct { - old codec.Reader -} - -func (r *streamRInternal) NextBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - ib, aFn, err := r.old.Next(ctx) - if err != nil { - return nil, nil, err - } - - batch := make(service.MessageBatch, len(ib)) - for i := range ib { - batch[i] = service.NewInternalMessage(ib[i]) - } - return batch, service.AckFunc(aFn), nil -} - -func (r *streamRInternal) Close(ctx context.Context) error { - return r.old.Close(ctx) -} - -type codecRPublic struct { - newCtor *service.OwnedScannerCreator -} - -func (r *codecRPublic) Create(rdr io.ReadCloser, aFn service.AckFunc, details *service.ScannerSourceDetails) (DeprecatedFallbackStream, error) { - sDetails := service.NewScannerSourceDetails() - sDetails.SetName(details.Name()) - return r.newCtor.Create(rdr, aFn, sDetails) -} - -func (r *codecRPublic) Close(ctx context.Context) error { - return r.newCtor.Close(ctx) -} diff --git a/public/service/codec/scanner_test.go b/public/service/codec/scanner_test.go deleted file mode 100644 index a7183af827..0000000000 --- a/public/service/codec/scanner_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package codec_test - -import ( - "bytes" - "context" - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestInteropCodecOldStyle(t *testing.T) { - confSpec := service.NewConfigSpec().Fields(codec.DeprecatedCodecFields("lines")...) - pConf, err := confSpec.ParseYAML(` -codec: lines -max_buffer: 1000000 -`, nil) - require.NoError(t, err) - - rdr, err := codec.DeprecatedCodecFromParsed(pConf) - require.NoError(t, err) - - buf := bytes.NewReader([]byte(`first -second -third`)) - var acked bool - strm, err := rdr.Create(io.NopCloser(buf), func(ctx context.Context, err error) error { - acked = true - return nil - }, service.NewScannerSourceDetails()) - require.NoError(t, err) - - for _, s := range []string{ - "first", "second", "third", - } { - m, aFn, err := strm.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, m, 1) - mBytes, err := m[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, s, string(mBytes)) - require.NoError(t, aFn(context.Background(), nil)) - assert.False(t, acked) - } - - _, _, err = strm.NextBatch(context.Background()) - require.Equal(t, io.EOF, err) - - require.NoError(t, strm.Close(context.Background())) - assert.True(t, acked) -} - -func TestInteropCodecNewStyle(t *testing.T) { - confSpec := service.NewConfigSpec().Fields(codec.DeprecatedCodecFields("lines")...) - pConf, err := confSpec.ParseYAML(` -scanner: - lines: - custom_delimiter: 'X' - max_buffer_size: 200 -`, nil) - require.NoError(t, err) - - rdr, err := codec.DeprecatedCodecFromParsed(pConf) - require.NoError(t, err) - - buf := bytes.NewReader([]byte(`firstXsecondXthird`)) - var acked bool - strm, err := rdr.Create(io.NopCloser(buf), func(ctx context.Context, err error) error { - acked = true - return nil - }, service.NewScannerSourceDetails()) - require.NoError(t, err) - - for _, s := range []string{ - "first", "second", "third", - } { - m, aFn, err := strm.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, m, 1) - mBytes, err := m[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, s, string(mBytes)) - require.NoError(t, aFn(context.Background(), nil)) - assert.False(t, acked) - } - - _, _, err = strm.NextBatch(context.Background()) - require.Equal(t, io.EOF, err) - - require.NoError(t, strm.Close(context.Background())) - assert.True(t, acked) -} - -func TestInteropCodecDefault(t *testing.T) { - confSpec := service.NewConfigSpec().Fields(codec.DeprecatedCodecFields("lines")...) - pConf, err := confSpec.ParseYAML(`{}`, nil) - require.NoError(t, err) - - rdr, err := codec.DeprecatedCodecFromParsed(pConf) - require.NoError(t, err) - - buf := bytes.NewReader([]byte("first\nsecond\nthird")) - var acked bool - strm, err := rdr.Create(io.NopCloser(buf), func(ctx context.Context, err error) error { - acked = true - return nil - }, service.NewScannerSourceDetails()) - require.NoError(t, err) - - for _, s := range []string{ - "first", "second", "third", - } { - m, aFn, err := strm.NextBatch(context.Background()) - require.NoError(t, err) - require.Len(t, m, 1) - mBytes, err := m[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, s, string(mBytes)) - require.NoError(t, aFn(context.Background(), nil)) - assert.False(t, acked) - } - - _, _, err = strm.NextBatch(context.Background()) - require.Equal(t, io.EOF, err) - - require.NoError(t, strm.Close(context.Background())) - assert.True(t, acked) -} diff --git a/public/service/config.go b/public/service/config.go deleted file mode 100644 index 64aab4e477..0000000000 --- a/public/service/config.go +++ /dev/null @@ -1,744 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "sort" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager" -) - -// ConfigField describes a field within a component configuration, to be added -// to a ConfigSpec. -type ConfigField struct { - field docs.FieldSpec -} - -// NewAnyField describes a new config field that can assume any value type -// without triggering a config parse or linting error. -func NewAnyField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldAnything(name, ""), - } -} - -// NewAnyListField describes a new config field consisting of a list of values -// that can assume any value type without triggering a config parse or linting -// error. -func NewAnyListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldAnything(name, "").Array(), - } -} - -// NewAnyMapField describes a new config field consisting of a map of values -// that can assume any value type without triggering a config parse or linting -// error. -func NewAnyMapField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldAnything(name, "").Map(), - } -} - -// NewStringField describes a new string type config field. -func NewStringField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldString(name, ""), - } -} - -// NewDurationField describes a new duration string type config field, allowing -// users to define a time interval with strings of the form 60s, 3m, etc. -func NewDurationField(name string) *ConfigField { - // TODO: Add linting rule for duration - return &ConfigField{ - field: docs.FieldString(name, ""), - } -} - -// NewStringEnumField describes a new string type config field that can have one -// of a discrete list of values. -func NewStringEnumField(name string, options ...string) *ConfigField { - return &ConfigField{ - field: docs.FieldString(name, "").HasOptions(options...), - } -} - -// NewStringAnnotatedEnumField describes a new string type config field that can -// have one of a discrete list of values, where each value must be accompanied -// by a description that annotates its behaviour in the documentation. -func NewStringAnnotatedEnumField(name string, options map[string]string) *ConfigField { - optionKeys := make([]string, 0, len(options)) - for key := range options { - optionKeys = append(optionKeys, key) - } - sort.Strings(optionKeys) - - flatOptions := make([]string, 0, len(options)*2) - for _, o := range optionKeys { - flatOptions = append(flatOptions, o, options[o]) - } - - return &ConfigField{ - field: docs.FieldString(name, "").HasAnnotatedOptions(flatOptions...), - } -} - -// NewStringListField describes a new config field consisting of a list of -// strings. -func NewStringListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldString(name, "").Array(), - } -} - -// NewStringListOfListsField describes a new config field consisting of a list -// of lists of strings (a 2D array of strings). -func NewStringListOfListsField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldString(name, "").ArrayOfArrays(), - } -} - -// NewStringMapField describes a new config field consisting of an object of -// arbitrary keys with string values. -func NewStringMapField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldString(name, "").Map(), - } -} - -// NewIntField describes a new int type config field. -func NewIntField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldInt(name, ""), - } -} - -// NewIntListField describes a new config field consisting of a list of -// integers. -func NewIntListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldInt(name, "").Array(), - } -} - -// NewIntMapField describes a new config field consisting of an object of -// arbitrary keys with integer values. -func NewIntMapField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldInt(name, "").Map(), - } -} - -// NewFloatField describes a new float type config field. -func NewFloatField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldFloat(name, ""), - } -} - -// NewFloatListField describes a new config field consisting of a list of -// floats. -func NewFloatListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldFloat(name, "").Array(), - } -} - -// NewFloatMapField describes a new config field consisting of an object of -// arbitrary keys with float values. -func NewFloatMapField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldFloat(name, "").Map(), - } -} - -// NewBoolField describes a new bool type config field. -func NewBoolField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldBool(name, ""), - } -} - -// NewObjectField describes a new object type config field, consisting of one -// or more child fields. -func NewObjectField(name string, fields ...*ConfigField) *ConfigField { - children := make([]docs.FieldSpec, len(fields)) - for i, f := range fields { - children[i] = f.field - } - return &ConfigField{ - field: docs.FieldObject(name, "").WithChildren(children...), - } -} - -// NewObjectListField describes a new list type config field consisting of -// objects with one or more child fields. -func NewObjectListField(name string, fields ...*ConfigField) *ConfigField { - objField := NewObjectField(name, fields...) - return &ConfigField{ - field: objField.field.Array(), - } -} - -// NewObjectMapField describes a new map type config field consisting of -// objects with one or more child fields. -func NewObjectMapField(name string, fields ...*ConfigField) *ConfigField { - objField := NewObjectField(name, fields...) - return &ConfigField{ - field: objField.field.Map(), - } -} - -// NewInternalField returns a ConfigField derived from an internal package field -// spec. This function is for internal use only. -func NewInternalField(ifield docs.FieldSpec) *ConfigField { - return &ConfigField{ - field: ifield, - } -} - -// Description adds a description to the field which will be shown when printing -// documentation for the component config spec. -func (c *ConfigField) Description(d string) *ConfigField { - c.field.Description = d - return c -} - -// Advanced marks a config field as being advanced, and therefore it will not -// appear in simplified documentation examples. -func (c *ConfigField) Advanced() *ConfigField { - c.field = c.field.Advanced() - return c -} - -// Deprecated marks a config field as being deprecated, and therefore it will not -// appear in documentation examples. -func (c *ConfigField) Deprecated() *ConfigField { - c.field = c.field.Deprecated() - return c -} - -// Default specifies a default value that this field will assume if it is -// omitted from a provided config. Fields that do not have a default value are -// considered mandatory, and so parsing a config will fail in their absence. -func (c *ConfigField) Default(v any) *ConfigField { - c.field = c.field.HasDefault(v) - return c -} - -// Optional specifies that a field is optional even when a default value has not -// been specified. When a field is marked as optional you can test its presence -// within a parsed config with the method Contains. -func (c *ConfigField) Optional() *ConfigField { - c.field = c.field.Optional() - return c -} - -// Secret marks this field as being a secret, which means it represents -// information that is generally considered sensitive such as passwords or -// access tokens. -func (c *ConfigField) Secret() *ConfigField { - c.field = c.field.Secret() - return c -} - -// Example adds an example value to the field which will be shown when printing -// documentation for the component config spec. -func (c *ConfigField) Example(e any) *ConfigField { - c.field.Examples = append(c.field.Examples, e) - return c -} - -// Examples adds a variadic list of example values to the field which will be -// shown when printing documentation for the component config spec. -func (c *ConfigField) Examples(e ...any) *ConfigField { - c.field.Examples = append(c.field.Examples, e...) - return c -} - -// Version specifies the specific version at which this field was added to the -// component. -func (c *ConfigField) Version(v string) *ConfigField { - c.field = c.field.AtVersion(v) - return c -} - -// LintRule adds a custom linting rule to the field in the form of a bloblang -// mapping. The mapping is provided the value of the field within a config as -// the context `this`, and if the mapping assigns to `root` an array of one or -// more strings these strings will be exposed to a config author as linting -// errors. -// -// For example, if we wanted to add a linting rule for a string field that -// ensures the value contains only lowercase values we might add the following -// linting rule: -// -// `root = if this.lowercase() != this { [ "field must be lowercase" ] }`. -func (c *ConfigField) LintRule(blobl string) *ConfigField { - c.field = c.field.LinterBlobl(blobl) - return c -} - -//------------------------------------------------------------------------------ - -// ConfigSpec describes the configuration specification for a plugin -// component. This will be used for validating and linting configuration files -// and providing a parsed configuration struct to the plugin constructor. -type ConfigSpec struct { - component docs.ComponentSpec -} - -func (c *ConfigSpec) configFromAny(mgr bundle.NewManagement, v any) (pConf *ParsedConfig, err error) { - pConf = &ParsedConfig{mgr: mgr} - pConf.i, err = c.component.Config.ParsedConfigFromAny(v) - return -} - -// ParseYAML attempts to parse a YAML document as the defined configuration spec -// and returns a parsed config view. The provided environment determines which -// child components and Bloblang functions can be created by the fields of the -// spec, you can leave the environment nil to use the global environment. -// -// This method is intended for testing purposes and is not required for normal -// use of plugin components, as parsing is managed by other components. -func (c *ConfigSpec) ParseYAML(yamlStr string, env *Environment) (*ParsedConfig, error) { - if env == nil { - env = globalEnvironment - } - - nconf, err := docs.UnmarshalYAML([]byte(yamlStr)) - if err != nil { - return nil, err - } - - mgr, err := manager.New( - manager.NewResourceConfig(), - manager.OptSetEnvironment(env.internal), - manager.OptSetBloblangEnvironment(env.getBloblangParserEnv()), - ) - if err != nil { - return nil, fmt.Errorf("failed to instantiate resources: %w", err) - } - return c.configFromAny(mgr, nconf) -} - -// NewConfigSpec creates a new empty component configuration spec. If the -// plugin does not require configuration fields the result of this call is -// enough. -func NewConfigSpec() *ConfigSpec { - return &ConfigSpec{ - component: docs.ComponentSpec{ - Status: docs.StatusExperimental, - Plugin: true, - Config: docs.FieldComponent(), - }, - } -} - -// Stable sets a documentation label on the component indicating that its -// configuration spec is stable. Plugins are considered experimental by default. -func (c *ConfigSpec) Stable() *ConfigSpec { - c.component.Status = docs.StatusStable - return c -} - -// Beta sets a documentation label on the component indicating that its -// configuration spec is ready for beta testing, meaning backwards incompatible -// changes will not be made unless a fundamental problem is found. Plugins are -// considered experimental by default. -func (c *ConfigSpec) Beta() *ConfigSpec { - c.component.Status = docs.StatusBeta - return c -} - -// Deprecated sets a documentation label on the component indicating that it is -// now deprecated. Plugins are considered experimental by default. -func (c *ConfigSpec) Deprecated() *ConfigSpec { - c.component.Status = docs.StatusDeprecated - return c -} - -// Categories adds one or more string tags to the component, these are used for -// arbitrarily grouping components in documentation. -func (c *ConfigSpec) Categories(categories ...string) *ConfigSpec { - c.component.Categories = categories - return c -} - -// Version specifies that this component was introduced in a given version. -func (c *ConfigSpec) Version(v string) *ConfigSpec { - c.component.Version = v - return c -} - -// Summary adds a short summary to the plugin configuration spec that describes -// the general purpose of the component. -func (c *ConfigSpec) Summary(summary string) *ConfigSpec { - c.component.Summary = summary - return c -} - -// Description adds a description to the plugin configuration spec that -// describes in more detail the behaviour of the component and how it should be -// used. -func (c *ConfigSpec) Description(description string) *ConfigSpec { - c.component.Description = description - return c -} - -// Footnotes adds a description to the plugin configuration spec that appears -// towards the bottom of the documentation page, this is usually best for long -// winded lists of docs. -func (c *ConfigSpec) Footnotes(description string) *ConfigSpec { - c.component.Footnotes = description - return c -} - -// Field sets the specification of a field within the config spec, used for -// linting and generating documentation for the component. -// -// If the provided field has an empty name then it is registered as the value at -// the root of the config spec. -func (c *ConfigSpec) Field(f *ConfigField) *ConfigSpec { - if f.field.Name == "" { - // Set field to root of config spec - c.component.Config = f.field - return c - } - - c.component.Config.Type = docs.FieldTypeObject - for i, s := range c.component.Config.Children { - if s.Name == f.field.Name { - c.component.Config.Children[i] = f.field - return c - } - } - - c.component.Config.Children = append(c.component.Config.Children, f.field) - return c -} - -// Fields sets the specification of multiple fields within the config spec, used -// for linting and generating documentation for the component. -// -// If any of the provided fields have an empty name then they are registered as -// the value at the root of the config spec. -func (c *ConfigSpec) Fields(fs ...*ConfigField) *ConfigSpec { - spec := c - for _, f := range fs { - spec = c.Field(f) - } - return spec -} - -// Example adds an example to the plugin configuration spec that demonstrates -// how the component can be used. An example has a title, summary, and a YAML -// configuration showing a real use case. -func (c *ConfigSpec) Example(title, summary, config string) *ConfigSpec { - c.component.Examples = append(c.component.Examples, docs.AnnotatedExample{ - Title: title, - Summary: summary, - Config: config, - }) - return c -} - -// EncodeJSON attempts to parse a JSON object as a byte slice and uses it to -// populate the configuration spec. The schema of this method is undocumented -// and is not intended for general use. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func (c *ConfigSpec) EncodeJSON(v []byte) error { - return json.Unmarshal(v, &c.component) -} - -// LintRule adds a custom linting rule to the ConfigSpec in the form of a -// bloblang mapping. The mapping is provided the value of the fields within -// the ConfigSpec as the context `this`, and if the mapping assigns to `root` an -// array of one or more strings these strings will be exposed to a config author -// as linting errors. -// -// For example, if we wanted to add a linting rule for several ConfigSpec fields -// that ensures some fields are mutually exclusive and some require others we -// might use the following: -// -// root = match { -// this.exists("meow") && this.exists("woof") => [ "both `+"`meow`"+` and `+"`woof`"+` can't be set simultaneously" ], -// this.exists("reticulation") && (!this.exists("splines") || this.splines == "") => [ "`+"`splines`"+` is required when setting `+"`reticulation`"+`" ], -// }. -func (c *ConfigSpec) LintRule(blobl string) *ConfigSpec { - c.component.Config = c.component.Config.LinterBlobl(blobl) - return c -} - -//------------------------------------------------------------------------------ - -// ConfigView is a struct returned by a Benthos service environment when walking -// the list of registered components and provides access to information about -// the component. -type ConfigView struct { - prov docs.Provider - component docs.ComponentSpec -} - -// Summary returns a documentation summary of the component, often formatted as -// markdown. -func (c *ConfigView) Summary() string { - return c.component.Summary -} - -// Description returns a documentation description of the component, often -// formatted as markdown. -func (c *ConfigView) Description() string { - return c.component.Description -} - -// IsDeprecated returns true if the component is marked as deprecated. -func (c *ConfigView) IsDeprecated() bool { - return c.component.Status == docs.StatusDeprecated -} - -// FormatJSON returns a byte slice of the component configuration formatted as a -// JSON object. The schema of this method is undocumented and is not intended -// for general use. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func (c *ConfigView) FormatJSON() ([]byte, error) { - return json.Marshal(c.component) -} - -// RenderDocs creates a markdown file that documents the configuration of the -// component config view. This markdown may include Docusaurus react elements as -// it matches the documentation generated for the official Benthos website. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func (c *ConfigView) RenderDocs() ([]byte, error) { - _, rootOnly := map[string]struct{}{ - "cache": {}, - "rate_limit": {}, - "processor": {}, - "scanner": {}, - }[string(c.component.Type)] - - conf := map[string]any{ - "type": c.component.Name, - } - for k, v := range docs.ReservedFieldsByType(c.component.Type) { - if k == "plugin" { - continue - } - if v.Default != nil { - conf[k] = *v.Default - } - } - - return c.component.AsMarkdown(c.prov, !rootOnly, conf) -} - -//------------------------------------------------------------------------------ - -// ParsedConfig represents a plugin configuration that has been validated and -// parsed from a ConfigSpec, and allows plugin constructors to access -// configuration fields. -type ParsedConfig struct { - i *docs.ParsedConfig - mgr bundle.NewManagement -} - -// EngineVersion returns the version stamp associated with the underlying -// benthos engine. The version string is not guaranteed to match any particular -// scheme. -func (p *ParsedConfig) EngineVersion() string { - return p.mgr.EngineVersion() -} - -// Namespace returns a version of the parsed config at a given field namespace. -// This is useful for extracting multiple fields under the same grouping. -func (p *ParsedConfig) Namespace(path ...string) *ParsedConfig { - return &ParsedConfig{ - i: p.i.Namespace(path...), - mgr: p.mgr.IntoPath(path...), - } -} - -// Contains checks whether the parsed config contains a given field identified -// by its name. -func (p *ParsedConfig) Contains(path ...string) bool { - return p.i.Contains(path...) -} - -// FieldAny accesses a field from the parsed config by its name that can assume -// any value type. If the field is not found an error is returned. -func (p *ParsedConfig) FieldAny(path ...string) (any, error) { - return p.i.FieldAny(path...) -} - -// FieldAnyList accesses a field that is a list of any value types from the -// parsed config by its name and returns the value as an array of *ParsedConfig -// types, where each one represents an object or value in the list. Returns an -// error if the field is not found, or is not a list of values. -func (p *ParsedConfig) FieldAnyList(path ...string) ([]*ParsedConfig, error) { - il, err := p.i.FieldAnyList(path...) - if err != nil { - return nil, err - } - - pl := make([]*ParsedConfig, len(il)) - for i, v := range il { - pl[i] = &ParsedConfig{ - mgr: p.mgr, - i: v, - } - } - return pl, nil -} - -// FieldAnyMap accesses a field that is an object of arbitrary keys and any -// values from the parsed config by its name and returns a map of *ParsedConfig -// types, where each one represents an object or value in the map. Returns an -// error if the field is not found, or is not an object. -func (p *ParsedConfig) FieldAnyMap(path ...string) (map[string]*ParsedConfig, error) { - im, err := p.i.FieldAnyMap(path...) - if err != nil { - return nil, err - } - - pm := make(map[string]*ParsedConfig, len(im)) - for k, v := range im { - pm[k] = &ParsedConfig{ - mgr: p.mgr, - i: v, - } - } - return pm, nil -} - -// FieldString accesses a string field from the parsed config by its name. If -// the field is not found or is not a string an error is returned. -func (p *ParsedConfig) FieldString(path ...string) (string, error) { - return p.i.FieldString(path...) -} - -// FieldDuration accesses a duration string field from the parsed config by its -// name. If the field is not found or is not a valid duration string an error is -// returned. -func (p *ParsedConfig) FieldDuration(path ...string) (time.Duration, error) { - return p.i.FieldDuration(path...) -} - -// FieldStringList accesses a field that is a list of strings from the parsed -// config by its name and returns the value. Returns an error if the field is -// not found, or is not a list of strings. -func (p *ParsedConfig) FieldStringList(path ...string) ([]string, error) { - return p.i.FieldStringList(path...) -} - -// FieldStringListOfLists accesses a field that is a list of lists of strings -// from the parsed config by its name and returns the value. Returns an error if -// the field is not found, or is not a list of lists of strings. -func (p *ParsedConfig) FieldStringListOfLists(path ...string) ([][]string, error) { - return p.i.FieldStringListOfLists(path...) -} - -// FieldStringMap accesses a field that is an object of arbitrary keys and -// string values from the parsed config by its name and returns the value. -// Returns an error if the field is not found, or is not an object of strings. -func (p *ParsedConfig) FieldStringMap(path ...string) (map[string]string, error) { - return p.i.FieldStringMap(path...) -} - -// FieldInt accesses an int field from the parsed config by its name and returns -// the value. Returns an error if the field is not found or is not an int. -func (p *ParsedConfig) FieldInt(path ...string) (int, error) { - return p.i.FieldInt(path...) -} - -// FieldIntList accesses a field that is a list of integers from the parsed -// config by its name and returns the value. Returns an error if the field is -// not found, or is not a list of integers. -func (p *ParsedConfig) FieldIntList(path ...string) ([]int, error) { - return p.i.FieldIntList(path...) -} - -// FieldIntMap accesses a field that is an object of arbitrary keys and -// integer values from the parsed config by its name and returns the value. -// Returns an error if the field is not found, or is not an object of integers. -func (p *ParsedConfig) FieldIntMap(path ...string) (map[string]int, error) { - return p.i.FieldIntMap(path...) -} - -// FieldFloat accesses a float field from the parsed config by its name and -// returns the value. Returns an error if the field is not found or is not a -// float. -func (p *ParsedConfig) FieldFloat(path ...string) (float64, error) { - return p.i.FieldFloat(path...) -} - -// FieldFloatList accesses a field that is a list of floats from the parsed -// config by its name and returns the value. Returns an error if the field is -// not found, or is not a list of floats. -func (p *ParsedConfig) FieldFloatList(path ...string) ([]float64, error) { - return p.i.FieldFloatList(path...) -} - -// FieldFloatMap accesses a field that is an object of arbitrary keys and -// float values from the parsed config by its name and returns the value. -// Returns an error if the field is not found, or is not an object of floats. -func (p *ParsedConfig) FieldFloatMap(path ...string) (map[string]float64, error) { - return p.i.FieldFloatMap(path...) -} - -// FieldBool accesses a bool field from the parsed config by its name and -// returns the value. Returns an error if the field is not found or is not a -// bool. -func (p *ParsedConfig) FieldBool(path ...string) (bool, error) { - return p.i.FieldBool(path...) -} - -// FieldObjectList accesses a field that is a list of objects from the parsed -// config by its name and returns the value as an array of *ParsedConfig types, -// where each one represents an object in the list. Returns an error if the -// field is not found, or is not a list of objects. -func (p *ParsedConfig) FieldObjectList(path ...string) ([]*ParsedConfig, error) { - il, err := p.i.FieldObjectList(path...) - if err != nil { - return nil, err - } - - pl := make([]*ParsedConfig, len(il)) - for i, v := range il { - pl[i] = &ParsedConfig{ - i: v, - mgr: p.mgr, - } - } - return pl, nil -} - -// FieldObjectMap accesses a field that is a map of objects from the parsed -// config by its name and returns the value as a map of *ParsedConfig types, -// where each one represents an object in the map. Returns an error if the -// field is not found, or is not a map of objects. -func (p *ParsedConfig) FieldObjectMap(path ...string) (map[string]*ParsedConfig, error) { - im, err := p.i.FieldObjectMap(path...) - if err != nil { - return nil, err - } - - pl := make(map[string]*ParsedConfig, len(im)) - for k, v := range im { - pl[k] = &ParsedConfig{ - i: v, - mgr: p.mgr, - } - } - return pl, nil -} diff --git a/public/service/config_backoff.go b/public/service/config_backoff.go deleted file mode 100644 index 36d7bd7904..0000000000 --- a/public/service/config_backoff.go +++ /dev/null @@ -1,140 +0,0 @@ -package service - -import ( - "github.com/cenkalti/backoff/v4" -) - -// NewBackOffField defines a new object type config field that describes an -// exponential back off policy, often used for timing retry attempts. It is then -// possible to extract a *backoff.ExponentialBackOff from the resulting parsed -// config with the method FieldBackOff. -// -// It is possible to configure a back off policy that has no upper bound (no -// maximum elapsed time set). In cases where this would be problematic the field -// allowUnbounded should be set `false` in order to add linting rules that -// ensure an upper bound is set. -// -// The defaults struct is optional, and if provided will be used to establish -// default values for time interval fields. Otherwise the chosen defaults result -// in one minute of retry attempts, starting at 500ms intervals. -func NewBackOffField(name string, allowUnbounded bool, defaults *backoff.ExponentialBackOff) *ConfigField { - var ( - initDefault = "500ms" - maxDefault = "10s" - maxElapsedDefault = "1m" - ) - if defaults != nil { - initDefault = defaults.InitialInterval.String() - maxDefault = defaults.MaxInterval.String() - maxElapsedDefault = defaults.MaxElapsedTime.String() - } - - maxElapsedTime := NewDurationField("max_elapsed_time"). - Description("The maximum overall period of time to spend on retry attempts before the request is aborted."). - Default(maxElapsedDefault).Example("1m").Example("1h") - if allowUnbounded { - maxElapsedTime.field.Description += " Setting this value to a zeroed duration (such as `0s`) will result in unbounded retries." - } - - // TODO: Add linting rule to ensure we aren't unbounded if necessary. - return NewObjectField(name, - NewDurationField("initial_interval"). - Description("The initial period to wait between retry attempts."). - Default(initDefault).Example("50ms").Example("1s"), - NewDurationField("max_interval"). - Description("The maximum period to wait between retry attempts"). - Default(maxDefault).Example("5s").Example("1m"), - maxElapsedTime, - ).Description("Determine time intervals and cut offs for retry attempts.") -} - -// FieldBackOff accesses a field from a parsed config that was defined with -// NewBackoffField and returns a *backoff.ExponentialBackOff, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldBackOff(path ...string) (*backoff.ExponentialBackOff, error) { - b := backoff.NewExponentialBackOff() - - var err error - if b.InitialInterval, err = p.FieldDuration(append(path, "initial_interval")...); err != nil { - return nil, err - } - if b.MaxInterval, err = p.FieldDuration(append(path, "max_interval")...); err != nil { - return nil, err - } - if b.MaxElapsedTime, err = p.FieldDuration(append(path, "max_elapsed_time")...); err != nil { - return nil, err - } - - return b, nil -} - -// NewBackOffToggledField defines a new object type config field that describes -// an exponential back off policy, often used for timing retry attempts. It is -// then possible to extract a *backoff.ExponentialBackOff from the resulting -// parsed config with the method FieldBackOff. This Toggled variant includes a -// field `enabled` that is `false` by default. -// -// It is possible to configure a back off policy that has no upper bound (no -// maximum elapsed time set). In cases where this would be problematic the field -// allowUnbounded should be set `false` in order to add linting rules that -// ensure an upper bound is set. -// -// The defaults struct is optional, and if provided will be used to establish -// default values for time interval fields. Otherwise the chosen defaults result -// in one minute of retry attempts, starting at 500ms intervals. -func NewBackOffToggledField(name string, allowUnbounded bool, defaults *backoff.ExponentialBackOff) *ConfigField { - var ( - initDefault = "500ms" - maxDefault = "10s" - maxElapsedDefault = "1m" - ) - if defaults != nil { - initDefault = defaults.InitialInterval.String() - maxDefault = defaults.MaxInterval.String() - maxElapsedDefault = defaults.MaxElapsedTime.String() - } - - maxElapsedTime := NewDurationField("max_elapsed_time"). - Description("The maximum overall period of time to spend on retry attempts before the request is aborted."). - Default(maxElapsedDefault).Example("1m").Example("1h") - if allowUnbounded { - maxElapsedTime.field.Description += " Setting this value to a zeroed duration (such as `0s`) will result in unbounded retries." - } - - // TODO: Add linting rule to ensure we aren't unbounded if necessary. - return NewObjectField(name, - NewBoolField("enabled"). - Description("Whether retries should be enabled."). - Default(false), - NewDurationField("initial_interval"). - Description("The initial period to wait between retry attempts."). - Default(initDefault).Example("50ms").Example("1s"), - NewDurationField("max_interval"). - Description("The maximum period to wait between retry attempts"). - Default(maxDefault).Example("5s").Example("1m"), - maxElapsedTime, - ).Description("Determine time intervals and cut offs for retry attempts.") -} - -// FieldBackOffToggled accesses a field from a parsed config that was defined -// with NewBackOffField and returns a *backoff.ExponentialBackOff and a boolean -// flag indicating whether retries are explicitly enabled, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldBackOffToggled(path ...string) (boff *backoff.ExponentialBackOff, enabled bool, err error) { - boff = backoff.NewExponentialBackOff() - - if enabled, err = p.FieldBool(append(path, "enabled")...); err != nil { - return - } - if boff.InitialInterval, err = p.FieldDuration(append(path, "initial_interval")...); err != nil { - return - } - if boff.MaxInterval, err = p.FieldDuration(append(path, "max_interval")...); err != nil { - return - } - if boff.MaxElapsedTime, err = p.FieldDuration(append(path, "max_elapsed_time")...); err != nil { - return - } - - return -} diff --git a/public/service/config_backoff_test.go b/public/service/config_backoff_test.go deleted file mode 100644 index 0df0c07c8b..0000000000 --- a/public/service/config_backoff_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package service - -import ( - "testing" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigBackOff(t *testing.T) { - spec := NewConfigSpec(). - Field(NewBackOffField("a", true, nil)) - - parsedConfig, err := spec.ParseYAML(` -a: - max_interval: 300s -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldBackOff("b") - require.Error(t, err) - - bConf, err := parsedConfig.FieldBackOff("a") - require.NoError(t, err) - - assert.Equal(t, time.Millisecond*500, bConf.InitialInterval) - assert.Equal(t, time.Second*300, bConf.MaxInterval) - assert.Equal(t, time.Minute*1, bConf.MaxElapsedTime) -} - -func TestConfigBackOffCustomDefaults(t *testing.T) { - defaults := backoff.NewExponentialBackOff() - defaults.InitialInterval = time.Minute - defaults.MaxInterval = time.Minute * 5 - defaults.MaxElapsedTime = time.Hour * 6 - - spec := NewConfigSpec(). - Field(NewBackOffField("a", false, defaults)) - - parsedConfig, err := spec.ParseYAML(` -a: - max_interval: 300s -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldBackOff("b") - require.Error(t, err) - - bConf, err := parsedConfig.FieldBackOff("a") - require.NoError(t, err) - - assert.Equal(t, time.Minute, bConf.InitialInterval) - assert.Equal(t, time.Second*300, bConf.MaxInterval) - assert.Equal(t, time.Hour*6, bConf.MaxElapsedTime) -} diff --git a/public/service/config_batch_policy.go b/public/service/config_batch_policy.go deleted file mode 100644 index 47746b3007..0000000000 --- a/public/service/config_batch_policy.go +++ /dev/null @@ -1,161 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/internal/batch/policy" - "github.com/benthosdev/benthos/v4/internal/batch/policy/batchconfig" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// BatchPolicy describes the mechanisms by which batching should be performed of -// messages destined for a Batch output. This is returned by constructors of -// batch outputs. -type BatchPolicy struct { - ByteSize int - Count int - Check string - Period string - - // Only available when using NewBatchPolicyField. - procs []processor.Config -} - -func (b BatchPolicy) toInternal() batchconfig.Config { - batchConf := batchconfig.NewConfig() - batchConf.ByteSize = b.ByteSize - batchConf.Count = b.Count - batchConf.Check = b.Check - batchConf.Period = b.Period - batchConf.Processors = b.procs - return batchConf -} - -// IsNoop returns true if the batching policy does not have any batching -// mechanisms configured. -func (b BatchPolicy) IsNoop() bool { - return b.toInternal().IsNoop() -} - -// Batcher provides a batching mechanism where messages can be added one-by-one -// with a boolean return indicating whether the batching policy has been -// triggered. -// -// Upon triggering the policy it is the responsibility of the owner of this -// batcher to call Flush, which returns all the pending messages in the batch. -// -// This batcher may contain processors that are executed during the flush, -// therefore it is important to call Close when this batcher is no longer -// required, having also called Flush if appropriate. -type Batcher struct { - mgr bundle.NewManagement - p *policy.Batcher -} - -// Add a message to the batch. Returns true if the batching policy has been -// triggered by this new addition, in which case Flush should be called. -func (b *Batcher) Add(msg *Message) bool { - return b.p.Add(msg.part) -} - -// UntilNext returns a duration indicating how long until the current batch -// should be flushed due to a configured period. A boolean is also returned -// indicating whether the batching policy has a timed factor, if this is false -// then the duration returned should be ignored. -func (b *Batcher) UntilNext() (time.Duration, bool) { - t := b.p.UntilNext() - if t > 0 { - return t, true - } - return 0, false -} - -// Flush pending messages into a batch, apply any batching processors that are -// part of the batching policy, and then return the result. -func (b *Batcher) Flush(ctx context.Context) (batch MessageBatch, err error) { - m := b.p.Flush(ctx) - if m == nil || m.Len() == 0 { - return - } - _ = m.Iter(func(i int, part *message.Part) error { - batch = append(batch, NewInternalMessage(part)) - return nil - }) - return -} - -// Close the batching policy, which cleans up any resources used by batching -// processors. -func (b *Batcher) Close(ctx context.Context) error { - return b.p.Close(ctx) -} - -// NewBatcher creates a batching mechanism from the policy. -func (b BatchPolicy) NewBatcher(res *Resources) (*Batcher, error) { - mgr := res.mgr.IntoPath("batching") - p, err := policy.New(b.toInternal(), mgr) - if err != nil { - return nil, err - } - return &Batcher{mgr: mgr, p: p}, nil -} - -type batcherUnwrapper struct { - p *policy.Batcher -} - -func (w batcherUnwrapper) Unwrap() *policy.Batcher { - return w.p -} - -// XUnwrapper is for internal use only, do not use this. -func (b *Batcher) XUnwrapper() any { - return batcherUnwrapper{p: b.p} -} - -//------------------------------------------------------------------------------ - -// NewBatchPolicyField defines a new object type config field that describes a -// batching policy for batched outputs. It is then possible to extract a -// BatchPolicy from the resulting parsed config with the method -// FieldBatchPolicy. -func NewBatchPolicyField(name string) *ConfigField { - bs := policy.FieldSpec() - bs.Name = name - bs.Type = docs.FieldTypeObject - var newChildren []docs.FieldSpec - for _, f := range bs.Children { - if f.Name == "count" { - f = f.HasDefault(0) - } - if !f.IsDeprecated { - newChildren = append(newChildren, f) - } - } - bs.Children = newChildren - return &ConfigField{field: bs} -} - -// FieldBatchPolicy accesses a field from a parsed config that was defined with -// NewBatchPolicyField and returns a BatchPolicy, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldBatchPolicy(path ...string) (conf BatchPolicy, err error) { - if conf.Count, err = p.FieldInt(append(path, "count")...); err != nil { - return - } - if conf.ByteSize, err = p.FieldInt(append(path, "byte_size")...); err != nil { - return - } - if conf.Check, err = p.FieldString(append(path, "check")...); err != nil { - return - } - if conf.Period, err = p.FieldString(append(path, "period")...); err != nil { - return - } - conf.procs, err = p.fieldProcessorListConfigs(append(path, "processors")...) - return -} diff --git a/public/service/config_batch_policy_test.go b/public/service/config_batch_policy_test.go deleted file mode 100644 index 93b3d84fad..0000000000 --- a/public/service/config_batch_policy_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package service - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager" -) - -func TestConfigBatching(t *testing.T) { - spec := NewConfigSpec(). - Field(NewBatchPolicyField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - count: 20 - period: 5s - processors: - - bloblang: 'root = content().uppercase()' -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldTLS("b") - require.Error(t, err) - - bConf, err := parsedConfig.FieldBatchPolicy("a") - require.NoError(t, err) - - assert.Equal(t, 20, bConf.Count) - assert.Equal(t, "5s", bConf.Period) - require.Len(t, bConf.procs, 1) - assert.Equal(t, "bloblang", bConf.procs[0].Type) -} - -func TestBatcherPeriod(t *testing.T) { - spec := NewConfigSpec(). - Field(NewBatchPolicyField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - period: 300ms - processors: - - bloblang: 'root = content().uppercase()' -`, nil) - require.NoError(t, err) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - res := newResourcesFromManager(mgr) - - bConf, err := parsedConfig.FieldBatchPolicy("a") - require.NoError(t, err) - - pol, err := bConf.NewBatcher(res) - require.NoError(t, err) - - assert.False(t, pol.Add(NewMessage([]byte("foo")))) - assert.False(t, pol.Add(NewMessage([]byte("bar")))) - - v, ok := pol.UntilNext() - assert.True(t, ok) - assert.InDelta(t, int(time.Millisecond*300), int(v), float64(time.Millisecond*50)) - - batch, err := pol.Flush(context.Background()) - require.NoError(t, err) - require.Len(t, batch, 2) - - bOne, err := batch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "FOO", string(bOne)) - - bTwo, err := batch[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, "BAR", string(bTwo)) - - batch, err = pol.Flush(context.Background()) - require.NoError(t, err) - require.Empty(t, batch) - - require.NoError(t, pol.Close(context.Background())) -} - -func TestBatcherSize(t *testing.T) { - spec := NewConfigSpec(). - Field(NewBatchPolicyField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - count: 3 -`, nil) - require.NoError(t, err) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - res := newResourcesFromManager(mgr) - - bConf, err := parsedConfig.FieldBatchPolicy("a") - require.NoError(t, err) - - pol, err := bConf.NewBatcher(res) - require.NoError(t, err) - - _, ok := pol.UntilNext() - assert.False(t, ok) - - assert.False(t, pol.Add(NewMessage([]byte("foo")))) - assert.False(t, pol.Add(NewMessage([]byte("bar")))) - assert.True(t, pol.Add(NewMessage([]byte("baz")))) - - _, ok = pol.UntilNext() - assert.False(t, ok) - - batch, err := pol.Flush(context.Background()) - require.NoError(t, err) - require.Len(t, batch, 3) - - bRes, err := batch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo", string(bRes)) - - bRes, err = batch[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, "bar", string(bRes)) - - bRes, err = batch[2].AsBytes() - require.NoError(t, err) - assert.Equal(t, "baz", string(bRes)) - - batch, err = pol.Flush(context.Background()) - require.NoError(t, err) - require.Empty(t, batch) - - require.NoError(t, pol.Close(context.Background())) -} diff --git a/public/service/config_bloblang.go b/public/service/config_bloblang.go deleted file mode 100644 index a6f62f2891..0000000000 --- a/public/service/config_bloblang.go +++ /dev/null @@ -1,38 +0,0 @@ -package service - -import ( - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// NewBloblangField defines a new config field that describes a Bloblang mapping -// string. It is then possible to extract a *bloblang.Executor from the -// resulting parsed config with the method FieldBloblang. -func NewBloblangField(name string) *ConfigField { - tf := docs.FieldBloblang(name, "") - return &ConfigField{field: tf} -} - -// FieldBloblang accesses a field from a parsed config that was defined with -// NewBloblangField and returns either a *bloblang.Executor or an error if the -// mapping was invalid. -func (p *ParsedConfig) FieldBloblang(path ...string) (*bloblang.Executor, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - str, ok := v.(string) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a string, got %T", strings.Join(path, "."), v) - } - - exec, err := bloblang.XWrapEnvironment(p.mgr.BloblEnvironment()).Parse(str) - if err != nil { - return nil, fmt.Errorf("failed to parse bloblang mapping '%v': %v", strings.Join(path, "."), err) - } - return exec, nil -} diff --git a/public/service/config_bloblang_test.go b/public/service/config_bloblang_test.go deleted file mode 100644 index fec814cd9a..0000000000 --- a/public/service/config_bloblang_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigBloblang(t *testing.T) { - spec := NewConfigSpec(). - Field(NewBloblangField("a")). - Field(NewStringField("b")) - - parsedConfig, err := spec.ParseYAML(` -a: 'root = this.uppercase()' -b: 'root = this.filter(' -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldBloblang("b") - require.Error(t, err) - - _, err = parsedConfig.FieldBloblang("c") - require.Error(t, err) - - exec, err := parsedConfig.FieldBloblang("a") - require.NoError(t, err) - - res, err := exec.Query("hello world") - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", res) -} diff --git a/public/service/config_extract_tracing.go b/public/service/config_extract_tracing.go deleted file mode 100644 index aa9792003d..0000000000 --- a/public/service/config_extract_tracing.go +++ /dev/null @@ -1,170 +0,0 @@ -package service - -import ( - "context" - "fmt" - "strings" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -const ( - etsField = "extract_tracing_map" -) - -// NewExtractTracingSpanMappingField returns a config field for mapping messages -// in order to extract distributed tracing information. -func NewExtractTracingSpanMappingField() *ConfigField { - return NewBloblangField(etsField). - Description("EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer."). - Examples(`root = @`, `root = this.meta.span`). - Version("3.45.0"). - Optional(). - Advanced() -} - -// WrapBatchInputExtractTracingSpanMapping wraps a BatchInput with a mechanism -// for extracting tracing spans using a bloblang mapping. -func (p *ParsedConfig) WrapBatchInputExtractTracingSpanMapping(inputName string, i BatchInput) ( - BatchInput, error) { - if str, _ := p.FieldString(etsField); str == "" { - return i, nil - } - exe, err := p.FieldBloblang(etsField) - if err != nil { - return nil, err - } - return &spanInjectBatchInput{inputName: inputName, mgr: p.mgr, mapping: exe, rdr: i}, nil -} - -// WrapInputExtractTracingSpanMapping wraps a Input with a mechanism for -// extracting tracing spans from the consumed message using a Bloblang mapping. -func (p *ParsedConfig) WrapInputExtractTracingSpanMapping(inputName string, i Input) (Input, error) { - if str, _ := p.FieldString(etsField); str == "" { - return i, nil - } - exe, err := p.FieldBloblang(etsField) - if err != nil { - return nil, err - } - return &spanInjectInput{inputName: inputName, mgr: p.mgr, mapping: exe, rdr: i}, nil -} - -func getPropMapCarrier(spanPart *Message) (propagation.MapCarrier, error) { - structured, err := spanPart.AsStructured() - if err != nil { - return nil, err - } - - spanMap, ok := structured.(map[string]any) - if !ok { - return nil, fmt.Errorf("expected an object, got: %T", structured) - } - - c := propagation.MapCarrier{} - for k, v := range spanMap { - if vStr, ok := v.(string); ok { - c[strings.ToLower(k)] = vStr - } - } - return c, nil -} - -// spanInjectBatchInput wraps a BatchInput with a mechanism for -// extracting tracing spans from the consumed message using a Bloblang mapping. -type spanInjectBatchInput struct { - inputName string - mgr bundle.NewManagement - - mapping *bloblang.Executor - rdr BatchInput -} - -func (s *spanInjectBatchInput) Connect(ctx context.Context) error { - return s.rdr.Connect(ctx) -} - -func (s *spanInjectBatchInput) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - m, afn, err := s.rdr.ReadBatch(ctx) - if err != nil { - return nil, nil, err - } - - spanPart, err := m.BloblangQuery(0, s.mapping) - if err != nil { - s.mgr.Logger().Error("Mapping failed for tracing span: %v", err) - return m, afn, nil - } - - c, err := getPropMapCarrier(spanPart) - if err != nil { - s.mgr.Logger().Error("Mapping failed for tracing span: %v", err) - return m, afn, nil - } - - prov := s.mgr.Tracer() - operationName := "input_" + s.inputName - - textProp := otel.GetTextMapPropagator() - for i, p := range m { - ctx := textProp.Extract(p.Context(), c) - pCtx, _ := prov.Tracer("benthos").Start(ctx, operationName) - m[i] = p.WithContext(pCtx) - } - return m, afn, nil -} - -func (s *spanInjectBatchInput) Close(ctx context.Context) error { - return s.rdr.Close(ctx) -} - -// spanInjectInput wraps a Input with a mechanism for extracting tracing -// spans from the consumed message using a Bloblang mapping. -type spanInjectInput struct { - inputName string - mgr bundle.NewManagement - - mapping *bloblang.Executor - rdr Input -} - -func (s *spanInjectInput) Connect(ctx context.Context) error { - return s.rdr.Connect(ctx) -} - -func (s *spanInjectInput) Read(ctx context.Context) (*Message, AckFunc, error) { - m, afn, err := s.rdr.Read(ctx) - if err != nil { - return nil, nil, err - } - - spanPart, err := m.BloblangQuery(s.mapping) - if err != nil { - s.mgr.Logger().Error("Mapping failed for tracing span: %v", err) - return m, afn, nil - } - - c, err := getPropMapCarrier(spanPart) - if err != nil { - s.mgr.Logger().Error("Mapping failed for tracing span: %v", err) - return m, afn, nil - } - - prov := s.mgr.Tracer() - operationName := "input_" + s.inputName - - textProp := otel.GetTextMapPropagator() - - pCtx, _ := prov.Tracer("benthos").Start(textProp.Extract(m.Context(), c), operationName) - m = m.WithContext(pCtx) - - return m, afn, nil -} - -func (s *spanInjectInput) Close(ctx context.Context) error { - return s.rdr.Close(ctx) -} diff --git a/public/service/config_extract_tracing_test.go b/public/service/config_extract_tracing_test.go deleted file mode 100644 index 9b40eab1b3..0000000000 --- a/public/service/config_extract_tracing_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package service - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type fnBatchReader struct { - connectWithContext func(ctx context.Context) error - readBatchWithContext func(ctx context.Context) (MessageBatch, AckFunc, error) - close func(ctx context.Context) error -} - -func (f *fnBatchReader) Connect(ctx context.Context) error { - return f.connectWithContext(ctx) -} - -func (f *fnBatchReader) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - return f.readBatchWithContext(ctx) -} - -func (f *fnBatchReader) Close(ctx context.Context) error { - return f.close(ctx) -} - -func TestSpanBatchReader(t *testing.T) { - tests := []struct { - name string - contents string - mapping string - }{ - { - name: "mapping fails", - contents: `{}`, - mapping: `root.foo = this.bar.not_null()`, - }, - { - name: "result not JSON", - contents: `{}`, - mapping: `root = "this isnt json"`, - }, - { - name: "result not an object", - contents: `{}`, - mapping: `root = ["foo","bar"]`, - }, - { - name: "result not a span", - contents: `{}`, - mapping: `root = {"foo":"bar"}`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var connCalled, closeCalled, waitCalled bool - - spec := NewConfigSpec().Field(NewExtractTracingSpanMappingField()) - pConf, err := spec.ParseYAML(fmt.Sprintf(`extract_tracing_map: '%v'`, test.mapping), nil) - require.NoError(t, err) - - r, err := pConf.WrapBatchInputExtractTracingSpanMapping("foo", &fnBatchReader{ - connectWithContext: func(ctx context.Context) error { - connCalled = true - return nil - }, - readBatchWithContext: func(ctx context.Context) (MessageBatch, AckFunc, error) { - m := MessageBatch{ - NewMessage([]byte(test.contents)), - } - return m, func(context.Context, error) error { - return nil - }, nil - }, - close: func(ctx context.Context) error { - closeCalled = true - waitCalled = true - return nil - }, - }) - require.NoError(t, err) - - assert.NoError(t, r.Connect(context.Background())) - - res, _, err := r.ReadBatch(context.Background()) - require.NoError(t, err) - assert.Len(t, res, 1) - - rBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, test.contents, string(rBytes)) - - assert.NoError(t, r.Close(context.Background())) - - assert.True(t, connCalled) - assert.True(t, closeCalled) - assert.True(t, waitCalled) - }) - } -} - -type fnReader struct { - connectWithContext func(ctx context.Context) error - readWithContext func(ctx context.Context) (*Message, AckFunc, error) - close func(ctx context.Context) error -} - -func (f *fnReader) Connect(ctx context.Context) error { - return f.connectWithContext(ctx) -} - -func (f *fnReader) Read(ctx context.Context) (*Message, AckFunc, error) { - return f.readWithContext(ctx) -} - -func (f *fnReader) Close(ctx context.Context) error { - return f.close(ctx) -} - -func TestSpanReader(t *testing.T) { - tests := []struct { - name string - contents string - mapping string - }{ - { - name: "mapping fails", - contents: `{}`, - mapping: `root.foo = this.bar.not_null()`, - }, - { - name: "result not JSON", - contents: `{}`, - mapping: `root = "this isnt json"`, - }, - { - name: "result not an object", - contents: `{}`, - mapping: `root = ["foo","bar"]`, - }, - { - name: "result not a span", - contents: `{}`, - mapping: `root = {"foo":"bar"}`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var connCalled, closeCalled, waitCalled bool - - spec := NewConfigSpec().Field(NewExtractTracingSpanMappingField()) - pConf, err := spec.ParseYAML(fmt.Sprintf(`extract_tracing_map: '%v'`, test.mapping), nil) - require.NoError(t, err) - - r, err := pConf.WrapInputExtractTracingSpanMapping("foo", &fnReader{ - connectWithContext: func(ctx context.Context) error { - connCalled = true - return nil - }, - readWithContext: func(ctx context.Context) (*Message, AckFunc, error) { - m := NewMessage([]byte(test.contents)) - return m, func(context.Context, error) error { - return nil - }, nil - }, - close: func(ctx context.Context) error { - closeCalled = true - waitCalled = true - return nil - }, - }) - require.NoError(t, err) - - assert.NoError(t, r.Connect(context.Background())) - - msg, _, err := r.Read(context.Background()) - require.NoError(t, err) - - rBytes, err := msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, test.contents, string(rBytes)) - - assert.NoError(t, r.Close(context.Background())) - - assert.True(t, connCalled) - assert.True(t, closeCalled) - assert.True(t, waitCalled) - }) - } -} diff --git a/public/service/config_http.go b/public/service/config_http.go deleted file mode 100644 index 301c5a8670..0000000000 --- a/public/service/config_http.go +++ /dev/null @@ -1,418 +0,0 @@ -package service - -import ( - "crypto" - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "fmt" - "io/fs" - "math/rand" - "net/http" - "net/url" - "strconv" - "sync" - "time" - - "github.com/golang-jwt/jwt/v5" -) - -const ( - aFieldBasicAuth = "basic_auth" - aFieldOAuth = "oauth" - aFieldJWT = "jwt" -) - -// NewHTTPRequestAuthSignerFields returns a list of config fields for adding -// authentication to HTTP requests. The options available with this field -// include OAuth (v1), basic authentication, and JWT as these are mechanisms -// that can be implemented by mutating a request object. -func NewHTTPRequestAuthSignerFields() []*ConfigField { - return []*ConfigField{ - oAuthFieldSpec(), - basicAuthField(), - jwtFieldSpec(), - } -} - -// HTTPRequestAuthSignerFromParsed takes a parsed config which is expected to -// contain fields from NewHTTPRequestAuthSignerFields, and returns a func that -// applies those configured authentication mechanisms to a given HTTP request. -func (p *ParsedConfig) HTTPRequestAuthSignerFromParsed() (fn func(fs.FS, *http.Request) error, err error) { - var oldConf authConfig - if oldConf.OAuth, err = oauthFromParsed(p); err != nil { - return - } - if oldConf.BasicAuth, err = basicAuthFromParsed(p); err != nil { - return - } - if oldConf.JWT, err = jwtAuthFromParsed(p); err != nil { - return - } - fn = oldConf.Sign - return -} - -type authConfig struct { - OAuth oauthConfig - BasicAuth basicAuthConfig - JWT jwtConfig -} - -// Sign method to sign an HTTP request for configured auth strategies. -func (c authConfig) Sign(f fs.FS, req *http.Request) error { - if err := c.OAuth.Sign(req); err != nil { - return err - } - if err := c.JWT.Sign(f, req); err != nil { - return err - } - return c.BasicAuth.Sign(req) -} - -//------------------------------------------------------------------------------ - -const ( - abFieldEnabled = "enabled" - abFieldUsername = "username" - abFieldPassword = "password" -) - -func basicAuthField() *ConfigField { - return NewObjectField(aFieldBasicAuth, - NewBoolField(abFieldEnabled). - Description("Whether to use basic authentication in requests."). - Default(false), - - NewStringField(abFieldUsername). - Description("A username to authenticate as."). - Default(""), - - NewStringField(abFieldPassword). - Description("A password to authenticate with."). - Default("").Secret(), - ).Description("Allows you to specify basic authentication."). - Advanced(). - Optional() -} - -func basicAuthFromParsed(conf *ParsedConfig) (res basicAuthConfig, err error) { - if !conf.Contains(aFieldBasicAuth) { - return - } - conf = conf.Namespace(aFieldBasicAuth) - if res.Enabled, err = conf.FieldBool(abFieldEnabled); err != nil { - return - } - if res.Username, err = conf.FieldString(abFieldUsername); err != nil { - return - } - if res.Password, err = conf.FieldString(abFieldPassword); err != nil { - return - } - return -} - -type basicAuthConfig struct { - Enabled bool - Username string - Password string -} - -// Sign method to sign an HTTP request for an OAuth exchange. -func (basic basicAuthConfig) Sign(req *http.Request) error { - if basic.Enabled { - req.SetBasicAuth(basic.Username, basic.Password) - } - return nil -} - -//------------------------------------------------------------------------------ - -const ( - aoFieldEnabled = "enabled" - aoFieldConsumerKey = "consumer_key" - aoFieldConsumerSecret = "consumer_secret" - aoFieldAccessToken = "access_token" - aoFieldAccessTokenSecret = "access_token_secret" -) - -func oAuthFieldSpec() *ConfigField { - return NewObjectField(aFieldOAuth, - NewBoolField(aoFieldEnabled). - Description("Whether to use OAuth version 1 in requests."). - Default(false), - - NewStringField(aoFieldConsumerKey). - Description("A value used to identify the client to the service provider."). - Default(""), - - NewStringField(aoFieldConsumerSecret). - Description("A secret used to establish ownership of the consumer key."). - Default("").Secret(), - - NewStringField(aoFieldAccessToken). - Description("A value used to gain access to the protected resources on behalf of the user."). - Default(""), - - NewStringField(aoFieldAccessTokenSecret). - Description("A secret provided in order to establish ownership of a given access token."). - Default("").Secret(), - ). - Description("Allows you to specify open authentication via OAuth version 1."). - Advanced(). - Optional() -} - -func oauthFromParsed(conf *ParsedConfig) (res oauthConfig, err error) { - if !conf.Contains(aFieldOAuth) { - return - } - conf = conf.Namespace(aFieldOAuth) - if res.Enabled, err = conf.FieldBool(aoFieldEnabled); err != nil { - return - } - if res.ConsumerKey, err = conf.FieldString(aoFieldConsumerKey); err != nil { - return - } - if res.ConsumerSecret, err = conf.FieldString(aoFieldConsumerSecret); err != nil { - return - } - if res.AccessToken, err = conf.FieldString(aoFieldAccessToken); err != nil { - return - } - if res.AccessTokenSecret, err = conf.FieldString(aoFieldAccessTokenSecret); err != nil { - return - } - return -} - -type oauthConfig struct { - Enabled bool - ConsumerKey string - ConsumerSecret string - AccessToken string - AccessTokenSecret string -} - -// Sign method to sign an HTTP request for an OAuth exchange. -func (oauth oauthConfig) Sign(req *http.Request) error { - if !oauth.Enabled { - return nil - } - - nonceGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) - nonce := strconv.FormatInt(nonceGenerator.Int63(), 10) - ts := strconv.FormatInt(time.Now().Unix(), 10) - - params := &url.Values{} - params.Add("oauth_consumer_key", oauth.ConsumerKey) - params.Add("oauth_nonce", nonce) - params.Add("oauth_signature_method", "HMAC-SHA1") - params.Add("oauth_timestamp", ts) - params.Add("oauth_token", oauth.AccessToken) - params.Add("oauth_version", "1.0") - - sig, err := oauth.getSignature(req, params) - if err != nil { - return err - } - - str := fmt.Sprintf( - ` oauth_consumer_key="%s", oauth_nonce="%s", oauth_signature="%s",`+ - ` oauth_signature_method="%s", oauth_timestamp="%s",`+ - ` oauth_token="%s", oauth_version="%s"`, - url.QueryEscape(oauth.ConsumerKey), - nonce, - url.QueryEscape(sig), - "HMAC-SHA1", - ts, - url.QueryEscape(oauth.AccessToken), - "1.0", - ) - req.Header.Add("Authorization", str) - - return nil -} - -func (oauth oauthConfig) getSignature( - req *http.Request, - params *url.Values, -) (string, error) { - baseSignatureString := req.Method + "&" + - url.QueryEscape(req.URL.String()) + "&" + - url.QueryEscape(params.Encode()) - - signingKey := url.QueryEscape(oauth.ConsumerSecret) + "&" + - url.QueryEscape(oauth.AccessTokenSecret) - - return oauth.computeHMAC(baseSignatureString, signingKey) -} - -func (oauth oauthConfig) computeHMAC( - message string, - key string, -) (string, error) { - h := hmac.New(sha1.New, []byte(key)) - if _, err := h.Write([]byte(message)); err != nil { - return "", err - } - return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil -} - -//------------------------------------------------------------------------------ - -const ( - ajFieldEnabled = "enabled" - ajFieldPrivateKeyFile = "private_key_file" - ajFieldSigningMethod = "signing_method" - ajFieldClaims = "claims" - ajFieldHeaders = "headers" -) - -func jwtFieldSpec() *ConfigField { - return NewObjectField(aFieldJWT, - NewBoolField(ajFieldEnabled). - Description("Whether to use JWT authentication in requests."). - Default(false), - - NewStringField(ajFieldPrivateKeyFile). - Description("A file with the PEM encoded via PKCS1 or PKCS8 as private key."). - Default(""), - - NewStringField(ajFieldSigningMethod). - Description("A method used to sign the token such as RS256, RS384, RS512 or EdDSA."). - Default(""), - - NewAnyMapField(ajFieldClaims). - Description("A value used to identify the claims that issued the JWT."). - Default(map[string]any{}). - Advanced(), - - NewAnyMapField(ajFieldHeaders). - Description("Add optional key/value headers to the JWT."). - Default(map[string]any{}). - Advanced(), - ). - Description("BETA: Allows you to specify JWT authentication."). - Advanced() -} - -func jwtAuthFromParsed(conf *ParsedConfig) (res jwtConfig, err error) { - if !conf.Contains(aFieldJWT) { - return - } - - var key crypto.PrivateKey - res.key = &key - res.keyMx = &sync.Mutex{} - - conf = conf.Namespace(aFieldJWT) - if res.Enabled, err = conf.FieldBool(ajFieldEnabled); err != nil { - return - } - var claimsConfs map[string]*ParsedConfig - if claimsConfs, err = conf.FieldAnyMap(ajFieldClaims); err != nil { - return - } - res.Claims = jwt.MapClaims{} - for k, v := range claimsConfs { - if res.Claims[k], err = v.FieldAny(); err != nil { - return - } - } - var headersConfs map[string]*ParsedConfig - if headersConfs, err = conf.FieldAnyMap(ajFieldHeaders); err != nil { - return - } - res.Headers = map[string]any{} - for k, v := range headersConfs { - if res.Headers[k], err = v.FieldAny(); err != nil { - return - } - } - if res.SigningMethod, err = conf.FieldString(ajFieldSigningMethod); err != nil { - return - } - if res.PrivateKeyFile, err = conf.FieldString(ajFieldPrivateKeyFile); err != nil { - return - } - return -} - -type jwtConfig struct { - Enabled bool - Claims jwt.MapClaims - Headers map[string]any - SigningMethod string - PrivateKeyFile string - - // internal private fields - keyMx *sync.Mutex - key *crypto.PrivateKey -} - -// Sign method to sign an HTTP request for an JWT exchange. -func (j jwtConfig) Sign(f fs.FS, req *http.Request) error { - if !j.Enabled { - return nil - } - - if err := j.parsePrivateKey(f); err != nil { - return err - } - - var token *jwt.Token - switch j.SigningMethod { - case "RS256": - token = jwt.NewWithClaims(jwt.SigningMethodRS256, j.Claims) - case "RS384": - token = jwt.NewWithClaims(jwt.SigningMethodRS384, j.Claims) - case "RS512": - token = jwt.NewWithClaims(jwt.SigningMethodRS512, j.Claims) - case "EdDSA": - token = jwt.NewWithClaims(jwt.SigningMethodEdDSA, j.Claims) - default: - return fmt.Errorf("jwt signing method %s not acepted. Try with RS256, RS384, RS512 or EdDSA", j.SigningMethod) - } - - for name, value := range j.Headers { - token.Header[name] = value - } - - ss, err := token.SignedString(*j.key) - if err != nil { - return fmt.Errorf("failed to sign jwt: %v", err) - } - - req.Header.Set("Authorization", "Bearer "+ss) - return nil -} - -// parsePrivateKey parses once the RSA private key. -// Needs mutex locking as Sign might be called by parallel threads. -func (j jwtConfig) parsePrivateKey(fs fs.FS) error { - j.keyMx.Lock() - defer j.keyMx.Unlock() - - if *j.key != nil { - return nil - } - - privateKey, err := ReadFile(fs, j.PrivateKeyFile) - if err != nil { - return fmt.Errorf("failed to read private key: %v", err) - } - - switch j.SigningMethod { - case "RS256", "RS384", "RS512": - *j.key, err = jwt.ParseRSAPrivateKeyFromPEM(privateKey) - case "EdDSA": - *j.key, err = jwt.ParseEdPrivateKeyFromPEM(privateKey) - } - if err != nil { - return fmt.Errorf("failed to parse %s private key: %v", j.SigningMethod, err) - } - - return nil -} diff --git a/public/service/config_inject_tracing.go b/public/service/config_inject_tracing.go deleted file mode 100644 index 98b0edc18d..0000000000 --- a/public/service/config_inject_tracing.go +++ /dev/null @@ -1,127 +0,0 @@ -package service - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/tracing" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -const ( - itsField = "inject_tracing_map" -) - -// NewInjectTracingSpanMappingField returns a field spec describing an inject -// tracing span mapping. -func NewInjectTracingSpanMappingField() *ConfigField { - return NewBloblangField(itsField). - Description("EXPERIMENTAL: A xref:guides:bloblang/about.adoc[Bloblang mapping] used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer."). - Examples( - `meta = @.merge(this)`, - `root.meta.span = this`, - ). - Version("3.45.0"). - Optional(). - Advanced() -} - -// WrapBatchOutputExtractTracingSpanMapping wraps a BatchOutput with a mechanism -// for extracting tracing spans from the consumed message and merging them into -// the written result using a Bloblang mapping. -func (p *ParsedConfig) WrapBatchOutputExtractTracingSpanMapping(outputName string, o BatchOutput) (BatchOutput, error) { - if str, _ := p.FieldString(itsField); str == "" { - return o, nil - } - exe, err := p.FieldBloblang(itsField) - if err != nil { - return nil, err - } - return &spanExtractBatchOutput{outputName: outputName, mgr: p.mgr, mapping: exe, wtr: o}, nil -} - -// WrapOutputExtractTracingSpanMapping wraps a Output with a mechanism for -// extracting tracing spans from the consumed message and merging them into the -// written result using a Bloblang mapping. -func (p *ParsedConfig) WrapOutputExtractTracingSpanMapping(outputName string, o Output) (Output, error) { - if str, _ := p.FieldString(itsField); str == "" { - return o, nil - } - exe, err := p.FieldBloblang(itsField) - if err != nil { - return nil, err - } - return &spanExtractOutput{outputName: outputName, mgr: p.mgr, mapping: exe, wtr: o}, nil -} - -type spanExtractBatchOutput struct { - outputName string - mgr bundle.NewManagement - - mapping *bloblang.Executor - wtr BatchOutput -} - -func (s *spanExtractBatchOutput) Connect(ctx context.Context) error { - return s.wtr.Connect(ctx) -} - -func (s *spanExtractBatchOutput) WriteBatch(ctx context.Context, batch MessageBatch) error { - for i := 0; i < len(batch); i++ { - span := tracing.GetSpanFromContext(batch[i].Context()) - spanMapGeneric, err := span.TextMap() - if err != nil { - s.mgr.Logger().Warn("Failed to inject span: %v", err) - continue - } - - spanMsg := NewMessage(nil) - spanMsg.SetStructuredMut(spanMapGeneric) - - if tmpRes, err := batch[i].BloblangMutateFrom(s.mapping, spanMsg); err != nil { - s.mgr.Logger().Warn("Failed to inject span: %v", err) - } else if tmpRes != nil { - batch[i] = tmpRes - } - } - return s.wtr.WriteBatch(ctx, batch) -} - -func (s *spanExtractBatchOutput) Close(ctx context.Context) error { - return s.wtr.Close(ctx) -} - -type spanExtractOutput struct { - outputName string - mgr bundle.NewManagement - - mapping *bloblang.Executor - wtr Output -} - -func (s *spanExtractOutput) Connect(ctx context.Context) error { - return s.wtr.Connect(ctx) -} - -func (s *spanExtractOutput) Write(ctx context.Context, msg *Message) error { - span := tracing.GetSpanFromContext(msg.Context()) - spanMapGeneric, err := span.TextMap() - if err != nil { - s.mgr.Logger().Warn("Failed to inject span: %v", err) - return s.wtr.Write(ctx, msg) - } - - spanMsg := NewMessage(nil) - spanMsg.SetStructuredMut(spanMapGeneric) - - if tmpRes, err := msg.BloblangMutateFrom(s.mapping, spanMsg); err != nil { - s.mgr.Logger().Warn("Failed to inject span: %v", err) - } else if tmpRes != nil { - msg = tmpRes - } - return s.wtr.Write(ctx, msg) -} - -func (s *spanExtractOutput) Close(ctx context.Context) error { - return s.wtr.Close(ctx) -} diff --git a/public/service/config_inject_tracing_test.go b/public/service/config_inject_tracing_test.go deleted file mode 100644 index 6b59ebf74c..0000000000 --- a/public/service/config_inject_tracing_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package service - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type fnBatchWriter struct { - connectWithContext func(ctx context.Context) error - writeBatchWithContext func(ctx context.Context, b MessageBatch) error - close func(ctx context.Context) error -} - -func (f *fnBatchWriter) Connect(ctx context.Context) error { - return f.connectWithContext(ctx) -} - -func (f *fnBatchWriter) WriteBatch(ctx context.Context, msg MessageBatch) error { - return f.writeBatchWithContext(ctx, msg) -} - -func (f *fnBatchWriter) Close(ctx context.Context) error { - return f.close(ctx) -} - -func TestSpanBatchWriter(t *testing.T) { - tests := []struct { - name string - outContents string - mapping string - }{ - { - name: "mapping succeeds", - outContents: `{"meow":{}}`, - mapping: `root.meow = this`, - }, - { - name: "mapping fails", - outContents: `{}`, - mapping: `root.meow = this.uhhhh.not_null()`, - }, - { - name: "result is deleted", - outContents: `{}`, - mapping: `root = deleted()`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var connCalled, closeCalled, waitCalled bool - - spec := NewConfigSpec().Field(NewInjectTracingSpanMappingField()) - pConf, err := spec.ParseYAML(fmt.Sprintf(`inject_tracing_map: '%v'`, test.mapping), nil) - require.NoError(t, err) - - r, err := pConf.WrapBatchOutputExtractTracingSpanMapping("foo", &fnBatchWriter{ - connectWithContext: func(ctx context.Context) error { - connCalled = true - return nil - }, - writeBatchWithContext: func(ctx context.Context, b MessageBatch) error { - assert.Len(t, b, 1) - msgBytes, err := b[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, test.outContents, string(msgBytes)) - return nil - }, - close: func(ctx context.Context) error { - closeCalled = true - waitCalled = true - return nil - }, - }) - require.NoError(t, err) - - assert.NoError(t, r.Connect(context.Background())) - - require.NoError(t, r.WriteBatch(context.Background(), MessageBatch{NewMessage([]byte(`{}`))})) - - assert.NoError(t, r.Close(context.Background())) - - assert.True(t, connCalled) - assert.True(t, closeCalled) - assert.True(t, waitCalled) - }) - } -} - -type fnWriter struct { - connectWithContext func(ctx context.Context) error - writeWithContext func(ctx context.Context, m *Message) error - close func(ctx context.Context) error -} - -func (f *fnWriter) Connect(ctx context.Context) error { - return f.connectWithContext(ctx) -} - -func (f *fnWriter) Write(ctx context.Context, m *Message) error { - return f.writeWithContext(ctx, m) -} - -func (f *fnWriter) Close(ctx context.Context) error { - return f.close(ctx) -} - -func TestSpanWriter(t *testing.T) { - tests := []struct { - name string - outContents string - mapping string - }{ - { - name: "mapping succeeds", - outContents: `{"meow":{}}`, - mapping: `root.meow = this`, - }, - { - name: "mapping fails", - outContents: `{}`, - mapping: `root.meow = this.uhhhh.not_null()`, - }, - { - name: "result is deleted", - outContents: `{}`, - mapping: `root = deleted()`, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - var connCalled, closeCalled, waitCalled bool - - spec := NewConfigSpec().Field(NewInjectTracingSpanMappingField()) - pConf, err := spec.ParseYAML(fmt.Sprintf(`inject_tracing_map: '%v'`, test.mapping), nil) - require.NoError(t, err) - - r, err := pConf.WrapOutputExtractTracingSpanMapping("foo", &fnWriter{ - connectWithContext: func(ctx context.Context) error { - connCalled = true - return nil - }, - writeWithContext: func(ctx context.Context, m *Message) error { - msgBytes, err := m.AsBytes() - require.NoError(t, err) - assert.Equal(t, test.outContents, string(msgBytes)) - return nil - }, - close: func(ctx context.Context) error { - closeCalled = true - waitCalled = true - return nil - }, - }) - require.NoError(t, err) - - assert.NoError(t, r.Connect(context.Background())) - - require.NoError(t, r.Write(context.Background(), NewMessage([]byte(`{}`)))) - - assert.NoError(t, r.Close(context.Background())) - - assert.True(t, connCalled) - assert.True(t, closeCalled) - assert.True(t, waitCalled) - }) - } -} diff --git a/public/service/config_input.go b/public/service/config_input.go deleted file mode 100644 index 6b5ec93ea3..0000000000 --- a/public/service/config_input.go +++ /dev/null @@ -1,138 +0,0 @@ -package service - -import ( - "fmt" - "strconv" - "strings" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -const AutoRetryNacksToggleFieldName = "auto_replay_nacks" - -// NewAutoRetryNacksToggleField creates a configuration field for toggling the -// behaviour of an input where nacks (rejections) of data results in the -// automatic replay of that data (the default). This field should be used for -// conditionally wrapping inputs with AutoRetryNacksToggled or -// AutoRetryNacksBatchedToggled. -func NewAutoRetryNacksToggleField() *ConfigField { - return NewBoolField(AutoRetryNacksToggleFieldName). - Description("Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation."). - Default(true) -} - -// NewInputField defines a new input field, it is then possible to extract an -// OwnedInput from the resulting parsed config with the method FieldInput. -func NewInputField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldInput(name, ""), - } -} - -// FieldInput accesses a field from a parsed config that was defined with -// NewInputField and returns an OwnedInput, or an error if the configuration was -// invalid. -func (p *ParsedConfig) FieldInput(path ...string) (*OwnedInput, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - conf, err := input.FromAny(p.mgr.Environment(), field) - if err != nil { - return nil, err - } - - iproc, err := p.mgr.IntoPath(path...).NewInput(conf) - if err != nil { - return nil, err - } - return &OwnedInput{iproc}, nil -} - -// NewInputListField defines a new input list field, it is then possible -// to extract a list of OwnedInput from the resulting parsed config with the -// method FieldInputList. -func NewInputListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldInput(name, "").Array(), - } -} - -// FieldInputList accesses a field from a parsed config that was defined -// with NewInputListField and returns a slice of OwnedInput, or an error -// if the configuration was invalid. -func (p *ParsedConfig) FieldInputList(path ...string) ([]*OwnedInput, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - fieldArray, ok := field.([]any) - if !ok { - return nil, fmt.Errorf("unexpected value, expected array, got %T", field) - } - - var configs []input.Config - for i, iConf := range fieldArray { - conf, err := input.FromAny(p.mgr.Environment(), iConf) - if err != nil { - return nil, fmt.Errorf("value %v: %w", i, err) - } - configs = append(configs, conf) - } - - tmpMgr := p.mgr.IntoPath(path...) - ins := make([]*OwnedInput, len(configs)) - for i, c := range configs { - iproc, err := tmpMgr.IntoPath(strconv.Itoa(i)).NewInput(c) - if err != nil { - return nil, fmt.Errorf("input %v: %w", i, err) - } - ins[i] = &OwnedInput{iproc} - } - - return ins, nil -} - -// NewInputMapField defines a new input map field, it is then possible to -// extract a map of OwnedInput from the resulting parsed config with the -// method FieldInputMap. -func NewInputMapField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldInput(name, "").Map(), - } -} - -// FieldInputMap accesses a field from a parsed config that was defined -// with NewInputMapField and returns a map of OwnedInput, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldInputMap(path ...string) (map[string]*OwnedInput, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - fieldMap, ok := field.(map[string]any) - if !ok { - return nil, fmt.Errorf("unexpected value, expected object, got %T", field) - } - - tmpMgr := p.mgr.IntoPath(path...) - ins := make(map[string]*OwnedInput, len(fieldMap)) - for k, v := range fieldMap { - conf, err := input.FromAny(p.mgr.Environment(), v) - if err != nil { - return nil, fmt.Errorf("value %v: %w", k, err) - } - - iproc, err := tmpMgr.IntoPath(k).NewInput(conf) - if err != nil { - return nil, fmt.Errorf("input %v: %w", k, err) - } - ins[k] = &OwnedInput{iproc} - } - - return ins, nil -} diff --git a/public/service/config_input_test.go b/public/service/config_input_test.go deleted file mode 100644 index 5b81cf3789..0000000000 --- a/public/service/config_input_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package service - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigInput(t *testing.T) { - spec := NewConfigSpec(). - Field(NewInputField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - generate: - count: 1 - interval: "" - mapping: 'root = "hello world"' -`, nil) - require.NoError(t, err) - - input, err := parsedConfig.FieldInput("a") - require.NoError(t, err) - - res, aFn, err := input.ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, res, 1) - - require.NoError(t, aFn(context.Background(), nil)) - - resBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "hello world", string(resBytes)) - - _, _, err = input.ReadBatch(context.Background()) - require.Equal(t, ErrEndOfInput, err) - - require.NoError(t, input.Close(context.Background())) -} - -func TestConfigInputList(t *testing.T) { - spec := NewConfigSpec(). - Field(NewInputListField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world"' - - generate: - count: 1 - interval: "" - mapping: 'root = "hello world two"' -`, nil) - require.NoError(t, err) - - inputs, err := parsedConfig.FieldInputList("a") - require.NoError(t, err) - require.Len(t, inputs, 2) - - res, aFn, err := inputs[0].ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, res, 1) - - require.NoError(t, aFn(context.Background(), nil)) - - resBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "hello world", string(resBytes)) - - _, _, err = inputs[0].ReadBatch(context.Background()) - require.Equal(t, ErrEndOfInput, err) - - res, aFn, err = inputs[1].ReadBatch(context.Background()) - require.NoError(t, err) - require.Len(t, res, 1) - - require.NoError(t, aFn(context.Background(), nil)) - - resBytes, err = res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "hello world two", string(resBytes)) - - _, _, err = inputs[1].ReadBatch(context.Background()) - require.Equal(t, ErrEndOfInput, err) - - require.NoError(t, inputs[0].Close(context.Background())) - require.NoError(t, inputs[1].Close(context.Background())) -} diff --git a/public/service/config_interpolated_string.go b/public/service/config_interpolated_string.go deleted file mode 100644 index a458322ea2..0000000000 --- a/public/service/config_interpolated_string.go +++ /dev/null @@ -1,145 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -var errInvalidInterpolation = errors.New("failed to parse interpolated field") - -// NewInterpolatedStringField defines a new config field that describes a -// dynamic string that supports Bloblang interpolation functions. It is then -// possible to extract an *InterpolatedString from the resulting parsed config -// with the method FieldInterpolatedString. -func NewInterpolatedStringField(name string) *ConfigField { - tf := docs.FieldString(name, "").IsInterpolated() - return &ConfigField{field: tf} -} - -// NewInterpolatedStringEnumField defines a new config field that describes a -// dynamic string that supports Bloblang interpolation functions, but also has a -// discrete list of acceptable values. It is then possible to extract an -// *InterpolatedString from the resulting parsed config with the method -// FieldInterpolatedString. Verifying that the interpolated result matches one -// of the specified options must be done at runtime. -func NewInterpolatedStringEnumField(name string, options ...string) *ConfigField { - tf := docs.FieldString(name, "").IsInterpolated().HasOptions(options...) - return &ConfigField{field: tf} -} - -// NewInterpolatedStringMapField describes a new config field consisting of an -// object of arbitrary keys with interpolated string values. It is then -// possible to extract an *InterpolatedString from the resulting parsed config -// with the method FieldInterpolatedStringMap. -func NewInterpolatedStringMapField(name string) *ConfigField { - tf := docs.FieldString(name, "").IsInterpolated().Map() - return &ConfigField{field: tf} -} - -// NewInterpolatedStringListField describes a new config field consisting of a -// list of interpolated string values. It is then possible to extract a slice of -// *InterpolatedString from the resulting parsed config with the method -// FieldInterpolatedStringList. -func NewInterpolatedStringListField(name string) *ConfigField { - tf := docs.FieldString(name, "").IsInterpolated().Array() - return &ConfigField{field: tf} -} - -// FieldInterpolatedString accesses a field from a parsed config that was -// defined with NewInterpolatedStringField and returns either an -// *InterpolatedString or an error if the string was invalid. -func (p *ParsedConfig) FieldInterpolatedString(path ...string) (*InterpolatedString, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - str, ok := v.(string) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a string, got %T", strings.Join(path, "."), v) - } - - e, err := p.mgr.BloblEnvironment().NewField(str) - if err != nil { - return nil, fmt.Errorf("failed to parse interpolated field '%v': %v", strings.Join(path, "."), err) - } - - return &InterpolatedString{expr: e}, nil -} - -// FieldInterpolatedStringMap accesses a field that is an object of arbitrary -// keys and interpolated string values from the parsed config by its name and -// returns the value. -// -// Returns an error if the field is not found, or is not an object of -// interpolated strings. -func (p *ParsedConfig) FieldInterpolatedStringMap(path ...string) (map[string]*InterpolatedString, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.i.FullDotPath(path...)) - } - iMap, ok := v.(map[string]any) - if !ok { - if sMap, ok := v.(map[string]string); ok { - iMap := make(map[string]*InterpolatedString, len(sMap)) - for k, sv := range sMap { - e, err := p.mgr.BloblEnvironment().NewField(sv) - if err != nil { - return nil, fmt.Errorf("failed to parse interpolated field '%v': %v", strings.Join(path, "."), err) - } - iMap[k] = &InterpolatedString{expr: e} - } - return iMap, nil - } - return nil, fmt.Errorf("expected field '%v' to be a string map, got %T", p.i.FullDotPath(path...), v) - } - sMap := make(map[string]*InterpolatedString, len(iMap)) - for k, ev := range iMap { - str, ok := ev.(string) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a string map, found an element of type %T", p.i.FullDotPath(path...), ev) - } - e, err := p.mgr.BloblEnvironment().NewField(str) - if err != nil { - return nil, fmt.Errorf("failed to parse interpolated field '%v': %v", strings.Join(path, "."), err) - } - sMap[k] = &InterpolatedString{expr: e} - } - return sMap, nil -} - -// FieldInterpolatedStringList accesses a field that is a list of interpolated -// string values from the parsed config. -// -// Returns an error if the field is not found, or is not an list of interpolated -// strings. -func (p *ParsedConfig) FieldInterpolatedStringList(path ...string) ([]*InterpolatedString, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.i.FullDotPath(path...)) - } - - raw, ok := v.([]any) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a string list, got %T", p.i.FullDotPath(path...), v) - } - - values := make([]*InterpolatedString, 0, len(raw)) - for _, rawValue := range raw { - var parsed string - var ok bool - if parsed, ok = rawValue.(string); !ok { - return nil, fmt.Errorf("expected field '%v' to be a string list, found an element of type %T", p.i.FullDotPath(path...), rawValue) - } - - e, err := p.mgr.BloblEnvironment().NewField(parsed) - if err != nil { - return nil, fmt.Errorf("%w '%v': %v", errInvalidInterpolation, strings.Join(path, "."), err) - } - values = append(values, &InterpolatedString{expr: e}) - } - return values, nil -} diff --git a/public/service/config_interpolated_string_test.go b/public/service/config_interpolated_string_test.go deleted file mode 100644 index 681789c69a..0000000000 --- a/public/service/config_interpolated_string_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package service - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestFieldInterpolatedStringList(t *testing.T) { - conf := ` -listfield: - - hello ${! json("name").uppercase() } - - see you in ${! meta("ttl_days") } days -` - - spec := NewConfigSpec().Field(NewInterpolatedStringListField("listfield")) - env := NewEnvironment() - parsed, err := spec.ParseYAML(conf, env) - require.NoError(t, err) - - field, err := parsed.FieldInterpolatedStringList("listfield") - require.NoError(t, err) - - msg := NewMessage([]byte(`{"name": "world"}`)) - msg.MetaSet("ttl_days", "3") - - var out []string - for _, f := range field { - str, err := f.TryString(msg) - require.NoError(t, err) - - out = append(out, str) - } - - require.Equal(t, []string{"hello WORLD", "see you in 3 days"}, out) -} - -func TestFieldInterpolatedStringList_InvalidInterpolation(t *testing.T) { - conf := ` -listfield: - - hello ${! json("name")$$uppercas } - - see you in ${! meta("ttl_days") } days -` - - spec := NewConfigSpec().Field(NewInterpolatedStringListField("listfield")) - env := NewEnvironment() - parsed, err := spec.ParseYAML(conf, env) - require.NoError(t, err) - - _, err = parsed.FieldInterpolatedStringList("listfield") - require.ErrorIs(t, err, errInvalidInterpolation) -} diff --git a/public/service/config_max_in_flight.go b/public/service/config_max_in_flight.go deleted file mode 100644 index 4d98f7262d..0000000000 --- a/public/service/config_max_in_flight.go +++ /dev/null @@ -1,18 +0,0 @@ -package service - -// NewOutputMaxInFlightField creates a common field for determining the maximum -// number of in-flight messages an output should allow. This function is a -// short-hand way of creating an integer field with the common name -// max_in_flight, with a typical default of 64. -func NewOutputMaxInFlightField() *ConfigField { - return NewIntField("max_in_flight"). - Description("The maximum number of messages to have in flight at a given time. Increase this to improve throughput."). - Default(64) -} - -// FieldMaxInFlight accesses a field from a parsed config that was defined -// either with NewInputMaxInFlightField or NewOutputMaxInFlightField, and -// returns either an integer or an error if the value was invalid. -func (p *ParsedConfig) FieldMaxInFlight() (int, error) { - return p.FieldInt("max_in_flight") -} diff --git a/public/service/config_metadata_filter.go b/public/service/config_metadata_filter.go deleted file mode 100644 index 3c81db5e30..0000000000 --- a/public/service/config_metadata_filter.go +++ /dev/null @@ -1,181 +0,0 @@ -package service - -import ( - "fmt" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/metadata" -) - -// NewMetadataFilterField creates a config field spec for describing which -// metadata keys to include for a given purpose. This includes prefix based and -// regular expression based methods. This field is often used for making -// metadata written to output destinations explicit. -func NewMetadataFilterField(name string) *ConfigField { - field := docs.FieldObject(name, "").WithChildren(metadata.IncludeFilterDocs()...) - return &ConfigField{field: field} -} - -// MetadataFilter provides a configured mechanism for filtering metadata -// key/values from a message. -type MetadataFilter struct { - f *metadata.IncludeFilter -} - -// IsEmpty returns true if there aren't any rules configured for matching. -func (m *MetadataFilter) IsEmpty() bool { - if m == nil || m.f == nil { - return true - } - return !m.f.IsSet() -} - -// Match returns true if the provided key matches the filter. -func (m *MetadataFilter) Match(k string) bool { - if m == nil || m.f == nil { - return false - } - return m.f.Match(k) -} - -// Walk iterates the filtered metadata key/value pairs from a message and -// executes a provided closure function for each pair. An error returned by the -// closure will be returned by this function and prevent subsequent pairs from -// being accessed. -func (m *MetadataFilter) Walk(msg *Message, fn func(key, value string) error) error { - if m == nil { - return nil - } - return msg.MetaWalk(func(key, value string) error { - if !m.f.Match(key) { - return nil - } - return fn(key, value) - }) -} - -// WalkMut iterates the filtered metadata key/value pairs as mutable structured -// values from a message and executes a provided closure function for each pair. -// An error returned by the closure will be returned by this function and -// prevent subsequent pairs from being accessed. -func (m *MetadataFilter) WalkMut(msg *Message, fn func(key string, value any) error) error { - if m == nil { - return nil - } - return msg.MetaWalkMut(func(key string, value any) error { - if !m.f.Match(key) { - return nil - } - return fn(key, value) - }) -} - -// FieldMetadataFilter accesses a field from a parsed config that was defined -// with NewMetdataFilterField and returns a MetadataFilter, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldMetadataFilter(path ...string) (f *MetadataFilter, err error) { - confNode, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.i.FullDotPath(path...)) - } - - var node yaml.Node - if err = node.Encode(confNode); err != nil { - return - } - - conf := metadata.NewIncludeFilterConfig() - if err = node.Decode(&conf); err != nil { - return - } - - var filter *metadata.IncludeFilter - if filter, err = conf.CreateFilter(); err != nil { - return - } - - f = &MetadataFilter{f: filter} - return -} - -//------------------------------------------------------------------------------ - -// NewMetadataExcludeFilterField creates a config field spec for describing -// which metadata keys to exclude for a given purpose, where all metadata is -// included by default. This includes prefix based and regular expression based -// methods. This field should be avoided in favour of NewMetadataFilterField as -// all components should be converging on opt-in metadata delivery. However, -// this field is useful for migrating existing components to the new plugin APIs -// with backwards compatibility. -func NewMetadataExcludeFilterField(name string) *ConfigField { - field := docs.FieldObject(name, "").WithChildren(metadata.ExcludeFilterFields()...) - return &ConfigField{field: field} -} - -// MetadataExcludeFilter provides a configured mechanism for filtering metadata -// key/values from a message. -type MetadataExcludeFilter struct { - f *metadata.ExcludeFilter -} - -// Walk iterates the filtered metadata key/value pairs from a message and -// executes a provided closure function for each pair. An error returned by the -// closure will be returned by this function and prevent subsequent pairs from -// being accessed. -func (m *MetadataExcludeFilter) Walk(msg *Message, fn func(key, value string) error) error { - if m == nil { - return nil - } - return msg.MetaWalk(func(key, value string) error { - if !m.f.Match(key) { - return nil - } - return fn(key, value) - }) -} - -// WalkMut iterates the filtered metadata key/value pairs as mutable structured -// values from a message and executes a provided closure function for each pair. -// An error returned by the closure will be returned by this function and -// prevent subsequent pairs from being accessed. -func (m *MetadataExcludeFilter) WalkMut(msg *Message, fn func(key string, value any) error) error { - if m == nil { - return nil - } - return msg.MetaWalkMut(func(key string, value any) error { - if !m.f.Match(key) { - return nil - } - return fn(key, value) - }) -} - -// FieldMetadataExcludeFilter accesses a field from a parsed config that was -// defined with NewMetdataExcludeFilterField and returns a -// MetadataExcludeFilter, or an error if the configuration was invalid. -func (p *ParsedConfig) FieldMetadataExcludeFilter(path ...string) (f *MetadataExcludeFilter, err error) { - confNode, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", p.i.FullDotPath(path...)) - } - - var node yaml.Node - if err = node.Encode(confNode); err != nil { - return - } - - conf := metadata.NewExcludeFilterConfig() - if err = node.Decode(&conf); err != nil { - return - } - - var filter *metadata.ExcludeFilter - if filter, err = conf.Filter(); err != nil { - return - } - - f = &MetadataExcludeFilter{f: filter} - return -} diff --git a/public/service/config_metadata_filter_test.go b/public/service/config_metadata_filter_test.go deleted file mode 100644 index 2d1ccc9753..0000000000 --- a/public/service/config_metadata_filter_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package service - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMetadataFilterConfig(t *testing.T) { - configSpec := NewConfigSpec().Field(NewMetadataFilterField("foo")) - - configParsed, err := configSpec.ParseYAML(` -foo: - include_prefixes: [ foo_ ] - include_patterns: [ "meo?w" ] -`, nil) - require.NoError(t, err) - - _, err = configParsed.FieldMetadataFilter("bar") - require.Error(t, err) - - f, err := configParsed.FieldMetadataFilter("foo") - require.NoError(t, err) - - msg := NewMessage(nil) - msg.MetaSet("foo_1", "foo value") - msg.MetaSet("bar_1", "bar value") - msg.MetaSet("baz_1", "baz value") - msg.MetaSet("woof_1", "woof value") - msg.MetaSet("meow_1", "meow value") - msg.MetaSet("mew_1", "mew value") - - seen := map[string]string{} - require.NoError(t, f.Walk(msg, func(key, value string) error { - seen[key] = value - return nil - })) - - assert.Equal(t, map[string]string{ - "foo_1": "foo value", - "meow_1": "meow value", - "mew_1": "mew value", - }, seen) -} diff --git a/public/service/config_output.go b/public/service/config_output.go deleted file mode 100644 index 1a76ac2057..0000000000 --- a/public/service/config_output.go +++ /dev/null @@ -1,129 +0,0 @@ -package service - -import ( - "fmt" - "strconv" - "strings" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// NewOutputField defines a new output field, it is then possible to extract an -// OwnedOutput from the resulting parsed config with the method FieldOutput. -func NewOutputField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldOutput(name, ""), - } -} - -// FieldOutput accesses a field from a parsed config that was defined with -// NewOutputField and returns an OwnedOutput, or an error if the configuration -// was invalid. -func (p *ParsedConfig) FieldOutput(path ...string) (*OwnedOutput, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - conf, err := output.FromAny(p.mgr.Environment(), field) - if err != nil { - return nil, err - } - - iproc, err := p.mgr.IntoPath(path...).NewOutput(conf) - if err != nil { - return nil, err - } - return newOwnedOutput(iproc) -} - -// NewOutputListField defines a new output list field, it is then possible -// to extract a list of OwnedOutput from the resulting parsed config with the -// method FieldOutputList. -func NewOutputListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldOutput(name, "").Array(), - } -} - -// FieldOutputList accesses a field from a parsed config that was defined -// with NewOutputListField and returns a slice of OwnedOutput, or an error -// if the configuration was invalid. -func (p *ParsedConfig) FieldOutputList(path ...string) ([]*OwnedOutput, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - fieldArray, ok := field.([]any) - if !ok { - return nil, fmt.Errorf("unexpected value, expected array, got %T", field) - } - - var configs []output.Config - for i, iConf := range fieldArray { - conf, err := output.FromAny(p.mgr.Environment(), iConf) - if err != nil { - return nil, fmt.Errorf("value %v: %w", i, err) - } - configs = append(configs, conf) - } - - tmpMgr := p.mgr.IntoPath(path...) - ins := make([]*OwnedOutput, len(configs)) - for i, c := range configs { - iproc, err := tmpMgr.IntoPath(strconv.Itoa(i)).NewOutput(c) - if err != nil { - return nil, fmt.Errorf("output %v: %w", i, err) - } - if ins[i], err = newOwnedOutput(iproc); err != nil { - return nil, fmt.Errorf("output %v: %w", i, err) - } - } - - return ins, nil -} - -// NewOutputMapField defines a new output list field, it is then possible -// to extract a map of OwnedOutput from the resulting parsed config with the -// method FieldOutputMap. -func NewOutputMapField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldOutput(name, "").Map(), - } -} - -// FieldOutputMap accesses a field from a parsed config that was defined -// with NewOutputMapField and returns a map of OwnedOutput, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldOutputMap(path ...string) (map[string]*OwnedOutput, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - fieldMap, ok := field.(map[string]any) - if !ok { - return nil, fmt.Errorf("unexpected value, expected object, got %T", field) - } - - tmpMgr := p.mgr.IntoPath(path...) - outs := make(map[string]*OwnedOutput, len(fieldMap)) - for k, v := range fieldMap { - conf, err := output.FromAny(p.mgr.Environment(), v) - if err != nil { - return nil, fmt.Errorf("value %v: %w", k, err) - } - - iproc, err := tmpMgr.IntoPath(k).NewOutput(conf) - if err != nil { - return nil, fmt.Errorf("output %v: %w", k, err) - } - if outs[k], err = newOwnedOutput(iproc); err != nil { - return nil, fmt.Errorf("output %v: %w", k, err) - } - } - - return outs, nil -} diff --git a/public/service/config_output_test.go b/public/service/config_output_test.go deleted file mode 100644 index 15fa4644cf..0000000000 --- a/public/service/config_output_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigOutput(t *testing.T) { - tmpDir := t.TempDir() - - testFile := filepath.Join(tmpDir, "foo.txt") - - spec := NewConfigSpec(). - Field(NewOutputField("a")) - - parsedConfig, err := spec.ParseYAML(fmt.Sprintf(` -a: - file: - path: %v - codec: lines -`, testFile), nil) - require.NoError(t, err) - - output, err := parsedConfig.FieldOutput("a") - require.NoError(t, err) - - require.NoError(t, output.Write(context.Background(), NewMessage([]byte("first line")))) - require.NoError(t, output.WriteBatch(context.Background(), MessageBatch{ - NewMessage([]byte("second line")), - NewMessage([]byte("third line")), - })) - - require.NoError(t, output.Close(context.Background())) - - resultBytes, err := os.ReadFile(testFile) - require.NoError(t, err) - assert.Equal(t, "first line\nsecond line\nthird line\n", string(resultBytes)) -} - -func TestConfigOutputList(t *testing.T) { - tmpDir := t.TempDir() - - firstFile := filepath.Join(tmpDir, "foo.txt") - secondFile := filepath.Join(tmpDir, "bar.txt") - - spec := NewConfigSpec(). - Field(NewOutputListField("a")) - - parsedConfig, err := spec.ParseYAML(fmt.Sprintf(` -a: - - file: - path: %v - codec: lines - - file: - path: %v - codec: lines -`, firstFile, secondFile), nil) - require.NoError(t, err) - - outputs, err := parsedConfig.FieldOutputList("a") - require.NoError(t, err) - require.Len(t, outputs, 2) - - require.NoError(t, outputs[0].Write(context.Background(), NewMessage([]byte("first line")))) - require.NoError(t, outputs[1].Write(context.Background(), NewMessage([]byte("second line")))) - - require.NoError(t, outputs[0].Close(context.Background())) - require.NoError(t, outputs[1].Close(context.Background())) - - resultBytes, err := os.ReadFile(firstFile) - require.NoError(t, err) - assert.Equal(t, "first line\n", string(resultBytes)) - - resultBytes, err = os.ReadFile(secondFile) - require.NoError(t, err) - assert.Equal(t, "second line\n", string(resultBytes)) -} diff --git a/public/service/config_processor.go b/public/service/config_processor.go deleted file mode 100644 index 89b48e5c50..0000000000 --- a/public/service/config_processor.go +++ /dev/null @@ -1,93 +0,0 @@ -package service - -import ( - "fmt" - "strconv" - "strings" - - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// NewProcessorField defines a new processor field, it is then possible to -// extract an OwnedProcessor from the resulting parsed config with the method -// FieldProcessor. -func NewProcessorField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldProcessor(name, ""), - } -} - -// FieldProcessor accesses a field from a parsed config that was defined with -// NewProcessorField and returns an OwnedProcessor, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldProcessor(path ...string) (*OwnedProcessor, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - conf, err := processor.FromAny(p.mgr.Environment(), v) - if err != nil { - return nil, err - } - - iproc, err := p.mgr.IntoPath(path...).NewProcessor(conf) - if err != nil { - return nil, err - } - return &OwnedProcessor{iproc}, nil -} - -// NewProcessorListField defines a new processor list field, it is then possible -// to extract a list of OwnedProcessor from the resulting parsed config with the -// method FieldProcessorList. -func NewProcessorListField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldProcessor(name, "").Array(), - } -} - -func (p *ParsedConfig) fieldProcessorListConfigs(path ...string) ([]processor.Config, error) { - proc, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - procsArray, ok := proc.([]any) - if !ok { - return nil, fmt.Errorf("unexpected value, expected array, got %T", proc) - } - - var procConfigs []processor.Config - for i, iConf := range procsArray { - pconf, err := processor.FromAny(p.mgr.Environment(), iConf) - if err != nil { - return nil, fmt.Errorf("value %v: %w", i, err) - } - procConfigs = append(procConfigs, pconf) - } - return procConfigs, nil -} - -// FieldProcessorList accesses a field from a parsed config that was defined -// with NewProcessorListField and returns a slice of OwnedProcessor, or an error -// if the configuration was invalid. -func (p *ParsedConfig) FieldProcessorList(path ...string) ([]*OwnedProcessor, error) { - procConfigs, err := p.fieldProcessorListConfigs(path...) - if err != nil { - return nil, err - } - - tmpMgr := p.mgr.IntoPath(path...) - procs := make([]*OwnedProcessor, len(procConfigs)) - for i, c := range procConfigs { - iproc, err := tmpMgr.IntoPath(strconv.Itoa(i)).NewProcessor(c) - if err != nil { - return nil, fmt.Errorf("processor %v: %w", i, err) - } - procs[i] = &OwnedProcessor{iproc} - } - - return procs, nil -} diff --git a/public/service/config_processor_test.go b/public/service/config_processor_test.go deleted file mode 100644 index 5910e59125..0000000000 --- a/public/service/config_processor_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package service - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigProcessor(t *testing.T) { - spec := NewConfigSpec(). - Field(NewProcessorField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - bloblang: 'root = content().uppercase()' -`, nil) - require.NoError(t, err) - - proc, err := parsedConfig.FieldProcessor("a") - require.NoError(t, err) - - res, err := proc.Process(context.Background(), NewMessage([]byte("hello world"))) - require.NoError(t, err) - require.Len(t, res, 1) - - resBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(resBytes)) - - // Batch processing should work the same - resBatches, err := proc.ProcessBatch(context.Background(), MessageBatch{ - NewMessage([]byte("hello world")), - NewMessage([]byte("hello world two")), - }) - require.NoError(t, err) - require.Len(t, resBatches, 1) - require.Len(t, resBatches[0], 2) - - resBytes, err = resBatches[0][0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(resBytes)) - - resBytes, err = resBatches[0][1].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD TWO", string(resBytes)) - - require.NoError(t, proc.Close(context.Background())) -} - -func TestConfigProcessorList(t *testing.T) { - spec := NewConfigSpec(). - Field(NewProcessorListField("a")) - - parsedConfig, err := spec.ParseYAML(` -a: - - bloblang: 'root = content().uppercase()' - - bloblang: 'root = "foo: " + content()' -`, nil) - require.NoError(t, err) - - procs, err := parsedConfig.FieldProcessorList("a") - require.NoError(t, err) - require.Len(t, procs, 2) - - res, err := procs[0].Process(context.Background(), NewMessage([]byte("hello world"))) - require.NoError(t, err) - require.Len(t, res, 1) - - resBytes, err := res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "HELLO WORLD", string(resBytes)) - - res, err = procs[1].Process(context.Background(), NewMessage([]byte("hello world"))) - require.NoError(t, err) - require.Len(t, res, 1) - - resBytes, err = res[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo: hello world", string(resBytes)) - - require.NoError(t, procs[0].Close(context.Background())) - require.NoError(t, procs[1].Close(context.Background())) -} diff --git a/public/service/config_scanner.go b/public/service/config_scanner.go deleted file mode 100644 index 11cbe92597..0000000000 --- a/public/service/config_scanner.go +++ /dev/null @@ -1,43 +0,0 @@ -package service - -import ( - "fmt" - "strings" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// NewScannerField defines a new scanner field, it is then possible to extract -// an OwnedScannerCreator from the resulting parsed config with the method -// FieldScanner. -func NewScannerField(name string) *ConfigField { - return &ConfigField{ - field: docs.FieldScanner(name, ""), - } -} - -func ownedScannerCreatorFromConfAny(mgr bundle.NewManagement, field any) (*OwnedScannerCreator, error) { - pluginConf, err := scanner.FromAny(mgr.Environment(), field) - if err != nil { - return nil, err - } - - irdr, err := mgr.NewScanner(pluginConf) - if err != nil { - return nil, err - } - return &OwnedScannerCreator{rdr: irdr}, nil -} - -// FieldScanner accesses a field from a parsed config that was defined with -// NewScannerField and returns an OwnedScannerCreator, or an error if the -// configuration was invalid. -func (p *ParsedConfig) FieldScanner(path ...string) (*OwnedScannerCreator, error) { - field, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - return ownedScannerCreatorFromConfAny(p.mgr.IntoPath(path...), field) -} diff --git a/public/service/config_test.go b/public/service/config_test.go deleted file mode 100644 index 791f8582e2..0000000000 --- a/public/service/config_test.go +++ /dev/null @@ -1,434 +0,0 @@ -package service - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -func TestConfigGeneric(t *testing.T) { - spec := NewConfigSpec(). - Field(NewStringField("a")). - Field(NewIntField("b").Default(11)). - Field(NewObjectField("c", - NewBoolField("d").Default(true), - NewStringField("e").Default("evalue"), - )) - - tests := []struct { - name string - config string - lints []docs.Lint - sanitized string - }{ - { - name: "no fields except mandatory", - config: `a: foovalue`, - sanitized: `a: foovalue -b: 11 -c: - d: true - e: evalue -`, - }, - { - name: "fields set", - config: `a: newavalue -c: - d: false -`, - sanitized: `a: newavalue -b: 11 -c: - d: false - e: evalue -`, - }, - { - name: "fields set unrecognized field", - config: `a: newavalue -not_real: this doesnt exist in the spec -c: - d: false -`, - sanitized: `a: newavalue -b: 11 -c: - d: false - e: evalue -`, - lints: []docs.Lint{ - docs.NewLintError(2, docs.LintUnknown, errors.New("field not_real not recognised")), - }, - }, - { - name: "fields set nested unrecognized field", - config: `a: newavalue -c: - d: false - not_real: this doesnt exist in the spec -`, - sanitized: `a: newavalue -b: 11 -c: - d: false - e: evalue -`, - lints: []docs.Lint{ - docs.NewLintError(4, docs.LintUnknown, errors.New("field not_real not recognised")), - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - confBytes := []byte(test.config) - - node, err := NewStreamBuilder().getYAMLNode(confBytes) - require.NoError(t, err) - - assert.Equal(t, test.lints, spec.component.Config.Children.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), node)) - - pConf, err := spec.configFromAny(nil, node) - require.NoError(t, err) - - a, err := pConf.FieldAny() - require.NoError(t, err) - - var sanitNode yaml.Node - require.NoError(t, sanitNode.Encode(a)) - - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.RemoveDeprecated = true - - require.NoError(t, spec.component.Config.Children.SanitiseYAML(&sanitNode, sanitConf)) - - sanitConfOutBytes, err := yaml.Marshal(sanitNode) - require.NoError(t, err) - assert.Equal(t, test.sanitized, string(sanitConfOutBytes)) - }) - } -} - -func TestConfigTypedFields(t *testing.T) { - spec := NewConfigSpec(). - Field(NewStringField("a")). - Field(NewIntField("b").Default(11)). - Field(NewObjectField("c", - NewBoolField("d").Default(true), - NewStringField("e").Default("evalue"), - NewObjectField("f", - NewIntField("g").Default(12), - NewStringField("h"), - NewFloatField("i").Default(13.0), - NewStringListField("j"), - NewStringMapField("k"), - NewIntListField("l"), - NewIntMapField("m"), - ), - )) - - parsedConfig, err := spec.ParseYAML(` -a: setavalue -c: - f: - g: 22 - h: sethvalue - i: 23.1 - j: - - first in list - - second in list - k: - first: one - second: two - l: - - 11 - - 12 - m: - first: 21 - second: 22 -`, nil) - require.NoError(t, err) - - s, err := parsedConfig.FieldString("a") - assert.NoError(t, err) - assert.Equal(t, "setavalue", s) - - _, err = parsedConfig.FieldString("z") - assert.Error(t, err) - - _, err = parsedConfig.FieldInt("c", "z") - assert.Error(t, err) - - _, err = parsedConfig.FieldFloat("c", "d", "z") - assert.Error(t, err) - - _, err = parsedConfig.FieldBool("c", "z") - assert.Error(t, err) - - i, err := parsedConfig.FieldInt("b") - assert.NoError(t, err) - assert.Equal(t, 11, i) - - b, err := parsedConfig.FieldBool("c", "d") - assert.NoError(t, err) - assert.True(t, b) - - i, err = parsedConfig.FieldInt("c", "f", "g") - assert.NoError(t, err) - assert.Equal(t, 22, i) - - f, err := parsedConfig.FieldFloat("c", "f", "i") - assert.NoError(t, err) - assert.Equal(t, 23.1, f) - - ll, err := parsedConfig.FieldStringList("c", "f", "j") - assert.NoError(t, err) - assert.Equal(t, []string{"first in list", "second in list"}, ll) - - sm, err := parsedConfig.FieldStringMap("c", "f", "k") - assert.NoError(t, err) - assert.Equal(t, map[string]string{"first": "one", "second": "two"}, sm) - - il, err := parsedConfig.FieldIntList("c", "f", "l") - assert.NoError(t, err) - assert.Equal(t, []int{11, 12}, il) - - im, err := parsedConfig.FieldIntMap("c", "f", "m") - assert.NoError(t, err) - assert.Equal(t, map[string]int{"first": 21, "second": 22}, im) - - // Testing namespaces - nsC := parsedConfig.Namespace("c") - nsFOne := nsC.Namespace("f") - nsFTwo := parsedConfig.Namespace("c", "f") - - b, err = nsC.FieldBool("d") - assert.NoError(t, err) - assert.True(t, b) - - i, err = nsFOne.FieldInt("g") - assert.NoError(t, err) - assert.Equal(t, 22, i) - - f, err = nsFTwo.FieldFloat("i") - assert.NoError(t, err) - assert.Equal(t, 23.1, f) -} - -func TestConfigRootString(t *testing.T) { - spec := NewConfigSpec(). - Field(NewStringField("")) - - parsedConfig, err := spec.ParseYAML(`"hello world"`, nil) - require.NoError(t, err) - - v, err := parsedConfig.FieldString() - require.NoError(t, err) - - assert.Equal(t, "hello world", v) -} - -func TestConfigListOfObjects(t *testing.T) { - spec := NewConfigSpec(). - Field(NewObjectListField("objects", - NewStringField("foo"), - NewStringField("bar").Default("bar value"), - NewIntField("baz"), - )) - - _, err := spec.ParseYAML(`objects: -- foo: "foo value 1" - bar: "bar value 1" -`, nil) - require.Error(t, err) - - _, err = spec.ParseYAML(`objects: -- bar: "bar value 1" - baz: 11 -`, nil) - require.Error(t, err) - - _, err = spec.ParseYAML(`objects: []`, nil) - require.NoError(t, err) - - parsedConfig, err := spec.ParseYAML(`objects: -- foo: "foo value 1" - bar: "bar value 1" - baz: 11 - -- foo: "foo value 2" - bar: "bar value 2" - baz: 12 - -- foo: "foo value 3" - baz: 13 -`, nil) - require.NoError(t, err) - - objs, err := parsedConfig.FieldObjectList("objects") - require.NoError(t, err) - require.Len(t, objs, 3) - - strValue, err := objs[0].FieldString("foo") - require.NoError(t, err) - assert.Equal(t, "foo value 1", strValue) - - strValue, err = objs[0].FieldString("bar") - require.NoError(t, err) - assert.Equal(t, "bar value 1", strValue) - - intValue, err := objs[0].FieldInt("baz") - require.NoError(t, err) - assert.Equal(t, 11, intValue) - - strValue, err = objs[1].FieldString("foo") - require.NoError(t, err) - assert.Equal(t, "foo value 2", strValue) - - strValue, err = objs[1].FieldString("bar") - require.NoError(t, err) - assert.Equal(t, "bar value 2", strValue) - - intValue, err = objs[1].FieldInt("baz") - require.NoError(t, err) - assert.Equal(t, 12, intValue) - - strValue, err = objs[2].FieldString("foo") - require.NoError(t, err) - assert.Equal(t, "foo value 3", strValue) - - strValue, err = objs[2].FieldString("bar") - require.NoError(t, err) - assert.Equal(t, "bar value", strValue) - - intValue, err = objs[2].FieldInt("baz") - require.NoError(t, err) - assert.Equal(t, 13, intValue) -} - -func TestConfigTLS(t *testing.T) { - spec := NewConfigSpec(). - Field(NewTLSField("a")). - Field(NewStringField("b")) - - parsedConfig, err := spec.ParseYAML(` -a: - skip_cert_verify: true -b: and this -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldTLS("b") - require.Error(t, err) - - _, err = parsedConfig.FieldTLS("c") - require.Error(t, err) - - tConf, err := parsedConfig.FieldTLS("a") - require.NoError(t, err) - - assert.True(t, tConf.InsecureSkipVerify) -} - -func TestConfigInterpolatedString(t *testing.T) { - spec := NewConfigSpec(). - Field(NewInterpolatedStringField("a")). - Field(NewStringField("b")) - - parsedConfig, err := spec.ParseYAML(` -a: foo ${! content() } bar -b: this is ${! json( } an invalid interp string -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldInterpolatedString("b") - require.Error(t, err) - - _, err = parsedConfig.FieldInterpolatedString("c") - require.Error(t, err) - - iConf, err := parsedConfig.FieldInterpolatedString("a") - require.NoError(t, err) - - res, err := iConf.TryString(NewMessage([]byte("hello world"))) - require.NoError(t, err) - assert.Equal(t, "foo hello world bar", res) -} - -func TestConfigInterpolatedStringMap(t *testing.T) { - spec := NewConfigSpec(). - Field(NewInterpolatedStringMapField("a")). - Field(NewStringMapField("b")) - - parsedConfig, err := spec.ParseYAML(` -a: - c: foo ${! content() } bar - d: xyzzy ${! content() } baz -b: - e: this is ${! json( } an invalid interp string - f: this is another invalid interp string -`, nil) - require.NoError(t, err) - - _, err = parsedConfig.FieldInterpolatedStringMap("b") - require.Error(t, err) - - _, err = parsedConfig.FieldInterpolatedStringMap("g") - require.Error(t, err) - - iConf, err := parsedConfig.FieldInterpolatedStringMap("a") - require.NoError(t, err) - - res, err := iConf["c"].TryString(NewMessage([]byte("hello world"))) - require.NoError(t, err) - assert.Equal(t, "foo hello world bar", res) - - res, err = iConf["d"].TryString(NewMessage([]byte("hello world"))) - require.NoError(t, err) - assert.Equal(t, "xyzzy hello world baz", res) -} - -func TestConfigFields(t *testing.T) { - spec := NewConfigSpec(). - Fields( - NewStringField("a"), - NewIntField("b").Default(11), - NewObjectField("c", - NewBoolField("d").Default(true), - NewStringField("e").Default("evalue"), - ), - ) - - parsed, err := spec.ParseYAML(` - a: sample value - c: - d: false - `, nil) - require.NoError(t, err) - - a, err := parsed.FieldString("a") - require.NoError(t, err) - assert.Equal(t, "sample value", a) - - b, err := parsed.FieldInt("b") - require.NoError(t, err) - assert.Equal(t, 11, b) - - c := parsed.Namespace("c") - - d, err := c.FieldBool("d") - require.NoError(t, err) - assert.False(t, d) - - e, err := c.FieldString("e") - require.NoError(t, err) - assert.Equal(t, "evalue", e) -} diff --git a/public/service/config_tls.go b/public/service/config_tls.go deleted file mode 100644 index cfa00f2057..0000000000 --- a/public/service/config_tls.go +++ /dev/null @@ -1,91 +0,0 @@ -package service - -import ( - "crypto/tls" - "fmt" - "strings" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" - btls "github.com/benthosdev/benthos/v4/internal/tls" -) - -// NewTLSField defines a new object type config field that describes TLS -// settings for networked components. It is then possible to extract a -// *tls.Config from the resulting parsed config with the method FieldTLS. -func NewTLSField(name string) *ConfigField { - tf := btls.FieldSpec() - tf.Name = name - var newChildren []docs.FieldSpec - for _, f := range tf.Children { - if f.Name != "enabled" { - newChildren = append(newChildren, f) - } - } - tf.Children = newChildren - return &ConfigField{field: tf} -} - -// FieldTLS accesses a field from a parsed config that was defined with -// NewTLSField and returns a *tls.Config, or an error if the configuration was -// invalid. -func (p *ParsedConfig) FieldTLS(path ...string) (*tls.Config, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - var node yaml.Node - if err := node.Encode(v); err != nil { - return nil, err - } - - conf := btls.NewConfig() - if err := node.Decode(&conf); err != nil { - return nil, err - } - - return conf.GetNonToggled(p.mgr.FS()) -} - -// NewTLSToggledField defines a new object type config field that describes TLS -// settings for networked components. This field differs from a standard -// TLSField as it includes a boolean field `enabled` which allows users to -// explicitly configure whether TLS should be enabled or not. -// -// A *tls.Config as well as an enabled boolean value can be extracted from the -// resulting parsed config with the method FieldTLSToggled. -func NewTLSToggledField(name string) *ConfigField { - tf := btls.FieldSpec() - tf.Name = name - return &ConfigField{field: tf} -} - -// FieldTLSToggled accesses a field from a parsed config that was defined with -// NewTLSFieldToggled and returns a *tls.Config and a boolean flag indicating -// whether tls is explicitly enabled, or an error if the configuration was -// invalid. -func (p *ParsedConfig) FieldTLSToggled(path ...string) (tconf *tls.Config, enabled bool, err error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, false, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - var node yaml.Node - if err = node.Encode(v); err != nil { - return - } - - conf := btls.NewConfig() - if err = node.Decode(&conf); err != nil { - return - } - - if enabled = conf.Enabled; !enabled { - return - } - - tconf, err = conf.Get(p.mgr.FS()) - return -} diff --git a/public/service/config_url.go b/public/service/config_url.go deleted file mode 100644 index fc684c86dc..0000000000 --- a/public/service/config_url.go +++ /dev/null @@ -1,112 +0,0 @@ -package service - -import ( - "fmt" - "net/url" - "strconv" - "strings" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// NewURLField defines a new config field that describes a string that should -// contain a valid URL. It is then possible to extract either a string or a -// *url.URL from the resulting parsed config with the methods FieldString or -// FieldURL respectively. -func NewURLField(name string) *ConfigField { - tf := docs.FieldURL(name, "") - return &ConfigField{field: tf} -} - -// FieldURL accesses a field from a parsed config that was defined with -// NewURLField and returns either a *url.URL or an error if the string was -// invalid. -func (p *ParsedConfig) FieldURL(path ...string) (*url.URL, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - str, ok := v.(string) - if !ok { - return nil, fmt.Errorf("expected field '%v' to be a string, got %T", strings.Join(path, "."), v) - } - - u, err := url.Parse(str) - if err != nil { - return nil, fmt.Errorf("failed to parse url field '%v': %v", strings.Join(path, "."), err) - } - - return u, nil -} - -// NewURLListField defines a new config field that describes an array of strings -// that should contain only valid URLs. It is then possible to extract either a -// string slice or a slice of *url.URL from the resulting parsed config with the -// methods FieldStringArray or FieldURLArray respectively. -func NewURLListField(name string) *ConfigField { - tf := docs.FieldURL(name, "").Array() - return &ConfigField{field: tf} -} - -func urlsFromStr(str string) (urls []*url.URL, err error) { - for _, s := range strings.Split(str, ",") { - if s = strings.TrimSpace(s); s == "" { - continue - } - var u *url.URL - if u, err = url.Parse(s); err != nil { - return - } - urls = append(urls, u) - } - return -} - -// FieldURLList accesses a field from a parsed config that was defined with -// NewURLListField and returns either a []*url.URL or an error if one or more -// strings were invalid. -func (p *ParsedConfig) FieldURLList(path ...string) ([]*url.URL, error) { - v, exists := p.i.Field(path...) - if !exists { - return nil, fmt.Errorf("field '%v' was not found in the config", strings.Join(path, ".")) - } - - iList, ok := v.([]any) - if !ok { - switch t := v.(type) { - case []*url.URL: - return t, nil - case []string: - uList := make([]*url.URL, 0, len(t)) - for i, s := range t { - urls, err := urlsFromStr(s) - if err != nil { - return nil, fmt.Errorf("failed to parse url field '%v': %v", strings.Join(path, ".")+"."+strconv.Itoa(i), err) - } - uList = append(uList, urls...) - } - return uList, nil - default: - return nil, fmt.Errorf("expected field '%v' to be a string list, got %T", p.i.FullDotPath(path...), v) - } - } - - uList := make([]*url.URL, 0, len(iList)) - for i, ev := range iList { - switch t := ev.(type) { - case *url.URL: - uList[i] = t - case string: - urls, err := urlsFromStr(t) - if err != nil { - return nil, fmt.Errorf("failed to parse url field '%v': %v", strings.Join(path, ".")+"."+strconv.Itoa(i), err) - } - uList = append(uList, urls...) - default: - return nil, fmt.Errorf("expected field '%v' to be a string, got %T", strings.Join(path, ".")+"."+strconv.Itoa(i), v) - } - } - - return uList, nil -} diff --git a/public/service/config_urls_test.go b/public/service/config_urls_test.go deleted file mode 100644 index 28e158b39f..0000000000 --- a/public/service/config_urls_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package service - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestURLListField(t *testing.T) { - mustURL := func(s string) *url.URL { - t.Helper() - u, err := url.Parse(s) - require.NoError(t, err) - return u - } - - tests := []struct { - name string - input string - output []*url.URL - errContains string - }{ - { - name: "no urls", - input: `[]`, - output: []*url.URL{}, - }, - { - name: "one valid url", - input: `- https://example.com`, - output: []*url.URL{ - mustURL("https://example.com"), - }, - }, - { - name: "multiple urls", - input: ` -- https://example.com/foo -- https://example.com/bar -`, - output: []*url.URL{ - mustURL("https://example.com/foo"), - mustURL("https://example.com/bar"), - }, - }, - { - name: "multiple urls some csvs", - input: ` -- https://example.com/foo,https://example.com/bar -- https://example.com/baz -- ",https://example.com/buz," -`, - output: []*url.URL{ - mustURL("https://example.com/foo"), - mustURL("https://example.com/bar"), - mustURL("https://example.com/baz"), - mustURL("https://example.com/buz"), - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - spec := NewConfigSpec().Field(NewStringListField("")) - - parsed, err := spec.ParseYAML(test.input, nil) - require.NoError(t, err) - - us, err := parsed.FieldURLList() - if test.errContains == "" { - require.NoError(t, err) - assert.Equal(t, test.output, us) - } else { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } - }) - } -} diff --git a/public/service/config_util.go b/public/service/config_util.go deleted file mode 100644 index 5a82fc800a..0000000000 --- a/public/service/config_util.go +++ /dev/null @@ -1,43 +0,0 @@ -package service - -import ( - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/value" -) - -type fieldUnwrapper struct { - child docs.FieldSpec -} - -func (f fieldUnwrapper) Unwrap() docs.FieldSpec { - return f.child -} - -// XUnwrapper is for internal use only, do not use this. -func (c *ConfigField) XUnwrapper() any { - return fieldUnwrapper{child: c.field} -} - -func extractConfig( - nm bundle.NewManagement, - spec *ConfigSpec, - componentName string, - pluginConfig any, -) (*ParsedConfig, error) { - // All nested fields are under the namespace of the component type, and - // therefore we need to namespace the manager such that metrics and logs - // from nested core component types are corrected labelled. - if nm != nil { - nm = nm.IntoPath(componentName) - } - - if pluginConfig == nil { - if spec.component.Config.Default != nil { - pluginConfig = value.IClone(*spec.component.Config.Default) - } else if len(spec.component.Config.Children) > 0 { - pluginConfig = map[string]any{} - } - } - return spec.configFromAny(nm, pluginConfig) -} diff --git a/public/service/environment.go b/public/service/environment.go deleted file mode 100644 index cbe2fd4070..0000000000 --- a/public/service/environment.go +++ /dev/null @@ -1,689 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - - "go.opentelemetry.io/otel/trace" - - ibloblang "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - iprocessors "github.com/benthosdev/benthos/v4/internal/component/input/processors" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/output/batcher" - oprocessors "github.com/benthosdev/benthos/v4/internal/component/output/processors" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/component/tracer" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/template" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// Environment is a collection of Benthos component plugins that can be used in -// order to build and run streaming pipelines with access to different sets of -// plugins. This can be useful for sandboxing, testing, etc, but most plugin -// authors do not need to create an Environment and can simply use the global -// environment. -type Environment struct { - internal *bundle.Environment - bloblangEnv *bloblang.Environment - fs ifs.FS -} - -var globalEnvironment = &Environment{ - internal: bundle.GlobalEnvironment, - bloblangEnv: bloblang.GlobalEnvironment(), - fs: ifs.OS(), -} - -// GlobalEnvironment returns a reference to the global environment, adding -// plugins to this environment is the equivalent to adding plugins using global -// Functions. -func GlobalEnvironment() *Environment { - return globalEnvironment -} - -// NewEnvironment creates a new environment that inherits all globally defined -// plugins, but can have plugins defined on it that are isolated. -func NewEnvironment() *Environment { - return globalEnvironment.Clone() -} - -// NewEmptyEnvironment creates a new environment with zero registered plugins. -func NewEmptyEnvironment() *Environment { - return &Environment{ - internal: bundle.NewEnvironment(), - bloblangEnv: bloblang.NewEmptyEnvironment(), - fs: ifs.OS(), - } -} - -// Clone an environment, creating a new environment containing the same plugins -// that can be modified independently of the source. -func (e *Environment) Clone() *Environment { - return &Environment{ - internal: e.internal.Clone(), - bloblangEnv: e.bloblangEnv.WithoutFunctions().WithoutMethods(), - fs: e.fs, - } -} - -// UseBloblangEnvironment configures the service environment to restrict -// components constructed with it to a specific Bloblang environment. -func (e *Environment) UseBloblangEnvironment(bEnv *bloblang.Environment) { - e.bloblangEnv = bEnv -} - -// UseFS configures the service environment to use an instantiation of *FS as -// its filesystem. This provides extra control over the file access of all -// Benthos components within the stream. However, this functionality is opt-in -// and there is no guarantee that plugin implementations will use this method -// of file access. -// -// The underlying bloblang environment will also be configured to import -// mappings and other files via this file access method. In order to avoid this -// behaviour add a fresh bloblang environment via UseBloblangEnvironment _after_ -// setting this file access. -func (e *Environment) UseFS(fs *FS) { - e.fs = fs - e.bloblangEnv = e.bloblangEnv.WithCustomImporter(func(name string) ([]byte, error) { - return ifs.ReadFile(fs, name) - }) -} - -// NewStreamBuilder creates a new StreamBuilder upon the defined environment, -// only components known to this environment will be available to the stream -// builder. -func (e *Environment) NewStreamBuilder() *StreamBuilder { - sb := NewStreamBuilder() - sb.env = e - return sb -} - -//------------------------------------------------------------------------------ - -func (e *Environment) getBloblangParserEnv() *ibloblang.Environment { - if unwrapper, ok := e.bloblangEnv.XUnwrapper().(interface { - Unwrap() *ibloblang.Environment - }); ok { - return unwrapper.Unwrap() - } - return ibloblang.GlobalEnvironment() -} - -//------------------------------------------------------------------------------ - -// RegisterBatchBuffer attempts to register a new buffer plugin by providing a -// description of the configuration for the buffer and a constructor for the -// buffer processor. The constructor will be called for each instantiation of -// the component within a config. -// -// Consumed message batches must be created by upstream components (inputs, etc) -// otherwise this buffer will simply receive batches containing single -// messages. -func (e *Environment) RegisterBatchBuffer(name string, spec *ConfigSpec, ctor BatchBufferConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeBuffer - return e.internal.BufferAdd(func(conf buffer.Config, nm bundle.NewManagement) (buffer.Streamed, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - b, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - return buffer.NewStream(conf.Type, newAirGapBatchBuffer(b), nm), nil - }, componentSpec) -} - -// WalkBuffers executes a provided function argument for every buffer component -// that has been registered to the environment. -func (e *Environment) WalkBuffers(fn func(name string, config *ConfigView)) { - for _, v := range e.internal.BufferDocs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetBufferConfig attempts to obtain a buffer configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetBufferConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllBuffers.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterCache attempts to register a new cache plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the cache itself. The constructor will be called for each instantiation of -// the component within a config. -func (e *Environment) RegisterCache(name string, spec *ConfigSpec, ctor CacheConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeCache - return e.internal.CacheAdd(func(conf cache.Config, nm bundle.NewManagement) (cache.V1, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - c, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - return newAirGapCache(c, nm.Metrics()), nil - }, componentSpec) -} - -// WalkCaches executes a provided function argument for every cache component -// that has been registered to the environment. -func (e *Environment) WalkCaches(fn func(name string, config *ConfigView)) { - for _, v := range e.internal.CacheDocs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetCacheConfig attempts to obtain a cache configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetCacheConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllCaches.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterInput attempts to register a new input plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the input itself. The constructor will be called for each instantiation of -// the component within a config. -// -// If your input implementation doesn't have a specific mechanism for dealing -// with a nack (when the AckFunc provides a non-nil error) then you can instead -// wrap your input implementation with AutoRetryNacks to get automatic retries. -func (e *Environment) RegisterInput(name string, spec *ConfigSpec, ctor InputConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeInput - return e.internal.InputAdd(iprocessors.WrapConstructor(func(conf input.Config, nm bundle.NewManagement) (input.Streamed, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - i, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - rdr := newAirGapReader(i) - return input.NewAsyncReader(conf.Type, rdr, nm) - }), componentSpec) -} - -// RegisterBatchInput attempts to register a new batched input plugin by -// providing a description of the configuration for the plugin as well as a -// constructor for the input itself. The constructor will be called for each -// instantiation of the component within a config. -// -// If your input implementation doesn't have a specific mechanism for dealing -// with a nack (when the AckFunc provides a non-nil error) then you can instead -// wrap your input implementation with AutoRetryNacksBatched to get automatic -// retries. -func (e *Environment) RegisterBatchInput(name string, spec *ConfigSpec, ctor BatchInputConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeInput - return e.internal.InputAdd(iprocessors.WrapConstructor(func(conf input.Config, nm bundle.NewManagement) (input.Streamed, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - i, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - if u, ok := i.(interface { - Unwrap() input.Streamed - }); ok { - return u.Unwrap(), nil - } - rdr := newAirGapBatchReader(i) - return input.NewAsyncReader(conf.Type, rdr, nm) - }), componentSpec) -} - -// WalkInputs executes a provided function argument for every input component -// that has been registered to the environment. -func (e *Environment) WalkInputs(fn func(name string, config *ConfigView)) { - for _, v := range e.internal.InputDocs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetInputConfig attempts to obtain an input configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetInputConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllInputs.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterOutput attempts to register a new output plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the output itself. The constructor will be called for each instantiation of -// the component within a config. -func (e *Environment) RegisterOutput(name string, spec *ConfigSpec, ctor OutputConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeOutput - return e.internal.OutputAdd(oprocessors.WrapConstructor( - func(conf output.Config, nm bundle.NewManagement) (output.Streamed, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - op, maxInFlight, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - if maxInFlight < 1 { - return nil, fmt.Errorf("invalid maxInFlight parameter: %v", maxInFlight) - } - w := newAirGapWriter(op) - o, err := output.NewAsyncWriter(conf.Type, maxInFlight, w, nm) - if err != nil { - return nil, err - } - return output.OnlySinglePayloads(o), nil - }, - ), componentSpec) -} - -// RegisterBatchOutput attempts to register a new output plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the output itself. The constructor will be called for each instantiation of -// the component within a config. -// -// The constructor of a batch output is able to return a batch policy to be -// applied before calls to write are made, creating batches from the stream of -// messages. However, batches can also be created by upstream components -// (inputs, buffers, etc). -// -// If a batch has been formed upstream it is possible that its size may exceed -// the policy specified in your constructor. -func (e *Environment) RegisterBatchOutput(name string, spec *ConfigSpec, ctor BatchOutputConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeOutput - return e.internal.OutputAdd(oprocessors.WrapConstructor( - func(conf output.Config, nm bundle.NewManagement) (output.Streamed, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - op, batchPolicy, maxInFlight, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - if u, ok := op.(interface { - Unwrap() output.Streamed - }); ok { - return u.Unwrap(), nil - } - - if maxInFlight < 1 { - return nil, fmt.Errorf("invalid maxInFlight parameter: %v", maxInFlight) - } - - w := newAirGapBatchWriter(op) - o, err := output.NewAsyncWriter(conf.Type, maxInFlight, w, nm) - if err != nil { - return nil, err - } - return batcher.NewFromConfig(batchPolicy.toInternal(), o, nm) - }, - ), componentSpec) -} - -// WalkOutputs executes a provided function argument for every output component -// that has been registered to the environment. -func (e *Environment) WalkOutputs(fn func(name string, config *ConfigView)) { - for _, v := range e.internal.OutputDocs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetOutputConfig attempts to obtain an output configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetOutputConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllOutputs.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterProcessor attempts to register a new processor plugin by providing -// a description of the configuration for the processor and a constructor for -// the processor itself. The constructor will be called for each instantiation -// of the component within a config. -// -// For simple transformations consider implementing a Bloblang plugin method -// instead. -func (e *Environment) RegisterProcessor(name string, spec *ConfigSpec, ctor ProcessorConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeProcessor - return e.internal.ProcessorAdd(func(conf processor.Config, nm bundle.NewManagement) (processor.V1, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - r, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - return newAirGapProcessor(conf.Type, r, nm), nil - }, componentSpec) -} - -// RegisterBatchProcessor attempts to register a new processor plugin by -// providing a description of the configuration for the processor and a -// constructor for the processor itself. The constructor will be called for each -// instantiation of the component within a config. -// -// Message batches must be created by upstream components (inputs, buffers, etc) -// otherwise this processor will simply receive batches containing single -// messages. -func (e *Environment) RegisterBatchProcessor(name string, spec *ConfigSpec, ctor BatchProcessorConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeProcessor - return e.internal.ProcessorAdd(func(conf processor.Config, nm bundle.NewManagement) (processor.V1, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - r, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - if u, ok := r.(interface { - Unwrap() processor.V1 - }); ok { - return u.Unwrap(), nil - } - return newAirGapBatchProcessor(conf.Type, r, nm), nil - }, componentSpec) -} - -// WalkProcessors executes a provided function argument for every processor -// component that has been registered to the environment. -func (e *Environment) WalkProcessors(fn func(name string, config *ConfigView)) { - for _, v := range e.internal.ProcessorDocs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetProcessorConfig attempts to obtain a processor configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetProcessorConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllProcessors.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterRateLimit attempts to register a new rate limit plugin by providing -// a description of the configuration for the plugin as well as a constructor -// for the rate limit itself. The constructor will be called for each -// instantiation of the component within a config. -func (e *Environment) RegisterRateLimit(name string, spec *ConfigSpec, ctor RateLimitConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeRateLimit - return e.internal.RateLimitAdd(func(conf ratelimit.Config, nm bundle.NewManagement) (ratelimit.V1, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - r, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - return newAirGapRateLimit(r, nm.Metrics()), nil - }, componentSpec) -} - -// WalkRateLimits executes a provided function argument for every rate limit -// component that has been registered to the environment. -func (e *Environment) WalkRateLimits(fn func(name string, config *ConfigView)) { - for _, v := range e.internal.RateLimitDocs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetRateLimitConfig attempts to obtain a rate limit configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetRateLimitConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllRateLimits.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterMetricsExporter attempts to register a new metrics exporter plugin by -// providing a description of the configuration for the plugin as well as a -// constructor for the metrics exporter itself. -func (e *Environment) RegisterMetricsExporter(name string, spec *ConfigSpec, ctor MetricsExporterConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeMetrics - return e.internal.MetricsAdd(func(conf metrics.Config, nm bundle.NewManagement) (metrics.Type, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - m, err := ctor(pluginConf, newReverseAirGapLogger(nm.Logger())) - if err != nil { - return nil, err - } - return newAirGapMetrics(m), nil - }, componentSpec) -} - -// WalkMetrics executes a provided function argument for every metrics component -// that has been registered to the environment. Note that metrics components -// available to an environment cannot be modified. -func (e *Environment) WalkMetrics(fn func(name string, config *ConfigView)) { - for _, v := range bundle.AllMetrics.Docs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetMetricsConfig attempts to obtain a metrics exporter configuration spec by -// the component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetMetricsConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllMetrics.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterOtelTracerProvider attempts to register a new open telemetry tracer -// provider plugin by providing a description of the configuration for the -// plugin as well as a constructor for the metrics exporter itself. The -// constructor will be called for each instantiation of the component within a -// config. -// -// Experimental: This type signature is experimental and therefore subject to -// change outside of major version releases. -func (e *Environment) RegisterOtelTracerProvider(name string, spec *ConfigSpec, ctor OtelTracerProviderConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeTracer - return e.internal.TracersAdd(func(conf tracer.Config, nm bundle.NewManagement) (trace.TracerProvider, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - t, err := ctor(pluginConf) - if err != nil { - return nil, err - } - return t, nil - }, componentSpec) -} - -// WalkTracers executes a provided function argument for every tracer component -// that has been registered to the environment. Note that tracer components -// available to an environment cannot be modified. -func (e *Environment) WalkTracers(fn func(name string, config *ConfigView)) { - for _, v := range bundle.AllTracers.Docs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetTracerConfig attempts to obtain a tracer configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetTracerConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllTracers.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterBatchScannerCreator attempts to register a new batched scanner plugin -// by providing a description of the configuration for the plugin as well as a -// constructor for the scanner creator itself. The constructor will be called -// for each instantiation of the component within a config. -func (e *Environment) RegisterBatchScannerCreator(name string, spec *ConfigSpec, ctor BatchScannerCreatorConstructor) error { - componentSpec := spec.component - componentSpec.Name = name - componentSpec.Type = docs.TypeScanner - return e.internal.ScannerAdd(func(conf scanner.Config, nm bundle.NewManagement) (scanner.Creator, error) { - pluginConf, err := extractConfig(nm, spec, name, conf.Plugin) - if err != nil { - return nil, err - } - c, err := ctor(pluginConf, newResourcesFromManager(nm)) - if err != nil { - return nil, err - } - return newAirGapBatchScannerCreator(c), nil - }, componentSpec) -} - -// WalkScanners executes a provided function argument for every scanner -// component that has been registered to the environment. Note that scanner -// components available to an environment cannot be modified. -func (e *Environment) WalkScanners(fn func(name string, config *ConfigView)) { - for _, v := range bundle.AllScanners.Docs() { - fn(v.Name, &ConfigView{ - prov: e.internal, - component: v, - }) - } -} - -// GetScannerConfig attempts to obtain a scanner configuration spec by the -// component name. Returns a nil ConfigView and false if the component is -// unknown. -func (e *Environment) GetScannerConfig(name string) (*ConfigView, bool) { - c, exists := bundle.AllScanners.DocsFor(name) - if !exists { - return nil, false - } - return &ConfigView{ - prov: e.internal, - component: c, - }, true -} - -// RegisterTemplateYAML attempts to register a template, defined as a YAML -// document, to the environment such that it may be used similarly to any other -// component plugin. -func (e *Environment) RegisterTemplateYAML(yamlStr string) error { - return template.RegisterTemplateYAML(e.internal, []byte(yamlStr)) -} - -// XFormatConfigJSON returns a byte slice of the Benthos configuration spec -// formatted as a JSON object. The schema of this method is undocumented and is -// not intended for general use. -// -// Experimental: This method is not intended for general use and could have its -// signature and/or behaviour changed outside of major version bumps. -func XFormatConfigJSON() ([]byte, error) { - return json.Marshal(config.Spec()) -} diff --git a/public/service/environment_schema.go b/public/service/environment_schema.go deleted file mode 100644 index 3cbac92f59..0000000000 --- a/public/service/environment_schema.go +++ /dev/null @@ -1,44 +0,0 @@ -package service - -import ( - "github.com/benthosdev/benthos/v4/internal/config/schema" - "github.com/benthosdev/benthos/v4/internal/cuegen" -) - -// EnvironmentSchema represents a schema definition for all components -// registered within the environment. -type EnvironmentSchema struct { - s schema.Full -} - -func (e *Environment) GenerateSchema(version, dateBuilt string) *EnvironmentSchema { - schema := schema.New(version, dateBuilt) - return &EnvironmentSchema{s: schema} -} - -// ReduceToStatus removes all components that aren't of the given stability -// status. -func (e *EnvironmentSchema) ReduceToStatus(status string) *EnvironmentSchema { - e.s.ReduceToStatus(status) - return e -} - -// Minimise removes all documentation from the schema definition. -func (e *EnvironmentSchema) Minimise() *EnvironmentSchema { - e.s.Scrub() - return e -} - -// ToCUE attempts to generate a CUE schema. -func (e *EnvironmentSchema) ToCUE() ([]byte, error) { - return cuegen.GenerateSchema(e.s) -} - -// XFlattened returns a generic structure of the schema as a map of component -// types to component names. -// -// Experimental: This method is experimental and therefore could be changed -// outside of major version releases. -func (e *EnvironmentSchema) XFlattened() map[string][]string { - return e.s.Flattened() -} diff --git a/public/service/environment_test.go b/public/service/environment_test.go deleted file mode 100644 index 2129b63ce2..0000000000 --- a/public/service/environment_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package service_test - -import ( - "context" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "testing" - "testing/fstest" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func walkForSummaries(fn func(func(name string, config *service.ConfigView))) map[string]string { - summaries := map[string]string{} - fn(func(name string, config *service.ConfigView) { - summaries[name] = config.Summary() - }) - return summaries -} - -func TestEnvironmentAdjustments(t *testing.T) { - envOne := service.NewEnvironment() - envTwo := envOne.Clone() - - assert.NoError(t, envOne.RegisterCache( - "one_cache", service.NewConfigSpec().Summary("cache one"), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - return nil, errors.New("cache one err") - }, - )) - assert.NoError(t, envOne.RegisterInput( - "one_input", service.NewConfigSpec().Summary("input one"), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("input one err") - }, - )) - assert.NoError(t, envOne.RegisterOutput( - "one_output", service.NewConfigSpec().Summary("output one"), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Output, int, error) { - return nil, 0, errors.New("output one err") - }, - )) - assert.NoError(t, envOne.RegisterProcessor( - "one_processor", service.NewConfigSpec().Summary("processor one"), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return nil, errors.New("processor one err") - }, - )) - assert.NoError(t, envOne.RegisterRateLimit( - "one_rate_limit", service.NewConfigSpec().Summary("rate limit one"), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.RateLimit, error) { - return nil, errors.New("rate limit one err") - }, - )) - - assert.Equal(t, "cache one", walkForSummaries(envOne.WalkCaches)["one_cache"]) - assert.Equal(t, "input one", walkForSummaries(envOne.WalkInputs)["one_input"]) - assert.Equal(t, "output one", walkForSummaries(envOne.WalkOutputs)["one_output"]) - assert.Equal(t, "processor one", walkForSummaries(envOne.WalkProcessors)["one_processor"]) - assert.Equal(t, "rate limit one", walkForSummaries(envOne.WalkRateLimits)["one_rate_limit"]) - - assert.NotContains(t, walkForSummaries(envTwo.WalkCaches), "one_cache") - assert.NotContains(t, walkForSummaries(envTwo.WalkInputs), "one_input") - assert.NotContains(t, walkForSummaries(envTwo.WalkOutputs), "one_output") - assert.NotContains(t, walkForSummaries(envTwo.WalkProcessors), "one_processor") - assert.NotContains(t, walkForSummaries(envTwo.WalkRateLimits), "one_rate_limit") - - testConfig := ` -input: - one_input: {} -pipeline: - processors: - - one_processor: {} -output: - one_output: {} -cache_resources: - - label: foocache - one_cache: {} -rate_limit_resources: - - label: foorl - one_rate_limit: {} -` - - assert.NoError(t, envOne.NewStreamBuilder().SetYAML(testConfig)) - assert.Error(t, envTwo.NewStreamBuilder().SetYAML(testConfig)) -} - -func TestEnvironmentBloblangIsolation(t *testing.T) { - bEnv := bloblang.NewEnvironment().WithoutFunctions("now") - require.NoError(t, bEnv.RegisterFunctionV2("meow", bloblang.NewPluginSpec(), func(args *bloblang.ParsedParams) (bloblang.Function, error) { - return func() (any, error) { - return "meow", nil - }, nil - })) - - envOne := service.NewEnvironment() - envOne.UseBloblangEnvironment(bEnv) - - badConfig := ` -pipeline: - processors: - - bloblang: 'root = now()' -` - - goodConfig := ` -pipeline: - processors: - - bloblang: 'root = meow()' - -output: - drop: {} - -logger: - level: OFF -` - - assert.Error(t, envOne.NewStreamBuilder().SetYAML(badConfig)) - - strmBuilder := envOne.NewStreamBuilder() - require.NoError(t, strmBuilder.SetYAML(goodConfig)) - - var received []string - require.NoError(t, strmBuilder.AddConsumerFunc(func(c context.Context, m *service.Message) error { - b, err := m.AsBytes() - if err != nil { - return err - } - received = append(received, string(b)) - return nil - })) - - pFn, err := strmBuilder.AddProducerFunc() - require.NoError(t, err) - - strm, err := strmBuilder.Build() - require.NoError(t, err) - - go func() { - require.NoError(t, strm.Run(context.Background())) - }() - - require.NoError(t, pFn(context.Background(), service.NewMessage([]byte("hello world")))) - - require.NoError(t, strm.StopWithin(time.Second)) - assert.Equal(t, []string{"meow"}, received) -} - -type testFS struct { - ifs.FS - override fstest.MapFS -} - -func (fs testFS) Open(name string) (fs.File, error) { - if f, err := fs.override.Open(name); err == nil { - return f, nil - } - - return fs.FS.Open(name) -} - -func (fs testFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - if f, err := fs.override.Open(name); err == nil { - return f, nil - } - - return fs.FS.OpenFile(name, flag, perm) -} - -func (fs testFS) Stat(name string) (fs.FileInfo, error) { - if f, err := fs.override.Stat(name); err == nil { - return f, nil - } - - return fs.FS.Stat(name) -} - -func TestEnvironmentUseFS(t *testing.T) { - tmpDir := t.TempDir() - outFilePath := filepath.Join(tmpDir, "out.txt") - - env := service.NewEnvironment() - env.UseFS(service.NewFS(testFS{ifs.OS(), fstest.MapFS{ - "hello.txt": { - Data: []byte("hello\nworld"), - }, - }})) - - b := env.NewStreamBuilder() - - require.NoError(t, b.SetYAML(fmt.Sprintf(` -input: - file: - paths: [hello.txt] - -output: - label: foo - file: - codec: lines - path: %v -`, outFilePath))) - - strm, err := b.Build() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - - outBytes, err := os.ReadFile(outFilePath) - require.NoError(t, err) - - assert.Equal(t, `hello -world -`, string(outBytes)) -} diff --git a/public/service/errors.go b/public/service/errors.go deleted file mode 100644 index a03d045259..0000000000 --- a/public/service/errors.go +++ /dev/null @@ -1,201 +0,0 @@ -package service - -import ( - "errors" - "time" - - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -var ( - // ErrNotConnected is returned by inputs and outputs when their Read or - // Write methods are called and the connection that they maintain is lost. - // This error prompts the upstream component to call Connect until the - // connection is re-established. - ErrNotConnected = errors.New("not connected") - - // ErrEndOfInput is returned by inputs that have exhausted their source of - // data to the point where subsequent Read calls will be ineffective. This - // error prompts the upstream component to gracefully terminate the - // pipeline. - ErrEndOfInput = errors.New("end of input") - - // ErrEndOfBuffer is returned by a buffer Read/ReadBatch method when the - // contents of the buffer has been emptied and the source of the data is - // ended (as indicated by EndOfInput). This error prompts the upstream - // component to gracefully terminate the pipeline. - ErrEndOfBuffer = errors.New("end of buffer") -) - -// ErrBackOff is an error that plugins can optionally wrap another error with -// which instructs upstream components to wait for a specified period of time -// before retrying the errored call. -// -// Not all plugin methods support this error, for a list refer to the -// documentation of NewErrBackOff. -type ErrBackOff struct { - Err error - Wait time.Duration -} - -// NewErrBackOff wraps an error with a specified time to wait. For specific -// plugin methods this will instruct upstream components to wait by the -// specified amount of time before re-attempting the errored call. -// -// NOTE: ErrBackOff is opt-in for upstream components and therefore only a -// subset of plugin calls will respect this error. Currently the following -// methods are known to support ErrBackOff: -// -// - Input.Connect -// - BatchInput.Connect -// - Output.Connect -// - BatchOutput.Connect -func NewErrBackOff(err error, wait time.Duration) *ErrBackOff { - return &ErrBackOff{err, wait} -} - -// Error returns the Error string. -func (e *ErrBackOff) Error() string { - return e.Err.Error() -} - -// BatchError groups the errors that were encountered while processing a -// collection (usually a batch) of messages and provides methods to iterate -// over these errors. -type BatchError struct { - wrapped *batch.Error -} - -// NewBatchError creates a BatchError that can be returned by batched outputs. -// The advantage of doing so is that nacks and retries can potentially be -// granularly dealt out in cases where only a subset of the batch failed. -// -// A headline error must be supplied which will be exposed when upstream -// components do not support granular batch errors. -func NewBatchError(b MessageBatch, headline error) *BatchError { - ib := make(message.Batch, len(b)) - for i, m := range b { - ib[i] = m.part - } - batchErr := batch.NewError(ib, headline) - return &BatchError{wrapped: batchErr} -} - -// Failed stores an error state for a particular message of a batch. Returns a -// pointer to the underlying error, allowing the method to be chained. -// -// If Failed is not called then all messages are assumed to have failed. If it -// is called at least once then all message indexes that aren't explicitly -// failed are assumed to have been processed successfully. -func (err *BatchError) Failed(i int, merr error) *BatchError { - _ = err.wrapped.Failed(i, merr) - return err -} - -// WalkMessagesIndexedBy applies a closure to each message of a batch that is -// included in this batch error. A batch error represents errors that occurred -// to only a subset of a batch of messages, in which case it is possible to use -// this error in order to avoid re-processing or re-delivering messages that -// didn't fail. -// -// However, the shape of the batch of messages at the time the errors occurred -// may differ significantly from the batch known by the component receiving this -// error. For example a processor that dispatches a batch to a list of child -// processors may receive a batch error that occurred after filtering and -// re-ordering has occurred to the batch. In such cases it is not possible to -// simply inspect the indexes of errored messages in order to associate them -// with the original batch as those indexes could have changed. -// -// Therefore, in order to solve this particular use case it is possible to -// create a batch indexer before dispatching the batch to the child components. -// Then, when a batch error is received WalkMessagesIndexedBy can be used as way -// to walk the errored messages with the indexes (and message contents) of the -// original batch. -// -// Important! The order of messages walked is not guaranteed to match that of -// the source batch. It is also possible for any given index to be represented -// zero, one or more times. -func (err *BatchError) WalkMessagesIndexedBy(s *Indexer, fn func(int, *Message, error) bool) { - parts := make(message.Batch, len(s.sourceBatch)) - for i, m := range s.sourceBatch { - parts[i] = m.part - } - err.wrapped.WalkPartsBySource(s.wrapped, parts, func(i int, p *message.Part, err error) bool { - var m *Message - if i >= 0 && i < len(s.sourceBatch) { - m = s.sourceBatch[i] - } else { - m = &Message{part: p} - } - return fn(i, m, err) - }) -} - -// WalkMessages applies a closure to each message that was part of the request -// that caused this error. The closure is provided the message index, a pointer -// to the message, and its individual error, which may be nil if the message -// itself was processed successfully. The closure should return a bool which -// indicates whether the iteration should be continued. -// -// Deprecated: This method is harmful and should be avoided as indexes are not -// guaranteed to match a hypothetical origin batch that they might be compared -// against. Use WalkMessagesIndexedBy instead. -func (err *BatchError) WalkMessages(fn func(int, *Message, error) bool) { - err.wrapped.WalkPartsNaively(func(i int, p *message.Part, err error) bool { - return fn(i, &Message{part: p}, err) - }) -} - -// IndexedErrors returns the number of indexed errors that have been registered -// within a walkable error. -func (err *BatchError) IndexedErrors() int { - return err.wrapped.IndexedErrors() -} - -// Error returns the underlying error message -func (err *BatchError) Error() string { - return err.wrapped.Error() -} - -func (err *BatchError) Unwrap() error { - return err.wrapped -} - -// If the provided error is not nil and can be cast to an internal batch error -// we return a public batch error. -func toPublicBatchError(err error) error { - var bErr *batch.Error - if err != nil && errors.As(err, &bErr) { - err = &BatchError{wrapped: bErr} - } - return err -} - -func publicToInternalErr(err error) error { - if err == nil { - return nil - } - - var e *ErrBackOff - if errors.As(err, &e) { - return &component.ErrBackOff{Err: publicToInternalErr(e.Err), Wait: e.Wait} - } - - var bErr *BatchError - if errors.As(err, &bErr) { - return bErr.wrapped - } - - if errors.Is(err, ErrEndOfInput) { - return component.ErrTypeClosed - } - if errors.Is(err, ErrEndOfBuffer) { - return component.ErrTypeClosed - } - if errors.Is(err, ErrNotConnected) { - return component.ErrNotConnected - } - return err -} diff --git a/public/service/errors_test.go b/public/service/errors_test.go deleted file mode 100644 index 2ab726159e..0000000000 --- a/public/service/errors_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMockWalkableError(t *testing.T) { - batch := MessageBatch{ - NewMessage([]byte("a")), - NewMessage([]byte("b")), - NewMessage([]byte("c")), - } - - batchError := errors.New("simulated error") - err := NewBatchError(batch, batchError). - Failed(0, errors.New("a error")). - Failed(1, errors.New("b error")). - Failed(2, errors.New("c error")) - - require.Len(t, batch, err.IndexedErrors(), "indexed errors did not match size of batch") - require.ErrorIs(t, err, batchError, "headline error is not propagated") - - runs := 0 - err.WalkMessages(func(i int, m *Message, err error) bool { - runs++ - - bs, berr := m.AsBytes() - require.NoErrorf(t, berr, "could not get bytes from message at %d", i) - require.Equal(t, err.Error(), fmt.Sprintf("%s error", bs)) - return true - }) - - require.Equal(t, len(batch), runs, "WalkMessages did not iterate the whole batch") -} - -func TestMockWalkableError_ExcessErrors(t *testing.T) { - batch := MessageBatch{ - NewMessage([]byte("a")), - NewMessage([]byte("b")), - NewMessage([]byte("c")), - } - - batchError := errors.New("simulated error") - err := NewBatchError(batch, batchError). - Failed(0, errors.New("a error")). - Failed(1, errors.New("b error")). - Failed(2, errors.New("c error")). - Failed(3, errors.New("d error")) - - require.Equal(t, len(batch), err.IndexedErrors(), "indexed errors did not match size of batch") -} - -func TestMockWalkableError_OmitSuccessfulMessages(t *testing.T) { - batch := MessageBatch{ - NewMessage([]byte("a")), - NewMessage([]byte("b")), - NewMessage([]byte("c")), - } - - batchError := errors.New("simulated error") - err := NewBatchError(batch, batchError). - Failed(0, errors.New("a error")). - Failed(2, errors.New("c error")) - - require.Equal(t, 2, err.IndexedErrors(), "indexed errors did not match size of batch") -} - -func TestBatchErrorIndexedBy(t *testing.T) { - batch := MessageBatch{ - NewMessage([]byte("a")), - NewMessage([]byte("b")), - NewMessage([]byte("c")), - NewMessage([]byte("d")), - } - - indexer := batch.Index() - - // Scramble the batch - batch[0], batch[1] = batch[1], batch[0] - batch[1], batch[2] = batch[2], batch[1] - batch[3] = NewMessage([]byte("e")) - batch = append(batch, batch[2], batch[1], batch[0]) - - batchError := errors.New("simulated error") - err := NewBatchError(batch, batchError). - Failed(0, errors.New("b error")). - Failed(2, errors.New("a error")). - Failed(3, errors.New("e error")). - Failed(6, errors.New("b error")) - - type walkResult struct { - i int - c string - e string - } - var results []walkResult - err.WalkMessagesIndexedBy(indexer, func(i int, m *Message, err error) bool { - bs, berr := m.AsBytes() - require.NoErrorf(t, berr, "could not get bytes from message at %d", i) - - errStr := "" - if err != nil { - errStr = err.Error() - } - results = append(results, walkResult{ - i: i, c: string(bs), e: errStr, - }) - return true - }) - - assert.Equal(t, []walkResult{ - {i: 1, c: "b", e: "b error"}, - {i: 2, c: "c", e: ""}, - {i: 0, c: "a", e: "a error"}, - {i: 0, c: "a", e: ""}, - {i: 2, c: "c", e: ""}, - {i: 1, c: "b", e: "b error"}, - }, results) -} diff --git a/public/service/example_buffer_plugin_test.go b/public/service/example_buffer_plugin_test.go deleted file mode 100644 index deb5237d98..0000000000 --- a/public/service/example_buffer_plugin_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package service_test - -import ( - "context" - "sync" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only pure Benthos components, switch with `components/all` for all - // standard components. - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type memoryBuffer struct { - messages chan service.MessageBatch - endOfInputChan chan struct{} - closeOnce sync.Once -} - -func newMemoryBuffer(n int) *memoryBuffer { - return &memoryBuffer{ - messages: make(chan service.MessageBatch, n), - endOfInputChan: make(chan struct{}), - } -} - -func (m *memoryBuffer) WriteBatch(ctx context.Context, batch service.MessageBatch, aFn service.AckFunc) error { - select { - case m.messages <- batch: - case <-ctx.Done(): - return ctx.Err() - } - // We weaken delivery guarantees here by acknowledging receipt of our batch - // immediately. - return aFn(ctx, nil) -} - -func yoloIgnoreNacks(context.Context, error) error { - // YOLO: Drop messages that are nacked - return nil -} - -func (m *memoryBuffer) ReadBatch(ctx context.Context) (service.MessageBatch, service.AckFunc, error) { - select { - case msg := <-m.messages: - return msg, yoloIgnoreNacks, nil - case <-ctx.Done(): - return nil, nil, ctx.Err() - case <-m.endOfInputChan: - // Input has ended, so return ErrEndOfBuffer if our buffer is empty. - select { - case msg := <-m.messages: - return msg, yoloIgnoreNacks, nil - default: - return nil, nil, service.ErrEndOfBuffer - } - } -} - -func (m *memoryBuffer) EndOfInput() { - m.closeOnce.Do(func() { - close(m.endOfInputChan) - }) -} - -func (m *memoryBuffer) Close(ctx context.Context) error { - // Nothing to clean up - return nil -} - -// This example demonstrates how to create a buffer plugin. Buffers are an -// advanced component type that most plugin authors aren't likely to require. -func Example_bufferPlugin() { - configSpec := service.NewConfigSpec(). - Summary("Creates a lame memory buffer that loses data on forced restarts or service crashes."). - Field(service.NewIntField("max_batches").Default(100)) - - err := service.RegisterBatchBuffer("lame_memory", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - capacity, err := conf.FieldInt("max_batches") - if err != nil { - return nil, err - } - return newMemoryBuffer(capacity), nil - }) - if err != nil { - panic(err) - } - - // And then execute Benthos with: - // service.RunCLI(context.Background()) -} diff --git a/public/service/example_cache_plugin_test.go b/public/service/example_cache_plugin_test.go deleted file mode 100644 index 6fedc1be34..0000000000 --- a/public/service/example_cache_plugin_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package service_test - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only pure Benthos components, switch with `components/all` for all - // standard components. - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -// LossyCache is a terrible cache example and silently drops items when the -// capacity is reached. It also doesn't respect TTLs. -type LossyCache struct { - capacity int - mDropped *service.MetricCounter - items map[string][]byte -} - -func (l *LossyCache) Get(ctx context.Context, key string) ([]byte, error) { - if b, ok := l.items[key]; ok { - return b, nil - } - return nil, service.ErrKeyNotFound -} - -func (l *LossyCache) Set(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if len(l.items) >= l.capacity { - // Dropped, whoopsie! - l.mDropped.Incr(1) - return nil - } - l.items[key] = value - return nil -} - -func (l *LossyCache) Add(ctx context.Context, key string, value []byte, ttl *time.Duration) error { - if _, exists := l.items[key]; exists { - return service.ErrKeyAlreadyExists - } - if len(l.items) >= l.capacity { - // Dropped, whoopsie! - l.mDropped.Incr(1) - return nil - } - l.items[key] = value - return nil -} - -func (l *LossyCache) Delete(ctx context.Context, key string) error { - delete(l.items, key) - return nil -} - -func (l *LossyCache) Close(ctx context.Context) error { - return nil -} - -// This example demonstrates how to create a cache plugin, where the -// implementation of the cache (type `LossyCache`) also contains fields that -// should be parsed within the Benthos config. -func Example_cachePlugin() { - configSpec := service.NewConfigSpec(). - Summary("Creates a terrible cache with a fixed capacity."). - Field(service.NewIntField("capacity").Default(100)) - - err := service.RegisterCache("lossy", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - capacity, err := conf.FieldInt("capacity") - if err != nil { - return nil, err - } - return &LossyCache{ - capacity: capacity, - mDropped: mgr.Metrics().NewCounter("dropped_just_cus"), - items: make(map[string][]byte, capacity), - }, nil - }) - if err != nil { - panic(err) - } - - // And then execute Benthos with: - // service.RunCLI(context.Background()) -} diff --git a/public/service/example_input_plugin_test.go b/public/service/example_input_plugin_test.go deleted file mode 100644 index eb74e87c83..0000000000 --- a/public/service/example_input_plugin_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package service_test - -import ( - "context" - "math/rand" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only pure Benthos components, switch with `components/all` for all - // standard components. - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type GibberishInput struct { - length int -} - -func (g *GibberishInput) Connect(ctx context.Context) error { - return nil -} - -func (g *GibberishInput) Read(ctx context.Context) (*service.Message, service.AckFunc, error) { - b := make([]byte, g.length) - for k := range b { - b[k] = byte((rand.Int() % 94) + 32) - } - return service.NewMessage(b), func(ctx context.Context, err error) error { - // A nack (when err is non-nil) is handled automatically when we - // construct using service.AutoRetryNacks, so we don't need to handle - // nacks here. - return nil - }, nil -} - -func (g *GibberishInput) Close(ctx context.Context) error { - return nil -} - -// This example demonstrates how to create an input plugin, which is configured -// by providing a struct containing the fields to be parsed from within the -// Benthos configuration. -func Example_inputPlugin() { - configSpec := service.NewConfigSpec(). - Summary("Creates a load of gibberish, putting us all out of work."). - Field(service.NewIntField("length").Default(100)) - - constructor := func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - length, err := conf.FieldInt("length") - if err != nil { - return nil, err - } - return service.AutoRetryNacks(&GibberishInput{length}), nil - } - - err := service.RegisterInput("gibberish", configSpec, constructor) - if err != nil { - panic(err) - } - - // And then execute Benthos with: - // service.RunCLI(context.Background()) -} diff --git a/public/service/example_output_batched_plugin_test.go b/public/service/example_output_batched_plugin_test.go deleted file mode 100644 index e2dfdd5697..0000000000 --- a/public/service/example_output_batched_plugin_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package service_test - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only pure Benthos components, switch with `components/all` for all - // standard components. - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type batchOfJSONWriter struct{} - -func (b *batchOfJSONWriter) Connect(ctx context.Context) error { - return nil -} - -func (b *batchOfJSONWriter) WriteBatch(ctx context.Context, msgs service.MessageBatch) error { - var messageObjs []any - for _, msg := range msgs { - msgObj, err := msg.AsStructured() - if err != nil { - return err - } - messageObjs = append(messageObjs, msgObj) - } - outBytes, err := json.Marshal(map[string]any{ - "count": len(msgs), - "objects": messageObjs, - }) - if err != nil { - return err - } - fmt.Println(string(outBytes)) - return nil -} - -func (b *batchOfJSONWriter) Close(ctx context.Context) error { - return nil -} - -// This example demonstrates how to create a batched output plugin, which allows -// us to specify a batching mechanism and implement an interface that writes a -// batch of messages in one call. -func Example_outputBatchedPlugin() { - spec := service.NewConfigSpec(). - Field(service.NewBatchPolicyField("batching")) - - // Register our new output, which doesn't require a config schema. - err := service.RegisterBatchOutput( - "batched_json_stdout", spec, - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.BatchOutput, policy service.BatchPolicy, maxInFlight int, err error) { - if policy, err = conf.FieldBatchPolicy("batching"); err != nil { - return - } - maxInFlight = 1 - out = &batchOfJSONWriter{} - return - }) - if err != nil { - panic(err) - } - - // Use the stream builder API to create a Benthos stream that uses our new - // output type. - builder := service.NewStreamBuilder() - - // Set the full Benthos configuration of the stream. - err = builder.SetYAML(` -input: - generate: - count: 5 - interval: 1ms - mapping: | - root.id = count("batched output example messages") - root.text = "some stuff" - -output: - batched_json_stdout: - batching: - count: 5 - -logger: - level: off -`) - if err != nil { - panic(err) - } - - // Build a stream with our configured components. - stream, err := builder.Build() - if err != nil { - panic(err) - } - - // And run it, blocking until it gracefully terminates once the generate - // input has generated a message and it has flushed through the stream. - if err = stream.Run(context.Background()); err != nil { - panic(err) - } - - // Output: {"count":5,"objects":[{"id":1,"text":"some stuff"},{"id":2,"text":"some stuff"},{"id":3,"text":"some stuff"},{"id":4,"text":"some stuff"},{"id":5,"text":"some stuff"}]} -} diff --git a/public/service/example_output_plugin_test.go b/public/service/example_output_plugin_test.go deleted file mode 100644 index fcc19c40f0..0000000000 --- a/public/service/example_output_plugin_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package service_test - -import ( - "context" - "fmt" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only pure Benthos components, switch with `components/all` for all - // standard components. - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type BlueOutput struct{} - -func (b *BlueOutput) Connect(ctx context.Context) error { - return nil -} - -func (b *BlueOutput) Write(ctx context.Context, msg *service.Message) error { - content, err := msg.AsBytes() - if err != nil { - return err - } - fmt.Printf("\033[01;34m%s\033[m\n", content) - return nil -} - -func (b *BlueOutput) Close(ctx context.Context) error { - return nil -} - -// This example demonstrates how to create an output plugin. This example is for -// an implementation that does not require any configuration parameters, and -// therefore doesn't defined any within the configuration specification. -func Example_outputPlugin() { - // Register our new output, which doesn't require a config schema. - err := service.RegisterOutput( - "blue_stdout", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - return &BlueOutput{}, 1, nil - }) - if err != nil { - panic(err) - } - - // Use the stream builder API to create a Benthos stream that uses our new - // output type. - builder := service.NewStreamBuilder() - - // Set the full Benthos configuration of the stream. - err = builder.SetYAML(` -input: - generate: - count: 1 - interval: 1ms - mapping: 'root = "hello world"' - -output: - blue_stdout: {} - -logger: - level: off -`) - if err != nil { - panic(err) - } - - // Build a stream with our configured components. - stream, err := builder.Build() - if err != nil { - panic(err) - } - - // And run it, blocking until it gracefully terminates once the generate - // input has generated a message and it has flushed through the stream. - if err = stream.Run(context.Background()); err != nil { - panic(err) - } - - // Output: hello world -} diff --git a/public/service/example_processor_plugin_test.go b/public/service/example_processor_plugin_test.go deleted file mode 100644 index e7889595a3..0000000000 --- a/public/service/example_processor_plugin_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package service_test - -import ( - "bytes" - "context" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only required Benthos components, switch with `components/all` for - // all standard components. - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type ReverseProcessor struct { - logger *service.Logger -} - -func (r *ReverseProcessor) Process(ctx context.Context, m *service.Message) (service.MessageBatch, error) { - bytesContent, err := m.AsBytes() - if err != nil { - return nil, err - } - - newBytes := make([]byte, len(bytesContent)) - for i, b := range bytesContent { - newBytes[len(newBytes)-i-1] = b - } - - if bytes.Equal(newBytes, bytesContent) { - r.logger.Infof("Woah! This is like totally a palindrome: %s", bytesContent) - } - - m.SetBytes(newBytes) - return []*service.Message{m}, nil -} - -func (r *ReverseProcessor) Close(ctx context.Context) error { - return nil -} - -// This example demonstrates how to create a processor plugin. This example is -// for an implementation that does not require any configuration parameters, and -// therefore doesn't defined any within the configuration specification. -func Example_processorPlugin() { - // Register our new processor, which doesn't require a config schema. - err := service.RegisterProcessor( - "reverse", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return &ReverseProcessor{logger: mgr.Logger()}, nil - }) - if err != nil { - panic(err) - } - - // Build a Benthos stream that uses our new output type. - builder := service.NewStreamBuilder() - - // Set the full Benthos configuration of the stream. - err = builder.SetYAML(` -input: - generate: - count: 1 - interval: 1ms - mapping: 'root = "hello world"' - -pipeline: - processors: - - reverse: {} - -output: - stdout: {} - -logger: - level: off -`) - if err != nil { - panic(err) - } - - // Build a stream with our configured components. - stream, err := builder.Build() - if err != nil { - panic(err) - } - - // And run it, blocking until it gracefully terminates once the generate - // input has generated a message and it has flushed through the stream. - if err = stream.Run(context.Background()); err != nil { - panic(err) - } - - // Output: dlrow olleh -} diff --git a/public/service/example_rate_limit_plugin_test.go b/public/service/example_rate_limit_plugin_test.go deleted file mode 100644 index 3770bc0a6d..0000000000 --- a/public/service/example_rate_limit_plugin_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package service_test - -import ( - "context" - "fmt" - "math/rand" - "time" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only pure Benthos components, switch with `components/all` for all - // standard components. - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type RandomRateLimit struct { - max time.Duration -} - -func (r *RandomRateLimit) Access(context.Context) (time.Duration, error) { - return time.Duration(rand.Int() % int(r.max)), nil -} - -func (r *RandomRateLimit) Close(ctx context.Context) error { - return nil -} - -// This example demonstrates how to create a rate limit plugin, which is -// configured by providing a struct containing the fields to be parsed from -// within the Benthos configuration. -func Example_rateLimitPlugin() { - configSpec := service.NewConfigSpec(). - Summary("A rate limit that's pretty much just random."). - Description("I guess this isn't really that useful, sorry."). - Field(service.NewStringField("maximum_duration").Default("1s")) - - constructor := func(conf *service.ParsedConfig, mgr *service.Resources) (service.RateLimit, error) { - maxDurStr, err := conf.FieldString("maximum_duration") - if err != nil { - return nil, err - } - maxDuration, err := time.ParseDuration(maxDurStr) - if err != nil { - return nil, fmt.Errorf("invalid max duration: %w", err) - } - return &RandomRateLimit{maxDuration}, nil - } - - err := service.RegisterRateLimit("random", configSpec, constructor) - if err != nil { - panic(err) - } - - // And then execute Benthos with: - // service.RunCLI(context.Background()) -} diff --git a/public/service/example_stream_builder_yaml_test.go b/public/service/example_stream_builder_yaml_test.go deleted file mode 100644 index 592e14fdeb..0000000000 --- a/public/service/example_stream_builder_yaml_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package service_test - -import ( - "bytes" - "context" - "fmt" - "sync" - "time" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import only required Benthos components, switch with `components/all` for - // all standard components. - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -// This example demonstrates how to use a stream builder to parse and execute a -// full Benthos config. -func Example_streamBuilderConfig() { - panicOnErr := func(err error) { - if err != nil { - panic(err) - } - } - - builder := service.NewStreamBuilder() - - // Set the full Benthos configuration of the stream. - err := builder.SetYAML(` -input: - generate: - count: 1 - interval: 1ms - mapping: 'root = "hello world"' - - processors: - - mapping: 'root = content().uppercase()' - -output: - stdout: {} - -logger: - level: none -`) - panicOnErr(err) - - // Build a stream with our configured components. - stream, err := builder.Build() - panicOnErr(err) - - // And run it, blocking until it gracefully terminates once the generate - // input has generated a message and it has flushed through the stream. - err = stream.Run(context.Background()) - panicOnErr(err) - - // Output: HELLO WORLD -} - -// This example demonstrates how to use a stream builder to assemble a stream of -// Benthos components by adding snippets of configs for different component -// types, and then execute it. You can use the Add methods to append any number -// of components to the stream, following fan in and fan out patterns for inputs -// and outputs respectively. -func Example_streamBuilderConfigAddMethods() { - panicOnErr := func(err error) { - if err != nil { - panic(err) - } - } - - builder := service.NewStreamBuilder() - - err := builder.AddInputYAML(` -generate: - count: 1 - interval: 1ms - mapping: 'root = "hello world"' -`) - panicOnErr(err) - - err = builder.AddProcessorYAML(`mapping: 'root = content().uppercase()'`) - panicOnErr(err) - - err = builder.AddOutputYAML(`stdout: {}`) - panicOnErr(err) - - err = builder.SetLoggerYAML(`level: off`) - panicOnErr(err) - - // Build a stream with our configured components. - stream, err := builder.Build() - panicOnErr(err) - - // And run it, blocking until it gracefully terminates once the generate - // input has generated a message and it has flushed through the stream. - err = stream.Run(context.Background()) - panicOnErr(err) - - // Output: HELLO WORLD -} - -// This example demonstrates how to use a stream builder to assemble a -// processing pipeline that you can push messages into and extract via closures. -func Example_streamBuilderPush() { - panicOnErr := func(err error) { - if err != nil { - panic(err) - } - } - - builder := service.NewStreamBuilder() - err := builder.SetLoggerYAML(`level: off`) - panicOnErr(err) - - err = builder.AddProcessorYAML(`mapping: 'root = content().uppercase()'`) - panicOnErr(err) - - err = builder.AddProcessorYAML(`mapping: 'root = "check this out: " + content()'`) - panicOnErr(err) - - // Obtain a closure func that allows us to push data into the stream, this - // is treated like any other input, which also means it's possible to use - // this along with regular configured inputs. - sendFn, err := builder.AddProducerFunc() - panicOnErr(err) - - // Define a closure func that receives messages as an output of the stream. - // It's also possible to use this along with regular configured outputs. - var outputBuf bytes.Buffer - err = builder.AddConsumerFunc(func(c context.Context, m *service.Message) error { - msgBytes, err := m.AsBytes() - if err != nil { - return err - } - - _, err = fmt.Fprintf(&outputBuf, "received: %s\n", msgBytes) - return err - }) - panicOnErr(err) - - stream, err := builder.Build() - panicOnErr(err) - - go func() { - perr := sendFn(context.Background(), service.NewMessage([]byte("hello world"))) - panicOnErr(perr) - - perr = sendFn(context.Background(), service.NewMessage([]byte("I'm pushing data into the stream"))) - panicOnErr(perr) - - perr = stream.StopWithin(time.Second) - panicOnErr(perr) - }() - - // And run it, blocking until it gracefully terminates once the generate - // input has generated a message and it has flushed through the stream. - err = stream.Run(context.Background()) - panicOnErr(err) - - fmt.Println(outputBuf.String()) - - // Output: received: check this out: HELLO WORLD - // received: check this out: I'M PUSHING DATA INTO THE STREAM -} - -// This example demonstrates using the stream builder API to create and run two -// independent streams. -func Example_streamBuilderMultipleStreams() { - panicOnErr := func(err error) { - if err != nil { - panic(err) - } - } - - // Build the first stream pipeline. Note that we configure each pipeline - // with its HTTP server disabled as otherwise we would see a port collision - // when they both attempt to bind to the default address `0.0.0.0:4195`. - // - // Alternatively, we could choose to configure each with their own address - // with the field `http.address`, or we could call `SetHTTPMux` on the - // builder in order to explicitly override the configured server. - builderOne := service.NewStreamBuilder() - - err := builderOne.SetYAML(` -http: - enabled: false - -input: - generate: - count: 1 - interval: 1ms - mapping: 'root = "hello world one"' - - processors: - - mapping: 'root = content().uppercase()' - -output: - stdout: {} - -logger: - level: off -`) - panicOnErr(err) - - streamOne, err := builderOne.Build() - panicOnErr(err) - - builderTwo := service.NewStreamBuilder() - - err = builderTwo.SetYAML(` -http: - enabled: false - -input: - generate: - count: 1 - interval: 1ms - mapping: 'root = "hello world two"' - - processors: - - sleep: - duration: 500ms - - mapping: 'root = content().capitalize()' - -output: - stdout: {} - -logger: - level: off -`) - panicOnErr(err) - - streamTwo, err := builderTwo.Build() - panicOnErr(err) - - var wg sync.WaitGroup - wg.Add(2) - - go func() { - defer wg.Done() - panicOnErr(streamOne.Run(context.Background())) - }() - go func() { - defer wg.Done() - panicOnErr(streamTwo.Run(context.Background())) - }() - - wg.Wait() - - // Output: HELLO WORLD ONE - // Hello World Two -} diff --git a/public/service/input.go b/public/service/input.go deleted file mode 100644 index 84a27138ae..0000000000 --- a/public/service/input.go +++ /dev/null @@ -1,269 +0,0 @@ -package service - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/input/batcher" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// AckFunc is a common function returned by inputs that must be called once for -// each message consumed. This function ensures that the source of the message -// receives either an acknowledgement (err is nil) or an error that can either -// be propagated upstream as a nack, or trigger a reattempt at delivering the -// same message. -// -// If your input implementation doesn't have a specific mechanism for dealing -// with a nack then you can wrap your input implementation with AutoRetryNacks -// to get automatic retries. -type AckFunc func(ctx context.Context, err error) error - -// Input is an interface implemented by Benthos inputs. Calls to Read should -// block until either a message has been received, the connection is lost, or -// the provided context is cancelled. -type Input interface { - // Establish a connection to the upstream service. Connect will always be - // called first when a reader is instantiated, and will be continuously - // called with back off until a nil error is returned. - // - // The provided context remains open only for the duration of the connecting - // phase, and should not be used to establish the lifetime of the connection - // itself. - // - // Once Connect returns a nil error the Read method will be called until - // either ErrNotConnected is returned, or the reader is closed. - Connect(context.Context) error - - // Read a single message from a source, along with a function to be called - // once the message can be either acked (successfully sent or intentionally - // filtered) or nacked (failed to be processed or dispatched to the output). - // - // The AckFunc will be called for every message at least once, but there are - // no guarantees as to when this will occur. If your input implementation - // doesn't have a specific mechanism for dealing with a nack then you can - // wrap your input implementation with AutoRetryNacks to get automatic - // retries. - // - // If this method returns ErrNotConnected then Read will not be called again - // until Connect has returned a nil error. If ErrEndOfInput is returned then - // Read will no longer be called and the pipeline will gracefully terminate. - Read(context.Context) (*Message, AckFunc, error) - - Closer -} - -//------------------------------------------------------------------------------ - -// BatchInput is an interface implemented by Benthos inputs that produce -// messages in batches, where there is a desire to process and send the batch as -// a logical group rather than as individual messages. -// -// Calls to ReadBatch should block until either a message batch is ready to -// process, the connection is lost, or the provided context is cancelled. -type BatchInput interface { - // Establish a connection to the upstream service. Connect will always be - // called first when a reader is instantiated, and will be continuously - // called with back off until a nil error is returned. - // - // The provided context remains open only for the duration of the connecting - // phase, and should not be used to establish the lifetime of the connection - // itself. - // - // Once Connect returns a nil error the Read method will be called until - // either ErrNotConnected is returned, or the reader is closed. - Connect(context.Context) error - - // Read a message batch from a source, along with a function to be called - // once the entire batch can be either acked (successfully sent or - // intentionally filtered) or nacked (failed to be processed or dispatched - // to the output). - // - // The AckFunc will be called for every message batch at least once, but - // there are no guarantees as to when this will occur. If your input - // implementation doesn't have a specific mechanism for dealing with a nack - // then you can wrap your input implementation with AutoRetryNacksBatched to - // get automatic retries. - // - // If this method returns ErrNotConnected then ReadBatch will not be called - // again until Connect has returned a nil error. If ErrEndOfInput is - // returned then Read will no longer be called and the pipeline will - // gracefully terminate. - ReadBatch(context.Context) (MessageBatch, AckFunc, error) - - Closer -} - -//------------------------------------------------------------------------------ - -// Implements input.AsyncReader. -type airGapReader struct { - r Input -} - -func newAirGapReader(r Input) input.Async { - return &airGapReader{r: r} -} - -func (a *airGapReader) Connect(ctx context.Context) error { - return publicToInternalErr(a.r.Connect(ctx)) -} - -func (a *airGapReader) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - msg, ackFn, err := a.r.Read(ctx) - if err != nil { - return nil, nil, publicToInternalErr(err) - } - - tMsg := message.Batch{msg.part} - return tMsg, func(c context.Context, r error) error { - return ackFn(c, r) - }, nil -} - -func (a *airGapReader) Close(ctx context.Context) error { - return a.r.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// Implements input.AsyncReader. -type airGapBatchReader struct { - r BatchInput -} - -func newAirGapBatchReader(r BatchInput) input.Async { - return &airGapBatchReader{r: r} -} - -func (a *airGapBatchReader) Connect(ctx context.Context) error { - return publicToInternalErr(a.r.Connect(ctx)) -} - -func (a *airGapBatchReader) ReadBatch(ctx context.Context) (message.Batch, input.AsyncAckFn, error) { - batch, ackFn, err := a.r.ReadBatch(ctx) - if err != nil { - return nil, nil, publicToInternalErr(err) - } - - mBatch := make(message.Batch, len(batch)) - for i, p := range batch { - mBatch[i] = p.part - } - return mBatch, func(c context.Context, r error) error { - r = toPublicBatchError(r) - return ackFn(c, r) - }, nil -} - -func (a *airGapBatchReader) Close(ctx context.Context) error { - return a.r.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// ResourceInput provides access to an input resource. -type ResourceInput struct { - i input.Streamed -} - -func newResourceInput(i input.Streamed) *ResourceInput { - return &ResourceInput{i: i} -} - -// ReadBatch attempts to read a message batch from the input, along with a -// function to be called once the entire batch can be either acked (successfully -// sent or intentionally filtered) or nacked (failed to be processed or -// dispatched to the output). -// -// If this method returns ErrEndOfInput then that indicates that the input has -// finished and will no longer yield new messages. -func (r *ResourceInput) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - var tran message.Transaction - var open bool - select { - case tran, open = <-r.i.TransactionChan(): - case <-ctx.Done(): - return nil, nil, ctx.Err() - } - if !open { - return nil, nil, ErrEndOfInput - } - - var b MessageBatch - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - b = append(b, NewInternalMessage(part)) - return nil - }) - return b, func(c context.Context, r error) error { - r = publicToInternalErr(r) - return tran.Ack(c, r) - }, nil -} - -//------------------------------------------------------------------------------ - -// OwnedInput provides direct ownership of an input extracted from a plugin -// config. Connectivity of the input is handled internally, and so the consumer -// of this type should only be concerned with reading messages and eventually -// calling Close to terminate the input. -type OwnedInput struct { - i input.Streamed -} - -// BatchedWith returns a copy of the OwnedInput where messages will be batched -// according to the provided batcher. -func (o *OwnedInput) BatchedWith(b *Batcher) *OwnedInput { - return &OwnedInput{ - i: batcher.New(b.p, o.i, b.mgr.Logger()), - } -} - -// ReadBatch attempts to read a message batch from the input, along with a -// function to be called once the entire batch can be either acked (successfully -// sent or intentionally filtered) or nacked (failed to be processed or -// dispatched to the output). -// -// If this method returns ErrEndOfInput then that indicates that the input has -// finished and will no longer yield new messages. -func (o *OwnedInput) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - var tran message.Transaction - var open bool - select { - case tran, open = <-o.i.TransactionChan(): - case <-ctx.Done(): - return nil, nil, ctx.Err() - } - if !open { - return nil, nil, ErrEndOfInput - } - - var b MessageBatch - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - b = append(b, NewInternalMessage(part)) - return nil - }) - return b, func(c context.Context, r error) error { - r = publicToInternalErr(r) - return tran.Ack(c, r) - }, nil -} - -// Close the input. -func (o *OwnedInput) Close(ctx context.Context) error { - o.i.TriggerStopConsuming() - return o.i.WaitForClose(ctx) -} - -type inputUnwrapper struct { - i input.Streamed -} - -func (w inputUnwrapper) Unwrap() input.Streamed { - return w.i -} - -// XUnwrapper is for internal use only, do not use this. -func (o *OwnedInput) XUnwrapper() any { - return inputUnwrapper{i: o.i} -} diff --git a/public/service/input_auto_retry.go b/public/service/input_auto_retry.go deleted file mode 100644 index edf1ac8d4d..0000000000 --- a/public/service/input_auto_retry.go +++ /dev/null @@ -1,83 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/autoretry" -) - -// AutoRetryNacksToggled wraps an input implementation with AutoRetryNacks only -// if a field defined by NewAutoRetryNacksToggleField has been set to true. -func AutoRetryNacksToggled(c *ParsedConfig, i Input) (Input, error) { - b, err := c.FieldBool(AutoRetryNacksToggleFieldName) - if err != nil { - return nil, err - } - if b { - return AutoRetryNacks(i), nil - } - return i, nil -} - -// AutoRetryNacks wraps an input implementation with a component that -// automatically reattempts messages that fail downstream. This is useful for -// inputs that do not support nacks, and therefore don't have an answer for -// when an ack func is called with an error. -// -// When messages fail to be delivered they will be reattempted with back off -// until success or the stream is stopped. -func AutoRetryNacks(i Input) Input { - return &autoRetryInput{ - retryList: autoretry.NewList(func(ctx context.Context) (*Message, autoretry.AckFunc, error) { - t, aFn, err := i.Read(ctx) - return t, autoretry.AckFunc(aFn), err - }, nil), - child: i, - } -} - -//------------------------------------------------------------------------------ - -type autoRetryInput struct { - retryList *autoretry.List[*Message] - child Input - inputClosed int32 -} - -func (i *autoRetryInput) Connect(ctx context.Context) error { - err := i.child.Connect(ctx) - // If our source has finished but we still have messages in flight then - // we act like we're still open. Read will be called and we can either - // return the pending messages or wait for them. - if errors.Is(err, ErrEndOfInput) && !i.retryList.Exhausted() { - atomic.StoreInt32(&i.inputClosed, 1) - err = nil - } - return err -} - -func (i *autoRetryInput) Read(ctx context.Context) (*Message, AckFunc, error) { - msg, rAckFn, err := i.retryList.Shift(ctx, atomic.LoadInt32(&i.inputClosed) == 0) - if err != nil { - if errors.Is(err, autoretry.ErrExhausted) { - return nil, nil, ErrEndOfInput - } - if errors.Is(err, ErrEndOfInput) { - // Mark our input as being closed and trigger an immediate re-read - // in order to clear any pending retries. - atomic.StoreInt32(&i.inputClosed, 1) - return i.Read(ctx) - } - // Otherwise we have an unknown error from our reader that we should - // escalate, this is most likely an ErrNotConnected or ErrTimeout. - return nil, nil, err - } - return msg.Copy(), AckFunc(rAckFn), nil -} - -func (i *autoRetryInput) Close(ctx context.Context) error { - _ = i.retryList.Close(ctx) - return i.child.Close(ctx) -} diff --git a/public/service/input_auto_retry_batched.go b/public/service/input_auto_retry_batched.go deleted file mode 100644 index 34c9423ad6..0000000000 --- a/public/service/input_auto_retry_batched.go +++ /dev/null @@ -1,138 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/autoretry" - "github.com/benthosdev/benthos/v4/internal/batch" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// AutoRetryNacksBatchedToggled wraps an input implementation with -// AutoRetryNacksBatched only if a field defined by NewAutoRetryNacksToggleField -// has been set to true. -func AutoRetryNacksBatchedToggled(c *ParsedConfig, i BatchInput) (BatchInput, error) { - b, err := c.FieldBool(AutoRetryNacksToggleFieldName) - if err != nil { - return nil, err - } - if b { - return AutoRetryNacksBatched(i), nil - } - return i, nil -} - -// AutoRetryNacksBatched wraps a batched input implementation with a component -// that automatically reattempts messages that fail downstream. This is useful -// for inputs that do not support nacks, and therefore don't have an answer for -// when an ack func is called with an error. -// -// When messages fail to be delivered they will be reattempted with back off -// until success or the stream is stopped. -func AutoRetryNacksBatched(i BatchInput) BatchInput { - return &autoRetryInputBatched{ - retryList: autoretry.NewList( - func(ctx context.Context) (MessageBatch, autoretry.AckFunc, error) { - t, aFn, err := i.ReadBatch(ctx) - - // Make sure we're able to track the position of messages in - // order to reassociate them after a batch-wide error - // downstream. - iParts := make([]*message.Part, len(t)) - for i, p := range t { - iParts[i] = p.part - } - - _, iParts = message.NewSortGroup(iParts) - for i, p := range iParts { - t[i] = NewInternalMessage(p) - } - - return t, autoretry.AckFunc(aFn), err - }, - func(t MessageBatch, err error) MessageBatch { - var bErr *batch.Error - if len(t) == 0 || !errors.As(err, &bErr) || bErr.IndexedErrors() == 0 { - return t - } - - sortGroup := message.TopLevelSortGroup(t[0].part) - if sortGroup == nil { - // We can't associate our source batch with the one that's associated - // with the batch error, therefore we fall back towards treating every - // message as if it was errored the same. - return t - } - - sortBatch := make(message.Batch, len(t)) - for i, p := range t { - sortBatch[i] = p.part - } - - seenIndexes := map[int]struct{}{} - newBatch := make(MessageBatch, 0, bErr.IndexedErrors()) - bErr.WalkPartsBySource(sortGroup, sortBatch, func(i int, p *message.Part, err error) bool { - if err == nil { - return true - } - if _, exists := seenIndexes[i]; exists { - return true - } - seenIndexes[i] = struct{}{} - newBatch = append(newBatch, &Message{part: p}) - return true - }) - if len(newBatch) == 0 { - return t - } - return newBatch - }), - child: i, - } -} - -//------------------------------------------------------------------------------ - -type autoRetryInputBatched struct { - retryList *autoretry.List[MessageBatch] - child BatchInput - inputClosed int32 -} - -func (i *autoRetryInputBatched) Connect(ctx context.Context) error { - err := i.child.Connect(ctx) - // If our source has finished but we still have messages in flight then - // we act like we're still open. Read will be called and we can either - // return the pending messages or wait for them. - if errors.Is(err, ErrEndOfInput) && !i.retryList.Exhausted() { - atomic.StoreInt32(&i.inputClosed, 1) - err = nil - } - return err -} - -func (i *autoRetryInputBatched) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - batch, rAckFn, err := i.retryList.Shift(ctx, atomic.LoadInt32(&i.inputClosed) == 0) - if err != nil { - if errors.Is(err, autoretry.ErrExhausted) { - return nil, nil, ErrEndOfInput - } - if errors.Is(err, ErrEndOfInput) { - // Mark our input as being closed and trigger an immediate re-read - // in order to clear any pending retries. - atomic.StoreInt32(&i.inputClosed, 1) - return i.ReadBatch(ctx) - } - // Otherwise we have an unknown error from our reader that we should - // escalate, this is most likely an ErrNotConnected or ErrTimeout. - return nil, nil, err - } - return batch.Copy(), AckFunc(rAckFn), nil -} - -func (i *autoRetryInputBatched) Close(ctx context.Context) error { - _ = i.retryList.Close(ctx) - return i.child.Close(ctx) -} diff --git a/public/service/input_auto_retry_batched_test.go b/public/service/input_auto_retry_batched_test.go deleted file mode 100644 index 41eb42bf2e..0000000000 --- a/public/service/input_auto_retry_batched_test.go +++ /dev/null @@ -1,370 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockBatchInput struct { - msgsToSnd []MessageBatch - ackRcvdMut sync.Mutex - ackRcvd []error - - connChan chan error - readChan chan error - ackChan chan error - closeChan chan error -} - -func newMockBatchInput() *mockBatchInput { - return &mockBatchInput{ - connChan: make(chan error), - readChan: make(chan error), - ackChan: make(chan error), - closeChan: make(chan error), - } -} - -func (i *mockBatchInput) Connect(ctx context.Context) error { - cerr, open := <-i.connChan - if !open { - return ErrEndOfInput - } - return cerr -} - -func (i *mockBatchInput) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - select { - case <-ctx.Done(): - return nil, nil, ctx.Err() - case err, open := <-i.readChan: - if !open { - return nil, nil, ErrEndOfInput - } - if err != nil { - return nil, nil, err - } - } - i.ackRcvdMut.Lock() - i.ackRcvd = append(i.ackRcvd, errors.New("ack not received")) - index := len(i.ackRcvd) - 1 - i.ackRcvdMut.Unlock() - - nextBatch := MessageBatch{} - if len(i.msgsToSnd) > 0 { - nextBatch = i.msgsToSnd[0] - i.msgsToSnd = i.msgsToSnd[1:] - } - - return nextBatch.Copy(), func(ctx context.Context, res error) error { - i.ackRcvdMut.Lock() - i.ackRcvd[index] = res - i.ackRcvdMut.Unlock() - return <-i.ackChan - }, nil -} - -func (i *mockBatchInput) Close(ctx context.Context) error { - return <-i.closeChan -} - -func TestBatchAutoRetryConfig(t *testing.T) { - spec := NewConfigSpec().Field(NewAutoRetryNacksToggleField()) - for conf, shouldRetry := range map[string]bool{ - `{}`: true, - `auto_replay_nacks: false`: false, - `auto_replay_nacks: true`: true, - } { - inConf, err := spec.ParseYAML(conf, nil) - require.NoError(t, err, conf) - - readerImpl := newMockBatchInput() - pres, err := AutoRetryNacksBatchedToggled(inConf, readerImpl) - require.NoError(t, err, conf) - - _, isWrapped := pres.(*autoRetryInputBatched) - assert.Equal(t, shouldRetry, isWrapped, conf) - } -} - -func TestBatchAutoRetryClose(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockBatchInput() - pres := AutoRetryNacksBatched(readerImpl) - - expErr := errors.New("foo error") - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - err := pres.Connect(ctx) - require.NoError(t, err) - - assert.Equal(t, expErr, pres.Close(ctx)) - }() - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.closeChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - - wg.Wait() -} - -func TestBatchAutoRetryHappy(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockBatchInput() - readerImpl.msgsToSnd = append(readerImpl.msgsToSnd, MessageBatch{ - NewMessage([]byte("foo")), - NewMessage([]byte("bar")), - }) - - pres := AutoRetryNacksBatched(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - require.NoError(t, pres.Connect(ctx)) - - batch, _, err := pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, batch, 2) - - act, err := batch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo", string(act)) - - act, err = batch[1].AsBytes() - require.NoError(t, err) - assert.Equal(t, "bar", string(act)) -} - -func TestBatchAutoRetryErrorProp(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockBatchInput() - pres := AutoRetryNacksBatched(readerImpl) - - expErr := errors.New("foo") - - go func() { - select { - case readerImpl.connChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.ackChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - assert.Equal(t, expErr, pres.Connect(ctx)) - - _, _, err := pres.ReadBatch(ctx) - assert.Equal(t, expErr, err) - - _, aFn, err := pres.ReadBatch(ctx) - require.NoError(t, err) - - assert.Equal(t, expErr, aFn(ctx, nil)) -} - -func TestBatchAutoRetryErrorBackoff(t *testing.T) { - t.Skip("Not liked by the race detector") - t.Parallel() - - readerImpl := newMockBatchInput() - pres := AutoRetryNacksBatched(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.closeChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) - defer cancel() - - require.NoError(t, pres.Connect(ctx)) - - i := 0 - for { - _, aFn, actErr := pres.ReadBatch(ctx) - if actErr != nil { - assert.Equal(t, ctx.Err(), actErr) - break - } - require.NoError(t, aFn(ctx, errors.New("no thanks"))) - i++ - if i == 10 { - t.Error("Expected backoff to prevent this") - break - } - } - - require.NoError(t, pres.Close(context.Background())) -} - -func TestBatchAutoRetryBuffer(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockBatchInput() - pres := AutoRetryNacksBatched(readerImpl) - - sendMsg := func(content string) { - readerImpl.msgsToSnd = []MessageBatch{ - {NewMessage([]byte(content))}, - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - sendAck := func() { - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - // Send message normally. - exp := "msg 1" - exp2 := "msg 2" - exp3 := "msg 3" - - go sendMsg(exp) - msg, aFn, err := pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - - b, err := msg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(b)) - - // Prime second message. - go sendMsg(exp2) - - // Fail previous message, expecting it to be resent. - _ = aFn(ctx, errors.New("failed")) - msg, aFn, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - - b, err = msg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(b)) - - // Read the primed message. - var aFn2 AckFunc - msg, aFn2, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - - b, err = msg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp2, string(b)) - - // Fail both messages, expecting them to be resent. - _ = aFn(ctx, errors.New("failed again")) - _ = aFn2(ctx, errors.New("failed again")) - - // Read both messages. - msg, aFn, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - - b, err = msg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(b)) - - msg, aFn2, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - - b, err = msg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp2, string(b)) - - // Prime a new message and also an acknowledgement. - go sendMsg(exp3) - go sendAck() - go sendAck() - - // Ack all messages. - _ = aFn(ctx, nil) - _ = aFn2(ctx, nil) - - msg, _, err = pres.ReadBatch(ctx) - require.NoError(t, err) - require.Len(t, msg, 1) - - b, err = msg[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, exp3, string(b)) -} diff --git a/public/service/input_auto_retry_test.go b/public/service/input_auto_retry_test.go deleted file mode 100644 index f6b908cbef..0000000000 --- a/public/service/input_auto_retry_test.go +++ /dev/null @@ -1,423 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockInput struct { - msgsToSnd []*Message - ackRcvdMut sync.Mutex - ackRcvd []error - - connChan chan error - readChan chan error - ackChan chan error - closeChan chan error -} - -func newMockInput() *mockInput { - return &mockInput{ - connChan: make(chan error), - readChan: make(chan error), - ackChan: make(chan error), - closeChan: make(chan error), - } -} - -func (i *mockInput) Connect(ctx context.Context) error { - cerr, open := <-i.connChan - if !open { - return ErrEndOfInput - } - return cerr -} - -func (i *mockInput) Read(ctx context.Context) (*Message, AckFunc, error) { - select { - case <-ctx.Done(): - return nil, nil, ctx.Err() - case err, open := <-i.readChan: - if !open { - return nil, nil, ErrEndOfInput - } - if err != nil { - return nil, nil, err - } - } - i.ackRcvdMut.Lock() - i.ackRcvd = append(i.ackRcvd, errors.New("ack not received")) - index := len(i.ackRcvd) - 1 - i.ackRcvdMut.Unlock() - - nextMsg := NewMessage(nil) - if len(i.msgsToSnd) > 0 { - nextMsg = i.msgsToSnd[0] - i.msgsToSnd = i.msgsToSnd[1:] - } - - return nextMsg.Copy(), func(ctx context.Context, res error) error { - i.ackRcvdMut.Lock() - i.ackRcvd[index] = res - i.ackRcvdMut.Unlock() - return <-i.ackChan - }, nil -} - -func (i *mockInput) Close(ctx context.Context) error { - return <-i.closeChan -} - -func TestAutoRetryConfig(t *testing.T) { - spec := NewConfigSpec().Field(NewAutoRetryNacksToggleField()) - for conf, shouldRetry := range map[string]bool{ - `{}`: true, - `auto_replay_nacks: false`: false, - `auto_replay_nacks: true`: true, - } { - inConf, err := spec.ParseYAML(conf, nil) - require.NoError(t, err, conf) - - readerImpl := newMockInput() - pres, err := AutoRetryNacksToggled(inConf, readerImpl) - require.NoError(t, err, conf) - - _, isWrapped := pres.(*autoRetryInput) - assert.Equal(t, shouldRetry, isWrapped, conf) - } -} - -func TestAutoRetryClose(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockInput() - pres := AutoRetryNacks(readerImpl) - - expErr := errors.New("foo error") - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - err := pres.Connect(ctx) - require.NoError(t, err) - - assert.Equal(t, expErr, pres.Close(ctx)) - }() - - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - select { - case readerImpl.closeChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - - wg.Wait() -} - -func TestAutoRetryHappy(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockInput() - readerImpl.msgsToSnd = append(readerImpl.msgsToSnd, NewMessage([]byte("foo"))) - - pres := AutoRetryNacks(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - require.NoError(t, pres.Connect(ctx)) - - msg, _, err := pres.Read(ctx) - require.NoError(t, err) - - act, err := msg.AsBytes() - require.NoError(t, err) - - assert.Equal(t, "foo", string(act)) -} - -func TestAutoRetryErrorProp(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockInput() - pres := AutoRetryNacks(readerImpl) - - expErr := errors.New("foo") - - go func() { - select { - case readerImpl.connChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.ackChan <- expErr: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - assert.Equal(t, expErr, pres.Connect(ctx)) - - _, _, err := pres.Read(ctx) - assert.Equal(t, expErr, err) - - _, aFn, err := pres.Read(ctx) - require.NoError(t, err) - - assert.Equal(t, expErr, aFn(ctx, nil)) -} - -func TestAutoRetryErrorBackoff(t *testing.T) { - t.Skip("Not liked by the race detector") - t.Parallel() - - readerImpl := newMockInput() - pres := AutoRetryNacks(readerImpl) - - go func() { - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.closeChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) - defer cancel() - - require.NoError(t, pres.Connect(ctx)) - - i := 0 - for { - _, aFn, actErr := pres.Read(ctx) - if actErr != nil { - assert.Equal(t, ctx.Err(), actErr) - break - } - require.NoError(t, aFn(ctx, errors.New("no thanks"))) - i++ - if i == 10 { - t.Error("Expected backoff to prevent this") - break - } - } - - require.NoError(t, pres.Close(context.Background())) -} - -func TestAutoRetryBuffer(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - - readerImpl := newMockInput() - pres := AutoRetryNacks(readerImpl) - - sendMsg := func(content string) { - readerImpl.msgsToSnd = []*Message{ - NewMessage([]byte(content)), - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - sendAck := func() { - select { - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - } - - // Send message normally. - exp := "msg 1" - exp2 := "msg 2" - exp3 := "msg 3" - - go sendMsg(exp) - msg, aFn, err := pres.Read(ctx) - require.NoError(t, err) - - b, err := msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(b)) - - // Mutate the message to ensure it's not changed - msg.SetBytes([]byte("mutated message")) - - // Prime second message. - go sendMsg(exp2) - - // Fail previous message, expecting it to be resent. - _ = aFn(ctx, errors.New("failed")) - msg, aFn, err = pres.Read(ctx) - require.NoError(t, err) - - b, err = msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(b)) - - // Read the primed message. - var aFn2 AckFunc - msg, aFn2, err = pres.Read(ctx) - require.NoError(t, err) - - b, err = msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, exp2, string(b)) - - // Fail both messages, expecting them to be resent. - _ = aFn(ctx, errors.New("failed again")) - _ = aFn2(ctx, errors.New("failed again")) - - // Read both messages. - msg, aFn, err = pres.Read(ctx) - require.NoError(t, err) - - b, err = msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, exp, string(b)) - - msg, aFn2, err = pres.Read(ctx) - require.NoError(t, err) - - b, err = msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, exp2, string(b)) - - // Prime a new message and also an acknowledgement. - go sendMsg(exp3) - go sendAck() - go sendAck() - - // Ack all messages. - _ = aFn(ctx, nil) - _ = aFn2(ctx, nil) - - msg, _, err = pres.Read(ctx) - require.NoError(t, err) - - b, err = msg.AsBytes() - require.NoError(t, err) - assert.Equal(t, exp3, string(b)) -} - -func TestAutoRetryReadAfterClose(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - readerImpl := newMockInput() - readerImpl.msgsToSnd = append(readerImpl.msgsToSnd, NewMessage([]byte("foo"))) - - pres := AutoRetryNacks(readerImpl) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - select { - case readerImpl.connChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - select { - case readerImpl.readChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - // Force mockInput.Read() to return ErrEndOfInput after the first - // message was read - close(readerImpl.readChan) - }() - - require.NoError(t, pres.Connect(ctx)) - - msg, fn, err := pres.Read(ctx) - require.NoError(t, err) - - wg.Add(1) - go func() { - defer wg.Done() - // Acknowledge the message explicitly so the input doesn't attempt to - // redeliver it - require.NoError(t, fn(ctx, err)) - }() - - select { - // Push a message on the ackChan to unblock the message acknowledgement - case readerImpl.ackChan <- nil: - case <-time.After(time.Second): - t.Error("Timed out") - } - - act, err := msg.AsBytes() - require.NoError(t, err) - - assert.Equal(t, "foo", string(act)) - - // Wait for the input to close and for the message ack to be sent - wg.Wait() - - _, _, err = pres.Read(ctx) - assert.Equal(t, ErrEndOfInput, err) -} diff --git a/public/service/input_max_in_flight.go b/public/service/input_max_in_flight.go deleted file mode 100644 index 3291d1d1b7..0000000000 --- a/public/service/input_max_in_flight.go +++ /dev/null @@ -1,105 +0,0 @@ -package service - -import ( - "context" - - "golang.org/x/sync/semaphore" -) - -// NewInputMaxInFlightField returns a field spec for a common max_in_flight -// input field. -func NewInputMaxInFlightField() *ConfigField { - return NewIntField("max_in_flight"). - Description("Optionally sets a limit on the number of messages that can be flowing through a Benthos stream pending acknowledgment from the input at any given time. Once a message has been either acknowledged or rejected (nacked) it is no longer considered pending. If the input produces logical batches then each batch is considered a single count against the maximum. **WARNING**: Batching policies at the output level will stall if this field limits the number of messages below the batching threshold. Zero (default) or lower implies no limit."). - Default(0). - Advanced() -} - -//------------------------------------------------------------------------------ - -// InputWithMaxInFlight wraps an input with a component that limits the number of -// messages being processed at a given time. When the limit is reached a new -// message will not be consumed until an ack/nack has been returned. -func InputWithMaxInFlight(n int, i Input) Input { - if n <= 0 { - return i - } - return &maxInFlight{ - i: i, - ackSema: semaphore.NewWeighted(int64(n)), - } -} - -type maxInFlight struct { - i Input - ackSema *semaphore.Weighted -} - -func (m *maxInFlight) Connect(ctx context.Context) error { - return m.i.Connect(ctx) -} - -func (m *maxInFlight) Read(ctx context.Context) (*Message, AckFunc, error) { - if err := m.ackSema.Acquire(ctx, 1); err != nil { - return nil, nil, err - } - mRes, aFn, err := m.i.Read(ctx) - if err != nil { - m.ackSema.Release(1) - return nil, nil, err - } - return mRes, func(ctx context.Context, err error) error { - aerr := aFn(ctx, err) - m.ackSema.Release(1) - return aerr - }, nil -} - -func (m *maxInFlight) Close(ctx context.Context) error { - return m.i.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// InputBatchedWithMaxInFlight wraps a batched input with a component that -// limits the number of batches being processed at a given time. When the limit -// is reached a new message batch will not be consumed until an ack/nack has -// been returned. -func InputBatchedWithMaxInFlight(n int, i BatchInput) BatchInput { - if n <= 0 { - return i - } - return &maxInFlightBatched{ - i: i, - ackSema: semaphore.NewWeighted(int64(n)), - } -} - -type maxInFlightBatched struct { - i BatchInput - ackSema *semaphore.Weighted -} - -func (m *maxInFlightBatched) Connect(ctx context.Context) error { - return m.i.Connect(ctx) -} - -func (m *maxInFlightBatched) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - if err := m.ackSema.Acquire(ctx, 1); err != nil { - return nil, nil, err - } - mRes, aFn, err := m.i.ReadBatch(ctx) - if err != nil { - m.ackSema.Release(1) - return nil, nil, err - } - return mRes, func(ctx context.Context, err error) error { - aerr := aFn(ctx, err) - m.ackSema.Release(1) - return aerr - }, nil -} - -func (m *maxInFlightBatched) Close(ctx context.Context) error { - return m.i.Close(ctx) -} diff --git a/public/service/input_max_in_flight_test.go b/public/service/input_max_in_flight_test.go deleted file mode 100644 index 542c35db2e..0000000000 --- a/public/service/input_max_in_flight_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package service - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMaxInFlightPassThrough(t *testing.T) { - readerImpl := newMockInput() - - wrapped := InputWithMaxInFlight(0, readerImpl) - - _, isMock := wrapped.(*mockInput) - require.True(t, isMock) -} - -func TestMaxInFlightLimit(t *testing.T) { - getCtx := func() context.Context { - t.Helper() - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*50) - t.Cleanup(done) - return ctx - } - - readerImpl := newMockInput() - readerImpl.msgsToSnd = []*Message{ - NewMessage([]byte("foo1")), - NewMessage([]byte("foo2")), - NewMessage([]byte("foo3")), - NewMessage([]byte("foo4")), - NewMessage([]byte("foo5")), - } - - wrapped := InputWithMaxInFlight(3, readerImpl) - - _, isMock := wrapped.(*mockInput) - require.False(t, isMock) - - go func() { - readerImpl.connChan <- nil - for i := 0; i < 5; i++ { - readerImpl.readChan <- nil - } - readerImpl.closeChan <- nil - }() - go func() { - for i := 0; i < 5; i++ { - readerImpl.ackChan <- nil - } - }() - - require.NoError(t, wrapped.Connect(getCtx())) - - // Message 1 of 3. - outMsg, aFn1, err := wrapped.Read(getCtx()) - require.NoError(t, err) - - mBytes, err := outMsg.AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo1", string(mBytes)) - - // Message 2 of 3. - outMsg, aFn2, err := wrapped.Read(getCtx()) - require.NoError(t, err) - - mBytes, err = outMsg.AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo2", string(mBytes)) - - // Message 3 of 3. - outMsg, aFn3, err := wrapped.Read(getCtx()) - require.NoError(t, err) - - mBytes, err = outMsg.AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo3", string(mBytes)) - - // Message 4 of 3 never comes. - tmpCtx, done := context.WithTimeout(context.Background(), time.Millisecond*50) - defer done() - - _, _, err = wrapped.Read(tmpCtx) - require.Error(t, err) - - // Ack our three messages. - require.NoError(t, aFn1(getCtx(), nil)) - require.NoError(t, aFn2(getCtx(), nil)) - require.NoError(t, aFn3(getCtx(), nil)) - - // Message 4 of 5. - outMsg, aFn4, err := wrapped.Read(getCtx()) - require.NoError(t, err) - - mBytes, err = outMsg.AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo4", string(mBytes)) - - // Message 5 of 5. - outMsg, aFn5, err := wrapped.Read(getCtx()) - require.NoError(t, err) - - mBytes, err = outMsg.AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo5", string(mBytes)) - - // Ack last messages. - require.NoError(t, aFn4(getCtx(), nil)) - require.NoError(t, aFn5(getCtx(), nil)) - - require.NoError(t, wrapped.Close(getCtx())) -} - -func TestMaxInFlightLimitBatched(t *testing.T) { - getCtx := func() context.Context { - t.Helper() - ctx, done := context.WithTimeout(context.Background(), time.Millisecond*50) - t.Cleanup(done) - return ctx - } - - readerImpl := newMockBatchInput() - readerImpl.msgsToSnd = []MessageBatch{ - {NewMessage([]byte("foo1"))}, - {NewMessage([]byte("foo2"))}, - {NewMessage([]byte("foo3"))}, - {NewMessage([]byte("foo4"))}, - {NewMessage([]byte("foo5"))}, - } - - wrapped := InputBatchedWithMaxInFlight(3, readerImpl) - - _, isMock := wrapped.(*mockBatchInput) - require.False(t, isMock) - - go func() { - readerImpl.connChan <- nil - for i := 0; i < 5; i++ { - readerImpl.readChan <- nil - } - readerImpl.closeChan <- nil - }() - go func() { - for i := 0; i < 5; i++ { - readerImpl.ackChan <- nil - } - }() - - require.NoError(t, wrapped.Connect(getCtx())) - - // Message 1 of 3. - outBatch, aFn1, err := wrapped.ReadBatch(getCtx()) - require.NoError(t, err) - - require.Len(t, outBatch, 1) - mBytes, err := outBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo1", string(mBytes)) - - // Message 2 of 3. - outBatch, aFn2, err := wrapped.ReadBatch(getCtx()) - require.NoError(t, err) - - require.Len(t, outBatch, 1) - mBytes, err = outBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo2", string(mBytes)) - - // Message 3 of 3. - outBatch, aFn3, err := wrapped.ReadBatch(getCtx()) - require.NoError(t, err) - - require.Len(t, outBatch, 1) - mBytes, err = outBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo3", string(mBytes)) - - // Message 4 of 3 never comes. - tmpCtx, done := context.WithTimeout(context.Background(), time.Millisecond*50) - defer done() - - _, _, err = wrapped.ReadBatch(tmpCtx) - require.Error(t, err) - - // Ack our three messages. - require.NoError(t, aFn1(getCtx(), nil)) - require.NoError(t, aFn2(getCtx(), nil)) - require.NoError(t, aFn3(getCtx(), nil)) - - // Message 4 of 5. - outBatch, aFn4, err := wrapped.ReadBatch(getCtx()) - require.NoError(t, err) - - require.Len(t, outBatch, 1) - mBytes, err = outBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo4", string(mBytes)) - - // Message 5 of 5. - outBatch, aFn5, err := wrapped.ReadBatch(getCtx()) - require.NoError(t, err) - - require.Len(t, outBatch, 1) - mBytes, err = outBatch[0].AsBytes() - require.NoError(t, err) - assert.Equal(t, "foo5", string(mBytes)) - - // Ack last messages. - require.NoError(t, aFn4(getCtx(), nil)) - require.NoError(t, aFn5(getCtx(), nil)) - - require.NoError(t, wrapped.Close(getCtx())) -} diff --git a/public/service/input_test.go b/public/service/input_test.go deleted file mode 100644 index df8c0d85d3..0000000000 --- a/public/service/input_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package service - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fnInput struct { - connect func() error - read func() (*Message, AckFunc, error) - closed bool -} - -func (f *fnInput) Connect(ctx context.Context) error { - return f.connect() -} - -func (f *fnInput) Read(ctx context.Context) (*Message, AckFunc, error) { - return f.read() -} - -func (f *fnInput) Close(ctx context.Context) error { - f.closed = true - return nil -} - -func TestInputAirGapShutdown(t *testing.T) { - i := &fnInput{} - agi := newAirGapReader(i) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, agi.Close(ctx)) - assert.True(t, i.closed) -} - -func TestInputAirGapSad(t *testing.T) { - i := &fnInput{ - connect: func() error { - return errors.New("bad connect") - }, - read: func() (*Message, AckFunc, error) { - return nil, nil, errors.New("bad read") - }, - } - agi := newAirGapReader(i) - - err := agi.Connect(context.Background()) - assert.EqualError(t, err, "bad connect") - - _, _, err = agi.ReadBatch(context.Background()) - assert.EqualError(t, err, "bad read") - - i.read = func() (*Message, AckFunc, error) { - return nil, nil, ErrNotConnected - } - - _, _, err = agi.ReadBatch(context.Background()) - assert.Equal(t, component.ErrNotConnected, err) - - i.read = func() (*Message, AckFunc, error) { - return nil, nil, ErrEndOfInput - } - - _, _, err = agi.ReadBatch(context.Background()) - assert.Equal(t, component.ErrTypeClosed, err) -} - -func TestInputAirGapHappy(t *testing.T) { - var ackErr error - ackFn := func(ctx context.Context, err error) error { - ackErr = err - return nil - } - i := &fnInput{ - connect: func() error { - return nil - }, - read: func() (*Message, AckFunc, error) { - m := &Message{ - part: message.NewPart([]byte("hello world")), - } - return m, ackFn, nil - }, - } - agi := newAirGapReader(i) - - err := agi.Connect(context.Background()) - assert.NoError(t, err) - - outMsg, outAckFn, err := agi.ReadBatch(context.Background()) - assert.NoError(t, err) - assert.Equal(t, 1, outMsg.Len()) - assert.Equal(t, "hello world", string(outMsg.Get(0).AsBytes())) - - assert.NoError(t, outAckFn(context.Background(), errors.New("foobar"))) - assert.EqualError(t, ackErr, "foobar") -} - -type fnBatchInput struct { - connect func() error - read func() (MessageBatch, AckFunc, error) - closed bool -} - -func (f *fnBatchInput) Connect(ctx context.Context) error { - return f.connect() -} - -func (f *fnBatchInput) ReadBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - return f.read() -} - -func (f *fnBatchInput) Close(ctx context.Context) error { - f.closed = true - return nil -} - -func TestBatchInputAirGapShutdown(t *testing.T) { - i := &fnBatchInput{} - agi := newAirGapBatchReader(i) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - require.NoError(t, agi.Close(ctx)) - assert.True(t, i.closed) -} - -func TestBatchInputAirGapSad(t *testing.T) { - i := &fnBatchInput{ - connect: func() error { - return errors.New("bad connect") - }, - read: func() (MessageBatch, AckFunc, error) { - return nil, nil, errors.New("bad read") - }, - } - agi := newAirGapBatchReader(i) - - err := agi.Connect(context.Background()) - assert.EqualError(t, err, "bad connect") - - _, _, err = agi.ReadBatch(context.Background()) - assert.EqualError(t, err, "bad read") - - i.read = func() (MessageBatch, AckFunc, error) { - return nil, nil, ErrNotConnected - } - - _, _, err = agi.ReadBatch(context.Background()) - assert.Equal(t, component.ErrNotConnected, err) - - i.read = func() (MessageBatch, AckFunc, error) { - return nil, nil, ErrEndOfInput - } - - _, _, err = agi.ReadBatch(context.Background()) - assert.Equal(t, component.ErrTypeClosed, err) -} - -func TestBatchInputAirGapSadWithBackOff(t *testing.T) { - i := &fnBatchInput{ - connect: func() error { - return NewErrBackOff(errors.New("bad connect"), time.Second*2) - }, - read: func() (MessageBatch, AckFunc, error) { - return nil, nil, NewErrBackOff(errors.New("bad read"), time.Second*3) - }, - } - agi := newAirGapBatchReader(i) - - err := agi.Connect(context.Background()) - assert.EqualError(t, err, "bad connect") - - var e *component.ErrBackOff - assert.ErrorAs(t, err, &e) - assert.Equal(t, time.Second*2, e.Wait) - assert.EqualError(t, e.Err, "bad connect") - assert.Equal(t, "bad connect", e.Error()) - - _, _, err = agi.ReadBatch(context.Background()) - assert.EqualError(t, err, "bad read") - - assert.ErrorAs(t, err, &e) - assert.Equal(t, time.Second*3, e.Wait) - assert.EqualError(t, e.Err, "bad read") - assert.Equal(t, "bad read", e.Error()) - - i.read = func() (MessageBatch, AckFunc, error) { - return nil, nil, NewErrBackOff(ErrNotConnected, time.Second*2) - } - - _, _, err = agi.ReadBatch(context.Background()) - - assert.ErrorAs(t, err, &e) - assert.Equal(t, time.Second*2, e.Wait) - assert.ErrorIs(t, e.Err, component.ErrNotConnected) - - i.read = func() (MessageBatch, AckFunc, error) { - return nil, nil, ErrEndOfInput - } - - _, _, err = agi.ReadBatch(context.Background()) - assert.Equal(t, component.ErrTypeClosed, err) -} - -func TestBatchInputAirGapHappy(t *testing.T) { - var ackErr error - ackFn := func(ctx context.Context, err error) error { - ackErr = err - return nil - } - i := &fnBatchInput{ - connect: func() error { - return nil - }, - read: func() (MessageBatch, AckFunc, error) { - m := MessageBatch{ - NewMessage([]byte("hello world")), - NewMessage([]byte("this is a test message")), - NewMessage([]byte("and it will work")), - } - return m, ackFn, nil - }, - } - agi := newAirGapBatchReader(i) - - err := agi.Connect(context.Background()) - assert.NoError(t, err) - - outMsg, outAckFn, err := agi.ReadBatch(context.Background()) - assert.NoError(t, err) - assert.Equal(t, 3, outMsg.Len()) - assert.Equal(t, "hello world", string(outMsg.Get(0).AsBytes())) - assert.Equal(t, "this is a test message", string(outMsg.Get(1).AsBytes())) - assert.Equal(t, "and it will work", string(outMsg.Get(2).AsBytes())) - - assert.NoError(t, outAckFn(context.Background(), errors.New("foobar"))) - assert.EqualError(t, ackErr, "foobar") -} diff --git a/public/service/integration/cache_test_definitions.go b/public/service/integration/cache_test_definitions.go deleted file mode 100644 index a8131175cf..0000000000 --- a/public/service/integration/cache_test_definitions.go +++ /dev/null @@ -1,124 +0,0 @@ -package integration - -import ( - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" -) - -// CacheTestOpenClose checks that the cache can be started, an item added, and -// then stopped. -func CacheTestOpenClose() CacheTestDefinition { - return namedCacheTest( - "can open and close", - func(t *testing.T, env *cacheTestEnvironment) { - cache := initCache(t, env) - t.Cleanup(func() { - closeCache(t, cache) - }) - - require.NoError(t, cache.Add(env.ctx, "foo", []byte("bar"), nil)) - - res, err := cache.Get(env.ctx, "foo") - require.NoError(t, err) - assert.Equal(t, "bar", string(res)) - }, - ) -} - -// CacheTestMissingKey checks that we get an error on missing key. -func CacheTestMissingKey() CacheTestDefinition { - return namedCacheTest( - "return consistent error on missing key", - func(t *testing.T, env *cacheTestEnvironment) { - cache := initCache(t, env) - t.Cleanup(func() { - closeCache(t, cache) - }) - - _, err := cache.Get(env.ctx, "missingkey") - assert.EqualError(t, err, "key does not exist") - }, - ) -} - -// CacheTestDoubleAdd ensures that a double add returns an error. -func CacheTestDoubleAdd() CacheTestDefinition { - return namedCacheTest( - "add with duplicate key fails", - func(t *testing.T, env *cacheTestEnvironment) { - cache := initCache(t, env) - t.Cleanup(func() { - closeCache(t, cache) - }) - - require.NoError(t, cache.Add(env.ctx, "addkey", []byte("first"), nil)) - - assert.Eventually(t, func() bool { - return errors.Is(cache.Add(env.ctx, "addkey", []byte("second"), nil), component.ErrKeyAlreadyExists) - }, time.Minute, time.Second) - - res, err := cache.Get(env.ctx, "addkey") - require.NoError(t, err) - assert.Equal(t, "first", string(res)) - }, - ) -} - -// CacheTestDelete checks that deletes work. -func CacheTestDelete() CacheTestDefinition { - return namedCacheTest( - "can set and delete keys", - func(t *testing.T, env *cacheTestEnvironment) { - cache := initCache(t, env) - t.Cleanup(func() { - closeCache(t, cache) - }) - - require.NoError(t, cache.Add(env.ctx, "addkey", []byte("first"), nil)) - - res, err := cache.Get(env.ctx, "addkey") - require.NoError(t, err) - assert.Equal(t, "first", string(res)) - - require.NoError(t, cache.Delete(env.ctx, "addkey")) - - _, err = cache.Get(env.ctx, "addkey") - require.EqualError(t, err, "key does not exist") - }, - ) -} - -// CacheTestGetAndSet checks that we can set and then get n items. -func CacheTestGetAndSet(n int) CacheTestDefinition { - return namedCacheTest( - "can get and set", - func(t *testing.T, env *cacheTestEnvironment) { - cache := initCache(t, env) - t.Cleanup(func() { - closeCache(t, cache) - }) - - for i := 0; i < n; i++ { - key := fmt.Sprintf("key%v", i) - value := fmt.Sprintf("value%v", i) - require.NoError(t, cache.Set(env.ctx, key, []byte(value), nil)) - } - - for i := 0; i < n; i++ { - key := fmt.Sprintf("key%v", i) - value := fmt.Sprintf("value%v", i) - - res, err := cache.Get(env.ctx, key) - require.NoError(t, err) - assert.Equal(t, value, string(res)) - } - }, - ) -} diff --git a/public/service/integration/cache_test_helpers.go b/public/service/integration/cache_test_helpers.go deleted file mode 100644 index fbeaf00e28..0000000000 --- a/public/service/integration/cache_test_helpers.go +++ /dev/null @@ -1,218 +0,0 @@ -package integration - -import ( - "context" - "os" - "strings" - "testing" - "time" - - "github.com/gofrs/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" -) - -// CacheTestConfigVars exposes some variables injected into template configs for -// cache unit tests. -type CacheTestConfigVars struct { - // A unique identifier for separating this test configuration from others. - // Usually used to access a different topic, consumer group, directory, etc. - ID string - - // A Port to use in connector URLs. Allowing tests to override this - // potentially enables tests that check for faulty connections by bridging. - Port string - - // General variables. - General map[string]string -} - -// CachePreTestFn is an optional closure to be called before tests are run, this -// is an opportunity to mutate test config variables and mess with the -// environment. -type CachePreTestFn func(t testing.TB, ctx context.Context, vars *CacheTestConfigVars) - -type cacheTestEnvironment struct { - configTemplate string - configVars CacheTestConfigVars - - preTest CachePreTestFn - - timeout time.Duration - ctx context.Context - log log.Modular - stats *metrics.Namespaced -} - -func newCacheTestEnvironment(t *testing.T, confTemplate string) cacheTestEnvironment { - t.Helper() - - u4, err := uuid.NewV4() - require.NoError(t, err) - - return cacheTestEnvironment{ - configTemplate: confTemplate, - configVars: CacheTestConfigVars{ - ID: u4.String(), - General: map[string]string{}, - }, - timeout: time.Second * 90, - ctx: context.Background(), - log: log.Noop(), - stats: metrics.Noop(), - } -} - -func (e cacheTestEnvironment) RenderConfig() string { - vars := []string{ - "$ID", e.configVars.ID, - "$PORT", e.configVars.Port, - } - for k, v := range e.configVars.General { - vars = append(vars, "$"+k, v) - } - return strings.NewReplacer(vars...).Replace(e.configTemplate) -} - -//------------------------------------------------------------------------------ - -// CacheTestOptFunc is an opt func for customizing the behaviour of cache tests, -// these are useful for things that are integration environment specific, such -// as the port of the service being interacted with. -type CacheTestOptFunc func(*cacheTestEnvironment) - -// CacheTestOptTimeout describes an optional timeout spanning the entirety of -// the test suite. -func CacheTestOptTimeout(timeout time.Duration) CacheTestOptFunc { - return func(env *cacheTestEnvironment) { - env.timeout = timeout - } -} - -// CacheTestOptLogging allows components to log with the given log level. This -// is useful for diagnosing issues. -func CacheTestOptLogging(level string) CacheTestOptFunc { - return func(env *cacheTestEnvironment) { - logConf := log.NewConfig() - logConf.LogLevel = level - var err error - env.log, err = log.New(os.Stdout, ifs.OS(), logConf) - if err != nil { - panic(err) - } - } -} - -// CacheTestOptPort defines the port of the integration service. -func CacheTestOptPort(port string) CacheTestOptFunc { - return func(env *cacheTestEnvironment) { - env.configVars.Port = port - } -} - -// CacheTestOptVarSet sets an arbitrary variable for the test that can be -// injected into templated configs. -func CacheTestOptVarSet(key, value string) CacheTestOptFunc { - return func(env *cacheTestEnvironment) { - env.configVars.General[key] = value - } -} - -// CacheTestOptPreTest adds a closure to be executed before each test. -func CacheTestOptPreTest(fn CachePreTestFn) CacheTestOptFunc { - return func(env *cacheTestEnvironment) { - env.preTest = fn - } -} - -//------------------------------------------------------------------------------ - -type cacheTestDefinitionFn func(*testing.T, *cacheTestEnvironment) - -// CacheTestDefinition encompasses a unit test to be executed against an -// integration environment. These tests are generic and can be run against any -// configuration containing an input and an output that are connected. -type CacheTestDefinition struct { - fn func(*testing.T, *cacheTestEnvironment) -} - -// CacheTestList is a list of cache test definitions that can be run with a -// single template and function args. -type CacheTestList []CacheTestDefinition - -// CacheTests creates a list of tests from variadic arguments. -func CacheTests(tests ...CacheTestDefinition) CacheTestList { - return tests -} - -// Run all the tests against a config template. Tests are run in parallel. -func (i CacheTestList) Run(t *testing.T, configTemplate string, opts ...CacheTestOptFunc) { - for _, test := range i { - env := newCacheTestEnvironment(t, configTemplate) - for _, opt := range opts { - opt(&env) - } - - var done func() - env.ctx, done = context.WithTimeout(env.ctx, env.timeout) - t.Cleanup(done) - - test.fn(t, &env) - } -} - -//------------------------------------------------------------------------------ - -func namedCacheTest(name string, test cacheTestDefinitionFn) CacheTestDefinition { - return CacheTestDefinition{ - fn: func(t *testing.T, env *cacheTestEnvironment) { - t.Run(name, func(t *testing.T) { - t.Parallel() - if env.preTest != nil { - env.preTest(t, env.ctx, &env.configVars) - } - test(t, env) - }) - }, - } -} - -//------------------------------------------------------------------------------ - -func initCache(t *testing.T, env *cacheTestEnvironment) cache.V1 { - t.Helper() - - node, err := docs.UnmarshalYAML([]byte(env.RenderConfig())) - require.NoError(t, err) - - spec := manager.Spec() - lints := spec.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), node) - assert.Empty(t, lints) - - pConf, err := spec.ParsedConfigFromAny(node) - require.NoError(t, err) - - conf, err := manager.FromParsed(bundle.GlobalEnvironment, pConf) - require.NoError(t, err) - - manager, err := manager.New(conf, manager.OptSetLogger(env.log), manager.OptSetMetrics(env.stats)) - require.NoError(t, err) - - var c cache.V1 - require.NoError(t, manager.AccessCache(env.ctx, "testcache", func(v cache.V1) { - c = v - })) - return c -} - -func closeCache(t *testing.T, cache cache.V1) { - require.NoError(t, cache.Close(context.Background())) -} diff --git a/public/service/integration/stream_benchmark_definitions.go b/public/service/integration/stream_benchmark_definitions.go deleted file mode 100644 index ac57ebd47d..0000000000 --- a/public/service/integration/stream_benchmark_definitions.go +++ /dev/null @@ -1,106 +0,0 @@ -package integration - -import ( - "fmt" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/message" -) - -// StreamBenchSend benchmarks the speed at which messages are sent over the -// templated output and then subsequently received from the input with a given -// batch size and parallelism. -func StreamBenchSend(batchSize, parallelism int) StreamBenchDefinition { - return namedBench( - fmt.Sprintf("send message batches %v with parallelism %v", batchSize, parallelism), - func(b *testing.B, env *streamTestEnvironment) { - require.Greater(b, parallelism, 0) - - tranChan := make(chan message.Transaction) - input, output := initConnectors(b, tranChan, env) - b.Cleanup(func() { - closeConnectors(b, env, input, output) - }) - - sends := b.N / batchSize - - set := map[string][]string{} - for j := 0; j < sends; j++ { - for i := 0; i < batchSize; i++ { - payload := fmt.Sprintf("hello world %v", j*sends+i) - set[payload] = nil - } - } - - b.ResetTimer() - - batchChan := make(chan []string) - - var wg sync.WaitGroup - for k := 0; k < parallelism; k++ { - wg.Add(1) - go func() { - defer wg.Done() - for { - batch, open := <-batchChan - if !open { - return - } - assert.NoError(b, sendBatch(env.ctx, b, tranChan, batch)) - } - }() - } - - wg.Add(1) - go func() { - defer wg.Done() - for len(set) > 0 { - messagesInSet(b, true, true, receiveBatch(env.ctx, b, input.TransactionChan(), nil), set) - } - }() - - for j := 0; j < sends; j++ { - payloads := []string{} - for i := 0; i < batchSize; i++ { - payload := fmt.Sprintf("hello world %v", j*sends+i) - payloads = append(payloads, payload) - } - batchChan <- payloads - } - close(batchChan) - - wg.Wait() - }, - ) -} - -// StreamBenchWrite benchmarks the speed at which messages can be written to the -// output, with no attempt made to consume the written data. -func StreamBenchWrite(batchSize int) StreamBenchDefinition { - return namedBench( - fmt.Sprintf("write message batches %v without reading", batchSize), - func(b *testing.B, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - output := initOutput(b, tranChan, env) - b.Cleanup(func() { - closeConnectors(b, env, nil, output) - }) - - sends := b.N / batchSize - - b.ResetTimer() - - batch := make([]string, batchSize) - for j := 0; j < sends; j++ { - for i := 0; i < batchSize; i++ { - batch[i] = fmt.Sprintf(`{"content":"hello world","id":%v}`, j*sends+i) - } - assert.NoError(b, sendBatch(env.ctx, b, tranChan, batch)) - } - }, - ) -} diff --git a/public/service/integration/stream_test_definitions.go b/public/service/integration/stream_test_definitions.go deleted file mode 100644 index aff05af2d7..0000000000 --- a/public/service/integration/stream_test_definitions.go +++ /dev/null @@ -1,835 +0,0 @@ -package integration - -import ( - "context" - "errors" - "fmt" - "strconv" - "sync" - "testing" - "time" - - "github.com/benthosdev/benthos/v4/internal/message" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// StreamTestOpenClose ensures that both the input and output can be started and -// stopped within a reasonable length of time. A single message is sent to check -// the connection. -func StreamTestOpenClose() StreamTestDefinition { - return namedStreamTest( - "can open and close", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - require.NoError(t, sendMessage(env.ctx, t, tranChan, "hello world")) - messageMatch(t, receiveMessage(env.ctx, t, input.TransactionChan(), nil), "hello world") - }, - ) -} - -// StreamTestOpenCloseIsolated ensures that both the input and output can be -// started and stopped within a reasonable length of time. A single message is -// sent to check the connection but the input is only started after the message -// is sent. -func StreamTestOpenCloseIsolated() StreamTestDefinition { - return namedStreamTest( - "can open and close isolated", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - output := initOutput(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - require.NoError(t, sendMessage(env.ctx, t, tranChan, "hello world")) - - input := initInput(t, env) - t.Cleanup(func() { - closeConnectors(t, env, input, nil) - }) - messageMatch(t, receiveMessage(env.ctx, t, input.TransactionChan(), nil), "hello world") - }, - ) -} - -// StreamTestMetadata ensures that we are able to send and receive metadata -// values. -func StreamTestMetadata() StreamTestDefinition { - return namedStreamTest( - "can send and receive metadata", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - require.NoError(t, sendMessage( - env.ctx, t, tranChan, - "hello world", - "foo", "foo_value", - "bar", "bar_value", - )) - messageMatch( - t, receiveMessage(env.ctx, t, input.TransactionChan(), nil), - "hello world", - "foo", "foo_value", - "bar", "bar_value", - ) - }, - ) -} - -// StreamTestMetadataFilter ensures that we are able to send and receive -// metadata values, and that they are filtered. The provided config template -// should inject the variable $OUTPUT_META_EXCLUDE_PREFIX into the output -// metadata filter field. -func StreamTestMetadataFilter() StreamTestDefinition { - return namedStreamTest( - "can send and receive metadata filtered", - func(t *testing.T, env *streamTestEnvironment) { - env.configVars.General["OUTPUT_META_EXCLUDE_PREFIX"] = "f" - - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - require.NoError(t, sendMessage( - env.ctx, t, tranChan, - "hello world", - "foo", "foo_value", - "bar", "bar_value", - )) - - p := receiveMessage(env.ctx, t, input.TransactionChan(), nil) - assert.Empty(t, p.MetaGetStr("foo")) - messageMatch(t, p, "hello world", "bar", "bar_value") - }, - ) -} - -// StreamTestSendBatch ensures we can send a batch of a given size. -func StreamTestSendBatch(n int) StreamTestDefinition { - return namedStreamTest( - "can send a message batch", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - payloads := []string{} - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world %v", i) - set[payload] = nil - payloads = append(payloads, payload) - } - err := sendBatch(env.ctx, t, tranChan, payloads) - assert.NoError(t, err) - - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }, - ) -} - -// StreamTestSendBatches ensures that we can send N batches of M parallelism. -func StreamTestSendBatches(batchSize, batches, parallelism int) StreamTestDefinition { - return namedStreamTest( - "can send many message batches", - func(t *testing.T, env *streamTestEnvironment) { - require.Greater(t, parallelism, 0) - - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - for j := 0; j < batches; j++ { - for i := 0; i < batchSize; i++ { - payload := fmt.Sprintf("hello world %v", j*batches+i) - set[payload] = nil - } - } - - batchChan := make(chan []string) - - var wg sync.WaitGroup - for k := 0; k < parallelism; k++ { - wg.Add(1) - go func() { - defer wg.Done() - for { - batch, open := <-batchChan - if !open { - return - } - assert.NoError(t, sendBatch(env.ctx, t, tranChan, batch)) - } - }() - } - - wg.Add(1) - go func() { - defer wg.Done() - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }() - - for j := 0; j < batches; j++ { - payloads := []string{} - for i := 0; i < batchSize; i++ { - payload := fmt.Sprintf("hello world %v", j*batches+i) - payloads = append(payloads, payload) - } - batchChan <- payloads - } - close(batchChan) - - wg.Wait() - }, - ) -} - -// StreamTestSendBatchCount ensures we can send batches using a configured batch -// count. -func StreamTestSendBatchCount(n int) StreamTestDefinition { - return namedStreamTest( - "can send messages with an output batch count", - func(t *testing.T, env *streamTestEnvironment) { - env.configVars.General["OUTPUT_BATCH_COUNT"] = strconv.Itoa(n) - - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - resChan := make(chan error) - - set := map[string][]string{} - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world %v", i) - set[payload] = nil - msg := message.Batch{ - message.NewPart([]byte(payload)), - } - select { - case tranChan <- message.NewTransaction(msg, resChan): - case res := <-resChan: - t.Fatalf("premature response: %v", res) - case <-env.ctx.Done(): - t.Fatal("timed out on send") - } - } - - for i := 0; i < n; i++ { - select { - case res := <-resChan: - assert.NoError(t, res) - case <-env.ctx.Done(): - t.Fatal("timed out on response") - } - } - - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }, - ) -} - -// StreamTestSendBatchCountIsolated checks batches can be sent and then -// received. The input is created after the output has written data. -func StreamTestSendBatchCountIsolated(n int) StreamTestDefinition { - return namedStreamTest( - "can send messages with an output batch count isolated", - func(t *testing.T, env *streamTestEnvironment) { - env.configVars.General["OUTPUT_BATCH_COUNT"] = strconv.Itoa(n) - - tranChan := make(chan message.Transaction) - output := initOutput(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - resChan := make(chan error) - - set := map[string][]string{} - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world %v", i) - set[payload] = nil - msg := message.Batch{ - message.NewPart([]byte(payload)), - } - select { - case tranChan <- message.NewTransaction(msg, resChan): - case res := <-resChan: - t.Fatalf("premature response: %v", res) - case <-env.ctx.Done(): - t.Fatal("timed out on send") - } - } - - for i := 0; i < n; i++ { - select { - case res := <-resChan: - assert.NoError(t, res) - case <-env.ctx.Done(): - t.Fatal("timed out on response") - } - } - - input := initInput(t, env) - t.Cleanup(func() { - closeConnectors(t, env, input, nil) - }) - - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }, - ) -} - -// StreamTestReceiveBatchCount tests that batches can be consumed with an input -// configured batch count. The batch count should be inserted with the variable -// $INPUT_BATCH_COUNT. -func StreamTestReceiveBatchCount(n int) StreamTestDefinition { - return namedStreamTest( - "can send messages with an input batch count", - func(t *testing.T, env *streamTestEnvironment) { - env.configVars.General["INPUT_BATCH_COUNT"] = strconv.Itoa(n) - - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - - var tran message.Transaction - select { - case tran = <-input.TransactionChan(): - case <-env.ctx.Done(): - t.Fatal("timed out on receive") - } - - assert.Equal(t, n, tran.Payload.Len()) - messagesInSet(t, true, false, tran.Payload, set) - - require.NoError(t, tran.Ack(env.ctx, nil)) - }, - ) -} - -// StreamTestStreamSequential tests that data can be sent and received, where -// data is sent sequentially. -func StreamTestStreamSequential(n int) StreamTestDefinition { - return namedStreamTest( - "can send and receive data sequentially", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }, - ) -} - -// StreamTestStreamIsolated tests that data can be sent and received, where data -// is sent sequentially. The input is created after the output has written data. -func StreamTestStreamIsolated(n int) StreamTestDefinition { - return namedStreamTest( - "can send and receive data isolated", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - output := initOutput(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - set := map[string][]string{} - - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - - input := initInput(t, env) - t.Cleanup(func() { - closeConnectors(t, env, input, nil) - }) - - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }, - ) -} - -// StreamTestCheckpointCapture ensures that data received out of order doesn't -// result in wrongly acknowledged messages. -func StreamTestCheckpointCapture() StreamTestDefinition { - return namedStreamTest( - "respects checkpointed offsets", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - go func() { - require.NoError(t, sendMessage(env.ctx, t, tranChan, "A")) - require.NoError(t, sendMessage(env.ctx, t, tranChan, "B")) - require.NoError(t, sendMessage(env.ctx, t, tranChan, "C")) - require.NoError(t, sendMessage(env.ctx, t, tranChan, "D")) - require.NoError(t, sendMessage(env.ctx, t, tranChan, "E")) - }() - - var msg *message.Part - responseFns := make([]func(context.Context, error) error, 5) - - msg, responseFns[0] = receiveMessageNoRes(env.ctx, t, input.TransactionChan()) - assert.Equal(t, "A", string(msg.AsBytes())) - require.NoError(t, responseFns[0](env.ctx, nil)) - - msg, responseFns[1] = receiveMessageNoRes(env.ctx, t, input.TransactionChan()) - assert.Equal(t, "B", string(msg.AsBytes())) - require.NoError(t, responseFns[1](env.ctx, nil)) - - msg, responseFns[2] = receiveMessageNoRes(env.ctx, t, input.TransactionChan()) - assert.Equal(t, "C", string(msg.AsBytes())) - - msg, responseFns[3] = receiveMessageNoRes(env.ctx, t, input.TransactionChan()) - assert.Equal(t, "D", string(msg.AsBytes())) - require.NoError(t, responseFns[3](env.ctx, nil)) - - msg, responseFns[4] = receiveMessageNoRes(env.ctx, t, input.TransactionChan()) - assert.Equal(t, "E", string(msg.AsBytes())) - - require.NoError(t, responseFns[2](env.ctx, errors.New("rejecting just cus"))) - require.NoError(t, responseFns[4](env.ctx, errors.New("rejecting just cus"))) - - closeConnectors(t, env, input, nil) - - select { - case <-time.After(time.Second * 5): - case <-env.ctx.Done(): - t.Fatal(env.ctx.Err()) - } - - input = initInput(t, env) - t.Cleanup(func() { - closeConnectors(t, env, input, nil) - }) - - msg = receiveMessage(env.ctx, t, input.TransactionChan(), nil) - assert.Equal(t, "C", string(msg.AsBytes())) - - msg = receiveMessage(env.ctx, t, input.TransactionChan(), nil) - assert.Equal(t, "D", string(msg.AsBytes())) - - msg = receiveMessage(env.ctx, t, input.TransactionChan(), nil) - assert.Equal(t, "E", string(msg.AsBytes())) - }, - ) -} - -// StreamTestStreamParallel tests data transfer with parallel senders. -func StreamTestStreamParallel(n int) StreamTestDefinition { - return namedStreamTest( - "can send and receive data in parallel", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction, n) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - } - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - }() - - go func() { - defer wg.Done() - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }() - - wg.Wait() - }, - ) -} - -// StreamTestStreamSaturatedUnacked writes N messages as a backlog, then -// consumes half of those messages without acking them, and then after a pause -// acknowledges them all and resumes consuming. -// -// The purpose of this test is to ensure that after a period of back pressure is -// applied the input correctly resumes. -func StreamTestStreamSaturatedUnacked(n int) StreamTestDefinition { - return namedStreamTest( - "can consume data without acking and resume", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction, n) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - for i := 0; i < n*2; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - - resFns := make([]func(context.Context, error) error, n/2) - for i := range resFns { - var b message.Batch - b, resFns[i] = receiveBatchNoRes(env.ctx, t, input.TransactionChan()) - messagesInSet(t, true, env.allowDuplicateMessages, b, set) - } - - <-time.After(time.Second * 5) - for _, rFn := range resFns { - require.NoError(t, rFn(env.ctx, nil)) - } - - // Consume all remaining messages - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }, - ) -} - -// StreamTestAtLeastOnceDelivery ensures data is delivered through nacks and -// restarts. -func StreamTestAtLeastOnceDelivery() StreamTestDefinition { - return namedStreamTest( - "at least once delivery", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - expectedMessages := map[string]struct{}{ - "A": {}, "B": {}, "C": {}, "D": {}, "E": {}, - } - go func() { - for k := range expectedMessages { - require.NoError(t, sendMessage(env.ctx, t, tranChan, k)) - } - }() - - var msg *message.Part - badResponseFns := []func(context.Context, error) error{} - - for i := 0; i < len(expectedMessages); i++ { - msg, responseFn := receiveMessageNoRes(env.ctx, t, input.TransactionChan()) - key := string(msg.AsBytes()) - assert.Contains(t, expectedMessages, key) - delete(expectedMessages, key) - if key != "C" && key != "E" { - require.NoError(t, responseFn(env.ctx, nil)) - } else { - badResponseFns = append(badResponseFns, responseFn) - } - } - - for _, rFn := range badResponseFns { - require.NoError(t, rFn(env.ctx, errors.New("rejecting just cus"))) - } - - select { - case <-time.After(time.Second * 5): - case <-env.ctx.Done(): - t.Fatal(env.ctx.Err()) - } - - closeConnectors(t, env, input, nil) - - select { - case <-time.After(time.Second * 5): - case <-env.ctx.Done(): - t.Fatal(env.ctx.Err()) - } - - input = initInput(t, env) - t.Cleanup(func() { - closeConnectors(t, env, input, nil) - }) - - expectedMessages = map[string]struct{}{ - "C": {}, "E": {}, - } - - for i := 0; i < len(expectedMessages); i++ { - msg = receiveMessage(env.ctx, t, input.TransactionChan(), nil) - key := string(msg.AsBytes()) - assert.Contains(t, expectedMessages, key) - delete(expectedMessages, key) - } - }, - ) -} - -// StreamTestStreamParallelLossy ensures that data is delivered through parallel -// nacks. -func StreamTestStreamParallelLossy(n int) StreamTestDefinition { - return namedStreamTest( - "can send and receive data in parallel lossy", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, input, output) - }) - - set := map[string][]string{} - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - } - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - }() - - go func() { - defer wg.Done() - rejected := 0 - for i := 0; i < n; i++ { - if i%10 == 1 { - rejected++ - messagesInSet( - t, false, true, - receiveBatch(env.ctx, t, input.TransactionChan(), errors.New("rejected just cus")), - set, - ) - } else { - messagesInSet(t, true, true, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - } - - t.Log("Finished first loop, looping through rejected messages.") - for len(set) > 0 { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }() - - wg.Wait() - }, - ) -} - -// StreamTestStreamParallelLossyThroughReconnect ensures data is delivered -// through nacks and restarts. -func StreamTestStreamParallelLossyThroughReconnect(n int) StreamTestDefinition { - return namedStreamTest( - "can send and receive data in parallel lossy through reconnect", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - input, output := initConnectors(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - set := map[string][]string{} - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - set[payload] = nil - } - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - for i := 0; i < n; i++ { - payload := fmt.Sprintf("hello world: %v", i) - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload)) - } - }() - - go func() { - defer wg.Done() - rejected := 0 - for i := 0; i < n; i++ { - if i%10 == 1 { - rejected++ - messagesInSet( - t, false, env.allowDuplicateMessages, - receiveBatch(env.ctx, t, input.TransactionChan(), errors.New("rejected just cus")), - set, - ) - } else { - messagesInSet(t, true, env.allowDuplicateMessages, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - } - - closeConnectors(t, env, input, nil) - - input = initInput(t, env) - t.Cleanup(func() { - closeConnectors(t, env, input, nil) - }) - - t.Log("Finished first loop, looping through rejected messages.") - for len(set) > 0 { - messagesInSet(t, true, true, receiveBatch(env.ctx, t, input.TransactionChan(), nil), set) - } - }() - - wg.Wait() - }, - ) -} - -// GetMessageFunc is a closure used to extract message contents from an output -// directly and can be used to test outputs without the need for an input in the -// config template. -type GetMessageFunc func(ctx context.Context, testID, messageID string) (string, []string, error) - -// StreamTestOutputOnlySendSequential tests a config template without an input. -func StreamTestOutputOnlySendSequential(n int, getFn GetMessageFunc) StreamTestDefinition { - return namedStreamTest( - "can send to output", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - output := initOutput(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - set := map[string]string{} - for i := 0; i < n; i++ { - id := strconv.Itoa(i) - payload := fmt.Sprintf(`{"content":"hello world","id":%v}`, id) - set[id] = payload - require.NoError(t, sendMessage(env.ctx, t, tranChan, payload, "id", id)) - } - - for k, exp := range set { - act, _, err := getFn(env.ctx, env.configVars.ID, k) - require.NoError(t, err) - assert.Equal(t, exp, act) - } - }, - ) -} - -// StreamTestOutputOnlySendBatch tests a config template without an input. -func StreamTestOutputOnlySendBatch(n int, getFn GetMessageFunc) StreamTestDefinition { - return namedStreamTest( - "can send to output as batch", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - output := initOutput(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - set := map[string]string{} - batch := []string{} - for i := 0; i < n; i++ { - id := strconv.Itoa(i) - payload := fmt.Sprintf(`{"content":"hello world","id":%v}`, id) - set[id] = payload - batch = append(batch, payload) - } - require.NoError(t, sendBatch(env.ctx, t, tranChan, batch)) - - for k, exp := range set { - act, _, err := getFn(env.ctx, env.configVars.ID, k) - require.NoError(t, err) - assert.Equal(t, exp, act) - } - }, - ) -} - -// StreamTestOutputOnlyOverride tests a config template without an input where -// duplicate IDs are sent (where we expect updates). -func StreamTestOutputOnlyOverride(getFn GetMessageFunc) StreamTestDefinition { - return namedStreamTest( - "can send to output and override value", - func(t *testing.T, env *streamTestEnvironment) { - tranChan := make(chan message.Transaction) - output := initOutput(t, tranChan, env) - t.Cleanup(func() { - closeConnectors(t, env, nil, output) - }) - - first := `{"content":"this should be overridden","id":1}` - exp := `{"content":"hello world","id":1}` - require.NoError(t, sendMessage(env.ctx, t, tranChan, first)) - require.NoError(t, sendMessage(env.ctx, t, tranChan, exp)) - - act, _, err := getFn(env.ctx, env.configVars.ID, "1") - require.NoError(t, err) - assert.Equal(t, exp, act) - }, - ) -} diff --git a/public/service/integration/stream_test_helpers.go b/public/service/integration/stream_test_helpers.go deleted file mode 100644 index a65c0ce4dd..0000000000 --- a/public/service/integration/stream_test_helpers.go +++ /dev/null @@ -1,588 +0,0 @@ -package integration - -import ( - "context" - "flag" - "fmt" - "net" - "os" - "regexp" - "strconv" - "strings" - "testing" - "time" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - - "github.com/gofrs/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// CheckSkip marks a test to be skipped unless the integration test has been -// specifically requested using the -run flag. -func CheckSkip(t testing.TB) { - if m := flag.Lookup("test.run").Value.String(); m == "" || regexp.MustCompile(strings.Split(m, "/")[0]).FindString(t.Name()) == "" { - t.Skip("Skipping as execution was not requested explicitly using go test -run ^Test.*Integration.*$") - } -} - -// CheckSkipExact skips a test unless the -run flag specifically targets it. -func CheckSkipExact(t testing.TB) { - if m := flag.Lookup("test.run").Value.String(); m == "" || m != t.Name() { - t.Skipf("Skipping as execution was not requested explicitly using go test -run %v", t.Name()) - } -} - -// GetFreePort attempts to get a free port. This involves creating a bind and -// then immediately dropping it and so it's ever so slightly flakey. -func GetFreePort() (int, error) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - return 0, err - } - port := listener.Addr().(*net.TCPAddr).Port - _ = listener.Close() - return port, nil -} - -// StreamTestConfigVars defines variables that will be accessed by test -// definitions when generating components through the config template. The main -// value is the id, which is generated for each test for isolation, and the port -// which is injected into the config template. -type StreamTestConfigVars struct { - // A unique identifier for separating this test configuration from others. - // Usually used to access a different topic, consumer group, directory, etc. - ID string - - // A Port to use in connector URLs. Allowing tests to override this - // potentially enables tests that check for faulty connections by bridging. - Port string - - // General variables. - General map[string]string -} - -// StreamPreTestFn is an optional closure to be called before tests are run, -// this is an opportunity to mutate test config variables and mess with the -// environment. -type StreamPreTestFn func(t testing.TB, ctx context.Context, vars *StreamTestConfigVars) - -type streamTestEnvironment struct { - configTemplate string - configVars StreamTestConfigVars - - preTest StreamPreTestFn - - timeout time.Duration - ctx context.Context - log log.Modular - stats *metrics.Namespaced - mgr bundle.NewManagement - - allowDuplicateMessages bool - - // Ugly work arounds for slow connectors. - sleepAfterInput time.Duration - sleepAfterOutput time.Duration -} - -func newStreamTestEnvironment(t testing.TB, confTemplate string) streamTestEnvironment { - t.Helper() - - u4, err := uuid.NewV4() - require.NoError(t, err) - - return streamTestEnvironment{ - configTemplate: confTemplate, - configVars: StreamTestConfigVars{ - ID: u4.String(), - General: map[string]string{ - "MAX_IN_FLIGHT": "1", - "INPUT_BATCH_COUNT": "0", - "OUTPUT_BATCH_COUNT": "0", - "OUTPUT_META_EXCLUDE_PREFIX": "", - }, - }, - timeout: time.Second * 90, - ctx: context.Background(), - log: log.Noop(), - stats: metrics.Noop(), - } -} - -func (e streamTestEnvironment) RenderConfig() string { - vars := []string{ - "$ID", e.configVars.ID, - "$PORT", e.configVars.Port, - } - for k, v := range e.configVars.General { - vars = append(vars, "$"+k, v) - } - return strings.NewReplacer(vars...).Replace(e.configTemplate) -} - -//------------------------------------------------------------------------------ - -// StreamTestOptFunc is an opt func for customizing the behaviour of stream -// tests, these are useful for things that are integration environment specific, -// such as the port of the service being interacted with. -type StreamTestOptFunc func(*streamTestEnvironment) - -// StreamTestOptTimeout describes an optional timeout spanning the entirety of -// the test suite. -func StreamTestOptTimeout(timeout time.Duration) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.timeout = timeout - } -} - -// StreamTestOptAllowDupes specifies across all stream tests that in this -// environment we can expect duplicates and these are not considered errors. -func StreamTestOptAllowDupes() StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.allowDuplicateMessages = true - } -} - -// StreamTestOptMaxInFlight configures a maximum inflight (to be injected into -// the config template) for all tests. -func StreamTestOptMaxInFlight(n int) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.configVars.General["MAX_IN_FLIGHT"] = strconv.Itoa(n) - } -} - -// StreamTestOptLogging allows components to log with the given log level. This -// is useful for diagnosing issues. -func StreamTestOptLogging(level string) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - logConf := log.NewConfig() - logConf.LogLevel = level - var err error - env.log, err = log.New(os.Stdout, ifs.OS(), logConf) - if err != nil { - panic(err) - } - } -} - -// StreamTestOptPort defines the port of the integration service. -func StreamTestOptPort(port string) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.configVars.Port = port - } -} - -// StreamTestOptVarSet sets an arbitrary variable for the test that can -// be injected into templated configs. -func StreamTestOptVarSet(key, value string) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.configVars.General[key] = value - } -} - -// StreamTestOptSleepAfterInput adds a sleep to tests after the input has been -// created. -func StreamTestOptSleepAfterInput(t time.Duration) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.sleepAfterInput = t - } -} - -// StreamTestOptSleepAfterOutput adds a sleep to tests after the output has been -// created. -func StreamTestOptSleepAfterOutput(t time.Duration) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.sleepAfterOutput = t - } -} - -// StreamTestOptPreTest adds a closure to be executed before each test. -func StreamTestOptPreTest(fn StreamPreTestFn) StreamTestOptFunc { - return func(env *streamTestEnvironment) { - env.preTest = fn - } -} - -//------------------------------------------------------------------------------ - -type streamTestDefinitionFn func(*testing.T, *streamTestEnvironment) - -// StreamTestDefinition encompasses a unit test to be executed against an -// integration environment. These tests are generic and can be run against any -// configuration containing an input and an output that are connected. -type StreamTestDefinition struct { - fn func(*testing.T, *streamTestEnvironment) -} - -// StreamTestList is a list of stream definitions that can be run with a single -// template and function args. -type StreamTestList []StreamTestDefinition - -// StreamTests creates a list of tests from variadic arguments. -func StreamTests(tests ...StreamTestDefinition) StreamTestList { - return tests -} - -// Run all the tests against a config template. Tests are run in parallel. -func (i StreamTestList) Run(t *testing.T, configTemplate string, opts ...StreamTestOptFunc) { - envs := make([]streamTestEnvironment, len(i)) - - for j := range i { - envs[j] = newStreamTestEnvironment(t, configTemplate) - for _, opt := range opts { - opt(&envs[j]) - } - - timeout := envs[j].timeout - if deadline, ok := t.Deadline(); ok { - timeout = time.Until(deadline) - (time.Second * 5) - } - - var done func() - envs[j].ctx, done = context.WithTimeout(envs[j].ctx, timeout) - t.Cleanup(done) - } - - for j, test := range i { - if envs[j].configVars.Port == "" { - p, err := GetFreePort() - if err != nil { - t.Fatal(err) - } - envs[j].configVars.Port = strconv.Itoa(p) - } - test.fn(t, &envs[j]) - } -} - -//------------------------------------------------------------------------------ - -func namedStreamTest(name string, test streamTestDefinitionFn) StreamTestDefinition { - return StreamTestDefinition{ - fn: func(t *testing.T, env *streamTestEnvironment) { - t.Run(name, func(t *testing.T) { - t.Parallel() - if env.preTest != nil { - env.preTest(t, env.ctx, &env.configVars) - } - test(t, env) - }) - }, - } -} - -//------------------------------------------------------------------------------ - -type streamBenchDefinitionFn func(*testing.B, *streamTestEnvironment) - -// StreamBenchDefinition encompasses a benchmark to be executed against an -// integration environment. These tests are generic and can be run against any -// configuration containing an input and an output that are connected. -type StreamBenchDefinition struct { - fn streamBenchDefinitionFn -} - -// StreamBenchList is a list of stream benchmark definitions that can be run -// with a single template and function args. -type StreamBenchList []StreamBenchDefinition - -// StreamBenchs creates a list of benchmarks from variadic arguments. -func StreamBenchs(tests ...StreamBenchDefinition) StreamBenchList { - return tests -} - -// Run the benchmarks against a config template. -func (i StreamBenchList) Run(b *testing.B, configTemplate string, opts ...StreamTestOptFunc) { - for _, bench := range i { - env := newStreamTestEnvironment(b, configTemplate) - for _, opt := range opts { - opt(&env) - } - - if env.preTest != nil { - env.preTest(b, env.ctx, &env.configVars) - } - bench.fn(b, &env) - } -} - -func namedBench(name string, test streamBenchDefinitionFn) StreamBenchDefinition { - return StreamBenchDefinition{ - fn: func(b *testing.B, env *streamTestEnvironment) { - b.Run(name, func(b *testing.B) { - test(b, env) - }) - }, - } -} - -//------------------------------------------------------------------------------ - -func initConnectors( - t testing.TB, - trans <-chan message.Transaction, - env *streamTestEnvironment, -) (input input.Streamed, output output.Streamed) { - t.Helper() - - out := initOutput(t, trans, env) - in := initInput(t, env) - return in, out -} - -func initInput(t testing.TB, env *streamTestEnvironment) input.Streamed { - t.Helper() - - node, err := docs.UnmarshalYAML([]byte(env.RenderConfig())) - require.NoError(t, err) - - spec := docs.FieldSpecs{ - docs.FieldAnything("output", "").Optional(), - docs.FieldInput("input", "An input to source messages from."), - } - spec = append(spec, manager.Spec()...) - - lints := spec.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), node) - assert.Empty(t, lints) - - pConf, err := spec.ParsedConfigFromAny(node) - require.NoError(t, err) - - pVal, err := pConf.FieldAny("input") - require.NoError(t, err) - - iConf, err := input.FromAny(bundle.GlobalEnvironment, pVal) - require.NoError(t, err) - - mConf, err := manager.FromParsed(bundle.GlobalEnvironment, pConf) - require.NoError(t, err) - - if env.mgr == nil { - env.mgr, err = manager.New(mConf, manager.OptSetLogger(env.log), manager.OptSetMetrics(env.stats)) - require.NoError(t, err) - } - - input, err := env.mgr.NewInput(iConf) - require.NoError(t, err) - - if env.sleepAfterInput > 0 { - time.Sleep(env.sleepAfterInput) - } - - return input -} - -func initOutput(t testing.TB, trans <-chan message.Transaction, env *streamTestEnvironment) output.Streamed { - t.Helper() - - node, err := docs.UnmarshalYAML([]byte(env.RenderConfig())) - require.NoError(t, err) - - spec := docs.FieldSpecs{ - docs.FieldAnything("input", "").Optional(), - docs.FieldOutput("output", "An output to source messages to."), - } - spec = append(spec, manager.Spec()...) - - lints := spec.LintYAML(docs.NewLintContext(docs.NewLintConfig(bundle.GlobalEnvironment)), node) - assert.Empty(t, lints) - - pConf, err := spec.ParsedConfigFromAny(node) - require.NoError(t, err) - - pVal, err := pConf.FieldAny("output") - require.NoError(t, err) - - oConf, err := output.FromAny(bundle.GlobalEnvironment, pVal) - require.NoError(t, err) - - mConf, err := manager.FromParsed(bundle.GlobalEnvironment, pConf) - require.NoError(t, err) - - if env.mgr == nil { - env.mgr, err = manager.New(mConf, manager.OptSetLogger(env.log), manager.OptSetMetrics(env.stats)) - require.NoError(t, err) - } - - output, err := env.mgr.NewOutput(oConf) - require.NoError(t, err) - - require.NoError(t, output.Consume(trans)) - - if env.sleepAfterOutput > 0 { - time.Sleep(env.sleepAfterOutput) - } - - return output -} - -func closeConnectors(t testing.TB, env *streamTestEnvironment, input input.Streamed, output output.Streamed) { - if output != nil { - output.TriggerCloseNow() - require.NoError(t, output.WaitForClose(env.ctx)) - } - if input != nil { - input.TriggerStopConsuming() - require.NoError(t, input.WaitForClose(env.ctx)) - } -} - -func sendMessage( - ctx context.Context, - t testing.TB, - tranChan chan message.Transaction, - content string, - metadata ...string, -) error { - t.Helper() - - p := message.NewPart([]byte(content)) - for i := 0; i < len(metadata); i += 2 { - p.MetaSetMut(metadata[i], metadata[i+1]) - } - msg := message.Batch{p} - resChan := make(chan error) - - select { - case tranChan <- message.NewTransaction(msg, resChan): - case <-ctx.Done(): - t.Fatal("timed out on send") - } - - select { - case res := <-resChan: - return res - case <-ctx.Done(): - } - t.Fatal("timed out on response") - return nil -} - -func sendBatch( - ctx context.Context, - t testing.TB, - tranChan chan message.Transaction, - content []string, -) error { - t.Helper() - - msg := message.QuickBatch(nil) - for _, payload := range content { - msg = append(msg, message.NewPart([]byte(payload))) - } - - resChan := make(chan error) - - select { - case tranChan <- message.NewTransaction(msg, resChan): - case <-ctx.Done(): - t.Fatal("timed out on send") - } - - select { - case res := <-resChan: - return res - case <-ctx.Done(): - } - - t.Fatal("timed out on response") - return nil -} - -func receiveMessage( - ctx context.Context, - t testing.TB, - tranChan <-chan message.Transaction, - err error, -) *message.Part { - t.Helper() - - b, ackFn := receiveBatchNoRes(ctx, t, tranChan) - require.NoError(t, ackFn(ctx, err)) - require.Len(t, b, 1) - - return b.Get(0) -} - -func receiveBatch( - ctx context.Context, - t testing.TB, - tranChan <-chan message.Transaction, - err error, -) message.Batch { - t.Helper() - - b, ackFn := receiveBatchNoRes(ctx, t, tranChan) - require.NoError(t, ackFn(ctx, err)) - return b -} - -func receiveBatchNoRes(ctx context.Context, t testing.TB, tranChan <-chan message.Transaction) (message.Batch, func(context.Context, error) error) { //nolint: gocritic // Ignore unnamedResult false positive - t.Helper() - - var tran message.Transaction - var open bool - select { - case tran, open = <-tranChan: - case <-ctx.Done(): - t.Fatal("timed out on receive") - } - - require.True(t, open) - return tran.Payload, tran.Ack -} - -func receiveMessageNoRes(ctx context.Context, t testing.TB, tranChan <-chan message.Transaction) (*message.Part, func(context.Context, error) error) { //nolint: gocritic // Ignore unnamedResult false positive - t.Helper() - - b, fn := receiveBatchNoRes(ctx, t, tranChan) - require.Len(t, b, 1) - - return b.Get(0), fn -} - -func messageMatch(t testing.TB, p *message.Part, content string, metadata ...string) { - t.Helper() - - assert.Equal(t, content, string(p.AsBytes())) - - allMetadata := map[string]string{} - _ = p.MetaIterStr(func(k, v string) error { - allMetadata[k] = v - return nil - }) - - for i := 0; i < len(metadata); i += 2 { - assert.Equal(t, metadata[i+1], p.MetaGetStr(metadata[i]), fmt.Sprintf("metadata: %v", allMetadata)) - } -} - -func messagesInSet(t testing.TB, pop, allowDupes bool, b message.Batch, set map[string][]string) { - t.Helper() - - for _, p := range b { - metadata, exists := set[string(p.AsBytes())] - if allowDupes && !exists { - return - } - require.True(t, exists, "in set: %v, set: %v", string(p.AsBytes()), set) - - for i := 0; i < len(metadata); i += 2 { - assert.Equal(t, metadata[i+1], p.MetaGetStr(metadata[i])) - } - - if pop { - delete(set, string(p.AsBytes())) - } - } -} diff --git a/public/service/interpolated_string.go b/public/service/interpolated_string.go deleted file mode 100644 index 68cef67d5f..0000000000 --- a/public/service/interpolated_string.go +++ /dev/null @@ -1,73 +0,0 @@ -package service - -import ( - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/field" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// InterpolatedString resolves a string containing dynamic interpolation -// functions for a given message. -type InterpolatedString struct { - expr *field.Expression -} - -// NewInterpolatedString parses an interpolated string expression. -func NewInterpolatedString(expr string) (*InterpolatedString, error) { - e, err := bloblang.GlobalEnvironment().NewField(expr) - if err != nil { - return nil, err - } - return &InterpolatedString{expr: e}, nil -} - -type fauxOldMessage struct { - p *message.Part -} - -func (f fauxOldMessage) Get(i int) *message.Part { - return f.p -} - -func (f fauxOldMessage) Len() int { - return 1 -} - -// Static returns the underlying contents of the interpolated string only if it -// contains zero dynamic expressions, and is therefore static, otherwise an -// empty string is returned. A second boolean parameter is also returned -// indicating whether the string was static, helping to distinguish between a -// static empty string versus a non-static string. -func (i *InterpolatedString) Static() (string, bool) { - if i.expr.NumDynamicExpressions() > 0 { - return "", false - } - s, _ := i.expr.String(0, nil) - return s, true -} - -// TryString resolves the interpolated field for a given message as a string, -// returns an error if any interpolation functions fail. -func (i *InterpolatedString) TryString(m *Message) (string, error) { - return i.expr.String(0, fauxOldMessage{m.part}) -} - -// TryBytes resolves the interpolated field for a given message as a slice of -// bytes, returns an error if any interpolation functions fail. -func (i *InterpolatedString) TryBytes(m *Message) ([]byte, error) { - return i.expr.Bytes(0, fauxOldMessage{m.part}) -} - -// String resolves the interpolated field for a given message as a string. -// Deprecated: Use TryString instead in order to capture errors. -func (i *InterpolatedString) String(m *Message) string { - s, _ := i.expr.String(0, fauxOldMessage{m.part}) - return s -} - -// Bytes resolves the interpolated field for a given message as a byte slice. -// Deprecated: Use TryBytes instead in order to capture errors. -func (i *InterpolatedString) Bytes(m *Message) []byte { - b, _ := i.expr.Bytes(0, fauxOldMessage{m.part}) - return b -} diff --git a/public/service/interpolated_string_test.go b/public/service/interpolated_string_test.go deleted file mode 100644 index 502c0d31bc..0000000000 --- a/public/service/interpolated_string_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package service - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestInterpolatedString(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - expr string - msg *Message - expected string - }{ - { - name: "content interpolation", - expr: `foo ${! content() } bar`, - msg: NewMessage([]byte("hello world")), - expected: `foo hello world bar`, - }, - { - name: "no interpolation", - expr: `foo bar`, - msg: NewMessage([]byte("hello world")), - expected: `foo bar`, - }, - { - name: "metadata interpolation", - expr: `foo ${! meta("var1") } bar`, - msg: func() *Message { - m := NewMessage([]byte("hello world")) - m.MetaSet("var1", "value1") - return m - }(), - expected: `foo value1 bar`, - }, - } - - for _, test := range tests { - test := test - - t.Run("deprecated api/"+test.name, func(t *testing.T) { - t.Parallel() - - i, err := NewInterpolatedString(test.expr) - require.NoError(t, err) - assert.Equal(t, test.expected, i.String(test.msg)) - assert.Equal(t, test.expected, string(i.Bytes(test.msg))) - }) - - t.Run("recommended api/"+test.name, func(t *testing.T) { - t.Parallel() - - i, err := NewInterpolatedString(test.expr) - require.NoError(t, err) - - { - got, err := i.TryString(test.msg) - require.NoError(t, err) - - assert.Equal(t, test.expected, got) - } - - { - got, err := i.TryBytes(test.msg) - require.NoError(t, err) - - assert.Equal(t, test.expected, string(got)) - } - }) - } -} - -func TestInterpolatedStringCtor(t *testing.T) { - t.Parallel() - - i, err := NewInterpolatedString(`foo ${! meta("var1") bar`) - - assert.EqualError(t, err, "required: expected end of expression, got: bar") - assert.Nil(t, i) -} - -func TestInterpolatedStringMethods(t *testing.T) { - t.Parallel() - - i, err := NewInterpolatedString(`foo ${! meta("var1") + 1 } bar`) - require.NoError(t, err) - - m := NewMessage([]byte("hello world")) - m.MetaSet("var1", "value1") - - { - got, err := i.TryString(m) - require.EqualError(t, err, "cannot add types string (from meta field var1) and number (from number literal)") - require.Empty(t, got) - } - - { - got, err := i.TryBytes(m) - require.EqualError(t, err, "cannot add types string (from meta field var1) and number (from number literal)") - require.Empty(t, got) - } -} diff --git a/public/service/lints.go b/public/service/lints.go deleted file mode 100644 index ce2e4ba467..0000000000 --- a/public/service/lints.go +++ /dev/null @@ -1,170 +0,0 @@ -package service - -import ( - "bytes" - "errors" - "fmt" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// LintType is a discrete linting type. -// NOTE: These should be kept in sync with ./internal/docs/field.go:586. -type LintType int - -const ( - // LintCustom means a custom linting rule failed. - LintCustom LintType = iota - - // LintFailedRead means a configuration could not be read. - LintFailedRead LintType = iota - - // LintMissingEnvVar means a configuration contained an environment variable - // interpolation without a default and the variable was undefined. - LintMissingEnvVar LintType = iota - - // LintInvalidOption means the field value was not one of the explicit list - // of options. - LintInvalidOption LintType = iota - - // LintBadLabel means the label contains invalid characters. - LintBadLabel LintType = iota - - // LintMissingLabel means the label is missing when required. - LintMissingLabel LintType = iota - - // LintDuplicateLabel means the label collides with another label. - LintDuplicateLabel LintType = iota - - // LintBadBloblang means the field contains invalid Bloblang. - LintBadBloblang LintType = iota - - // LintShouldOmit means the field should be omitted. - LintShouldOmit LintType = iota - - // LintComponentMissing means a component value was expected but the type is - // missing. - LintComponentMissing LintType = iota - - // LintComponentNotFound means the specified component value is not - // recognised. - LintComponentNotFound LintType = iota - - // LintUnknown means the field is unknown. - LintUnknown LintType = iota - - // LintMissing means a field was required but missing. - LintMissing LintType = iota - - // LintExpectedArray means an array value was expected but something else - // was provided. - LintExpectedArray LintType = iota - - // LintExpectedObject means an object value was expected but something else - // was provided. - LintExpectedObject LintType = iota - - // LintExpectedScalar means a scalar value was expected but something else - // was provided. - LintExpectedScalar LintType = iota - - // LintDeprecated means a field is deprecated and should not be used. - LintDeprecated LintType = iota -) - -func convertDocsLintType(d docs.LintType) LintType { - switch d { - case docs.LintCustom: - return LintCustom - case docs.LintFailedRead: - return LintFailedRead - case docs.LintMissingEnvVar: - return LintMissingEnvVar - case docs.LintInvalidOption: - return LintInvalidOption - case docs.LintBadLabel: - return LintBadLabel - case docs.LintMissingLabel: - return LintMissingLabel - case docs.LintDuplicateLabel: - return LintDuplicateLabel - case docs.LintBadBloblang: - return LintBadBloblang - case docs.LintShouldOmit: - return LintShouldOmit - case docs.LintComponentMissing: - return LintComponentMissing - case docs.LintComponentNotFound: - return LintComponentNotFound - case docs.LintUnknown: - return LintUnknown - case docs.LintMissing: - return LintMissing - case docs.LintExpectedArray: - return LintExpectedArray - case docs.LintExpectedObject: - return LintExpectedObject - case docs.LintExpectedScalar: - return LintExpectedScalar - case docs.LintDeprecated: - return LintDeprecated - } - return LintCustom -} - -// Lint represents a configuration file linting error. -type Lint struct { - Line int - Column int - Type LintType - What string -} - -// Error returns an error string. -func (l Lint) Error() string { - return fmt.Sprintf("(%v,%v) %v", l.Line, l.Column, l.What) -} - -// LintError is an error type that represents one or more configuration file -// linting errors that were encountered. -type LintError []Lint - -// Error returns an error string. -func (e LintError) Error() string { - var lintsCollapsed bytes.Buffer - for i, l := range e { - if i > 0 { - lintsCollapsed.WriteString("\n") - } - fmt.Fprint(&lintsCollapsed, l.Error()) - } - return fmt.Sprintf("lint errors: %v", lintsCollapsed.String()) -} - -func convertDocsLint(l docs.Lint) Lint { - return Lint{ - Line: l.Line, - Column: l.Column, - Type: convertDocsLintType(l.Type), - What: l.What, - } -} - -func lintsToErr(lints []docs.Lint) error { - if len(lints) == 0 { - return nil - } - var e LintError - for _, l := range lints { - e = append(e, convertDocsLint(l)) - } - return e -} - -func convertDocsLintErr(err error) error { - var l docs.Lint - if errors.As(err, &l) { - return convertDocsLint(l) - } - return err -} diff --git a/public/service/logger.go b/public/service/logger.go deleted file mode 100644 index 484c64aef1..0000000000 --- a/public/service/logger.go +++ /dev/null @@ -1,121 +0,0 @@ -package service - -import ( - "fmt" - - "github.com/benthosdev/benthos/v4/internal/log" -) - -// Logger allows plugin authors to write custom logs from components that are -// exported the same way as native Benthos logs. It's safe to pass around a nil -// pointer for testing components. -type Logger struct { - m log.Modular -} - -func newReverseAirGapLogger(l log.Modular) *Logger { - return &Logger{l} -} - -// Tracef logs a trace message using fmt.Sprintf when args are specified. -func (l *Logger) Tracef(template string, args ...any) { - if l == nil { - return - } - l.m.Trace(template, args...) -} - -// Trace logs a trace message. -func (l *Logger) Trace(message string) { - if l == nil { - return - } - l.m.Trace(message) -} - -// Debugf logs a debug message using fmt.Sprintf when args are specified. -func (l *Logger) Debugf(template string, args ...any) { - if l == nil { - return - } - l.m.Debug(template, args...) -} - -// Debug logs a debug message. -func (l *Logger) Debug(message string) { - if l == nil { - return - } - l.m.Debug(message) -} - -// Infof logs an info message using fmt.Sprintf when args are specified. -func (l *Logger) Infof(template string, args ...any) { - if l == nil { - return - } - l.m.Info(template, args...) -} - -// Info logs an info message. -func (l *Logger) Info(message string) { - if l == nil { - return - } - l.m.Info(message) -} - -// Warnf logs a warning message using fmt.Sprintf when args are specified. -func (l *Logger) Warnf(template string, args ...any) { - if l == nil { - return - } - l.m.Warn(template, args...) -} - -// Warn logs a warning message. -func (l *Logger) Warn(message string) { - if l == nil { - return - } - l.m.Warn(message) -} - -// Errorf logs an error message using fmt.Sprintf when args are specified. -func (l *Logger) Errorf(template string, args ...any) { - if l == nil { - return - } - l.m.Error(template, args...) -} - -// Error logs an error message. -func (l *Logger) Error(message string) { - if l == nil { - return - } - l.m.Error(message) -} - -// With adds a variadic set of fields to a logger. Each field must consist -// of a string key and a value of any type. An odd number of key/value pairs -// will therefore result in malformed log messages, but should never panic. -func (l *Logger) With(keyValuePairs ...any) *Logger { - if l == nil { - return nil - } - fields := map[string]string{} - for i := 0; i < (len(keyValuePairs) - 1); i += 2 { - key, ok := keyValuePairs[i].(string) - if !ok { - key = fmt.Sprintf("%v", keyValuePairs[i]) - } - value, ok := keyValuePairs[i+1].(string) - if !ok { - value = fmt.Sprintf("%v", keyValuePairs[i+1]) - } - fields[key] = value - } - lg := l.m.WithFields(fields) - return &Logger{lg} -} diff --git a/public/service/logger_test.go b/public/service/logger_test.go deleted file mode 100644 index 680c5006d3..0000000000 --- a/public/service/logger_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package service - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -func TestReverseAirGapLogger(t *testing.T) { - lConf := log.NewConfig() - lConf.AddTimeStamp = false - lConf.Format = "json" - - var buf bytes.Buffer - logger, err := log.New(&buf, ifs.OS(), lConf) - require.NoError(t, err) - - agLogger := newReverseAirGapLogger(logger) - agLogger2 := agLogger.With("field1", "value1", "field2", "value2") - - agLogger.Debugf("foo: %v", "bar1") - agLogger.Infof("foo: %v", "bar2") - - agLogger2.Debugf("foo2: %v", "bar1") - agLogger2.Infof("foo2: %v", "bar2") - - agLogger.Warnf("foo: %v", "bar3") - agLogger.Errorf("foo: %v", "bar4") - - agLogger2.Warnf("foo2: %v", "bar3") - agLogger2.Errorf("foo2: %v", "bar4") - - assert.Equal(t, `{"@service":"benthos","level":"info","msg":"foo: bar2"} -{"@service":"benthos","field1":"value1","field2":"value2","level":"info","msg":"foo2: bar2"} -{"@service":"benthos","level":"warning","msg":"foo: bar3"} -{"@service":"benthos","level":"error","msg":"foo: bar4"} -{"@service":"benthos","field1":"value1","field2":"value2","level":"warning","msg":"foo2: bar3"} -{"@service":"benthos","field1":"value1","field2":"value2","level":"error","msg":"foo2: bar4"} -`, buf.String()) -} - -func TestReverseAirGapLoggerDodgyFields(t *testing.T) { - lConf := log.NewConfig() - lConf.AddTimeStamp = false - lConf.Format = "json" - - var buf bytes.Buffer - logger, err := log.New(&buf, ifs.OS(), lConf) - require.NoError(t, err) - - agLogger := newReverseAirGapLogger(logger) - - agLogger.With("field1", "value1", "field2").Infof("foo1") - agLogger.With(10, 20).Infof("foo2") - agLogger.With("field3", 30).Infof("foo3") - agLogger.With("field4", "value4").With("field5", "value5").Infof("foo4") - - assert.Equal(t, `{"@service":"benthos","field1":"value1","level":"info","msg":"foo1"} -{"10":"20","@service":"benthos","level":"info","msg":"foo2"} -{"@service":"benthos","field3":"30","level":"info","msg":"foo3"} -{"@service":"benthos","field4":"value4","field5":"value5","level":"info","msg":"foo4"} -`, buf.String()) -} diff --git a/public/service/message.go b/public/service/message.go deleted file mode 100644 index d44f62b066..0000000000 --- a/public/service/message.go +++ /dev/null @@ -1,632 +0,0 @@ -package service - -import ( - "context" - "errors" - - "github.com/benthosdev/benthos/v4/internal/bloblang/mapping" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/transaction" - "github.com/benthosdev/benthos/v4/internal/value" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// MessageHandlerFunc is a function signature defining a component that consumes -// Benthos messages. An error must be returned if the context is cancelled, or -// if the message could not be delivered or processed. -type MessageHandlerFunc func(context.Context, *Message) error - -// MessageBatchHandlerFunc is a function signature defining a component that -// consumes Benthos message batches. An error must be returned if the context is -// cancelled, or if the messages could not be delivered or processed. -type MessageBatchHandlerFunc func(context.Context, MessageBatch) error - -// Message represents a single discrete message passing through a Benthos -// pipeline. It is safe to mutate the message via Set methods, but the -// underlying byte data should not be edited directly. -type Message struct { - part *message.Part - onErr func(err error) -} - -// MessageBatch describes a collection of one or more messages. -type MessageBatch []*Message - -// Copy creates a new slice of the same messages, which can be modified without -// changing the contents of the original batch. -func (b MessageBatch) Copy() MessageBatch { - bCopy := make(MessageBatch, len(b)) - for i, m := range b { - bCopy[i] = m.Copy() - } - return bCopy -} - -// DeepCopy creates a new slice of the same messages, which can be modified -// without changing the contents of the original batch and are unchanged from -// deep mutations performed on the source message. -// -// This is required in situations where a component wishes to retain a copy of a -// message batch beyond the boundaries of a process or write command. This is -// specifically required for buffer implementations that operate by keeping a -// reference to the message. -func (b MessageBatch) DeepCopy() MessageBatch { - bCopy := make(MessageBatch, len(b)) - for i, m := range b { - bCopy[i] = m.DeepCopy() - } - return bCopy -} - -// WalkWithBatchedErrors walks a batch and executes a closure function for each -// message. If the provided closure returns an error then iteration of the batch -// is not stopped and instead a *BatchError is created and populated. -// -// The one exception to this behaviour is when an error is returned that is -// considered fatal such as ErrNotConnected, in which case iteration is -// terminated early and that error is returned immediately. -// -// This is a useful pattern for batched outputs that deliver messages -// individually. -func (b MessageBatch) WalkWithBatchedErrors(fn func(int, *Message) error) error { - if len(b) == 1 { - return fn(0, b[0]) - } - - var batchErr *BatchError - for i, m := range b { - tmpErr := fn(i, m) - if tmpErr != nil { - if errors.Is(tmpErr, ErrNotConnected) { - return tmpErr - } - if batchErr == nil { - batchErr = NewBatchError(b, tmpErr) - } - _ = batchErr.Failed(i, tmpErr) - } - } - - if batchErr != nil { - return batchErr - } - return nil -} - -// Index mutates the batch in situ such that each message in the batch retains -// knowledge of where in the batch it currently resides. An indexer is then -// returned which can be used as a way of re-acquiring the original order of a -// batch derived from this one even after filtering, duplication and reordering -// has been done by other components. -// -// This can be useful in situations where a batch of messages is going to be -// mutated outside of the control of this component (by processors, for example) -// in ways that may change the ordering or presence of messages in the resulting -// batch. Having an indexer that we created prior to this processing allows us -// to take the resulting batch and join the messages within to the messages we -// started with. -func (b MessageBatch) Index() *Indexer { - parts := make(message.Batch, len(b)) - for i, m := range b { - parts[i] = m.part - } - - var s *message.SortGroup - s, parts = message.NewSortGroup(parts) - - for i, p := range parts { - b[i].part = p - } - - return &Indexer{ - wrapped: s, - sourceBatch: b.Copy(), - } -} - -// Indexer encapsulates the ability to acquire the original index of a message -// from a derivative batch as it was when the indexer was created. This can be -// useful in situations where a batch is being dispatched to processors or -// outputs and a derivative batch needs to be associated with the origin. -type Indexer struct { - wrapped *message.SortGroup - sourceBatch MessageBatch -} - -// IndexOf attempts to obtain the index of a message as it occurred within the -// origin batch known at the time the indexer was created. If the message is an -// orphan and does not originate from that batch then -1 is returned. It is -// possible that zero, one or more derivative messages yield any given index of -// the origin batch due to filtering and/or duplication enacted on the batch. -func (s *Indexer) IndexOf(m *Message) int { - return s.wrapped.GetIndex(m.part) -} - -// NewMessage creates a new message with an initial raw bytes content. The -// initial content can be nil, which is recommended if you intend to set it with -// structured contents. -func NewMessage(content []byte) *Message { - return &Message{ - part: message.NewPart(content), - } -} - -// NewInternalMessage returns a message wrapped around an instantiation of the -// internal message package. This function is for internal use only and intended -// as a scaffold for internal components migrating to the new APIs. -func NewInternalMessage(imsg *message.Part) *Message { - return &Message{part: imsg} -} - -// Copy creates a shallow copy of a message that is safe to mutate with Set -// methods without mutating the original. Both messages will share a context, -// and therefore a tracing ID, if one has been associated with them. -func (m *Message) Copy() *Message { - return &Message{ - part: m.part.ShallowCopy(), - } -} - -// DeepCopy creates a deep copy of a message and its contents that is safe to -// mutate with Set methods without mutating the original, and mutations on the -// inner (deep) contents of the source message will not mutate the copy. -// -// This is required in situations where a component wishes to retain a copy of a -// message beyond the boundaries of a process or write command. This is -// specifically required for buffer implementations that operate by keeping a -// reference to the message. -func (m *Message) DeepCopy() *Message { - return &Message{ - part: m.part.DeepCopy(), - } -} - -// Context returns a context associated with the message, or a background -// context in the absence of one. -func (m *Message) Context() context.Context { - return message.GetContext(m.part) -} - -// WithContext returns a new message with a provided context associated with it. -func (m *Message) WithContext(ctx context.Context) *Message { - return &Message{ - part: message.WithContext(ctx, m.part), - } -} - -// AsBytes returns the underlying byte array contents of a message or, if the -// contents are a structured type, attempts to marshal the contents as a JSON -// document and returns either the byte array result or an error. -// -// It is NOT safe to mutate the contents of the returned slice. -func (m *Message) AsBytes() ([]byte, error) { - // TODO: Escalate errors in marshalling once we're able. - return m.part.AsBytes(), nil -} - -// AsStructured returns the underlying structured contents of a message or, if -// the contents are a byte array, attempts to parse the bytes contents as a JSON -// document and returns either the structured result or an error. -// -// It is NOT safe to mutate the contents of the returned value if it is a -// reference type (slice or map). In order to safely mutate the structured -// contents of a message use AsStructuredMut. -func (m *Message) AsStructured() (any, error) { - return m.part.AsStructured() -} - -// AsStructuredMut returns the underlying structured contents of a message or, -// if the contents are a byte array, attempts to parse the bytes contents as a -// JSON document and returns either the structured result or an error. -// -// It is safe to mutate the contents of the returned value even if it is a -// reference type (slice or map), as the structured contents will be lazily deep -// cloned if it is still owned by an upstream component. -func (m *Message) AsStructuredMut() (any, error) { - v, err := m.part.AsStructuredMut() - if err != nil { - return nil, err - } - return v, nil -} - -// SetBytes sets the underlying contents of the message as a byte slice. -func (m *Message) SetBytes(b []byte) { - m.part.SetBytes(b) -} - -// SetStructured sets the underlying contents of the message as a structured -// type. This structured value should be a scalar Go type, or either a -// map[string]interface{} or []interface{} containing the same types all the way -// through the hierarchy, this ensures that other processors are able to work -// with the contents and that they can be JSON marshalled when coerced into a -// byte array. -// -// The provided structure is considered read-only, which means subsequent -// processors will need to fully clone the structure in order to perform -// mutations on the data. -func (m *Message) SetStructured(i any) { - m.part.SetStructured(i) -} - -// SetStructuredMut sets the underlying contents of the message as a structured -// type. This structured value should be a scalar Go type, or either a -// map[string]interface{} or []interface{} containing the same types all the way -// through the hierarchy, this ensures that other processors are able to work -// with the contents and that they can be JSON marshalled when coerced into a -// byte array. -// -// The provided structure is considered mutable, which means subsequent -// processors might mutate the structure without performing a deep copy. -func (m *Message) SetStructuredMut(i any) { - m.part.SetStructuredMut(i) -} - -// SetError marks the message as having failed a processing step and adds the -// error to it as context. Messages marked with errors can be handled using a -// range of methods outlined in https://www.docs.redpanda.com/redpanda-connect/configuration/error_handling. -func (m *Message) SetError(err error) { - if m.onErr != nil { - m.onErr(err) - } - m.part.ErrorSet(err) -} - -// GetError returns an error associated with a message, or nil if there isn't -// one. Messages marked with errors can be handled using a range of methods -// outlined in https://www.docs.redpanda.com/redpanda-connect/configuration/error_handling. -func (m *Message) GetError() error { - return m.part.ErrorGet() -} - -// MetaGet attempts to find a metadata key from the message and returns a string -// result and a boolean indicating whether it was found. -// -// Strong advice: Use MetaGetMut instead. -func (m *Message) MetaGet(key string) (string, bool) { - v, exists := m.part.MetaGetMut(key) - if !exists { - return "", false - } - return value.IToString(v), true -} - -// MetaGetMut attempts to find a metadata key from the message and returns the -// value if found, and a boolean indicating whether it was found. The value -// returned is mutable, and so it is safe to modify even though it may be a -// reference type such as a slice or map. -func (m *Message) MetaGetMut(key string) (any, bool) { - v, exists := m.part.MetaGetMut(key) - if !exists { - return "", false - } - return v, true -} - -// MetaSet sets the value of a metadata key. If the value is an empty string the -// metadata key is deleted. -// -// Strong advice: Use MetaSetMut instead. -func (m *Message) MetaSet(key, value string) { - if value == "" { - m.part.MetaDelete(key) - } else { - m.part.MetaSetMut(key, value) - } -} - -// MetaSetMut sets the value of a metadata key to any value. The value provided -// is stored as mutable, and therefore if it is a reference type such as a slice -// or map then it could be modified by a downstream component. -func (m *Message) MetaSetMut(key string, value any) { - m.part.MetaSetMut(key, value) -} - -// MetaDelete removes a key from the message metadata. -func (m *Message) MetaDelete(key string) { - m.part.MetaDelete(key) -} - -// MetaWalk iterates each metadata key/value pair and executes a provided -// closure on each iteration. To stop iterating, return an error from the -// closure. An error returned by the closure will be returned by this function. -// -// Strong advice: Use MetaWalkMut instead. -func (m *Message) MetaWalk(fn func(string, string) error) error { - return m.part.MetaIterStr(fn) -} - -// MetaWalkMut iterates each metadata key/value pair and executes a provided -// closure on each iteration. To stop iterating, return an error from the -// closure. An error returned by the closure will be returned by this function. -func (m *Message) MetaWalkMut(fn func(key string, value any) error) error { - return m.part.MetaIterMut(fn) -} - -//------------------------------------------------------------------------------ - -// BloblangQuery executes a parsed Bloblang mapping on a message and returns a -// message back or an error if the mapping fails. If the mapping results in the -// root being deleted the returned message will be nil, which indicates it has -// been filtered. -func (m *Message) BloblangQuery(blobl *bloblang.Executor) (*Message, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := message.Batch{m.part} - - res, err := uw.MapPart(0, msg) - if err != nil { - return nil, err - } - if res != nil { - return NewInternalMessage(res), nil - } - return nil, nil -} - -// BloblangQueryValue executes a parsed Bloblang mapping on a message and -// returns the raw value result, or an error if either the mapping fails. -// The error bloblang.ErrRootDeleted is returned if the root of the mapping -// value is deleted, this is in order to allow distinction between a real nil -// value and a deleted value. -func (m *Message) BloblangQueryValue(blobl *bloblang.Executor) (any, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := message.Batch{m.part} - - res, err := uw.Exec(query.FunctionContext{ - Maps: uw.Maps(), - Vars: map[string]any{}, - Index: 0, - MsgBatch: msg, - }) - if err != nil { - return nil, err - } - - switch res.(type) { - case value.Delete: - return nil, bloblang.ErrRootDeleted - case value.Nothing: - return nil, nil - } - return res, nil -} - -// BloblangMutate executes a parsed Bloblang mapping onto a message where the -// contents of the message are mutated directly rather than creating an entirely -// new object. -// -// Returns the same message back in a mutated form, or an error if the mapping -// fails. If the mapping results in the root being deleted the returned message -// will be nil, which indicates it has been filtered. -// -// Note that using a Mutate execution means certain functions within the -// Bloblang mapping will behave differently. In the root of the mapping the -// right-hand keywords `root` and `this` refer to the same mutable root of the -// output document. -func (m *Message) BloblangMutate(blobl *bloblang.Executor) (*Message, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := message.Batch{m.part} - - res, err := uw.MapOnto(m.part, 0, msg) - if err != nil { - return nil, err - } - if res != nil { - return NewInternalMessage(res), nil - } - return nil, nil -} - -// BloblangMutateFrom executes a parsed Bloblang mapping onto a message where -// the reference material for the mapping comes from a provided message rather -// than the target message of the map. Contents of the target message are -// mutated directly rather than creating an entirely new object. -// -// Returns the same message back in a mutated form, or an error if the mapping -// fails. If the mapping results in the root being deleted the returned message -// will be nil, which indicates it has been filtered. -// -// Note that using a MutateFrom execution means certain functions within the -// Bloblang mapping will behave differently. In the root of the mapping the -// right-hand keyword `root` refers to the same mutable root of the output -// document, but the keyword `this` refers to the message being provided as an -// argument. -func (m *Message) BloblangMutateFrom(blobl *bloblang.Executor, from *Message) (*Message, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := message.Batch{from.part} - - res, err := uw.MapOnto(m.part, 0, msg) - if err != nil { - return nil, err - } - if res != nil { - return NewInternalMessage(res), nil - } - return nil, nil -} - -// BloblangQuery executes a parsed Bloblang mapping on a message batch, from the -// perspective of a particular message index, and returns a message back or an -// error if the mapping fails. If the mapping results in the root being deleted -// the returned message will be nil, which indicates it has been filtered. -// -// This method allows mappings to perform windowed aggregations across message -// batches. -func (b MessageBatch) BloblangQuery(index int, blobl *bloblang.Executor) (*Message, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - - res, err := uw.MapPart(index, msg) - if err != nil { - return nil, err - } - if res != nil { - return NewInternalMessage(res), nil - } - return nil, nil -} - -// BloblangQueryValue executes a parsed Bloblang mapping on a message batch, -// from the perspective of a particular message index, and returns the raw value -// result or an error if the mapping fails. The error bloblang.ErrRootDeleted is -// returned if the root of the mapping value is deleted, this is in order to -// allow distinction between a real nil value and a deleted value. -// -// This method allows mappings to perform windowed aggregations across message -// batches. -func (b MessageBatch) BloblangQueryValue(index int, blobl *bloblang.Executor) (any, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - - res, err := uw.Exec(query.FunctionContext{ - Maps: uw.Maps(), - Vars: map[string]any{}, - Index: index, - MsgBatch: msg, - }) - if err != nil { - return nil, err - } - - switch res.(type) { - case value.Delete: - return nil, bloblang.ErrRootDeleted - case value.Nothing: - return nil, nil - } - return res, nil -} - -// BloblangMutate executes a parsed Bloblang mapping onto a message within the -// batch, where the contents of the message are mutated directly rather than -// creating an entirely new object. -// -// Returns the same message back in a mutated form, or an error if the mapping -// fails. If the mapping results in the root being deleted the returned message -// will be nil, which indicates it has been filtered. -// -// This method allows mappings to perform windowed aggregations across message -// batches. -// -// Note that using overlay means certain functions within the Bloblang mapping -// will behave differently. In the root of the mapping the right-hand keywords -// `root` and `this` refer to the same mutable root of the output document. -func (b MessageBatch) BloblangMutate(index int, blobl *bloblang.Executor) (*Message, error) { - uw := blobl.XUnwrapper().(interface { - Unwrap() *mapping.Executor - }).Unwrap() - - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - - res, err := uw.MapOnto(b[index].part, index, msg) - if err != nil { - return nil, err - } - if res != nil { - return NewInternalMessage(res), nil - } - return nil, nil -} - -// TryInterpolatedString resolves an interpolated string expression on a message -// batch, from the perspective of a particular message index. -// -// This method allows interpolation functions to perform windowed aggregations -// across message batches, and is a more powerful way to interpolate strings -// than the standard .String method. -func (b MessageBatch) TryInterpolatedString(index int, i *InterpolatedString) (string, error) { - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - return i.expr.String(index, msg) -} - -// TryInterpolatedBytes resolves an interpolated string expression on a message -// batch, from the perspective of a particular message index. -// -// This method allows interpolation functions to perform windowed aggregations -// across message batches, and is a more powerful way to interpolate strings -// than the standard .String method. -func (b MessageBatch) TryInterpolatedBytes(index int, i *InterpolatedString) ([]byte, error) { - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - return i.expr.Bytes(index, msg) -} - -// InterpolatedString resolves an interpolated string expression on a message -// batch, from the perspective of a particular message index. -// -// This method allows interpolation functions to perform windowed aggregations -// across message batches, and is a more powerful way to interpolate strings -// than the standard .String method. -// -// Deprecated: Use TryInterpolatedString instead. -func (b MessageBatch) InterpolatedString(index int, i *InterpolatedString) string { - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - s, _ := i.expr.String(index, msg) - return s -} - -// InterpolatedBytes resolves an interpolated string expression on a message -// batch, from the perspective of a particular message index. -// -// This method allows interpolation functions to perform windowed aggregations -// across message batches, and is a more powerful way to interpolate strings -// than the standard .String method. -// -// Deprecated: Use TryInterpolatedBytes instead. -func (b MessageBatch) InterpolatedBytes(index int, i *InterpolatedString) []byte { - msg := make(message.Batch, len(b)) - for i, m := range b { - msg[i] = m.part - } - bRes, _ := i.expr.Bytes(index, msg) - return bRes -} - -// AddSyncResponse attempts to add this batch of messages, in its exact current -// condition, to the synchronous response destined for the original source input -// of this data. Synchronous responses aren't supported by all inputs, and so -// it's possible that attempting to mark a batch as ready for a synchronous -// response will return an error. -func (b MessageBatch) AddSyncResponse() error { - parts := make([]*message.Part, len(b)) - for i, m := range b { - parts[i] = m.part - } - return transaction.SetAsResponse(parts) -} diff --git a/public/service/message_test.go b/public/service/message_test.go deleted file mode 100644 index 872b012659..0000000000 --- a/public/service/message_test.go +++ /dev/null @@ -1,492 +0,0 @@ -package service - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - ibloblang "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -func TestMessageCopyAirGap(t *testing.T) { - p := message.NewPart([]byte("hello world")) - p.MetaSetMut("foo", "bar") - g1 := NewInternalMessage(p.ShallowCopy()) - g2 := g1.Copy() - - b := p.AsBytes() - v, _ := p.MetaGetMut("foo") - assert.Equal(t, "hello world", string(b)) - assert.Equal(t, "bar", v) - - b, err := g1.AsBytes() - v, _ = g1.MetaGet("foo") - require.NoError(t, err) - assert.Equal(t, "hello world", string(b)) - assert.Equal(t, "bar", v) - - b, err = g2.AsBytes() - v, _ = g2.MetaGetMut("foo") - require.NoError(t, err) - assert.Equal(t, "hello world", string(b)) - assert.Equal(t, "bar", v) - - g2.SetBytes([]byte("and now this")) - g2.MetaSetMut("foo", "baz") - - b = p.AsBytes() - v, _ = p.MetaGetMut("foo") - assert.Equal(t, "hello world", string(b)) - assert.Equal(t, "bar", v) - - b, err = g1.AsBytes() - v, _ = g1.MetaGetMut("foo") - require.NoError(t, err) - assert.Equal(t, "hello world", string(b)) - assert.Equal(t, "bar", v) - - b, err = g2.AsBytes() - v, _ = g2.MetaGetMut("foo") - require.NoError(t, err) - assert.Equal(t, "and now this", string(b)) - assert.Equal(t, "baz", v) - - g1.SetBytes([]byte("but not this")) - g1.MetaSetMut("foo", "buz") - - b = p.AsBytes() - v, _ = p.MetaGetMut("foo") - assert.Equal(t, "hello world", string(b)) - assert.Equal(t, "bar", v) - - b, err = g1.AsBytes() - v, _ = g1.MetaGetMut("foo") - require.NoError(t, err) - assert.Equal(t, "but not this", string(b)) - assert.Equal(t, "buz", v) - - b, err = g2.AsBytes() - v, _ = g2.MetaGetMut("foo") - require.NoError(t, err) - assert.Equal(t, "and now this", string(b)) - assert.Equal(t, "baz", v) -} - -func TestMessageQuery(t *testing.T) { - p := message.NewPart([]byte(`{"foo":"bar"}`)) - p.MetaSetMut("foo", "bar") - p.MetaSetMut("bar", "baz") - g1 := NewInternalMessage(p) - - b, err := g1.AsBytes() - assert.NoError(t, err) - assert.Equal(t, `{"foo":"bar"}`, string(b)) - - s, err := g1.AsStructured() - assert.NoError(t, err) - assert.Equal(t, map[string]any{"foo": "bar"}, s) - - m, ok := g1.MetaGetMut("foo") - assert.True(t, ok) - assert.Equal(t, "bar", m) - - seen := map[string]any{} - err = g1.MetaWalkMut(func(k string, v any) error { - seen[k] = v - return errors.New("stop") - }) - assert.EqualError(t, err, "stop") - assert.Len(t, seen, 1) - - seen = map[string]any{} - err = g1.MetaWalkMut(func(k string, v any) error { - seen[k] = v - return nil - }) - assert.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": "bar", - "bar": "baz", - }, seen) -} - -func TestMessageQueryValue(t *testing.T) { - msg := NewMessage(nil) - msg.SetStructured(map[string]any{ - "content": "hello world", - }) - - tests := map[string]struct { - mapping string - exp any - err string - }{ - "returns string": { - mapping: `root = json("content")`, - exp: "hello world", - }, - "returns integer": { - mapping: `root = json("content").length()`, - exp: int64(11), - }, - "returns float": { - mapping: `root = json("content").length() / 2`, - exp: float64(5.5), - }, - "returns bool": { - mapping: `root = json("content").length() > 0`, - exp: true, - }, - "returns bytes": { - mapping: `root = content()`, - exp: []byte(`{"content":"hello world"}`), - }, - "returns nil": { - mapping: `root = null`, - exp: nil, - }, - "returns null string": { - mapping: `root = "null"`, - exp: "null", - }, - "returns an array": { - mapping: `root = [ json("content") ]`, - exp: []any{"hello world"}, - }, - "returns an object": { - mapping: `root.new_content = json("content")`, - exp: map[string]any{"new_content": "hello world"}, - }, - "returns an error if the mapping throws": { - mapping: `root = throw("kaboom")`, - exp: nil, - err: "failed assignment (line 1): kaboom", - }, - "returns an error if the root is deleted": { - mapping: `root = deleted()`, - exp: nil, - err: "root was deleted", - }, - "doesn't error out if a field is deleted": { - mapping: `root.foo = deleted()`, - exp: map[string]any{}, - err: "", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - blobl, err := bloblang.Parse(test.mapping) - require.NoError(t, err) - - res, err := msg.BloblangQueryValue(blobl) - if test.err != "" { - require.ErrorContains(t, err, test.err) - } else { - require.NoError(t, err) - } - - assert.Equal(t, test.exp, res) - }) - } -} - -func TestMessageMutate(t *testing.T) { - p := message.NewPart([]byte(`not a json doc`)) - p.MetaSetMut("foo", "bar") - p.MetaSetMut("bar", "baz") - g1 := NewInternalMessage(p.ShallowCopy()) - - _, err := g1.AsStructured() - assert.Error(t, err) - - g1.SetStructured(map[string]any{ - "foo": "bar", - }) - assert.Equal(t, "not a json doc", string(p.AsBytes())) - - s, err := g1.AsStructured() - assert.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": "bar", - }, s) - - g1.SetBytes([]byte("foo bar baz")) - assert.Equal(t, "not a json doc", string(p.AsBytes())) - - _, err = g1.AsStructured() - assert.Error(t, err) - - b, err := g1.AsBytes() - assert.NoError(t, err) - assert.Equal(t, "foo bar baz", string(b)) - - g1.MetaDelete("foo") - - seen := map[string]any{} - err = g1.MetaWalkMut(func(k string, v any) error { - seen[k] = v - return nil - }) - assert.NoError(t, err) - assert.Equal(t, map[string]any{"bar": "baz"}, seen) - - g1.MetaSetMut("foo", "new bar") - - seen = map[string]any{} - err = g1.MetaWalkMut(func(k string, v any) error { - seen[k] = v - return nil - }) - assert.NoError(t, err) - assert.Equal(t, map[string]any{"foo": "new bar", "bar": "baz"}, seen) -} - -func TestNewMessageMutate(t *testing.T) { - g0 := NewMessage([]byte(`not a json doc`)) - g0.MetaSetMut("foo", "bar") - g0.MetaSetMut("bar", "baz") - - g1 := g0.Copy() - - _, err := g1.AsStructured() - assert.Error(t, err) - - g1.SetStructured(map[string]any{ - "foo": "bar", - }) - g0Bytes, err := g0.AsBytes() - require.NoError(t, err) - assert.Equal(t, "not a json doc", string(g0Bytes)) - - s, err := g1.AsStructuredMut() - assert.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": "bar", - }, s) - - g1.SetBytes([]byte("foo bar baz")) - g0Bytes, err = g0.AsBytes() - require.NoError(t, err) - assert.Equal(t, "not a json doc", string(g0Bytes)) - - _, err = g1.AsStructured() - assert.Error(t, err) - - b, err := g1.AsBytes() - assert.NoError(t, err) - assert.Equal(t, "foo bar baz", string(b)) - - g1.MetaDelete("foo") - - seen := map[string]any{} - err = g1.MetaWalkMut(func(k string, v any) error { - seen[k] = v - return nil - }) - assert.NoError(t, err) - assert.Equal(t, map[string]any{"bar": "baz"}, seen) - - g1.MetaSetMut("foo", "new bar") - - seen = map[string]any{} - err = g1.MetaWalkMut(func(k string, v any) error { - seen[k] = v - return nil - }) - assert.NoError(t, err) - assert.Equal(t, map[string]any{"foo": "new bar", "bar": "baz"}, seen) -} - -func TestMessageMapping(t *testing.T) { - part := NewMessage(nil) - part.SetStructured(map[string]any{ - "content": "hello world", - }) - - blobl, err := bloblang.Parse("root.new_content = this.content.uppercase()") - require.NoError(t, err) - - res, err := part.BloblangQuery(blobl) - require.NoError(t, err) - - resI, err := res.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "new_content": "HELLO WORLD", - }, resI) -} - -func TestMessageBatchMapping(t *testing.T) { - partOne := NewMessage(nil) - partOne.SetStructured(map[string]any{ - "content": "hello world 1", - }) - - partTwo := NewMessage(nil) - partTwo.SetStructured(map[string]any{ - "content": "hello world 2", - }) - - blobl, err := bloblang.Parse(`root.new_content = json("content").from_all().join(" - ")`) - require.NoError(t, err) - - res, err := MessageBatch{partOne, partTwo}.BloblangQuery(0, blobl) - require.NoError(t, err) - - resI, err := res.AsStructured() - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "new_content": "hello world 1 - hello world 2", - }, resI) -} - -func TestMessageBatchQueryValue(t *testing.T) { - partOne := NewMessage(nil) - partOne.SetStructured(map[string]any{ - "content": "hello world 1", - }) - - partTwo := NewMessage(nil) - partTwo.SetStructured(map[string]any{ - "content": "hello world 2", - }) - - tests := map[string]struct { - mapping string - batchIndex int - exp any - err string - }{ - "returns string": { - mapping: `root = json("content")`, - exp: "hello world 1", - }, - "returns integer": { - mapping: `root = json("content").length()`, - exp: int64(13), - }, - "returns float": { - mapping: `root = json("content").length() / 2`, - exp: float64(6.5), - }, - "returns bool": { - mapping: `root = json("content").length() > 0`, - exp: true, - }, - "returns bytes": { - mapping: `root = content()`, - exp: []byte(`{"content":"hello world 1"}`), - }, - "returns nil": { - mapping: `root = null`, - exp: nil, - }, - "returns null string": { - mapping: `root = "null"`, - exp: "null", - }, - "returns an array": { - mapping: `root = [ json("content") ]`, - exp: []any{"hello world 1"}, - }, - "returns an object": { - mapping: `root.new_content = json("content")`, - exp: map[string]any{"new_content": "hello world 1"}, - }, - "supports batch-wide queries": { - mapping: `root.new_content = json("content").from_all().join(" - ")`, - exp: map[string]any{"new_content": "hello world 1 - hello world 2"}, - }, - "handles the specified message index correctly": { - mapping: `root = json("content")`, - batchIndex: 1, - exp: "hello world 2", - }, - "returns an error if the mapping throws": { - mapping: `root = throw("kaboom")`, - exp: nil, - err: "failed assignment (line 1): kaboom", - }, - "returns an error if the root is deleted": { - mapping: `root = deleted()`, - exp: nil, - err: "root was deleted", - }, - "doesn't error out if a field is deleted": { - mapping: `root.foo = deleted()`, - exp: map[string]any{}, - err: "", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - blobl, err := bloblang.Parse(test.mapping) - require.NoError(t, err) - - res, err := MessageBatch{partOne, partTwo}.BloblangQueryValue(test.batchIndex, blobl) - if test.err != "" { - require.ErrorContains(t, err, test.err) - } else { - require.NoError(t, err) - } - - assert.Equal(t, test.exp, res) - }) - } -} - -func BenchmarkMessageMappingNew(b *testing.B) { - part := NewMessage(nil) - part.SetStructured(map[string]any{ - "content": "hello world", - }) - - blobl, err := bloblang.Parse("root.new_content = this.content.uppercase()") - require.NoError(b, err) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - res, err := part.BloblangQuery(blobl) - require.NoError(b, err) - - resI, err := res.AsStructured() - require.NoError(b, err) - assert.Equal(b, map[string]any{ - "new_content": "HELLO WORLD", - }, resI) - } -} - -func BenchmarkMessageMappingOld(b *testing.B) { - part := message.NewPart(nil) - part.SetStructured(map[string]any{ - "content": "hello world", - }) - - msg := message.Batch{part} - - blobl, err := ibloblang.GlobalEnvironment().NewMapping("root.new_content = this.content.uppercase()") - require.NoError(b, err) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - res, err := blobl.MapPart(0, msg) - require.NoError(b, err) - - resI, err := res.AsStructuredMut() - require.NoError(b, err) - assert.Equal(b, map[string]any{ - "new_content": "HELLO WORLD", - }, resI) - } -} diff --git a/public/service/metrics.go b/public/service/metrics.go deleted file mode 100644 index acc6165055..0000000000 --- a/public/service/metrics.go +++ /dev/null @@ -1,320 +0,0 @@ -package service - -import ( - "context" - "net/http" - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -// Metrics allows plugin authors to emit custom metrics from components that are -// exported the same way as native Benthos metrics. It's safe to pass around a -// nil pointer for testing components. -type Metrics struct { - t metrics.Type -} - -func newReverseAirGapMetrics(t metrics.Type) *Metrics { - return &Metrics{t} -} - -// NewCounter creates a new counter metric with a name and variant list of label -// keys. -func (m *Metrics) NewCounter(name string, labelKeys ...string) *MetricCounter { - if m == nil { - return nil - } - cv := m.t.GetCounterVec(name, labelKeys...) - return &MetricCounter{cv} -} - -// NewTimer creates a new timer metric with a name and variant list of label -// keys. -func (m *Metrics) NewTimer(name string, labelKeys ...string) *MetricTimer { - if m == nil { - return nil - } - tv := m.t.GetTimerVec(name, labelKeys...) - return &MetricTimer{tv} -} - -// NewGauge creates a new gauge metric with a name and variant list of label -// keys. -func (m *Metrics) NewGauge(name string, labelKeys ...string) *MetricGauge { - if m == nil { - return nil - } - gv := m.t.GetGaugeVec(name, labelKeys...) - return &MetricGauge{gv} -} - -//------------------------------------------------------------------------------ - -// MetricCounter represents a counter metric of a given name and labels. -type MetricCounter struct { - cv metrics.StatCounterVec -} - -// Incr increments a counter metric by an integer amount, the number of label values -// must match the number and order of labels specified when the counter was -// created. -func (c *MetricCounter) Incr(count int64, labelValues ...string) { - if c == nil { - return - } - c.cv.With(labelValues...).Incr(count) -} - -// IncrFloat64 increments a counter metric by a decimal amount, the number of label values -// must match the number and order of labels specified when the counter was -// created. -func (c *MetricCounter) IncrFloat64(count float64, labelValues ...string) { - if c == nil { - return - } - c.cv.With(labelValues...).IncrFloat64(count) -} - -// MetricTimer represents a timing metric of a given name and labels. -type MetricTimer struct { - tv metrics.StatTimerVec -} - -// Timing adds a delta to a timing metric. Delta should be measured in -// nanoseconds for consistency with other Benthos timing metrics. -// -// The number of label values must match the number and order of labels -// specified when the timing was created. -func (t *MetricTimer) Timing(delta int64, labelValues ...string) { - if t == nil { - return - } - t.tv.With(labelValues...).Timing(delta) -} - -// MetricGauge represents a gauge metric of a given name and labels. -type MetricGauge struct { - gv metrics.StatGaugeVec -} - -// Set a gauge metric, the number of label values must match the number and -// order of labels specified when the gauge was created. -func (g *MetricGauge) Set(value int64, labelValues ...string) { - if g == nil { - return - } - g.gv.With(labelValues...).Set(value) -} - -// SetFloat64 sets a gauge metric to a float64 value. Not all metrics exporters -// support floats, in which case the value will be cast to an int64. The number -// of label values must match the number and order of labels specified when the -// gauge was created. -func (g *MetricGauge) SetFloat64(value float64, labelValues ...string) { - if g == nil { - return - } - g.gv.With(labelValues...).SetFloat64(value) -} - -//------------------------------------------------------------------------------ - -// MetricsExporter is an interface implemented by Benthos metrics exporters. -type MetricsExporter interface { - NewCounterCtor(name string, labelKeys ...string) MetricsExporterCounterCtor - NewTimerCtor(name string, labelKeys ...string) MetricsExporterTimerCtor - NewGaugeCtor(name string, labelKeys ...string) MetricsExporterGaugeCtor - Close(ctx context.Context) error -} - -// MetricsExporterCounterCtor is a constructor for a MetricsExporterCounter that -// must be called with a variadic list of label values exactly matching the -// length and order of the label keys provided. -type MetricsExporterCounterCtor func(labelValues ...string) MetricsExporterCounter - -// MetricsExporterTimerCtor is a constructor for a MetricsExporterTimer that -// must be called with a variadic list of label values exactly matching the -// length and order of the label keys provided. -type MetricsExporterTimerCtor func(labelValues ...string) MetricsExporterTimer - -// MetricsExporterGaugeCtor is a constructor for a MetricsExporterGauge that -// must be called with a variadic list of label values exactly matching the -// length and order of the label keys provided. -type MetricsExporterGaugeCtor func(labelValues ...string) MetricsExporterGauge - -// MetricsExporterCounter represents a counter metric of a given name and -// labels. -type MetricsExporterCounter interface { - // Incr increments a counter metric by an integer amount, the number of label values - // must match the number and order of labels specified when the counter was - // created. - Incr(count int64) - - // IncrFloat64 increments a counter metric by a decimal amount, the number of label values - // must match the number and order of labels specified when the counter was - // created. - // TODO: V5 Add this (or replace the int based method) - // IncrFloat64(count float64) -} - -// MetricsExporterTimer represents a timing metric of a given name and labels. -type MetricsExporterTimer interface { - // Timing adds a delta to a timing metric. Delta should be measured in - // nanoseconds for consistency with other Benthos timing metrics. - // - // The number of label values must match the number and order of labels - // specified when the timing was created. - Timing(delta int64) -} - -// MetricsExporterGauge represents a gauge metric of a given name and labels. -type MetricsExporterGauge interface { - // Set sets a gauge metric with an int64 value, the number of label values must match the number and - // order of labels specified when the gauge was created. - Set(value int64) - - // SetFloat64 sets a gauge metric with a float64 value, the number of label values must match the number and - // order of labels specified when the gauge was created. - // TODO: V5 Add this (or replace the int based method) - // SetFloat64(value float64) -} - -//------------------------------------------------------------------------------ - -// Implements internal metrics plugin interface. -type airGapMetrics struct { - airGapped MetricsExporter -} - -func newAirGapMetrics(m MetricsExporter) metrics.Type { - return &airGapMetrics{m} -} - -type airGapGauge struct { - // TODO: This is a hack and we don't really use incr/decr internally in our - // metrics. Can we ditch it? - v int64 - airGapped MetricsExporterGauge -} - -func (a *airGapGauge) Incr(by int64) { - value := atomic.AddInt64(&a.v, by) - a.airGapped.Set(value) -} - -func (a *airGapGauge) IncrFloat64(count float64) { - a.Incr(int64(count)) -} - -func (a *airGapGauge) SetFloat64(value float64) { - atomic.StoreInt64(&a.v, int64(value)) - if fer, ok := a.airGapped.(interface { - SetFloat64(float64) - }); ok { - fer.SetFloat64(value) - } else { - a.airGapped.Set(int64(value)) - } -} - -func (a *airGapGauge) Decr(by int64) { - value := atomic.AddInt64(&a.v, -by) - a.airGapped.Set(value) -} - -func (a *airGapGauge) DecrFloat64(count float64) { - a.Decr(int64(count)) -} - -func (a *airGapGauge) Set(value int64) { - atomic.StoreInt64(&a.v, value) - a.airGapped.Set(value) -} - -type airGapCounter struct { - airGapped MetricsExporterCounter -} - -func (a *airGapCounter) Incr(count int64) { - a.airGapped.Incr(count) -} - -func (a *airGapCounter) IncrFloat64(count float64) { - if fer, ok := a.airGapped.(interface { - IncrFloat64(float64) - }); ok { - fer.IncrFloat64(count) - } else { - a.airGapped.Incr(int64(count)) - } -} - -type airGapTiming struct { - airGapped MetricsExporterTimer -} - -func (a *airGapTiming) Timing(val int64) { - a.airGapped.Timing(val) -} - -type airGapCounterVec struct { - ctor MetricsExporterCounterCtor -} - -func (a *airGapCounterVec) With(labelValues ...string) metrics.StatCounter { - return &airGapCounter{a.ctor(labelValues...)} -} - -type airGapTimingVec struct { - ctor MetricsExporterTimerCtor -} - -func (a *airGapTimingVec) With(labelValues ...string) metrics.StatTimer { - return &airGapTiming{a.ctor(labelValues...)} -} - -type airGapGaugeVec struct { - ctor MetricsExporterGaugeCtor -} - -func (a *airGapGaugeVec) With(labelValues ...string) metrics.StatGauge { - return &airGapGauge{airGapped: a.ctor(labelValues...)} -} - -func (m *airGapMetrics) GetCounter(path string) metrics.StatCounter { - return m.GetCounterVec(path).With() -} - -func (m *airGapMetrics) GetCounterVec(path string, labelNames ...string) metrics.StatCounterVec { - return &airGapCounterVec{m.airGapped.NewCounterCtor(path, labelNames...)} -} - -func (m *airGapMetrics) GetTimer(path string) metrics.StatTimer { - return m.GetTimerVec(path).With() -} - -func (m *airGapMetrics) GetTimerVec(path string, labelNames ...string) metrics.StatTimerVec { - return &airGapTimingVec{m.airGapped.NewTimerCtor(path, labelNames...)} -} - -func (m *airGapMetrics) GetGauge(path string) metrics.StatGauge { - return m.GetGaugeVec(path).With() -} - -func (m *airGapMetrics) GetGaugeVec(path string, labelNames ...string) metrics.StatGaugeVec { - return &airGapGaugeVec{m.airGapped.NewGaugeCtor(path, labelNames...)} -} - -func (m *airGapMetrics) HandlerFunc() http.HandlerFunc { - if hf, ok := m.airGapped.(interface { - HandlerFunc() http.HandlerFunc - }); ok { - return hf.HandlerFunc() - } - return nil -} - -func (m *airGapMetrics) Close() error { - return m.airGapped.Close(context.Background()) -} diff --git a/public/service/metrics_test.go b/public/service/metrics_test.go deleted file mode 100644 index 1afbe24073..0000000000 --- a/public/service/metrics_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package service - -import ( - "context" - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -func TestMetricsNil(t *testing.T) { - var m *Metrics - - m.NewCounter("foo").Incr(1) - m.NewGauge("bar").Set(10) - m.NewTimer("baz").Timing(10) -} - -func TestMetricsNoLabels(t *testing.T) { - stats := metrics.NewLocal() - nm := newReverseAirGapMetrics(stats) - - ctr := nm.NewCounter("counterone") - ctr.Incr(10) - ctr.Incr(11) - - gge := nm.NewGauge("gaugeone") - gge.Set(12) - - tmr := nm.NewTimer("timerone") - tmr.Timing(13) - - assert.Equal(t, map[string]int64{ - "counterone": 21, - "gaugeone": 12, - }, stats.GetCounters()) - - assert.Equal(t, int64(13), stats.GetTimings()["timerone"].Max()) -} - -func TestMetricsWithLabels(t *testing.T) { - stats := metrics.NewLocal() - nm := newReverseAirGapMetrics(stats) - - ctr := nm.NewCounter("countertwo", "label1") - ctr.Incr(10, "value1") - ctr.Incr(11, "value2") - - gge := nm.NewGauge("gaugetwo", "label2") - gge.Set(12, "value3") - - tmr := nm.NewTimer("timertwo", "label3", "label4") - tmr.Timing(13, "value4", "value5") - - assert.Equal(t, map[string]int64{ - `countertwo{label1="value1"}`: 10, - `countertwo{label1="value2"}`: 11, - `gaugetwo{label2="value3"}`: 12, - }, stats.GetCounters()) - - assert.Equal(t, int64(13), stats.GetTimings()[`timertwo{label3="value4",label4="value5"}`].Max()) -} - -//------------------------------------------------------------------------------ - -type mockMetricsExporter struct { - testField string - values map[string]int64 - lock *sync.Mutex -} - -type mockMetricsExporterType struct { - name string - values map[string]int64 - lock *sync.Mutex -} - -func (m *mockMetricsExporterType) IncrFloat64(count float64) { - m.lock.Lock() - m.values[m.name] += int64(count) - m.lock.Unlock() -} - -func (m *mockMetricsExporterType) Incr(count int64) { - m.lock.Lock() - m.values[m.name] += count - m.lock.Unlock() -} - -func (m *mockMetricsExporterType) Timing(delta int64) { - m.lock.Lock() - m.values[m.name] = delta - m.lock.Unlock() -} - -func (m *mockMetricsExporterType) Set(value int64) { - m.lock.Lock() - m.values[m.name] = value - m.lock.Unlock() -} - -func (m *mockMetricsExporterType) SetFloat64(value float64) { - m.Set(int64(value)) -} - -func (m *mockMetricsExporter) NewCounterCtor(name string, labelKeys ...string) MetricsExporterCounterCtor { - return func(labelValues ...string) MetricsExporterCounter { - return &mockMetricsExporterType{ - name: fmt.Sprintf("counter:%v:%v:%v", name, labelKeys, labelValues), - values: m.values, - lock: m.lock, - } - } -} - -func (m *mockMetricsExporter) NewTimerCtor(name string, labelKeys ...string) MetricsExporterTimerCtor { - return func(labelValues ...string) MetricsExporterTimer { - return &mockMetricsExporterType{ - name: fmt.Sprintf("timer:%v:%v:%v", name, labelKeys, labelValues), - values: m.values, - lock: m.lock, - } - } -} - -func (m *mockMetricsExporter) NewGaugeCtor(name string, labelKeys ...string) MetricsExporterGaugeCtor { - return func(labelValues ...string) MetricsExporterGauge { - return &mockMetricsExporterType{ - name: fmt.Sprintf("gauge:%v:%v:%v", name, labelKeys, labelValues), - values: m.values, - lock: m.lock, - } - } -} - -func (m *mockMetricsExporter) Close(ctx context.Context) error { - return nil -} - -func TestMetricsPlugin(t *testing.T) { - testMetrics := &mockMetricsExporter{ - values: map[string]int64{}, - lock: &sync.Mutex{}, - } - - env := NewEnvironment() - confSpec := NewConfigSpec().Field(NewStringField("foo")) - - require.NoError(t, env.RegisterMetricsExporter( - "meow", confSpec, - func(conf *ParsedConfig, log *Logger) (MetricsExporter, error) { - testStr, err := conf.FieldString("foo") - if err != nil { - return nil, err - } - testMetrics.testField = testStr - return testMetrics, nil - })) - - builder := env.NewStreamBuilder() - require.NoError(t, builder.SetYAML(` -input: - label: fooinput - generate: - count: 2 - interval: 1ns - mapping: 'root.id = uuid_v4()' - -pipeline: - processors: - - metric: - name: customthing - type: gauge - labels: - topic: testtopic - value: 1234 - -output: - label: foooutput - drop: {} - -metrics: - meow: - foo: foo value from config - -logger: - level: none -`)) - - strm, err := builder.Build() - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Minute) - defer done() - - require.NoError(t, strm.Run(ctx)) - - testMetrics.lock.Lock() - assert.Equal(t, "foo value from config", testMetrics.testField) - - assert.Greater(t, testMetrics.values["timer:input_latency_ns:[label path]:[fooinput root.input]"], int64(1)) - delete(testMetrics.values, "timer:input_latency_ns:[label path]:[fooinput root.input]") - - assert.GreaterOrEqual(t, testMetrics.values["timer:output_latency_ns:[label path]:[foooutput root.output]"], int64(1)) - delete(testMetrics.values, "timer:output_latency_ns:[label path]:[foooutput root.output]") - - assert.Equal(t, map[string]int64{ - "counter:input_connection_up:[label path]:[fooinput root.input]": 1, - "counter:input_received:[label path]:[fooinput root.input]": 2, - "counter:output_batch_sent:[label path]:[foooutput root.output]": 2, - "counter:output_connection_up:[label path]:[foooutput root.output]": 1, - "counter:output_sent:[label path]:[foooutput root.output]": 2, - "gauge:customthing:[label path topic]:[ root.pipeline.processors.0 testtopic]": 1234, - }, testMetrics.values) - testMetrics.lock.Unlock() -} diff --git a/public/service/output.go b/public/service/output.go deleted file mode 100644 index 03f4eeb81b..0000000000 --- a/public/service/output.go +++ /dev/null @@ -1,373 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync" - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/output/batcher" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Output is an interface implemented by Benthos outputs that support single -// message writes. Each call to Write should block until either the message has -// been successfully or unsuccessfully sent, or the context is cancelled. -// -// Multiple write calls can be performed in parallel, and the constructor of an -// output must provide a MaxInFlight parameter indicating the maximum number of -// parallel write calls the output supports. -type Output interface { - // Establish a connection to the downstream service. Connect will always be - // called first when a writer is instantiated, and will be continuously - // called with back off until a nil error is returned. - // - // The provided context remains open only for the duration of the connecting - // phase, and should not be used to establish the lifetime of the connection - // itself. - // - // Once Connect returns a nil error the write method will be called until - // either ErrNotConnected is returned, or the writer is closed. - Connect(context.Context) error - - // Write a message to a sink, or return an error if delivery is not - // possible. - // - // If this method returns ErrNotConnected then write will not be called - // again until Connect has returned a nil error. - Write(context.Context, *Message) error - - Closer -} - -//------------------------------------------------------------------------------ - -// BatchOutput is an interface implemented by Benthos outputs that require -// Benthos to batch messages before dispatch in order to improve throughput. -// Each call to WriteBatch should block until either all messages in the batch -// have been successfully or unsuccessfully sent, or the context is cancelled. -// -// Multiple write calls can be performed in parallel, and the constructor of an -// output must provide a MaxInFlight parameter indicating the maximum number of -// parallel batched write calls the output supports. -type BatchOutput interface { - // Establish a connection to the downstream service. Connect will always be - // called first when a writer is instantiated, and will be continuously - // called with back off until a nil error is returned. - // - // Once Connect returns a nil error the write method will be called until - // either ErrNotConnected is returned, or the writer is closed. - Connect(context.Context) error - - // Write a batch of messages to a sink, or return an error if delivery is - // not possible. - // - // If this method returns ErrNotConnected then write will not be called - // again until Connect has returned a nil error. - WriteBatch(context.Context, MessageBatch) error - - Closer -} - -//------------------------------------------------------------------------------ - -// Implements output.AsyncSink. -type airGapWriter struct { - w Output -} - -func newAirGapWriter(w Output) output.AsyncSink { - return &airGapWriter{w: w} -} - -func (a *airGapWriter) Connect(ctx context.Context) error { - return a.w.Connect(ctx) -} - -func (a *airGapWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - return publicToInternalErr(a.w.Write(ctx, NewInternalMessage(msg.Get(0)))) -} - -func (a *airGapWriter) Close(ctx context.Context) error { - return a.w.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// Implements output.AsyncSink. -type airGapBatchWriter struct { - w BatchOutput -} - -func newAirGapBatchWriter(w BatchOutput) output.AsyncSink { - return &airGapBatchWriter{w: w} -} - -func (a *airGapBatchWriter) Connect(ctx context.Context) error { - return a.w.Connect(ctx) -} - -func (a *airGapBatchWriter) WriteBatch(ctx context.Context, msg message.Batch) error { - parts := make([]*Message, msg.Len()) - _ = msg.Iter(func(i int, part *message.Part) error { - parts[i] = NewInternalMessage(part) - return nil - }) - return publicToInternalErr(a.w.WriteBatch(ctx, parts)) -} - -func (a *airGapBatchWriter) Close(ctx context.Context) error { - return a.w.Close(context.Background()) -} - -//------------------------------------------------------------------------------ - -// ResourceOutput provides access to an output resource. -type ResourceOutput struct { - o output.Sync -} - -func newResourceOutput(o output.Sync) *ResourceOutput { - return &ResourceOutput{o: o} -} - -// Write a message to the output, or return an error either if delivery is not -// possible or the context is cancelled. -func (o *ResourceOutput) Write(ctx context.Context, m *Message) error { - payload := message.Batch{m.part} - return o.writeMsg(ctx, payload) -} - -// WriteBatch attempts to write a message batch to the output, and returns an -// error either if delivery is not possible or the context is cancelled. -func (o *ResourceOutput) WriteBatch(ctx context.Context, b MessageBatch) error { - payload := make(message.Batch, len(b)) - for i, m := range b { - payload[i] = m.part - } - return toPublicBatchError(o.writeMsg(ctx, payload)) -} - -func (o *ResourceOutput) writeMsg(ctx context.Context, payload message.Batch) error { - var wg sync.WaitGroup - var ackErr error - wg.Add(1) - - if err := o.o.WriteTransaction(ctx, message.NewTransactionFunc(payload, func(ctx context.Context, err error) error { - ackErr = err - wg.Done() - return nil - })); err != nil { - return err - } - - wg.Wait() - return ackErr -} - -//------------------------------------------------------------------------------ - -// OwnedOutput provides direct ownership of an output extracted from a plugin -// config. Connectivity of the output is handled internally, and so the owner -// of this type should only be concerned with writing messages and eventually -// calling Close to terminate the output. -type OwnedOutput struct { - o output.Streamed - closeOnce sync.Once - t atomic.Pointer[chan message.Transaction] - primeMut sync.Mutex -} - -func newOwnedOutput(o output.Streamed) (*OwnedOutput, error) { - return &OwnedOutput{ - o: o, - }, nil -} - -// BatchedWith returns a copy of the OwnedOutput where messages will be batched -// according to the provided batcher. -func (o *OwnedOutput) BatchedWith(b *Batcher) *OwnedOutput { - return &OwnedOutput{ - o: batcher.New(b.p, o.o, b.mgr), - } -} - -// Prime attempts to establish the output connection ready for consuming data. -// This is done automatically once data is written. However, pre-emptively -// priming the connection before data is received is generally a better idea for -// short lived outputs as it'll speed up the first write. -func (o *OwnedOutput) Prime() error { - o.primeMut.Lock() - defer o.primeMut.Unlock() - - tChan := make(chan message.Transaction) - if err := o.o.Consume(tChan); err != nil { - return err - } - o.t.Store(&tChan) - return nil -} - -// PrimeBuffered performs the same output preparation as Prime but the internal -// transaction channel used for delivering data is buffered with the provided -// size. This allows for multiple write transactions to be written to the buffer -// and may improve the chance of delivery when using the WriteBatchNonBlocking -// method. -func (o *OwnedOutput) PrimeBuffered(n int) error { - o.primeMut.Lock() - defer o.primeMut.Unlock() - - tChan := make(chan message.Transaction, n) - if err := o.o.Consume(tChan); err != nil { - return err - } - o.t.Store(&tChan) - return nil -} - -func (o *OwnedOutput) getTChan() (chan message.Transaction, error) { - if t := o.t.Load(); t != nil { - return *t, nil - } - if err := o.Prime(); err != nil { - return nil, err - } - return *o.t.Load(), nil -} - -// Write a message to the output, or return an error either if delivery is not -// possible or the context is cancelled. -func (o *OwnedOutput) Write(ctx context.Context, m *Message) error { - t, err := o.getTChan() - if err != nil { - return err - } - - payload := message.Batch{m.part} - - resChan := make(chan error, 1) - select { - case t <- message.NewTransaction(payload, resChan): - case <-ctx.Done(): - return ctx.Err() - } - - select { - case res := <-resChan: - return res - case <-ctx.Done(): - return ctx.Err() - } -} - -// WriteBatch attempts to write a message batch to the output, and returns an -// error either if delivery is not possible or the context is cancelled. -func (o *OwnedOutput) WriteBatch(ctx context.Context, b MessageBatch) error { - t, err := o.getTChan() - if err != nil { - return err - } - - payload := make(message.Batch, len(b)) - for i, m := range b { - payload[i] = m.part - } - - resChan := make(chan error, 1) - select { - case t <- message.NewTransaction(payload, resChan): - case <-ctx.Done(): - return ctx.Err() - } - - select { - case res := <-resChan: - return toPublicBatchError(res) - case <-ctx.Done(): - return ctx.Err() - } -} - -// ErrBlockingWrite is returned when a non-blocking write is aborted -var ErrBlockingWrite = errors.New("a blocking write attempt was aborted") - -// WriteBatchNonBlocking attempts to write a message batch to the output, but if -// the write is blocked (the read channel is full or not being listened to) then -// the write is aborted immediately in order to prevent blocking the caller. -// -// Instead of blocking until an acknowledgement of delivery is returned this -// method returns immediately and the provided acknowledgement function is -// called when appropriate. -// -// If the write is aborted then ErrBlockingWrite is returned. An error may also -// be returned if the output cannot be primed. -func (o *OwnedOutput) WriteBatchNonBlocking(b MessageBatch, aFn AckFunc) error { - t, err := o.getTChan() - if err != nil { - return err - } - - payload := make(message.Batch, len(b)) - for i, m := range b { - payload[i] = m.part - } - - select { - case t <- message.NewTransactionFunc(payload, func(ctx context.Context, err error) error { - err = toPublicBatchError(err) - return aFn(ctx, err) - }): - default: - return ErrBlockingWrite - } - return nil -} - -// Close the output. -func (o *OwnedOutput) Close(ctx context.Context) error { - o.closeOnce.Do(func() { - if t := o.t.Load(); t != nil { - close(*t) - } - }) - return o.o.WaitForClose(ctx) -} - -type outputUnwrapper struct { - o output.Streamed -} - -func (w outputUnwrapper) Unwrap() output.Streamed { - return w.o -} - -// XUnwrapper is for internal use only, do not use this. -func (o *OwnedOutput) XUnwrapper() any { - return outputUnwrapper{o: o.o} -} - -//------------------------------------------------------------------------------ - -var docsAsync = ` -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field ` + "`max_in_flight`" + `.` - -var docsBatches = ` -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more xref:configuration:batching.adoc[in this doc].` - -// OutputPerformanceDocs returns a string of markdown documentation that can be -// added to outputs as standard performance advice based on whether the output -// benefits from a max_in_flight field, batching or both. -func OutputPerformanceDocs(benefitsFromMaxInFlight, benefitsFromBatching bool) (content string) { - if !benefitsFromMaxInFlight && !benefitsFromBatching { - return - } - content += "\n\n== Performance" - if benefitsFromMaxInFlight { - content += "\n" + docsAsync - } - if benefitsFromBatching { - content += "\n" + docsBatches - } - return content -} diff --git a/public/service/output_test.go b/public/service/output_test.go deleted file mode 100644 index ab77cecb54..0000000000 --- a/public/service/output_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package service - -import ( - "context" - "errors" - "strconv" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/component" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fnOutput struct { - connect func() error - write func(msg *Message) error - closed bool -} - -func (f *fnOutput) Connect(ctx context.Context) error { - return f.connect() -} - -func (f *fnOutput) Write(ctx context.Context, msg *Message) error { - return f.write(msg) -} - -func (f *fnOutput) Close(ctx context.Context) error { - f.closed = true - return nil -} - -func TestOutputAirGapShutdown(t *testing.T) { - o := &fnOutput{} - agi := newAirGapWriter(o) - - ctx, done := context.WithTimeout(context.Background(), time.Second*30) - defer done() - - assert.NoError(t, agi.Close(ctx)) - assert.True(t, o.closed) -} - -func TestOutputAirGapSad(t *testing.T) { - o := &fnOutput{ - connect: func() error { - return errors.New("bad connect") - }, - write: func(m *Message) error { - return errors.New("bad read") - }, - } - agi := newAirGapWriter(o) - - err := agi.Connect(context.Background()) - assert.EqualError(t, err, "bad connect") - - err = agi.WriteBatch(context.Background(), message.QuickBatch(nil)) - assert.EqualError(t, err, "bad read") - - o.write = func(m *Message) error { - return ErrNotConnected - } - - err = agi.WriteBatch(context.Background(), message.QuickBatch(nil)) - assert.Equal(t, component.ErrNotConnected, err) -} - -func TestOutputAirGapHappy(t *testing.T) { - var wroteMsg string - o := &fnOutput{ - connect: func() error { - return nil - }, - write: func(m *Message) error { - wroteBytes, _ := m.AsBytes() - wroteMsg = string(wroteBytes) - return nil - }, - } - agi := newAirGapWriter(o) - - err := agi.Connect(context.Background()) - assert.NoError(t, err) - - inMsg := message.QuickBatch([][]byte{[]byte("hello world")}) - - err = agi.WriteBatch(context.Background(), inMsg) - assert.NoError(t, err) - - assert.Equal(t, "hello world", wroteMsg) -} - -type fnBatchOutput struct { - connect func() error - writeBatch func(msgs MessageBatch) error - closed bool -} - -func (f *fnBatchOutput) Connect(ctx context.Context) error { - return f.connect() -} - -func (f *fnBatchOutput) WriteBatch(ctx context.Context, msgs MessageBatch) error { - return f.writeBatch(msgs) -} - -func (f *fnBatchOutput) Close(ctx context.Context) error { - f.closed = true - return nil -} - -func TestBatchOutputAirGapShutdown(t *testing.T) { - o := &fnBatchOutput{} - agi := newAirGapBatchWriter(o) - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - assert.NoError(t, agi.Close(ctx)) - assert.True(t, o.closed) -} - -func TestBatchOutputAirGapSad(t *testing.T) { - o := &fnBatchOutput{ - connect: func() error { - return errors.New("bad connect") - }, - writeBatch: func(m MessageBatch) error { - return errors.New("bad read") - }, - } - agi := newAirGapBatchWriter(o) - - err := agi.Connect(context.Background()) - assert.EqualError(t, err, "bad connect") - - err = agi.WriteBatch(context.Background(), message.QuickBatch(nil)) - assert.EqualError(t, err, "bad read") - - o.writeBatch = func(m MessageBatch) error { - return ErrNotConnected - } - - err = agi.WriteBatch(context.Background(), message.QuickBatch(nil)) - assert.Equal(t, component.ErrNotConnected, err) -} - -func TestBatchOutputAirGapHappy(t *testing.T) { - var wroteMsg string - o := &fnBatchOutput{ - connect: func() error { - return nil - }, - writeBatch: func(m MessageBatch) error { - wroteBytes, _ := m[0].AsBytes() - wroteMsg = string(wroteBytes) - return nil - }, - } - agi := newAirGapBatchWriter(o) - - err := agi.Connect(context.Background()) - assert.NoError(t, err) - - inMsg := message.QuickBatch([][]byte{[]byte("hello world")}) - - err = agi.WriteBatch(context.Background(), inMsg) - assert.NoError(t, err) - - assert.Equal(t, "hello world", wroteMsg) -} - -func TestOutputOwnedNonBlockingNonBuffered(t *testing.T) { - bChan := make(chan struct{}) - - var received []string - var mut sync.Mutex - - o := &fnBatchOutput{ - connect: func() error { - return nil - }, - writeBatch: func(b MessageBatch) error { - <-bChan - mut.Lock() - defer mut.Unlock() - for _, m := range b { - mBytes, _ := m.AsBytes() - received = append(received, string(mBytes)) - } - return nil - }, - } - - mo, err := MockResources().ManagedBatchOutput("foo", 1, o) - require.NoError(t, err) - - require.NoError(t, mo.Prime()) - - assert.Eventually(t, func() bool { - tmpErr := mo.WriteBatchNonBlocking(MessageBatch{ - NewMessage([]byte("first")), - }, func(ctx context.Context, err error) error { - return nil - }) - return tmpErr == nil - }, time.Second, time.Millisecond*10) - - for i := 0; i < 3; i++ { - require.Error(t, mo.WriteBatchNonBlocking(MessageBatch{ - NewMessage([]byte("first")), - }, func(ctx context.Context, err error) error { - return nil - })) - } - - close(bChan) - - assert.Eventually(t, func() bool { - tmpErr := mo.WriteBatchNonBlocking(MessageBatch{ - NewMessage([]byte("second")), - }, func(ctx context.Context, err error) error { - return nil - }) - return tmpErr == nil - }, time.Second, time.Millisecond*10) - - assert.Eventually(t, func() bool { - mut.Lock() - l := len(received) - mut.Unlock() - return l == 2 - }, time.Second, time.Millisecond*50) - - assert.Equal(t, []string{"first", "second"}, received) -} - -func TestOutputOwnedNonBlockingBuffered(t *testing.T) { - bChan := make(chan struct{}) - - var received []string - var mut sync.Mutex - - o := &fnBatchOutput{ - connect: func() error { - return nil - }, - writeBatch: func(b MessageBatch) error { - <-bChan - mut.Lock() - defer mut.Unlock() - for _, m := range b { - mBytes, _ := m.AsBytes() - received = append(received, string(mBytes)) - } - return nil - }, - } - - mo, err := MockResources().ManagedBatchOutput("foo", 1, o) - require.NoError(t, err) - - require.NoError(t, mo.PrimeBuffered(5)) - - for i := 0; i < 6; i++ { - assert.Eventually(t, func() bool { - tmpErr := mo.WriteBatchNonBlocking(MessageBatch{ - NewMessage(strconv.AppendInt(nil, int64(i), 10)), - }, func(ctx context.Context, err error) error { - return nil - }) - return tmpErr == nil - }, time.Second, time.Millisecond*10) - } - - for i := 0; i < 3; i++ { - require.Error(t, mo.WriteBatchNonBlocking(MessageBatch{ - NewMessage([]byte("another")), - }, func(ctx context.Context, err error) error { - return nil - })) - } - - close(bChan) - - assert.Eventually(t, func() bool { - tmpErr := mo.WriteBatchNonBlocking(MessageBatch{ - NewMessage([]byte("last message")), - }, func(ctx context.Context, err error) error { - return nil - }) - return tmpErr == nil - }, time.Second, time.Millisecond*10) - - assert.Eventually(t, func() bool { - mut.Lock() - l := len(received) - mut.Unlock() - return l == 7 - }, time.Second, time.Millisecond*50) - - assert.Equal(t, []string{"0", "1", "2", "3", "4", "5", "last message"}, received) -} diff --git a/public/service/package.go b/public/service/package.go deleted file mode 100644 index c3b187d5a3..0000000000 --- a/public/service/package.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package service provides a high level API for registering custom plugin -// components and executing either a standard Benthos CLI, or programmatically -// building isolated pipelines with a StreamBuilder API. -// -// For a video guide on Benthos plugins check out: https://youtu.be/uH6mKw-Ly0g -// And an example repo containing component plugins and tests can be found at: -// https://github.com/benthosdev/benthos-plugin-example -// -// In order to add custom Bloblang functions and methods use the -// ./public/bloblang package. -package service - -import ( - "context" -) - -// Closer is implemented by components that support stopping and cleaning up -// their underlying resources. -type Closer interface { - // Close the component, blocks until either the underlying resources are - // cleaned up or the context is cancelled. Returns an error if the context - // is cancelled. - Close(ctx context.Context) error -} diff --git a/public/service/plugins.go b/public/service/plugins.go deleted file mode 100644 index 775d41c80e..0000000000 --- a/public/service/plugins.go +++ /dev/null @@ -1,207 +0,0 @@ -package service - -import ( - "go.opentelemetry.io/otel/trace" -) - -// BatchBufferConstructor is a func that's provided a configuration type and -// access to a service manager and must return an instantiation of a buffer -// based on the config, or an error. -// -// Consumed message batches must be created by upstream components (inputs, etc) -// otherwise this buffer will simply receive batches containing single messages. -type BatchBufferConstructor func(conf *ParsedConfig, mgr *Resources) (BatchBuffer, error) - -// RegisterBatchBuffer attempts to register a new buffer plugin by providing a -// description of the configuration for the buffer and a constructor for the -// buffer processor. The constructor will be called for each instantiation of -// the component within a config. -// -// Consumed message batches must be created by upstream components (inputs, etc) -// otherwise this buffer will simply receive batches containing single -// messages. -func RegisterBatchBuffer(name string, spec *ConfigSpec, ctor BatchBufferConstructor) error { - return globalEnvironment.RegisterBatchBuffer(name, spec, ctor) -} - -// CacheConstructor is a func that's provided a configuration type and access to -// a service manager and must return an instantiation of a cache based on the -// config, or an error. -type CacheConstructor func(conf *ParsedConfig, mgr *Resources) (Cache, error) - -// RegisterCache attempts to register a new cache plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the cache itself. The constructor will be called for each instantiation of -// the component within a config. -func RegisterCache(name string, spec *ConfigSpec, ctor CacheConstructor) error { - return globalEnvironment.RegisterCache(name, spec, ctor) -} - -// InputConstructor is a func that's provided a configuration type and access to -// a service manager, and must return an instantiation of a reader based on the -// config, or an error. -type InputConstructor func(conf *ParsedConfig, mgr *Resources) (Input, error) - -// RegisterInput attempts to register a new input plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the input itself. The constructor will be called for each instantiation of -// the component within a config. -// -// If your input implementation doesn't have a specific mechanism for dealing -// with a nack (when the AckFunc provides a non-nil error) then you can instead -// wrap your input implementation with AutoRetryNacks to get automatic retries. -func RegisterInput(name string, spec *ConfigSpec, ctor InputConstructor) error { - return globalEnvironment.RegisterInput(name, spec, ctor) -} - -// BatchInputConstructor is a func that's provided a configuration type and -// access to a service manager, and must return an instantiation of a batched -// reader based on the config, or an error. -type BatchInputConstructor func(conf *ParsedConfig, mgr *Resources) (BatchInput, error) - -// RegisterBatchInput attempts to register a new batched input plugin by -// providing a description of the configuration for the plugin as well as a -// constructor for the input itself. The constructor will be called for each -// instantiation of the component within a config. -// -// If your input implementation doesn't have a specific mechanism for dealing -// with a nack (when the AckFunc provides a non-nil error) then you can instead -// wrap your input implementation with AutoRetryNacksBatched to get automatic -// retries. -func RegisterBatchInput(name string, spec *ConfigSpec, ctor BatchInputConstructor) error { - return globalEnvironment.RegisterBatchInput(name, spec, ctor) -} - -// OutputConstructor is a func that's provided a configuration type and access -// to a service manager, and must return an instantiation of a writer based on -// the config and a maximum number of in-flight messages to allow, or an error. -type OutputConstructor func(conf *ParsedConfig, mgr *Resources) (out Output, maxInFlight int, err error) - -// RegisterOutput attempts to register a new output plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the output itself. The constructor will be called for each instantiation of -// the component within a config. -func RegisterOutput(name string, spec *ConfigSpec, ctor OutputConstructor) error { - return globalEnvironment.RegisterOutput(name, spec, ctor) -} - -// BatchOutputConstructor is a func that's provided a configuration type and -// access to a service manager, and must return an instantiation of a writer -// based on the config, a batching policy, and a maximum number of in-flight -// message batches to allow, or an error. -type BatchOutputConstructor func(conf *ParsedConfig, mgr *Resources) (out BatchOutput, batchPolicy BatchPolicy, maxInFlight int, err error) - -// RegisterBatchOutput attempts to register a new output plugin by providing a -// description of the configuration for the plugin as well as a constructor for -// the output itself. The constructor will be called for each instantiation of -// the component within a config. -// -// The constructor of a batch output is able to return a batch policy to be -// applied before calls to write are made, creating batches from the stream of -// messages. However, batches can also be created by upstream components -// (inputs, buffers, etc). -// -// If a batch has been formed upstream it is possible that its size may exceed -// the policy specified in your constructor. -func RegisterBatchOutput(name string, spec *ConfigSpec, ctor BatchOutputConstructor) error { - return globalEnvironment.RegisterBatchOutput(name, spec, ctor) -} - -// ProcessorConstructor is a func that's provided a configuration type and -// access to a service manager and must return an instantiation of a processor -// based on the config, or an error. -type ProcessorConstructor func(conf *ParsedConfig, mgr *Resources) (Processor, error) - -// RegisterProcessor attempts to register a new processor plugin by providing -// a description of the configuration for the processor and a constructor for -// the processor itself. The constructor will be called for each instantiation -// of the component within a config. -// -// For simple transformations consider implementing a Bloblang plugin method -// instead. -func RegisterProcessor(name string, spec *ConfigSpec, ctor ProcessorConstructor) error { - return globalEnvironment.RegisterProcessor(name, spec, ctor) -} - -// BatchProcessorConstructor is a func that's provided a configuration type and -// access to a service manager and must return an instantiation of a processor -// based on the config, or an error. -// -// Message batches must be created by upstream components (inputs, buffers, etc) -// otherwise this processor will simply receive batches containing single -// messages. -type BatchProcessorConstructor func(conf *ParsedConfig, mgr *Resources) (BatchProcessor, error) - -// RegisterBatchProcessor attempts to register a new processor plugin by -// providing a description of the configuration for the processor and a -// constructor for the processor itself. The constructor will be called for each -// instantiation of the component within a config. -func RegisterBatchProcessor(name string, spec *ConfigSpec, ctor BatchProcessorConstructor) error { - return globalEnvironment.RegisterBatchProcessor(name, spec, ctor) -} - -// RateLimitConstructor is a func that's provided a configuration type and -// access to a service manager and must return an instantiation of a rate limit -// based on the config, or an error. -type RateLimitConstructor func(conf *ParsedConfig, mgr *Resources) (RateLimit, error) - -// RegisterRateLimit attempts to register a new rate limit plugin by providing -// a description of the configuration for the plugin as well as a constructor -// for the rate limit itself. The constructor will be called for each -// instantiation of the component within a config. -func RegisterRateLimit(name string, spec *ConfigSpec, ctor RateLimitConstructor) error { - return globalEnvironment.RegisterRateLimit(name, spec, ctor) -} - -// MetricsExporterConstructor is a func that's provided a configuration type and -// access to a service manager and must return an instantiation of a metrics -// exporter based on the config, or an error. -type MetricsExporterConstructor func(conf *ParsedConfig, log *Logger) (MetricsExporter, error) - -// RegisterMetricsExporter attempts to register a new metrics exporter plugin by -// providing a description of the configuration for the plugin as well as a -// constructor for the metrics exporter itself. The constructor will be called -// for each instantiation of the component within a config. -func RegisterMetricsExporter(name string, spec *ConfigSpec, ctor MetricsExporterConstructor) error { - return globalEnvironment.RegisterMetricsExporter(name, spec, ctor) -} - -// OtelTracerProviderConstructor is a func that's provided a configuration type -// and access to a service manager and must return an instantiation of an open -// telemetry tracer provider. -// -// Experimental: This type signature is experimental and therefore subject to -// change outside of major version releases. -type OtelTracerProviderConstructor func(conf *ParsedConfig) (trace.TracerProvider, error) - -// RegisterOtelTracerProvider attempts to register a new open telemetry tracer -// provider plugin by providing a description of the configuration for the -// plugin as well as a constructor for the metrics exporter itself. The -// constructor will be called for each instantiation of the component within a -// config. -// -// Experimental: This type signature is experimental and therefore subject to -// change outside of major version releases. -func RegisterOtelTracerProvider(name string, spec *ConfigSpec, ctor OtelTracerProviderConstructor) error { - return globalEnvironment.RegisterOtelTracerProvider(name, spec, ctor) -} - -// BatchScannerCreatorConstructor is a func that's provided a configuration type -// and access to a service manager and must return an instantiation of a batch -// scanner creator. -type BatchScannerCreatorConstructor func(conf *ParsedConfig, mgr *Resources) (BatchScannerCreator, error) - -// RegisterBatchScannerCreator attempts to register a new batch scanner exporter -// plugin by providing a description of the configuration for the plugin as well -// as a constructor for the scanner itself. The constructor will be called for -// each instantiation of the component within a config. -func RegisterBatchScannerCreator(name string, spec *ConfigSpec, ctor BatchScannerCreatorConstructor) error { - return globalEnvironment.RegisterBatchScannerCreator(name, spec, ctor) -} - -// RegisterTemplateYAML attempts to register a template to the global -// environment, defined as a YAML document, to the environment such that it may -// be used similarly to any other component plugin. -func RegisterTemplateYAML(yamlStr string) error { - return globalEnvironment.RegisterTemplateYAML(yamlStr) -} diff --git a/public/service/plugins_test.go b/public/service/plugins_test.go deleted file mode 100644 index 6e55ad1f0a..0000000000 --- a/public/service/plugins_test.go +++ /dev/null @@ -1,568 +0,0 @@ -package service_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/testutil" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/public/service" -) - -func testSanitConf() docs.SanitiseConfig { - sanitConf := docs.NewSanitiseConfig(bundle.GlobalEnvironment) - sanitConf.RemoveTypeField = true - sanitConf.RemoveDeprecated = true - return sanitConf -} - -func TestCachePluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec().Field( - service.NewIntField("a").Default(100), - ) - - var aValue int - var errValue error - var initLabel string - - require.NoError(t, service.RegisterCache("test_cache_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - aValue, errValue = conf.FieldInt("a") - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - cacheConfStr := `label: foo -test_cache_plugin_with_config: - a: 20 -` - - cacheConf, err := testutil.CacheFromYAML(cacheConfStr) - require.NoError(t, err) - - var cacheNode yaml.Node - require.NoError(t, cacheNode.Encode(cacheConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeCache, &cacheNode, testSanitConf())) - - cacheConfOutBytes, err := yaml.Marshal(cacheNode) - require.NoError(t, err) - assert.Equal(t, cacheConfStr, string(cacheConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewCache(cacheConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.NoError(t, errValue) - assert.Equal(t, 20, aValue) - assert.Equal(t, "foo", initLabel) -} - -func TestCachePluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterCache("test_cache_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - cacheConfStr := `label: foo -test_cache_plugin_without_config: null # No default (required) -` - - cacheConf, err := testutil.CacheFromYAML(cacheConfStr) - require.NoError(t, err) - - var cacheNode yaml.Node - require.NoError(t, cacheNode.Encode(cacheConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeCache, &cacheNode, testSanitConf())) - - cacheConfOutBytes, err := yaml.Marshal(cacheNode) - require.NoError(t, err) - assert.Equal(t, cacheConfStr, string(cacheConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewCache(cacheConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} - -func TestInputPluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec().Field( - service.NewIntField("a").Default(100), - ) - - var aValue int - var errValue error - var initLabel string - - require.NoError(t, service.RegisterInput("test_input_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - aValue, errValue = conf.FieldInt("a") - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_input_plugin_with_config: - a: 20 -` - - inConf, err := testutil.InputFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeInput, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewInput(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.NoError(t, errValue) - assert.Equal(t, 20, aValue) - assert.Equal(t, "foo", initLabel) -} - -func TestInputPluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterInput("test_input_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_input_plugin_without_config: null # No default (required) -` - - inConf, err := testutil.InputFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeInput, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewInput(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} - -func TestOutputPluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec(). - Field(service.NewIntField("a").Default(100)) - - var aValue int - var errValue error - var initLabel string - - require.NoError(t, service.RegisterOutput("test_output_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Output, int, error) { - aValue, errValue = conf.FieldInt("a") - initLabel = mgr.Label() - return nil, 1, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_output_plugin_with_config: - a: 20 -` - - inConf, err := testutil.OutputFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeOutput, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewOutput(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.NoError(t, errValue) - assert.Equal(t, 20, aValue) - assert.Equal(t, "foo", initLabel) -} - -func TestOutputPluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterOutput("test_output_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Output, int, error) { - initLabel = mgr.Label() - return nil, 1, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_output_plugin_without_config: null # No default (required) -` - - inConf, err := testutil.OutputFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeOutput, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewOutput(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} - -func TestBatchOutputPluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec(). - Field(service.NewIntField("a").Default(100)). - Field(service.NewIntField("count").Default(10)) - - var aValue, countValue int - var initLabel string - - require.NoError(t, service.RegisterBatchOutput("test_batch_output_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchOutput, service.BatchPolicy, int, error) { - aValue, _ = conf.FieldInt("a") - countValue, _ = conf.FieldInt("count") - initLabel = mgr.Label() - batchPolicy := service.BatchPolicy{} - return nil, batchPolicy, 1, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_batch_output_plugin_with_config: - a: 20 - count: 21 -` - - inConf, err := testutil.OutputFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeOutput, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewOutput(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, 20, aValue) - assert.Equal(t, 21, countValue) - assert.Equal(t, "foo", initLabel) -} - -func TestBatchOutputPluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterOutput("test_batch_output_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Output, int, error) { - initLabel = mgr.Label() - return nil, 1, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_batch_output_plugin_without_config: null # No default (required) -` - - inConf, err := testutil.OutputFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeOutput, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewOutput(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} - -func TestProcessorPluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec(). - Field(service.NewIntField("a").Default(100)) - - var aValue int - var initLabel string - - require.NoError(t, service.RegisterProcessor("test_processor_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - aValue, _ = conf.FieldInt("a") - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_processor_plugin_with_config: - a: 20 -` - - inConf, err := testutil.ProcessorFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeProcessor, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewProcessor(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, 20, aValue) - assert.Equal(t, "foo", initLabel) -} - -func TestProcessorPluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterProcessor("test_processor_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_processor_plugin_without_config: null # No default (required) -` - - inConf, err := testutil.ProcessorFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeProcessor, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewProcessor(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} - -func TestBatchProcessorPluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec(). - Field(service.NewIntField("a").Default(100)) - - var aValue int - var initLabel string - - require.NoError(t, service.RegisterBatchProcessor("test_batch_processor_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - aValue, _ = conf.FieldInt("a") - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_batch_processor_plugin_with_config: - a: 20 -` - - inConf, err := testutil.ProcessorFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeProcessor, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewProcessor(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, 20, aValue) - assert.Equal(t, "foo", initLabel) -} - -func TestBatchProcessorPluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterBatchProcessor("test_batch_processor_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchProcessor, error) { - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_batch_processor_plugin_without_config: null # No default (required) -` - - inConf, err := testutil.ProcessorFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeProcessor, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewProcessor(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} - -func TestRateLimitPluginWithConfig(t *testing.T) { - configSpec := service.NewConfigSpec(). - Field(service.NewIntField("a").Default(100)) - - var aValue int - var initLabel string - - require.NoError(t, service.RegisterRateLimit("test_rate_limit_plugin_with_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.RateLimit, error) { - aValue, _ = conf.FieldInt("a") - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_rate_limit_plugin_with_config: - a: 20 -` - - inConf, err := testutil.RateLimitFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeRateLimit, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewRateLimit(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, 20, aValue) - assert.Equal(t, "foo", initLabel) -} - -func TestRateLimitPluginWithoutConfig(t *testing.T) { - configSpec := service.NewConfigSpec() - - var initLabel string - require.NoError(t, service.RegisterRateLimit("test_rate_limit_plugin_without_config", configSpec, - func(conf *service.ParsedConfig, mgr *service.Resources) (service.RateLimit, error) { - initLabel = mgr.Label() - return nil, errors.New("this is a test error") - })) - - inConfStr := `label: foo -test_rate_limit_plugin_without_config: null # No default (required) -` - - inConf, err := testutil.RateLimitFromYAML(inConfStr) - require.NoError(t, err) - - var outNode yaml.Node - require.NoError(t, outNode.Encode(inConf)) - - require.NoError(t, docs.SanitiseYAML(docs.TypeRateLimit, &outNode, testSanitConf())) - - outConfOutBytes, err := yaml.Marshal(outNode) - require.NoError(t, err) - assert.Equal(t, inConfStr, string(outConfOutBytes)) - - mgr, err := manager.New(manager.NewResourceConfig()) - require.NoError(t, err) - - _, err = mgr.NewRateLimit(inConf) - require.Error(t, err) - assert.Contains(t, err.Error(), "this is a test error") - assert.Equal(t, "foo", initLabel) -} diff --git a/public/service/processor.go b/public/service/processor.go deleted file mode 100644 index 9b9b36a607..0000000000 --- a/public/service/processor.go +++ /dev/null @@ -1,234 +0,0 @@ -package service - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// Processor is a Benthos processor implementation that works against single -// messages. -type Processor interface { - // Process a message into one or more resulting messages, or return an error - // if the message could not be processed. If zero messages are returned and - // the error is nil then the message is filtered. - // - // When an error is returned the input message will continue down the - // pipeline but will be marked with the error with *message.SetError, and - // metrics and logs will be emitted. The failed message can then be handled - // with the patterns outlined in https://www.docs.redpanda.com/redpanda-connect/configuration/error_handling. - // - // The Message types returned MUST be derived from the provided message, and - // CANNOT be custom implementations of Message. In order to copy the - // provided message use the Copy method. - Process(context.Context, *Message) (MessageBatch, error) - - Closer -} - -//------------------------------------------------------------------------------ - -// BatchProcessor is a Benthos processor implementation that works against -// batches of messages, which allows windowed processing. -// -// Message batches must be created by upstream components (inputs, buffers, etc) -// otherwise this processor will simply receive batches containing single -// messages. -type BatchProcessor interface { - // Process a batch of messages into one or more resulting batches, or return - // an error if the entire batch could not be processed. If zero messages are - // returned and the error is nil then all messages are filtered. - // - // The provided MessageBatch should NOT be modified, in order to return a - // mutated batch a copy of the slice should be created instead. - // - // When an error is returned all of the input messages will continue down - // the pipeline but will be marked with the error with *message.SetError, - // and metrics and logs will be emitted. - // - // In order to add errors to individual messages of the batch for downstream - // handling use *message.SetError(err) and return it in the resulting batch - // with a nil error. - // - // The Message types returned MUST be derived from the provided messages, - // and CANNOT be custom implementations of Message. In order to copy the - // provided messages use the Copy method. - ProcessBatch(context.Context, MessageBatch) ([]MessageBatch, error) - - Closer -} - -//------------------------------------------------------------------------------ - -// Implements types.Processor for a Processor. -type airGapProcessor struct { - p Processor -} - -func newAirGapProcessor(typeStr string, p Processor, mgr bundle.NewManagement) processor.V1 { - return processor.NewAutoObservedProcessor(typeStr, &airGapProcessor{p}, mgr) -} - -func (a *airGapProcessor) Process(ctx context.Context, msg *message.Part) ([]*message.Part, error) { - msgs, err := a.p.Process(ctx, NewInternalMessage(msg)) - if err != nil { - return nil, err - } - parts := make([]*message.Part, 0, len(msgs)) - for _, msg := range msgs { - parts = append(parts, msg.part) - } - return parts, nil -} - -func (a *airGapProcessor) Close(ctx context.Context) error { - return a.p.Close(context.Background()) -} - -//------------------------------------------------------------------------------ - -// Implements types.Processor for a BatchProcessor. -type airGapBatchProcessor struct { - p BatchProcessor -} - -func newAirGapBatchProcessor(typeStr string, p BatchProcessor, mgr bundle.NewManagement) processor.V1 { - return processor.NewAutoObservedBatchedProcessor(typeStr, &airGapBatchProcessor{p}, mgr) -} - -func (a *airGapBatchProcessor) ProcessBatch(ctx *processor.BatchProcContext, batch message.Batch) ([]message.Batch, error) { - inputBatch := make([]*Message, batch.Len()) - _ = batch.Iter(func(i int, p *message.Part) error { - newP := NewInternalMessage(p) - newP.onErr = func(err error) { - ctx.OnError(err, i, p) - } - inputBatch[i] = newP - return nil - }) - - outputBatches, err := a.p.ProcessBatch(ctx.Context(), inputBatch) - if err != nil { - return nil, err - } - - newBatches := make([]message.Batch, len(outputBatches)) - for i, batch := range outputBatches { - newBatch := make(message.Batch, len(batch)) - for i, m := range batch { - newBatch[i] = m.part - } - newBatches[i] = newBatch - } - - return newBatches, nil -} - -func (a *airGapBatchProcessor) Close(ctx context.Context) error { - return a.p.Close(context.Background()) -} - -//------------------------------------------------------------------------------ - -// OwnedProcessor provides direct ownership of a processor extracted from a -// plugin config. -type OwnedProcessor struct { - p processor.V1 -} - -// Process a single message, returns either a batch of zero or more resulting -// messages or an error if the message could not be processed. -func (o *OwnedProcessor) Process(ctx context.Context, msg *Message) (MessageBatch, error) { - outMsg := message.Batch{msg.part} - - iMsgs, res := o.p.ProcessBatch(ctx, outMsg) - if res != nil { - return nil, res - } - - var b MessageBatch - for _, iMsg := range iMsgs { - _ = iMsg.Iter(func(i int, part *message.Part) error { - b = append(b, NewInternalMessage(part)) - return nil - }) - } - return b, nil -} - -// ProcessBatch attempts to process a batch of messages, returns zero or more -// batches of resulting messages, or an error if the context is cancelled during -// execution. -// -// However, for general processing errors unrelated to context cancellation the -// error is marked against individual messages with the `SetError` method and a -// nil error is returned by this method. -func (o *OwnedProcessor) ProcessBatch(ctx context.Context, batch MessageBatch) ([]MessageBatch, error) { - outMsg := make(message.Batch, len(batch)) - for i, msg := range batch { - outMsg[i] = msg.part - } - - iMsgs, res := o.p.ProcessBatch(ctx, outMsg) - if res != nil { - return nil, res - } - - var batches []MessageBatch - for _, iMsg := range iMsgs { - var b MessageBatch - _ = iMsg.Iter(func(i int, part *message.Part) error { - b = append(b, NewInternalMessage(part)) - return nil - }) - batches = append(batches, b) - } - return batches, nil -} - -// Close the processor, allowing it to clean up resources. -func (o *OwnedProcessor) Close(ctx context.Context) error { - return o.p.Close(ctx) -} - -// ExecuteProcessors runs a set of batches through a series processors. If a -// context error occurs during execution then this function terminates and -// returns the error. -// -// However, for general processing errors unrelated to context cancellation the -// errors are marked against individual messages with the `SetError` method and -// processing continues against subsequent processors. -func ExecuteProcessors(ctx context.Context, processors []*OwnedProcessor, inbatches ...MessageBatch) ([]MessageBatch, error) { - if len(processors) == 0 { - return inbatches, nil - } - - proc := processors[0] - - nextBatches := make([]MessageBatch, 0, len(inbatches)) - for _, batch := range inbatches { - batches, err := proc.ProcessBatch(ctx, batch) - if err != nil { - return nil, err - } - - nextBatches = append(nextBatches, batches...) - } - - return ExecuteProcessors(ctx, processors[1:], nextBatches...) -} - -type processorUnwrapper struct { - p processor.V1 -} - -func (w processorUnwrapper) Unwrap() processor.V1 { - return w.p -} - -// XUnwrapper is for internal use only, do not use this. -func (o *OwnedProcessor) XUnwrapper() any { - return processorUnwrapper{p: o.p} -} diff --git a/public/service/processor_test.go b/public/service/processor_test.go deleted file mode 100644 index b70168cbf8..0000000000 --- a/public/service/processor_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package service - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/manager/mock" - "github.com/benthosdev/benthos/v4/internal/message" -) - -type fnProcessor struct { - fn func(context.Context, *Message) (MessageBatch, error) - closed bool -} - -func (p *fnProcessor) Process(ctx context.Context, msg *Message) (MessageBatch, error) { - return p.fn(ctx, msg) -} - -func (p *fnProcessor) Close(ctx context.Context) error { - p.closed = true - return nil -} - -func TestProcessorAirGapShutdown(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - rp := &fnProcessor{} - agrp := newAirGapProcessor("foo", rp, mock.NewManager()) - - err := agrp.Close(tCtx) - assert.NoError(t, err) - assert.True(t, rp.closed) -} - -func TestProcessorAirGapOneToOne(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - agrp := newAirGapProcessor("foo", &fnProcessor{ - fn: func(c context.Context, m *Message) (MessageBatch, error) { - if b, err := m.AsBytes(); err != nil || string(b) != "unchanged" { - return nil, errors.New("nope") - } - m.SetBytes([]byte("changed")) - return MessageBatch{m}, nil - }, - }, mock.NewManager()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg.ShallowCopy()) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "changed", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) -} - -func TestProcessorAirGapOneToError(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - agrp := newAirGapProcessor("foo", &fnProcessor{ - fn: func(c context.Context, m *Message) (MessageBatch, error) { - _, err := m.AsStructured() - return nil, err - }, - }, mock.NewManager()) - - msg := message.QuickBatch([][]byte{[]byte("not a structured doc")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.EqualError(t, msgs[0].Get(0).ErrorGet(), "invalid character 'o' in literal null (expecting 'u')") -} - -func TestProcessorAirGapOneToMany(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - agrp := newAirGapProcessor("foo", &fnProcessor{ - fn: func(c context.Context, m *Message) (MessageBatch, error) { - if b, err := m.AsBytes(); err != nil || string(b) != "unchanged" { - return nil, errors.New("nope") - } - second := m.Copy() - third := m.Copy() - m.SetBytes([]byte("changed 1")) - second.SetBytes([]byte("changed 2")) - third.SetBytes([]byte("changed 3")) - return MessageBatch{m, second, third}, nil - }, - }, mock.NewManager()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg.ShallowCopy()) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 3, msgs[0].Len()) - assert.Equal(t, "changed 1", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "changed 2", string(msgs[0].Get(1).AsBytes())) - assert.Equal(t, "changed 3", string(msgs[0].Get(2).AsBytes())) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) -} - -//------------------------------------------------------------------------------ - -type fnBatchProcessor struct { - fn func(context.Context, MessageBatch) ([]MessageBatch, error) - closed bool -} - -func (p *fnBatchProcessor) ProcessBatch(ctx context.Context, msg MessageBatch) ([]MessageBatch, error) { - return p.fn(ctx, msg) -} - -func (p *fnBatchProcessor) Close(ctx context.Context) error { - p.closed = true - return nil -} - -func TestBatchProcessorAirGapShutdown(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - rp := &fnBatchProcessor{} - agrp := newAirGapBatchProcessor("foo", rp, mock.NewManager()) - - err := agrp.Close(tCtx) - assert.NoError(t, err) - assert.True(t, rp.closed) -} - -func TestBatchProcessorAirGapOneToOne(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - agrp := newAirGapBatchProcessor("foo", &fnBatchProcessor{ - fn: func(c context.Context, msgs MessageBatch) ([]MessageBatch, error) { - if b, err := msgs[0].AsBytes(); err != nil || string(b) != "unchanged" { - return nil, errors.New("nope") - } - msgs[0].SetBytes([]byte("changed")) - return []MessageBatch{{msgs[0]}}, nil - }, - }, mock.NewManager()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg.ShallowCopy()) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "changed", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) -} - -func TestBatchProcessorAirGapOneToError(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - agrp := newAirGapBatchProcessor("foo", &fnBatchProcessor{ - fn: func(c context.Context, msgs MessageBatch) ([]MessageBatch, error) { - _, err := msgs[0].AsStructured() - return nil, err - }, - }, mock.NewManager()) - - msg := message.QuickBatch([][]byte{[]byte("not a structured doc")}) - msgs, res := agrp.ProcessBatch(tCtx, msg) - require.NoError(t, res) - require.Len(t, msgs, 1) - assert.Equal(t, 1, msgs[0].Len()) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "not a structured doc", string(msgs[0].Get(0).AsBytes())) - assert.EqualError(t, msgs[0].Get(0).ErrorGet(), "invalid character 'o' in literal null (expecting 'u')") -} - -func TestBatchProcessorAirGapOneToMany(t *testing.T) { - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - agrp := newAirGapBatchProcessor("foo", &fnBatchProcessor{ - fn: func(c context.Context, msgs MessageBatch) ([]MessageBatch, error) { - if b, err := msgs[0].AsBytes(); err != nil || string(b) != "unchanged" { - return nil, errors.New("nope") - } - second := msgs[0].Copy() - third := msgs[0].Copy() - msgs[0].SetBytes([]byte("changed 1")) - second.SetBytes([]byte("changed 2")) - third.SetBytes([]byte("changed 3")) - return []MessageBatch{{msgs[0], second}, {third}}, nil - }, - }, mock.NewManager()) - - msg := message.QuickBatch([][]byte{[]byte("unchanged")}) - msgs, res := agrp.ProcessBatch(tCtx, msg.ShallowCopy()) - require.NoError(t, res) - require.Len(t, msgs, 2) - assert.Equal(t, "unchanged", string(msg.Get(0).AsBytes())) - - assert.Equal(t, 2, msgs[0].Len()) - assert.Equal(t, "changed 1", string(msgs[0].Get(0).AsBytes())) - assert.Equal(t, "changed 2", string(msgs[0].Get(1).AsBytes())) - - assert.Equal(t, 1, msgs[1].Len()) - assert.Equal(t, "changed 3", string(msgs[1].Get(0).AsBytes())) -} diff --git a/public/service/rate_limit.go b/public/service/rate_limit.go deleted file mode 100644 index 84c439099b..0000000000 --- a/public/service/rate_limit.go +++ /dev/null @@ -1,45 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" -) - -// RateLimit is an interface implemented by Benthos rate limits. -type RateLimit interface { - // Access the rate limited resource. Returns a duration or an error if the - // rate limit check fails. The returned duration is either zero (meaning the - // resource may be accessed) or a reasonable length of time to wait before - // requesting again. - Access(context.Context) (time.Duration, error) - - Closer -} - -//------------------------------------------------------------------------------ - -func newAirGapRateLimit(c RateLimit, stats metrics.Type) ratelimit.V1 { - return ratelimit.MetricsForRateLimit(c, stats) -} - -//------------------------------------------------------------------------------ - -// Implements RateLimit around a types.RateLimit. -type reverseAirGapRateLimit struct { - r ratelimit.V1 -} - -func newReverseAirGapRateLimit(r ratelimit.V1) *reverseAirGapRateLimit { - return &reverseAirGapRateLimit{r} -} - -func (a *reverseAirGapRateLimit) Access(ctx context.Context) (time.Duration, error) { - return a.r.Access(ctx) -} - -func (a *reverseAirGapRateLimit) Close(ctx context.Context) error { - return a.r.Close(ctx) -} diff --git a/public/service/rate_limit_test.go b/public/service/rate_limit_test.go deleted file mode 100644 index 74015177f8..0000000000 --- a/public/service/rate_limit_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package service - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/benthosdev/benthos/v4/internal/component/metrics" -) - -type closableRateLimit struct { - next time.Duration - err error - closed bool -} - -func (c *closableRateLimit) Access(ctx context.Context) (time.Duration, error) { - return c.next, c.err -} - -func (c *closableRateLimit) Close(ctx context.Context) error { - c.closed = true - return nil -} - -func TestRateLimitAirGapShutdown(t *testing.T) { - ctx := context.Background() - rl := &closableRateLimit{ - next: time.Second, - } - agrl := newAirGapRateLimit(rl, metrics.Noop()) - - tout, err := agrl.Access(ctx) - assert.NoError(t, err) - assert.Equal(t, time.Second, tout) - - rl.next = time.Millisecond - rl.err = errors.New("test error") - - tout, err = agrl.Access(ctx) - assert.EqualError(t, err, "test error") - assert.Equal(t, time.Millisecond, tout) - - err = agrl.Close(ctx) - assert.NoError(t, err) - assert.True(t, rl.closed) -} - -//------------------------------------------------------------------------------ - -type closableRateLimitType struct { - next time.Duration - err error - closed bool -} - -func (c *closableRateLimitType) Access(ctx context.Context) (time.Duration, error) { - return c.next, c.err -} - -func (c *closableRateLimitType) Close(ctx context.Context) error { - c.closed = true - return nil -} - -func TestRateLimitReverseAirGapShutdown(t *testing.T) { - rl := &closableRateLimitType{ - next: time.Second, - } - agrl := newReverseAirGapRateLimit(rl) - - tout, err := agrl.Access(context.Background()) - assert.NoError(t, err) - assert.Equal(t, time.Second, tout) - - rl.next = time.Millisecond - rl.err = errors.New("test error") - - tout, err = agrl.Access(context.Background()) - assert.EqualError(t, err, "test error") - assert.Equal(t, time.Millisecond, tout) - - assert.NoError(t, agrl.Close(context.Background())) - assert.True(t, rl.closed) -} diff --git a/public/service/resources.go b/public/service/resources.go deleted file mode 100644 index 779e0a70a3..0000000000 --- a/public/service/resources.go +++ /dev/null @@ -1,281 +0,0 @@ -package service - -import ( - "context" - "io/fs" - "time" - - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/manager/mock" -) - -// Resources provides access to service-wide resources. -type Resources struct { - mgr bundle.NewManagement -} - -func newResourcesFromManager(nm bundle.NewManagement) *Resources { - return &Resources{mgr: nm} -} - -// MockResources returns an instantiation of a resources struct that provides -// valid but ineffective methods and observability components. It is possible to -// instantiate this with mocked (in-memory) cache and rate limit types for -// testing purposed. -func MockResources(opts ...MockResourcesOptFn) *Resources { - m := mock.NewManager() - for _, o := range opts { - o(m) - } - return newResourcesFromManager(m) -} - -// MockResourcesOptFn provides a func based optional argument to MockResources. -type MockResourcesOptFn func(*mock.Manager) - -// MockResourcesOptUseLogger sets the logger to be used by components -// referencing these resources. -func MockResourcesOptUseLogger(l *Logger) MockResourcesOptFn { - return func(m *mock.Manager) { - if l != nil { - m.L = l.m - } - } -} - -// MockResourcesOptAddCache instantiates the resources type with a mock cache -// with a given name. Cached items are held in memory. -func MockResourcesOptAddCache(name string) MockResourcesOptFn { - return func(m *mock.Manager) { - m.Caches[name] = map[string]mock.CacheItem{} - } -} - -// MockResourcesOptAddRateLimit instantiates the resources type with a mock rate -// limit with a given name, the provided closure will be called for each -// invocation of the rate limit. -func MockResourcesOptAddRateLimit(name string, fn func(context.Context) (time.Duration, error)) MockResourcesOptFn { - return func(m *mock.Manager) { - m.RateLimits[name] = mock.RateLimit(fn) - } -} - -// EngineVersion returns the version stamp associated with the underlying -// benthos engine. The version string is not guaranteed to match any particular -// scheme. -func (r *Resources) EngineVersion() string { - return r.mgr.EngineVersion() -} - -// Label returns a label that identifies the component instantiation. This could -// be an explicit label set in config, or is otherwise a generated label based -// on the position of the component within a config. -func (r *Resources) Label() string { - return r.mgr.Label() -} - -// Logger returns a logger preset with context about the component the resources -// were provided to. -func (r *Resources) Logger() *Logger { - return newReverseAirGapLogger(r.mgr.Logger()) -} - -// Metrics returns a mechanism for creating custom metrics. -func (r *Resources) Metrics() *Metrics { - return newReverseAirGapMetrics(r.mgr.Metrics()) -} - -// OtelTracer returns an open telemetry tracer provider that can be used to -// create new tracers. -// -// Experimental: This type signature is experimental and therefore subject to -// change outside of major version releases. -func (r *Resources) OtelTracer() trace.TracerProvider { - return r.mgr.Tracer() -} - -// wrapperFS provides extra methods support around a bare fs.FS that does -// fully implement ifs.FS, this allows us to keep some clean interfaces while -// also ensuring backward compatibility. -type wrapperFS struct { - fs fs.FS - fallback ifs.FS -} - -// Open opens the named file for reading. -func (f *wrapperFS) Open(name string) (fs.File, error) { - return f.fs.Open(name) -} - -// OpenFile is the generalized open call. -func (f *wrapperFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - return f.fallback.OpenFile(name, flag, perm) -} - -// Stat returns a FileInfo describing the named file. -func (f *wrapperFS) Stat(name string) (fs.FileInfo, error) { - return f.fallback.Stat(name) -} - -// Remove removes the named file or (empty) directory. -func (f *wrapperFS) Remove(name string) error { - return f.fallback.Remove(name) -} - -// MkdirAll creates a directory named path, along with any necessary parents, -// and returns nil, or else returns an error. -func (f *wrapperFS) MkdirAll(path string, perm fs.FileMode) error { - return f.fallback.MkdirAll(path, perm) -} - -// FS implements a superset of fs.FS and includes goodies that benthos -// components specifically need. -type FS struct { - i ifs.FS -} - -// NewFS provides a new instance of a filesystem. The fs.FS passed in can -// optionally implement methods from benthos ifs.FS -func NewFS(filesystem fs.FS) *FS { - if fsimpl, ok := filesystem.(ifs.FS); ok { - return &FS{fsimpl} - } - return &FS{&wrapperFS{filesystem, ifs.OS()}} -} - -// Open opens the named file for reading. -func (f *FS) Open(name string) (fs.File, error) { - return f.i.Open(name) -} - -// OpenFile is the generalized open call. -func (f *FS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { - return f.i.OpenFile(name, flag, perm) -} - -// Stat returns a FileInfo describing the named file. -func (f *FS) Stat(name string) (fs.FileInfo, error) { - return f.i.Stat(name) -} - -// Remove removes the named file or (empty) directory. -func (f *FS) Remove(name string) error { - return f.i.Remove(name) -} - -// MkdirAll creates a directory named path, along with any necessary parents, -// and returns nil, or else returns an error. -func (f *FS) MkdirAll(path string, perm fs.FileMode) error { - return f.i.MkdirAll(path, perm) -} - -// FS returns an fs.FS implementation that provides isolation or customised -// behaviour for components that access the filesystem. For example, this might -// be used to tally files being accessed by components for observability -// purposes, or to customise where relative paths are resolved from. -// -// Components should use this instead of accessing the os directly. However, the -// default behaviour of an environment FS is to access the OS from the directory -// the process is running from, which matches calling the os package directly. -func (r *Resources) FS() *FS { - if r == nil || r.mgr == nil { - return &FS{i: ifs.OS()} - } - return &FS{i: r.mgr.FS()} -} - -// AccessCache attempts to access a cache resource by name. This action can -// block if CRUD operations are being actively performed on the resource. -func (r *Resources) AccessCache(ctx context.Context, name string, fn func(c Cache)) error { - return r.mgr.AccessCache(ctx, name, func(c cache.V1) { - fn(newReverseAirGapCache(c)) - }) -} - -// HasCache confirms whether a cache with a given name has been registered as a -// resource. This method is useful during component initialisation as it is -// defensive against ordering. -func (r *Resources) HasCache(name string) bool { - return r.mgr.ProbeCache(name) -} - -// AccessInput attempts to access a input resource by name. -func (r *Resources) AccessInput(ctx context.Context, name string, fn func(i *ResourceInput)) error { - return r.mgr.AccessInput(ctx, name, func(in input.Streamed) { - fn(newResourceInput(in)) - }) -} - -// HasInput confirms whether an input with a given name has been registered as a -// resource. This method is useful during component initialisation as it is -// defensive against ordering. -func (r *Resources) HasInput(name string) bool { - return r.mgr.ProbeInput(name) -} - -// AccessOutput attempts to access an output resource by name. This action can -// block if CRUD operations are being actively performed on the resource. -func (r *Resources) AccessOutput(ctx context.Context, name string, fn func(o *ResourceOutput)) error { - return r.mgr.AccessOutput(ctx, name, func(o output.Sync) { - fn(newResourceOutput(o)) - }) -} - -// HasOutput confirms whether an output with a given name has been registered as -// a resource. This method is useful during component initialisation as it is -// defensive against ordering. -func (r *Resources) HasOutput(name string) bool { - return r.mgr.ProbeOutput(name) -} - -// AccessRateLimit attempts to access a rate limit resource by name. This action -// can block if CRUD operations are being actively performed on the resource. -func (r *Resources) AccessRateLimit(ctx context.Context, name string, fn func(r RateLimit)) error { - return r.mgr.AccessRateLimit(ctx, name, func(r ratelimit.V1) { - fn(newReverseAirGapRateLimit(r)) - }) -} - -// HasRateLimit confirms whether a rate limit with a given name has been -// registered as a resource. This method is useful during component -// initialisation as it is defensive against ordering. -func (r *Resources) HasRateLimit(name string) bool { - return r.mgr.ProbeRateLimit(name) -} - -//------------------------------------------------------------------------------ - -type resourcesUnwrapper struct { - mgr bundle.NewManagement -} - -func (r resourcesUnwrapper) Unwrap() bundle.NewManagement { - return r.mgr -} - -// XUnwrapper is for internal use only, do not use this. -func (r *Resources) XUnwrapper() any { - return resourcesUnwrapper{mgr: r.mgr} -} - -//------------------------------------------------------------------------------ - -// ManagedBatchOutput takes a BatchOutput implementation and wraps it within a -// mechanism that automatically manages QOL details such as connect/reconnect -// looping, max in flight, back pressure, and so on. This is similar to how an -// output would be executed within a standard Benthos pipeline. -func (r *Resources) ManagedBatchOutput(typeName string, maxInFlight int, b BatchOutput) (*OwnedOutput, error) { - w := newAirGapBatchWriter(b) - o, err := output.NewAsyncWriter(typeName, maxInFlight, w, r.mgr) - if err != nil { - return nil, err - } - return newOwnedOutput(o) -} diff --git a/public/service/resources_test.go b/public/service/resources_test.go deleted file mode 100644 index c2ca0eb86d..0000000000 --- a/public/service/resources_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package service_test - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -type fooReader struct { - mgr *service.Resources -} - -func (r fooReader) Connect(context.Context) error { - return nil -} - -func (r fooReader) ReadBatch(ctx context.Context) (b service.MessageBatch, aFn service.AckFunc, err error) { - if accessErr := r.mgr.AccessInput(ctx, "foo", func(i *service.ResourceInput) { - b, aFn, err = i.ReadBatch(ctx) - }); accessErr != nil { - err = accessErr - } - return -} - -func (r fooReader) Close(ctx context.Context) error { - return nil -} - -func TestResourceInput(t *testing.T) { - env := service.NewEnvironment() - require.NoError(t, env.RegisterBatchInput( - "foo_reader", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) { - return fooReader{mgr: mgr}, nil - })) - - b := env.NewStreamBuilder() - - require.NoError(t, b.SetYAML(` -input: - foo_reader: {} - -input_resources: - - label: foo - generate: - count: 3 - mapping: | - root.id = count("public/service/resources_test.TestResourceInput") - root.purpose = "test resource inputs" - -output: - drop: {} -`)) - - var consumedMessages []string - var consumedMut sync.Mutex - require.NoError(t, b.AddConsumerFunc(func(ctx context.Context, m *service.Message) error { - consumedMut.Lock() - mBytes, _ := m.AsBytes() - consumedMessages = append(consumedMessages, string(mBytes)) - consumedMut.Unlock() - return nil - })) - - strm, err := b.Build() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - assert.Equal(t, []string{ - `{"id":1,"purpose":"test resource inputs"}`, - `{"id":2,"purpose":"test resource inputs"}`, - `{"id":3,"purpose":"test resource inputs"}`, - }, consumedMessages) -} - -type fooWriter struct { - mgr *service.Resources -} - -func (r fooWriter) Connect(context.Context) error { - return nil -} - -func (r fooWriter) Write(ctx context.Context, msg *service.Message) (err error) { - if accessErr := r.mgr.AccessOutput(ctx, "foo", func(o *service.ResourceOutput) { - err = o.Write(ctx, msg) - }); accessErr != nil { - err = accessErr - } - return -} - -func (r fooWriter) Close(ctx context.Context) error { - return nil -} - -func TestResourceOutput(t *testing.T) { - tmpDir := t.TempDir() - - outFilePath := filepath.Join(tmpDir, "out.txt") - - env := service.NewEnvironment() - require.NoError(t, env.RegisterOutput( - "foo_writer", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Output, int, error) { - return fooWriter{mgr: mgr}, 1, nil - })) - - b := env.NewStreamBuilder() - - require.NoError(t, b.SetYAML(fmt.Sprintf(` -input: - generate: - count: 3 - mapping: | - root.id = count("public/service/resources_test.TestResourceOutput") - root.purpose = "test resource outputs" - -output_resources: - - label: foo - file: - codec: lines - path: %v - -output: - foo_writer: {} -`, outFilePath))) - - strm, err := b.Build() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - - outBytes, err := os.ReadFile(outFilePath) - require.NoError(t, err) - - assert.Equal(t, `{"id":1,"purpose":"test resource outputs"} -{"id":2,"purpose":"test resource outputs"} -{"id":3,"purpose":"test resource outputs"} -`, string(outBytes)) -} diff --git a/public/service/scanner.go b/public/service/scanner.go deleted file mode 100644 index 8af14e7887..0000000000 --- a/public/service/scanner.go +++ /dev/null @@ -1,245 +0,0 @@ -package service - -import ( - "context" - "errors" - "io" - "sync" - - "github.com/benthosdev/benthos/v4/internal/component/scanner" - "github.com/benthosdev/benthos/v4/internal/message" -) - -// ScannerSourceDetails contains exclusively optional information which could be -// used by scanner implementations in order to determine the underlying data -// format. -type ScannerSourceDetails struct { - details scanner.SourceDetails -} - -// NewScannerSourceDetails creates a ScannerSourceDetails object with default -// values. -func NewScannerSourceDetails() *ScannerSourceDetails { - return &ScannerSourceDetails{ - details: scanner.SourceDetails{}, - } -} - -// SetName sets a filename (or other equivalent name of the source) to details. -func (r *ScannerSourceDetails) SetName(name string) { - r.details.Name = name -} - -// Name returns a filename (or other equivalent name of the source), or an -// empty string if it has not been set. -func (r *ScannerSourceDetails) Name() string { - return r.details.Name -} - -// BatchScannerCreator is an interface implemented by Benthos scanner plugins. -// Calls to Create must create a new instantiation of BatchScanner that consumes -// the provided io.ReadCloser, produces batches of messages (batches containing -// a single message are valid) and calls the provided AckFunc once all derived -// data is delivered (or rejected). -type BatchScannerCreator interface { - Create(io.ReadCloser, AckFunc, *ScannerSourceDetails) (BatchScanner, error) - Close(context.Context) error -} - -// BatchScanner is an interface implemented by instantiations of -// BatchScannerCreator responsible for consuming an io.ReadCloser and converting -// the stream of bytes into discrete message batches based on the underlying -// format of the scanner. -// -// The returned ack func will be called by downstream components once the -// produced message batch has been successfully processed and delivered. Only -// once all message batches extracted from a BatchScanner should the ack func -// provided at instantiation be called, unless an ack call is returned with an -// error. -// -// Once the input data has been fully consumed io.EOF should be returned. -type BatchScanner interface { - NextBatch(context.Context) (MessageBatch, AckFunc, error) - Close(context.Context) error -} - -//------------------------------------------------------------------------------ - -// SimpleBatchScanner is a reduced version of BatchScanner where managing the -// aggregation of acknowledgments from yielded message batches is omitted. -type SimpleBatchScanner interface { - NextBatch(context.Context) (MessageBatch, error) - Close(context.Context) error -} - -// AutoAggregateBatchScannerAcks wraps a simplified SimpleBatchScanner in a -// mechanism that automatically aggregates acknowledgments from yielded batches. -func AutoAggregateBatchScannerAcks(strm SimpleBatchScanner, aFn AckFunc) BatchScanner { - return &managedAckBatchScanner{ - strm: strm, - sourceAck: ackOnce(aFn), - } -} - -func ackOnce(fn AckFunc) AckFunc { - var once sync.Once - return func(ctx context.Context, err error) error { - var ackErr error - once.Do(func() { - ackErr = fn(ctx, err) - }) - return ackErr - } -} - -type managedAckBatchScanner struct { - strm SimpleBatchScanner - - sourceAck AckFunc - mut sync.Mutex - finished bool - pending int32 -} - -func (s *managedAckBatchScanner) ack(ctx context.Context, err error) error { - s.mut.Lock() - s.pending-- - doAck := s.pending == 0 && s.finished - s.mut.Unlock() - - if err != nil { - return s.sourceAck(ctx, err) - } - if doAck { - return s.sourceAck(ctx, nil) - } - return nil -} - -func (s *managedAckBatchScanner) NextBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - b, err := s.strm.NextBatch(ctx) - - s.mut.Lock() - defer s.mut.Unlock() - - if err == nil { - s.pending++ - return b, s.ack, nil - } - - if errors.Is(err, io.EOF) { - s.finished = true - } else { - _ = s.sourceAck(ctx, err) - } - return nil, nil, err -} - -func (s *managedAckBatchScanner) Close(ctx context.Context) error { - s.mut.Lock() - defer s.mut.Unlock() - - if !s.finished { - _ = s.sourceAck(ctx, errors.New("service shutting down")) - } - if s.pending == 0 { - _ = s.sourceAck(ctx, nil) - } - return s.strm.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// Implements reader.Codec. -type airGapBatchScannerCreator struct { - r BatchScannerCreator -} - -func newAirGapBatchScannerCreator(r BatchScannerCreator) scanner.Creator { - return &airGapBatchScannerCreator{r: r} -} - -func (a *airGapBatchScannerCreator) Create(rdr io.ReadCloser, aFn scanner.AckFn, details scanner.SourceDetails) (scanner.Scanner, error) { - s, err := a.r.Create(rdr, AckFunc(aFn), &ScannerSourceDetails{details: details}) - if err != nil { - return nil, err - } - return &airGapBatchScanner{r: s}, nil -} - -func (a *airGapBatchScannerCreator) Close(ctx context.Context) error { - return a.r.Close(ctx) -} - -// Implements reader.Stream. -type airGapBatchScanner struct { - r BatchScanner -} - -func (a *airGapBatchScanner) Next(ctx context.Context) (message.Batch, scanner.AckFn, error) { - b, ackFn, err := a.r.NextBatch(ctx) - if err != nil { - return nil, nil, publicToInternalErr(err) - } - tBatch := make(message.Batch, len(b)) - for i, m := range b { - tBatch[i] = m.part - } - return tBatch, scanner.AckFn(ackFn), nil -} - -func (a *airGapBatchScanner) Close(ctx context.Context) error { - return a.r.Close(ctx) -} - -//------------------------------------------------------------------------------ - -// OwnedScannerCreator provides direct ownership of a batch scanner -// extracted from a plugin config. -type OwnedScannerCreator struct { - rdr scanner.Creator -} - -// Create a new scanner from an io.ReadCloser along with optional information -// about the source of the reader and a function to be called once the -// underlying data has been read and acknowledged in its entirety. -func (s *OwnedScannerCreator) Create(rdr io.ReadCloser, aFn AckFunc, details *ScannerSourceDetails) (*OwnedScanner, error) { - var iDetails scanner.SourceDetails - if details != nil { - iDetails = details.details - } - is, err := s.rdr.Create(rdr, scanner.AckFn(aFn), iDetails) - if err != nil { - return nil, err - } - return &OwnedScanner{strm: is}, nil -} - -func (s *OwnedScannerCreator) Close(ctx context.Context) error { - return s.rdr.Close(ctx) -} - -// OwnedScanner provides direct ownership of a scanner. -type OwnedScanner struct { - strm scanner.Scanner -} - -// NextBatch attempts to further consume the underlying reader in order to -// extract another message (or multiple). -func (s *OwnedScanner) NextBatch(ctx context.Context) (MessageBatch, AckFunc, error) { - ib, aFn, err := s.strm.Next(ctx) - if err != nil { - return nil, nil, err - } - - batch := make(MessageBatch, len(ib)) - for i := range ib { - batch[i] = NewInternalMessage(ib[i]) - } - return batch, AckFunc(aFn), nil -} - -// Close the scanner, indicating that it will no longer be consumed. -func (s *OwnedScanner) Close(ctx context.Context) error { - return s.strm.Close(ctx) -} diff --git a/public/service/service.go b/public/service/service.go deleted file mode 100644 index c636d35ead..0000000000 --- a/public/service/service.go +++ /dev/null @@ -1,126 +0,0 @@ -package service - -import ( - "context" - "log/slog" - "os" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/cli" - "github.com/benthosdev/benthos/v4/internal/cli/common" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/log" -) - -// RunCLI executes Benthos as a CLI, allowing users to specify a configuration -// file path(s) and execute subcommands for linting configs, testing configs, -// etc. This is how a standard distribution of Benthos operates. -// -// This call blocks until either: -// -// 1. The service shuts down gracefully due to the inputs closing -// 2. A termination signal is received -// 3. The provided context has a deadline that is reached, triggering graceful termination -// 4. The provided context is cancelled (WARNING, this prevents graceful termination) -// -// This function must only be called once during the entire lifecycle of your -// program, as it interacts with singleton state. In order to manage multiple -// Benthos stream lifecycles in a program use the StreamBuilder API instead. -func RunCLI(ctx context.Context, optFuncs ...CLIOptFunc) { - cliOpts := &CLIOptBuilder{ - opts: common.NewCLIOpts(cli.Version, cli.DateBuilt), - } - for _, o := range optFuncs { - o(cliOpts) - } - cliOpts.opts.OnLoggerInit = func(l log.Modular) (log.Modular, error) { - if cliOpts.outLoggerFn != nil { - cliOpts.outLoggerFn(&Logger{m: l}) - } - if cliOpts.teeLogger != nil { - return log.TeeLogger(l, log.NewBenthosLogAdapter(cliOpts.teeLogger)), nil - } - return l, nil - } - _ = cli.App(cliOpts.opts).RunContext(ctx, os.Args) -} - -type CLIOptBuilder struct { - opts *common.CLIOpts - teeLogger *slog.Logger - outLoggerFn func(*Logger) -} - -// CLIOptFunc defines an option to pass through the standard Benthos CLI in order -// to customise it's behaviour. -type CLIOptFunc func(*CLIOptBuilder) - -// CLIOptSetVersion overrides the default version and date built stamps. -func CLIOptSetVersion(version, dateBuilt string) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.opts.Version = version - c.opts.DateBuilt = dateBuilt - } -} - -// CLIOptSetProductName overrides the default product name in CLI help docs. -func CLIOptSetProductName(n string) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.opts.ProductName = n - } -} - -// CLIOptSetDocumentationURL overrides the default documentation URL in CLI help -// docs. -func CLIOptSetDocumentationURL(n string) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.opts.DocumentationURL = n - } -} - -// CLIOptOnLoggerInit sets a closure to be called when the service-wide logger -// is initialised. A modified version can be returned, allowing you to mutate -// the fields and settings that it has. -func CLIOptOnLoggerInit(fn func(*Logger)) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.outLoggerFn = fn - } -} - -// CLIOptAddTeeLogger adds another logger to receive all log events from the -// service initialised via the CLI. -func CLIOptAddTeeLogger(l *slog.Logger) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.teeLogger = l - } -} - -// CLIOptSetMainSchemaFrom overrides the default Benthos configuration schema -// for another. A constructor is provided such that downstream components can -// still modify copies of the schema when needed. -// -// NOTE: This transfers the configuration schema but NOT the Environment plugins -// themselves, which is the global set by default. -func CLIOptSetMainSchemaFrom(fn func() *ConfigSchema) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.opts.MainConfigSpecCtor = func() docs.FieldSpecs { - return fn().fields - } - } -} - -// CLIOptOnConfigParsed sets a closure function to be called when a main -// configuration file load has occurred. -// -// If an error is returned this will be treated by the CLI the same as any other -// failure to parse the bootstrap config. -func CLIOptOnConfigParse(fn func(fn *ParsedConfig) error) CLIOptFunc { - return func(c *CLIOptBuilder) { - c.opts.OnManagerInitialised = func(mgr bundle.NewManagement, pConf *docs.ParsedConfig) error { - return fn(&ParsedConfig{ - i: pConf, - mgr: mgr, - }) - } - } -} diff --git a/public/service/servicetest/service.go b/public/service/servicetest/service.go deleted file mode 100644 index 81890163c0..0000000000 --- a/public/service/servicetest/service.go +++ /dev/null @@ -1,23 +0,0 @@ -// Package servicetest provides functions and utilities that might be useful for -// testing custom Benthos builds. -package servicetest - -import ( - "context" - - "github.com/benthosdev/benthos/v4/internal/cli" - "github.com/benthosdev/benthos/v4/internal/cli/common" -) - -// RunCLIWithArgs executes Benthos as a CLI with an explicit set of arguments. -// This is useful for testing commands without needing to modify os.Args. -// -// This call blocks until either: -// -// 1. The service shuts down gracefully due to the inputs closing -// 2. A termination signal is received -// 3. The provided context has a deadline that is reached, triggering graceful termination -// 4. The provided context is cancelled (WARNING, this prevents graceful termination) -func RunCLIWithArgs(ctx context.Context, args ...string) { - _ = cli.App(common.NewCLIOpts(cli.Version, cli.DateBuilt)).RunContext(ctx, args) -} diff --git a/public/service/servicetest/service_test.go b/public/service/servicetest/service_test.go deleted file mode 100644 index 0a26ec05be..0000000000 --- a/public/service/servicetest/service_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package servicetest_test - -import ( - "context" - "fmt" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service/servicetest" -) - -func TestRunCLIShutdown(t *testing.T) { - tmpDir := t.TempDir() - confPath := filepath.Join(tmpDir, "foo.yaml") - outPath := filepath.Join(tmpDir, "out.txt") - - require.NoError(t, os.WriteFile(confPath, fmt.Appendf(nil, ` -input: - generate: - mapping: 'root.id = "foobar"' - interval: "100ms" -output: - file: - codec: lines - path: %v -`, outPath), 0o644)) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) - defer cancel() - - servicetest.RunCLIWithArgs(ctx, "benthos", "-c", confPath) - - data, _ := os.ReadFile(outPath) - assert.Contains(t, string(data), "foobar") -} diff --git a/public/service/stream.go b/public/service/stream.go deleted file mode 100644 index 33b30fb4d7..0000000000 --- a/public/service/stream.go +++ /dev/null @@ -1,181 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync" - "time" - - "go.opentelemetry.io/otel/trace" - - "github.com/Jeffail/shutdown" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -// Stream executes a full Benthos stream and provides methods for performing -// status checks, terminating the stream, and blocking until the stream ends. -type Stream struct { - strm *stream.Type - httpAPI *api.Type - strmMut sync.Mutex - shutSig *shutdown.Signaller - onStart func() - - conf stream.Config - mgr *manager.Type - stats metrics.Type - tracer trace.TracerProvider - logger log.Modular -} - -func newStream( - conf stream.Config, - httpAPI *api.Type, - mgr *manager.Type, - stats metrics.Type, - tracer trace.TracerProvider, - logger log.Modular, - onStart func(), -) *Stream { - return &Stream{ - conf: conf, - httpAPI: httpAPI, - mgr: mgr, - stats: stats, - tracer: tracer, - logger: logger, - shutSig: shutdown.NewSignaller(), - onStart: onStart, - } -} - -// Run attempts to start the stream pipeline and blocks until either the stream -// has gracefully come to a stop, or the provided context is cancelled. -func (s *Stream) Run(ctx context.Context) (err error) { - s.strmMut.Lock() - if s.strm != nil { - err = errors.New("stream has already been run") - } else { - s.strm, err = stream.New(s.conf, s.mgr, - stream.OptOnClose(func() { - s.shutSig.TriggerHasStopped() - })) - } - s.strmMut.Unlock() - if err != nil { - return - } - - if s.httpAPI != nil { - go func() { - _ = s.httpAPI.ListenAndServe() - }() - } - go s.onStart() - - select { - case <-s.shutSig.HasStoppedChan(): - return s.Stop(ctx) - case <-ctx.Done(): - } - return ctx.Err() -} - -// StopWithin attempts to close the stream within the specified timeout period. -// Initially the attempt is graceful, but as the timeout draws close the attempt -// becomes progressively less graceful. -// -// An ungraceful shutdown increases the likelihood of processing duplicate -// messages on the next start up, but never results in dropped messages as long -// as the input source supports at-least-once delivery. -func (s *Stream) StopWithin(timeout time.Duration) error { - ctx, done := context.WithTimeout(context.Background(), timeout) - defer done() - return s.Stop(ctx) -} - -// Stop attempts to close the stream gracefully, but if the context is closed or -// draws near to a deadline the attempt becomes less graceful. -// -// An ungraceful shutdown increases the likelihood of processing duplicate -// messages on the next start up, but never results in dropped messages as long -// as the input source supports at-least-once delivery. -func (s *Stream) Stop(ctx context.Context) (err error) { - s.strmMut.Lock() - strm := s.strm - s.strmMut.Unlock() - if strm == nil { - return errors.New("stream has not been run yet") - } - - stopStats := s.stats - closeStats := func() error { - if stopStats == nil { - return nil - } - err := stopStats.Close() - stopStats = nil - return err - } - - stopTracer := s.tracer - closeTracer := func(ctx context.Context) error { - if stopTracer == nil { - return nil - } - if shutter, ok := stopTracer.(interface { - Shutdown(context.Context) error - }); ok { - return shutter.Shutdown(ctx) - } - return nil - } - - stopHTTP := s.httpAPI - closeHTTP := func(ctx context.Context) error { - if stopHTTP == nil { - return nil - } - err := s.httpAPI.Shutdown(ctx) - stopHTTP = nil - return err - } - - defer func() { - if err == nil { - return - } - - // Still attempt to shut down other resources on an error, but do not - // block. - s.mgr.TriggerStopConsuming() - _ = closeStats() - _ = closeTracer(context.Background()) - _ = closeHTTP(context.Background()) - }() - - if err = strm.Stop(ctx); err != nil { - return - } - - s.mgr.TriggerStopConsuming() - if err = s.mgr.WaitForClose(ctx); err != nil { - return - } - - if err = closeStats(); err != nil { - return - } - - if err = closeTracer(ctx); err != nil { - return - } - - err = closeHTTP(ctx) - return -} diff --git a/public/service/stream_builder.go b/public/service/stream_builder.go deleted file mode 100644 index 284b90f13a..0000000000 --- a/public/service/stream_builder.go +++ /dev/null @@ -1,1014 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net/http" - "os" - "sync/atomic" - - "github.com/Jeffail/gabs/v2" - "github.com/gofrs/uuid" - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/bundle/tracing" - "github.com/benthosdev/benthos/v4/internal/cli" - "github.com/benthosdev/benthos/v4/internal/component/buffer" - "github.com/benthosdev/benthos/v4/internal/component/cache" - "github.com/benthosdev/benthos/v4/internal/component/input" - "github.com/benthosdev/benthos/v4/internal/component/metrics" - "github.com/benthosdev/benthos/v4/internal/component/output" - "github.com/benthosdev/benthos/v4/internal/component/processor" - "github.com/benthosdev/benthos/v4/internal/component/ratelimit" - "github.com/benthosdev/benthos/v4/internal/component/tracer" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/manager" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/stream" -) - -// StreamBuilder provides methods for building a Benthos stream configuration. -// When parsing Benthos configs this builder follows the schema and field -// defaults of a standard Benthos configuration. Environment variable -// interpolations are also parsed and resolved the same as regular configs. -// -// Streams built with a stream builder have the HTTP server for exposing metrics -// and ready checks disabled by default, which is the only deviation away from a -// standard Benthos default configuration. In order to enable the server set the -// configuration field `http.enabled` to `true` explicitly, or use `SetHTTPMux` -// in order to provide an explicit HTTP multiplexer for registering those -// endpoints. -type StreamBuilder struct { - engineVersion string - - http api.Config - threads int - inputs []input.Config - buffer buffer.Config - processors []processor.Config - outputs []output.Config - resources manager.ResourceConfig - metrics metrics.Config - tracer tracer.Config - logger log.Config - - producerChan chan message.Transaction - producerID string - consumerFunc MessageBatchHandlerFunc - consumerID string - - apiMut manager.APIReg - customLogger log.Modular - - env *Environment - lintingDisabled bool - envVarLookupFn func(string) (string, bool) -} - -// NewStreamBuilder creates a new StreamBuilder. -func NewStreamBuilder() *StreamBuilder { - httpConf := api.NewConfig() - httpConf.Enabled = false - return &StreamBuilder{ - engineVersion: cli.Version, - http: httpConf, - buffer: buffer.NewConfig(), - resources: manager.NewResourceConfig(), - metrics: metrics.NewConfig(), - tracer: tracer.NewConfig(), - logger: log.NewConfig(), - env: globalEnvironment, - envVarLookupFn: os.LookupEnv, - } -} - -func (s *StreamBuilder) getLintContext() docs.LintContext { - conf := docs.NewLintConfig(s.env.internal) - conf.DocsProvider = s.env.internal - conf.BloblangEnv = s.env.bloblangEnv.Deactivated() - return docs.NewLintContext(conf) -} - -//------------------------------------------------------------------------------ - -// SetEngineVersion sets the version string representing the Benthos engine that -// components will see. By default a best attempt will be made to determine a -// version either from the benthos module import or a build-time flag. -func (s *StreamBuilder) SetEngineVersion(ev string) { - s.engineVersion = ev -} - -// DisableLinting configures the stream builder to no longer lint YAML configs, -// allowing you to add snippets of config to the builder without failing on -// linting rules. -func (s *StreamBuilder) DisableLinting() { - s.lintingDisabled = true -} - -// SetEnvVarLookupFunc changes the behaviour of the stream builder so that the -// value of environment variable interpolations (of the form `${FOO}`) are -// obtained via a provided function rather than the default of os.LookupEnv. -// -// TODO V5: Add context here, Travis is onto us. -func (s *StreamBuilder) SetEnvVarLookupFunc(fn func(string) (string, bool)) { - s.envVarLookupFn = fn -} - -// SetThreads configures the number of pipeline processor threads should be -// configured. By default the number will be zero, which means the thread count -// will match the number of logical CPUs on the machine. -func (s *StreamBuilder) SetThreads(n int) { - s.threads = n -} - -// PrintLogger is a simple Print based interface implemented by custom loggers. -type PrintLogger interface { - Printf(format string, v ...any) - Println(v ...any) -} - -// SetPrintLogger sets a custom logger supporting a simple Print based interface -// to be used by stream components. This custom logger will override any logging -// fields set via config. -func (s *StreamBuilder) SetPrintLogger(l PrintLogger) { - s.customLogger = log.Wrap(l) -} - -// SetLogger sets a customer logger via Go's standard logging interface, -// allowing you to replace the default Benthos logger with your own. -func (s *StreamBuilder) SetLogger(l *slog.Logger) { - s.customLogger = log.NewBenthosLogAdapter(l) -} - -// HTTPMultiplexer is an interface supported by most HTTP multiplexers. -type HTTPMultiplexer interface { - HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) -} - -type muxWrapper struct { - m HTTPMultiplexer -} - -func (w *muxWrapper) RegisterEndpoint(path, desc string, h http.HandlerFunc) { - w.m.HandleFunc(path, h) -} - -// SetHTTPMux sets an HTTP multiplexer to be used by stream components when -// registering endpoints instead of a new server spawned following the `http` -// fields of a Benthos config. -func (s *StreamBuilder) SetHTTPMux(m HTTPMultiplexer) { - s.apiMut = &muxWrapper{m} -} - -//------------------------------------------------------------------------------ - -// AddProducerFunc adds an input to the builder that allows you to write -// messages directly into the stream with a closure function. If any other input -// has or will be added to the stream builder they will be automatically -// composed within a broker when the pipeline is built. -// -// The returned MessageHandlerFunc can be called concurrently from any number of -// goroutines, and each call will block until the message is successfully -// delivered downstream, was rejected (or otherwise could not be delivered) or -// the context is cancelled. -// -// Only one producer func can be added to a stream builder, and subsequent calls -// will return an error. -func (s *StreamBuilder) AddProducerFunc() (MessageHandlerFunc, error) { - if s.producerChan != nil { - return nil, errors.New("unable to add multiple producer funcs to a stream builder") - } - - uuid, err := uuid.NewV4() - if err != nil { - return nil, fmt.Errorf("failed to generate a producer uuid: %w", err) - } - - tChan := make(chan message.Transaction) - s.producerChan = tChan - s.producerID = uuid.String() - - conf := input.NewConfig() - conf.Type = "inproc" - conf.Plugin = s.producerID - s.inputs = append(s.inputs, conf) - - return func(ctx context.Context, m *Message) error { - tmpMsg := message.Batch{m.part} - resChan := make(chan error) - select { - case tChan <- message.NewTransaction(tmpMsg, resChan): - case <-ctx.Done(): - return ctx.Err() - } - select { - case res := <-resChan: - return res - case <-ctx.Done(): - return ctx.Err() - } - }, nil -} - -// AddBatchProducerFunc adds an input to the builder that allows you to write -// message batches directly into the stream with a closure function. If any -// other input has or will be added to the stream builder they will be -// automatically composed within a broker when the pipeline is built. -// -// The returned MessageBatchHandlerFunc can be called concurrently from any -// number of goroutines, and each call will block until all messages within the -// batch are successfully delivered downstream, were rejected (or otherwise -// could not be delivered) or the context is cancelled. -// -// Only one producer func can be added to a stream builder, and subsequent calls -// will return an error. -func (s *StreamBuilder) AddBatchProducerFunc() (MessageBatchHandlerFunc, error) { - if s.producerChan != nil { - return nil, errors.New("unable to add multiple producer funcs to a stream builder") - } - - uuid, err := uuid.NewV4() - if err != nil { - return nil, fmt.Errorf("failed to generate a producer uuid: %w", err) - } - - tChan := make(chan message.Transaction) - s.producerChan = tChan - s.producerID = uuid.String() - - conf := input.NewConfig() - conf.Type = "inproc" - conf.Plugin = s.producerID - s.inputs = append(s.inputs, conf) - - return func(ctx context.Context, b MessageBatch) error { - tmpMsg := make(message.Batch, len(b)) - for i, m := range b { - tmpMsg[i] = m.part - } - resChan := make(chan error) - select { - case tChan <- message.NewTransaction(tmpMsg, resChan): - case <-ctx.Done(): - return ctx.Err() - } - select { - case res := <-resChan: - return res - case <-ctx.Done(): - return ctx.Err() - } - }, nil -} - -// AddInputYAML parses an input YAML configuration and adds it to the builder. -// If more than one input configuration is added they will automatically be -// composed within a broker when the pipeline is built. -func (s *StreamBuilder) AddInputYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeInput); err != nil { - return err - } - - iconf, err := input.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - - s.inputs = append(s.inputs, iconf) - return nil -} - -// AddProcessorYAML parses a processor YAML configuration and adds it to the -// builder to be executed within the pipeline.processors section, after all -// prior added processor configs. -func (s *StreamBuilder) AddProcessorYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeProcessor); err != nil { - return err - } - - pconf, err := processor.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - - s.processors = append(s.processors, pconf) - return nil -} - -// AddConsumerFunc adds an output to the builder that executes a closure -// function argument for each message. If more than one output configuration is -// added they will automatically be composed within a fan out broker when the -// pipeline is built. -// -// The provided MessageHandlerFunc may be called from any number of goroutines, -// and therefore it is recommended to implement some form of throttling or mutex -// locking in cases where the call is non-blocking. -// -// Only one consumer can be added to a stream builder, and subsequent calls will -// return an error. -func (s *StreamBuilder) AddConsumerFunc(fn MessageHandlerFunc) error { - if s.consumerFunc != nil { - return errors.New("unable to add multiple consumer funcs to a stream builder") - } - - uuid, err := uuid.NewV4() - if err != nil { - return fmt.Errorf("failed to generate a consumer uuid: %w", err) - } - - s.consumerFunc = func(c context.Context, mb MessageBatch) error { - for _, m := range mb { - if err := fn(c, m); err != nil { - return err - } - } - return nil - } - s.consumerID = uuid.String() - - conf := output.NewConfig() - conf.Type = "inproc" - conf.Plugin = s.consumerID - s.outputs = append(s.outputs, conf) - - return nil -} - -// AddBatchConsumerFunc adds an output to the builder that executes a closure -// function argument for each message batch. If more than one output -// configuration is added they will automatically be composed within a fan out -// broker when the pipeline is built. -// -// The provided MessageBatchHandlerFunc may be called from any number of -// goroutines, and therefore it is recommended to implement some form of -// throttling or mutex locking in cases where the call is non-blocking. -// -// Only one consumer can be added to a stream builder, and subsequent calls will -// return an error. -// -// Message batches must be created by upstream components (inputs, buffers, etc) -// otherwise message batches received by this consumer will have a single -// message contents. -func (s *StreamBuilder) AddBatchConsumerFunc(fn MessageBatchHandlerFunc) error { - if s.consumerFunc != nil { - return errors.New("unable to add multiple consumer funcs to a stream builder") - } - - uuid, err := uuid.NewV4() - if err != nil { - return fmt.Errorf("failed to generate a consumer uuid: %w", err) - } - - s.consumerFunc = fn - s.consumerID = uuid.String() - - conf := output.NewConfig() - conf.Type = "inproc" - conf.Plugin = s.consumerID - s.outputs = append(s.outputs, conf) - - return nil -} - -// AddOutputYAML parses an output YAML configuration and adds it to the builder. -// If more than one output configuration is added they will automatically be -// composed within a fan out broker when the pipeline is built. -func (s *StreamBuilder) AddOutputYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeOutput); err != nil { - return err - } - - oconf, err := output.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - - s.outputs = append(s.outputs, oconf) - return nil -} - -// AddCacheYAML parses a cache YAML configuration and adds it to the builder as -// a resource. -func (s *StreamBuilder) AddCacheYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeCache); err != nil { - return err - } - - cconf, err := cache.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - if cconf.Label == "" { - return errors.New("a label must be specified for cache resources") - } - for _, cc := range s.resources.ResourceCaches { - if cc.Label == cconf.Label { - return fmt.Errorf("label %v collides with a previously defined resource", cc.Label) - } - } - - s.resources.ResourceCaches = append(s.resources.ResourceCaches, cconf) - return nil -} - -// AddRateLimitYAML parses a rate limit YAML configuration and adds it to the -// builder as a resource. -func (s *StreamBuilder) AddRateLimitYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeRateLimit); err != nil { - return err - } - - rconf, err := ratelimit.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - if rconf.Label == "" { - return errors.New("a label must be specified for rate limit resources") - } - for _, rl := range s.resources.ResourceRateLimits { - if rl.Label == rconf.Label { - return fmt.Errorf("label %v collides with a previously defined resource", rl.Label) - } - } - - s.resources.ResourceRateLimits = append(s.resources.ResourceRateLimits, rconf) - return nil -} - -// AddResourcesYAML parses resource configurations and adds them to the config. -func (s *StreamBuilder) AddResourcesYAML(conf string) error { - node, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - spec := manager.Spec() - if err := s.lintYAMLSpec(spec, node); err != nil { - return err - } - - pConf, err := spec.ParsedConfigFromAny(node) - if err != nil { - return convertDocsLintErr(err) - } - - rconf, err := manager.FromParsed(s.env.internal, pConf) - if err != nil { - return convertDocsLintErr(err) - } - - return s.resources.AddFrom(&rconf) -} - -//------------------------------------------------------------------------------ - -// SetYAML parses a full Benthos config and uses it to configure the builder. If -// any inputs, processors, outputs, resources, etc, have previously been added -// to the builder they will be overridden by this new config. -func (s *StreamBuilder) SetYAML(conf string) error { - if s.producerChan != nil { - return errors.New("attempted to override inputs config after adding a func producer") - } - if s.consumerFunc != nil { - return errors.New("attempted to override outputs config after adding a func consumer") - } - - node, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - spec := configSpec() - if err := s.lintYAMLSpec(spec, node); err != nil { - return err - } - - pConf, err := spec.ParsedConfigFromAny(node) - if err != nil { - return convertDocsLintErr(err) - } - - sconf, err := config.FromParsed(s.env.internal, pConf, nil) - if err != nil { - return convertDocsLintErr(err) - } - - s.setFromConfig(sconf) - return nil -} - -var builderConfigSpec atomic.Pointer[docs.FieldSpecs] - -func configSpec() docs.FieldSpecs { - spec := builderConfigSpec.Load() - if spec == nil { - tmpSpec := config.Spec() - tmpSpec.SetDefault(false, "http", "enabled") - spec = &tmpSpec - builderConfigSpec.Store(spec) - } - return *spec -} - -// SetFields modifies the config by setting one or more fields identified by a -// dot path to a value. The argument must be a variadic list of pairs, where the -// first element is a string containing the target field dot path, and the -// second element is a typed value to set the field to. -func (s *StreamBuilder) SetFields(pathValues ...any) error { - if s.producerChan != nil { - return errors.New("attempted to override config after adding a func producer") - } - if s.consumerFunc != nil { - return errors.New("attempted to override config after adding a func consumer") - } - if len(pathValues)%2 != 0 { - return errors.New("invalid odd number of pathValues provided") - } - - var rootNode yaml.Node - if err := rootNode.Encode(s.buildConfig()); err != nil { - return err - } - - sanitConf := docs.NewSanitiseConfig(s.env.internal) - sanitConf.RemoveTypeField = true - sanitConf.RemoveDeprecated = false - sanitConf.DocsProvider = s.env.internal - - if err := configSpec().SanitiseYAML(&rootNode, sanitConf); err != nil { - return err - } - - for i := 0; i < len(pathValues)-1; i += 2 { - var valueNode yaml.Node - if err := valueNode.Encode(pathValues[i+1]); err != nil { - return err - } - pathString, ok := pathValues[i].(string) - if !ok { - return fmt.Errorf("variadic pair element %v should be a string, got a %T", i, pathValues[i]) - } - if err := configSpec().SetYAMLPath(s.env.internal, &rootNode, &valueNode, gabs.DotPathToSlice(pathString)...); err != nil { - return err - } - } - - spec := configSpec() - if err := s.lintYAMLSpec(spec, &rootNode); err != nil { - return err - } - - pConf, err := spec.ParsedConfigFromAny(&rootNode) - if err != nil { - return err - } - - sconf, err := config.FromParsed(s.env.internal, pConf, nil) - if err != nil { - return convertDocsLintErr(err) - } - - s.setFromConfig(sconf) - return nil -} - -func (s *StreamBuilder) setFromConfig(sconf config.Type) { - s.http = sconf.HTTP - s.inputs = []input.Config{sconf.Input} - s.buffer = sconf.Buffer - s.processors = sconf.Pipeline.Processors - s.threads = sconf.Pipeline.Threads - s.outputs = []output.Config{sconf.Output} - s.resources = sconf.ResourceConfig - s.logger = sconf.Logger - s.metrics = sconf.Metrics - s.tracer = sconf.Tracer -} - -// SetBufferYAML parses a buffer YAML configuration and sets it to the builder -// to be placed between the input and the pipeline (processors) sections. This -// config will replace any prior configured buffer. -func (s *StreamBuilder) SetBufferYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeBuffer); err != nil { - return err - } - - bconf, err := buffer.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - - s.buffer = bconf - return nil -} - -// SetMetricsYAML parses a metrics YAML configuration and adds it to the builder -// such that all stream components emit metrics through it. -func (s *StreamBuilder) SetMetricsYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeMetrics); err != nil { - return err - } - - mconf, err := metrics.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - - s.metrics = mconf - return nil -} - -// SetTracerYAML parses a tracer YAML configuration and adds it to the builder -// such that all stream components emit tracing spans through it. -func (s *StreamBuilder) SetTracerYAML(conf string) error { - nconf, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - if err := s.lintYAMLComponent(nconf, docs.TypeTracer); err != nil { - return err - } - - tconf, err := tracer.FromAny(s.env.internal, nconf) - if err != nil { - return convertDocsLintErr(err) - } - - s.tracer = tconf - return nil -} - -// SetLoggerYAML parses a logger YAML configuration and adds it to the builder -// such that all stream components emit logs through it. -func (s *StreamBuilder) SetLoggerYAML(conf string) error { - node, err := s.getYAMLNode([]byte(conf)) - if err != nil { - return err - } - - spec := log.Spec() - if err := s.lintYAMLSpec(spec, node); err != nil { - return err - } - - pConf, err := spec.ParsedConfigFromAny(node) - if err != nil { - return convertDocsLintErr(err) - } - - lconf, err := log.FromParsed(pConf) - if err != nil { - return err - } - - s.logger = lconf - return nil -} - -//------------------------------------------------------------------------------ - -// AsYAML prints a YAML representation of the stream config as it has been -// currently built. -func (s *StreamBuilder) AsYAML() (string, error) { - conf := s.buildConfig() - - var node yaml.Node - if err := node.Encode(conf); err != nil { - return "", err - } - - sanitConf := docs.NewSanitiseConfig(s.env.internal) - sanitConf.RemoveTypeField = true - sanitConf.RemoveDeprecated = false - sanitConf.DocsProvider = s.env.internal - - if err := configSpec().SanitiseYAML(&node, sanitConf); err != nil { - return "", err - } - - b, err := yaml.Marshal(node) - if err != nil { - return "", err - } - return string(b), nil -} - -// WalkedComponent is a struct containing information about a component yielded -// via the WalkComponents method. -type WalkedComponent struct { - ComponentType string - Name string - Label string - confYAML string -} - -// ConfigYAML returns the configuration of a walked component in YAML form. -func (w *WalkedComponent) ConfigYAML() string { - return w.confYAML -} - -// WalkComponents walks the Benthos configuration as it is currently built and -// for each component type (input, processor, output, etc) calls a provided -// function with a struct containing information about the component. -// -// This can be useful for taking an inventory of the contents of a config. -func (s *StreamBuilder) WalkComponents(fn func(w *WalkedComponent) error) error { - conf := s.buildConfig() - - var node yaml.Node - if err := node.Encode(conf); err != nil { - return err - } - - sanitConf := docs.NewSanitiseConfig(s.env.internal) - sanitConf.RemoveTypeField = true - sanitConf.RemoveDeprecated = false - sanitConf.DocsProvider = s.env.internal - - spec := configSpec() - if err := spec.SanitiseYAML(&node, sanitConf); err != nil { - return err - } - - return spec.WalkYAML(&node, s.env.internal, - func(c docs.WalkedYAMLComponent) error { - yamlBytes, err := yaml.Marshal(c.Conf) - if err != nil { - return err - } - return fn(&WalkedComponent{ - ComponentType: string(c.ComponentType), - Name: c.Name, - Label: c.Label, - confYAML: string(yamlBytes), - }) - }) -} - -//------------------------------------------------------------------------------ - -func (s *StreamBuilder) runConsumerFunc(mgr *manager.Type) error { - if s.consumerFunc == nil { - return nil - } - tChan, err := mgr.GetPipe(s.consumerID) - if err != nil { - return err - } - go func() { - for { - tran, open := <-tChan - if !open { - return - } - batch := make(MessageBatch, tran.Payload.Len()) - _ = tran.Payload.Iter(func(i int, part *message.Part) error { - batch[i] = NewInternalMessage(part) - return nil - }) - err := s.consumerFunc(context.Background(), batch) - _ = tran.Ack(context.Background(), err) - } - }() - return nil -} - -// Build a Benthos stream pipeline according to the components specified by this -// stream builder. -func (s *StreamBuilder) Build() (*Stream, error) { - return s.buildWithEnv(s.env.internal) -} - -// BuildTraced creates a Benthos stream pipeline according to the components -// specified by this stream builder, where each major component (input, -// processor, output) is wrapped with a tracing module that, during the lifetime -// of the stream, aggregates tracing events into the returned *TracingSummary. -// Once the stream has ended the TracingSummary can be queried for events that -// occurred. -// -// Experimental: The behaviour of this method could change outside of major -// version releases. -func (s *StreamBuilder) BuildTraced() (*Stream, *TracingSummary, error) { - tenv, summary := tracing.TracedBundle(s.env.internal) - strm, err := s.buildWithEnv(tenv) - return strm, &TracingSummary{summary}, err -} - -func (s *StreamBuilder) buildWithEnv(env *bundle.Environment) (*Stream, error) { - conf := s.buildConfig() - - logger := s.customLogger - if logger == nil { - var err error - if logger, err = log.New(os.Stdout, s.env.fs, s.logger); err != nil { - return nil, err - } - } - - // This temporary manager is a very lazy way of instantiating a manager that - // restricts the bloblang and component environments to custom plugins. - // Ideally we would break out the constructor for our general purpose - // manager to allow for a two-tier initialisation where we can defer - // resource constructors until after this metrics exporter is initialised. - tmpMgr, err := manager.New( - manager.NewResourceConfig(), - manager.OptSetEngineVersion(s.engineVersion), - manager.OptSetLogger(logger), - manager.OptSetEnvironment(env), - manager.OptSetBloblangEnvironment(s.env.getBloblangParserEnv()), - ) - if err != nil { - return nil, err - } - - tracer, err := env.TracersInit(s.tracer, tmpMgr) - if err != nil { - return nil, err - } - - stats, err := env.MetricsInit(s.metrics, tmpMgr) - if err != nil { - return nil, err - } - - apiMut := s.apiMut - var apiType *api.Type - if apiMut == nil { - var sanitNode yaml.Node - err := sanitNode.Encode(conf) - if err == nil { - sanitConf := docs.NewSanitiseConfig(s.env.internal) - sanitConf.RemoveTypeField = true - sanitConf.ScrubSecrets = true - sanitConf.DocsProvider = env - _ = configSpec().SanitiseYAML(&sanitNode, sanitConf) - } - if apiType, err = api.New("", "", s.http, sanitNode, logger, stats); err != nil { - return nil, fmt.Errorf("unable to create stream HTTP server due to: %w. Tip: you can disable the server with `http.enabled` set to `false`, or override the configured server with SetHTTPMux", err) - } - apiMut = apiType - } else if hler := stats.HandlerFunc(); hler != nil { - apiMut.RegisterEndpoint("/stats", "Exposes service-wide metrics in the format configured.", hler) - apiMut.RegisterEndpoint("/metrics", "Exposes service-wide metrics in the format configured.", hler) - } - - mgr, err := manager.New( - conf.ResourceConfig, - manager.OptSetAPIReg(apiMut), - manager.OptSetEngineVersion(s.engineVersion), - manager.OptSetLogger(logger), - manager.OptSetMetrics(stats), - manager.OptSetTracer(tracer), - manager.OptSetEnvironment(env), - manager.OptSetBloblangEnvironment(s.env.getBloblangParserEnv()), - manager.OptSetFS(s.env.fs), - ) - if err != nil { - return nil, err - } - - if s.producerChan != nil { - mgr.SetPipe(s.producerID, s.producerChan) - } - - return newStream(conf.Config, apiType, mgr, stats, tracer, logger, func() { - if err := s.runConsumerFunc(mgr); err != nil { - logger.Error("Failed to run func consumer: %v", err) - } - }), nil -} - -type builderConfig struct { - HTTP *api.Config `yaml:"http,omitempty"` - stream.Config `yaml:",inline"` - manager.ResourceConfig `yaml:",inline"` - Metrics metrics.Config `yaml:"metrics"` - Logger *log.Config `yaml:"logger,omitempty"` - Tracer tracer.Config `yaml:"tracer"` -} - -func (s *StreamBuilder) buildConfig() builderConfig { - conf := builderConfig{} - - if s.apiMut == nil { - conf.HTTP = &s.http - } - - if len(s.inputs) == 1 { - conf.Input = s.inputs[0] - } else if len(s.inputs) > 1 { - conf.Input.Type = "broker" - iSlice := make([]any, len(s.inputs)) - for i, v := range s.inputs { - iSlice[i] = v - } - conf.Input.Plugin = map[string]any{ - "inputs": iSlice, - } - } else { - // TODO: V5 Prevent default input/output - conf.Input = input.NewConfig() - } - - conf.Buffer = s.buffer - - conf.Pipeline.Threads = s.threads - conf.Pipeline.Processors = s.processors - - if len(s.outputs) == 1 { - conf.Output = s.outputs[0] - } else if len(s.outputs) > 1 { - conf.Output.Type = "broker" - iSlice := make([]any, len(s.outputs)) - for i, v := range s.outputs { - iSlice[i] = v - } - conf.Output.Plugin = map[string]any{ - "outputs": iSlice, - } - } else { - // TODO: V5 Prevent default input/output - conf.Output = output.NewConfig() - } - - conf.ResourceConfig = s.resources - conf.Metrics = s.metrics - conf.Tracer = s.tracer - if s.customLogger == nil { - conf.Logger = &s.logger - } - return conf -} - -//------------------------------------------------------------------------------ - -func (s *StreamBuilder) getYAMLNode(b []byte) (*yaml.Node, error) { - var err error - if b, err = config.ReplaceEnvVariables(b, s.envVarLookupFn); err != nil { - // TODO: Allow users to specify whether they care about env variables - // missing, in which case we error or not based on that. - var errEnvMissing *config.ErrMissingEnvVars - if errors.As(err, &errEnvMissing) { - b = errEnvMissing.BestAttempt - } else { - return nil, err - } - } - return docs.UnmarshalYAML(b) -} - -func (s *StreamBuilder) lintYAMLSpec(spec docs.FieldSpecs, node *yaml.Node) error { - if s.lintingDisabled { - return nil - } - return lintsToErr(spec.LintYAML(s.getLintContext(), node)) -} - -func (s *StreamBuilder) lintYAMLComponent(node *yaml.Node, ctype docs.Type) error { - if s.lintingDisabled { - return nil - } - return lintsToErr(docs.LintYAML(s.getLintContext(), ctype, node)) -} diff --git a/public/service/stream_builder_test.go b/public/service/stream_builder_test.go deleted file mode 100644 index e9f94a27b6..0000000000 --- a/public/service/stream_builder_test.go +++ /dev/null @@ -1,1321 +0,0 @@ -package service_test - -import ( - "bytes" - "context" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestStreamBuilderDefault(t *testing.T) { - b := service.NewStreamBuilder() - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := []string{ - `http: - enabled: false`, - `input: - label: "" - stdin:`, - `buffer: - none: {}`, - `pipeline: - threads: 0 - processors: []`, - `output: - label: "" - stdout:`, - `logger: - level: INFO`, - `metrics: - none:`, - } - - for _, str := range exp { - assert.Contains(t, act, str) - } -} - -func TestStreamBuilderProducerFunc(t *testing.T) { - tmpDir := t.TempDir() - - outFilePath := filepath.Join(tmpDir, "out.txt") - - b := service.NewStreamBuilder() - require.NoError(t, b.SetLoggerYAML("level: NONE")) - require.NoError(t, b.AddProcessorYAML(`bloblang: 'root = content().uppercase()'`)) - require.NoError(t, b.AddOutputYAML(fmt.Sprintf(` -file: - codec: lines - path: %v`, outFilePath))) - - pushFn, err := b.AddProducerFunc() - require.NoError(t, err) - - // Fails on second call. - _, err = b.AddProducerFunc() - require.Error(t, err) - - // Don't allow input overrides now. - err = b.SetYAML(`input: {}`) - require.Error(t, err) - - strm, err := b.Build() - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - require.NoError(t, pushFn(ctx, service.NewMessage([]byte("hello world 1")))) - require.NoError(t, pushFn(ctx, service.NewMessage([]byte("hello world 2")))) - require.NoError(t, pushFn(ctx, service.NewMessage([]byte("hello world 3")))) - - require.NoError(t, strm.StopWithin(time.Second*5)) - }() - - require.NoError(t, strm.Run(context.Background())) - wg.Wait() - - outBytes, err := os.ReadFile(outFilePath) - require.NoError(t, err) - - assert.Equal(t, "HELLO WORLD 1\nHELLO WORLD 2\nHELLO WORLD 3\n", string(outBytes)) -} - -func TestStreamBuilderBatchProducerFunc(t *testing.T) { - tmpDir := t.TempDir() - - outFilePath := filepath.Join(tmpDir, "out.txt") - - b := service.NewStreamBuilder() - require.NoError(t, b.SetLoggerYAML("level: NONE")) - require.NoError(t, b.AddProcessorYAML(`bloblang: 'root = content().uppercase()'`)) - require.NoError(t, b.AddOutputYAML(fmt.Sprintf(` -file: - codec: lines - path: %v`, outFilePath))) - - pushFn, err := b.AddBatchProducerFunc() - require.NoError(t, err) - - // Fails on second call. - _, err = b.AddProducerFunc() - require.Error(t, err) - - // Don't allow input overrides now. - err = b.SetYAML(`input: {}`) - require.Error(t, err) - - strm, err := b.Build() - require.NoError(t, err) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - - ctx, done := context.WithTimeout(context.Background(), time.Second*10) - defer done() - - require.NoError(t, pushFn(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello world 1")), - service.NewMessage([]byte("hello world 2")), - })) - require.NoError(t, pushFn(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello world 3")), - service.NewMessage([]byte("hello world 4")), - })) - require.NoError(t, pushFn(ctx, service.MessageBatch{ - service.NewMessage([]byte("hello world 5")), - service.NewMessage([]byte("hello world 6")), - })) - - require.NoError(t, strm.StopWithin(time.Second*5)) - }() - - require.NoError(t, strm.Run(context.Background())) - wg.Wait() - - outBytes, err := os.ReadFile(outFilePath) - require.NoError(t, err) - - assert.Equal(t, "HELLO WORLD 1\nHELLO WORLD 2\nHELLO WORLD 3\nHELLO WORLD 4\nHELLO WORLD 5\nHELLO WORLD 6\n", string(outBytes)) -} - -func TestStreamBuilderEnvVarInterpolation(t *testing.T) { - t.Setenv("BENTHOS_TEST_ONE", "foo") - t.Setenv("BENTHOS_TEST_TWO", "warn") - - b := service.NewStreamBuilder() - require.NoError(t, b.AddInputYAML(` -generate: - mapping: 'root = "${BENTHOS_TEST_ONE}"' -`)) - - require.NoError(t, b.SetLoggerYAML(`level: ${BENTHOS_TEST_TWO}`)) - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := []string{ - ` mapping: 'root = "foo"'`, - `level: warn`, - } - - for _, str := range exp { - assert.Contains(t, act, str) - } - - b = service.NewStreamBuilder() - require.NoError(t, b.SetYAML(` -input: - generate: - mapping: 'root = "${BENTHOS_TEST_ONE}"' -logger: - level: ${BENTHOS_TEST_TWO} -`)) - - act, err = b.AsYAML() - require.NoError(t, err) - - for _, str := range exp { - assert.Contains(t, act, str) - } -} - -func TestStreamBuilderConsumerFunc(t *testing.T) { - tmpDir := t.TempDir() - - inFilePath := filepath.Join(tmpDir, "in.txt") - require.NoError(t, os.WriteFile(inFilePath, []byte(`HELLO WORLD 1 -HELLO WORLD 2 -HELLO WORLD 3`), 0o755)) - - b := service.NewStreamBuilder() - require.NoError(t, b.SetLoggerYAML("level: NONE")) - require.NoError(t, b.AddInputYAML(fmt.Sprintf(` -file: - codec: lines - paths: [ %v ]`, inFilePath))) - require.NoError(t, b.AddProcessorYAML(`bloblang: 'root = content().lowercase()'`)) - - outMsgs := map[string]struct{}{} - var outMut sync.Mutex - handler := func(_ context.Context, m *service.Message) error { - outMut.Lock() - defer outMut.Unlock() - - b, err := m.AsBytes() - assert.NoError(t, err) - - outMsgs[string(b)] = struct{}{} - return nil - } - require.NoError(t, b.AddConsumerFunc(handler)) - - // Fails on second call. - require.Error(t, b.AddConsumerFunc(handler)) - - // Don't allow output overrides now. - err := b.SetYAML(`output: {}`) - require.Error(t, err) - - strm, err := b.Build() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - - outMut.Lock() - assert.Equal(t, map[string]struct{}{ - "hello world 1": {}, - "hello world 2": {}, - "hello world 3": {}, - }, outMsgs) - outMut.Unlock() -} - -func TestStreamBuilderConsumerFuncInlineProcs(t *testing.T) { - tmpDir := t.TempDir() - - inFilePath := filepath.Join(tmpDir, "in.txt") - require.NoError(t, os.WriteFile(inFilePath, []byte(`HELLO WORLD 1 -HELLO WORLD 2 -HELLO WORLD 3`), 0o755)) - - b := service.NewStreamBuilder() - require.NoError(t, b.SetLoggerYAML("level: NONE")) - require.NoError(t, b.AddInputYAML(fmt.Sprintf(` -file: - codec: lines - paths: [ %v ] -processors: - - bloblang: 'root = content().lowercase()' -`, inFilePath))) - - outMsgs := map[string]struct{}{} - var outMut sync.Mutex - handler := func(_ context.Context, m *service.Message) error { - outMut.Lock() - defer outMut.Unlock() - - b, err := m.AsBytes() - assert.NoError(t, err) - - outMsgs[string(b)] = struct{}{} - return nil - } - require.NoError(t, b.AddConsumerFunc(handler)) - - // Fails on second call. - require.Error(t, b.AddConsumerFunc(handler)) - - // Don't allow output overrides now. - err := b.SetYAML(`output: {}`) - require.Error(t, err) - - strm, err := b.Build() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - - outMut.Lock() - assert.Equal(t, map[string]struct{}{ - "hello world 1": {}, - "hello world 2": {}, - "hello world 3": {}, - }, outMsgs) - outMut.Unlock() -} - -func TestStreamBuilderBatchConsumerFunc(t *testing.T) { - tmpDir := t.TempDir() - - inFilePath := filepath.Join(tmpDir, "in.txt") - require.NoError(t, os.WriteFile(inFilePath, []byte(`HELLO WORLD 1 -HELLO WORLD 2 - -HELLO WORLD 3 -HELLO WORLD 4 - -HELLO WORLD 5 -HELLO WORLD 6 -`), 0o755)) - - b := service.NewStreamBuilder() - require.NoError(t, b.SetLoggerYAML("level: NONE")) - require.NoError(t, b.AddInputYAML(fmt.Sprintf(` -file: - codec: lines/multipart - paths: [ %v ]`, inFilePath))) - require.NoError(t, b.AddProcessorYAML(`bloblang: 'root = content().lowercase()'`)) - - outBatches := map[string]struct{}{} - var outMut sync.Mutex - handler := func(_ context.Context, mb service.MessageBatch) error { - outMut.Lock() - defer outMut.Unlock() - - outMsgs := []string{} - for _, m := range mb { - b, err := m.AsBytes() - assert.NoError(t, err) - outMsgs = append(outMsgs, string(b)) - } - - outBatches[strings.Join(outMsgs, ",")] = struct{}{} - return nil - } - require.NoError(t, b.AddBatchConsumerFunc(handler)) - - // Fails on second call. - require.Error(t, b.AddBatchConsumerFunc(handler)) - - // Don't allow output overrides now. - err := b.SetYAML(`output: {}`) - require.Error(t, err) - - strm, err := b.Build() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - - outMut.Lock() - assert.Equal(t, map[string]struct{}{ - "hello world 1,hello world 2": {}, - "hello world 3,hello world 4": {}, - "hello world 5,hello world 6": {}, - }, outBatches) - outMut.Unlock() -} - -func TestStreamBuilderCustomLogger(t *testing.T) { - b := service.NewStreamBuilder() - b.SetPrintLogger(nil) - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := `logger: - level: INFO` - - assert.NotContains(t, act, exp) -} - -func TestStreamBuilderSetYAML(t *testing.T) { - b := service.NewStreamBuilder() - b.SetThreads(10) - require.NoError(t, b.AddCacheYAML(`label: foocache -type: memory`)) - require.NoError(t, b.AddInputYAML(`type: generate`)) - require.NoError(t, b.AddOutputYAML(`type: drop`)) - require.NoError(t, b.AddProcessorYAML(`type: bloblang`)) - require.NoError(t, b.AddProcessorYAML(`type: jmespath`)) - require.NoError(t, b.AddRateLimitYAML(`label: foorl -type: local`)) - require.NoError(t, b.SetMetricsYAML(`type: none`)) - require.NoError(t, b.SetLoggerYAML(`level: DEBUG`)) - require.NoError(t, b.SetBufferYAML(`type: memory`)) - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := []string{ - `input: - label: "" - generate:`, - `buffer: - memory: {}`, - `pipeline: - threads: 10 - processors:`, - ` - - label: "" - bloblang: ""`, - ` - - label: "" - jmespath: {}`, - `output: - label: "" - drop:`, - `metrics: - none:`, - `cache_resources: - - label: foocache - memory:`, - `rate_limit_resources: - - label: foorl - local:`, - ` level: DEBUG`, - } - - for _, str := range exp { - assert.Contains(t, act, str) - } -} - -func TestStreamBuilderSetResourcesYAML(t *testing.T) { - b := service.NewStreamBuilder() - require.NoError(t, b.AddResourcesYAML(` -cache_resources: - - label: foocache - type: memory - -rate_limit_resources: - - label: foorl - type: local - -processor_resources: - - label: fooproc1 - type: bloblang - - label: fooproc2 - type: jmespath - -input_resources: - - label: fooinput - generate: - mapping: 'root = "meow"' - -output_resources: - - label: foooutput - type: drop -`)) - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := []string{ - `cache_resources: - - label: foocache - memory:`, - `rate_limit_resources: - - label: foorl - local:`, - `processor_resources: - - label: fooproc1 - bloblang:`, - ` - label: fooproc2 - jmespath:`, - `input_resources: - - label: fooinput - generate:`, - `output_resources: - - label: foooutput - drop:`, - } - - for _, str := range exp { - assert.Contains(t, act, str) - } -} - -func TestStreamBuilderSetYAMLBrokers(t *testing.T) { - b := service.NewStreamBuilder() - b.SetThreads(10) - require.NoError(t, b.AddInputYAML(` -label: foo -generate: - mapping: root = deleted() -`)) - require.NoError(t, b.AddInputYAML(` -label: bar -generate: - mapping: root = deleted() -`)) - require.NoError(t, b.AddOutputYAML(` -label: baz -drop: {} -`)) - require.NoError(t, b.AddOutputYAML(` -label: buz -drop: {} -`)) - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := []string{ - `input: - label: "" - broker: - inputs:`, - ` - label: foo - generate:`, - ` - label: bar - generate:`, - `output: - label: "" - broker: - outputs:`, - ` - label: baz - drop:`, - ` - label: buz - drop:`, - } - - for _, str := range exp { - assert.Contains(t, act, str) - } -} - -func TestStreamBuilderYAMLErrors(t *testing.T) { - b := service.NewStreamBuilder() - - err := b.AddCacheYAML(`{ label: "", type: memory }`) - require.Error(t, err) - assert.EqualError(t, err, "a label must be specified for cache resources") - - err = b.AddInputYAML(`not valid ! yaml 34324`) - require.Error(t, err) - assert.Contains(t, err.Error(), "expected object") - - err = b.SetYAML(`not valid ! yaml 34324`) - require.Error(t, err) - assert.Contains(t, err.Error(), "expected object value") - - err = b.SetYAML(`input: { foo: nope }`) - require.Error(t, err) - assert.Contains(t, err.Error(), "unable to infer") - - err = b.SetYAML(`input: { generate: { not_a_field: nope } }`) - require.Error(t, err) - assert.Contains(t, err.Error(), "field not_a_field not recognised") - - err = b.AddInputYAML(`not_a_field: nah`) - require.Error(t, err) - assert.Contains(t, err.Error(), "unable to infer") - - err = b.AddInputYAML(`generate: { not_a_field: nah }`) - require.Error(t, err) - assert.Contains(t, err.Error(), "field not_a_field not recognised") - - err = b.SetLoggerYAML(`not_a_field: nah`) - require.Error(t, err) - assert.Contains(t, err.Error(), "field not_a_field not recognised") - - err = b.AddRateLimitYAML(`{ label: "", local: {} }`) - require.Error(t, err) - assert.EqualError(t, err, "a label must be specified for rate limit resources") -} - -func TestStreamBuilderSetFields(t *testing.T) { - tests := []struct { - name string - input string - args []any - output string - errContains string - }{ - { - name: "odd number of args", - input: `{}`, - args: []any{ - "just a field", - }, - errContains: "odd number of pathValues", - }, - { - name: "a path isnt a string", - input: `{}`, - args: []any{ - 10, "hello world", - }, - errContains: "should be a string", - }, - { - name: "unknown field error", - input: ` -input: - generate: - mapping: 'root = deleted()' -`, - args: []any{ - "input.generate.unknown_field", "baz", - }, - errContains: "field not recognised", - }, - { - name: "create lint error", - input: ` -input: - generate: - mapping: 'root = deleted()' -`, - args: []any{ - "input.label", "foo", - "output.label", "foo", - }, - errContains: "collides with a previously", - }, - { - name: "set file paths", - input: ` -input: - file: - paths: [ foo, bar ] -`, - args: []any{ - "input.file.paths.1", "baz", - }, - output: ` -input: - file: - paths: [ foo, baz ] -`, - }, - { - name: "append file paths", - input: ` -input: - file: - paths: [ foo, bar ] -`, - args: []any{ - "input.file.paths.-", "baz", - "input.file.paths.-", "buz", - "input.file.paths.-", "bev", - }, - output: ` -input: - file: - paths: [ foo, bar, baz, buz, bev ] -`, - }, - { - name: "add a processor", - input: ` -input: - generate: - mapping: 'root = deleted()' -`, - args: []any{ - "pipeline.processors.-.bloblang", `root = "meow"`, - }, - output: ` -input: - generate: - mapping: 'root = deleted()' -pipeline: - processors: - - bloblang: 'root = "meow"' -`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - b := service.NewStreamBuilder() - require.NoError(t, b.SetYAML(test.input)) - err := b.SetFields(test.args...) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - } else { - require.NoError(t, err) - - b2 := service.NewStreamBuilder() - require.NoError(t, b2.SetYAML(test.output)) - - bAsYAML, err := b.AsYAML() - require.NoError(t, err) - - b2AsYAML, err := b2.AsYAML() - require.NoError(t, err) - - assert.YAMLEq(t, b2AsYAML, bAsYAML) - } - }) - } -} - -func TestStreamBuilderWalk(t *testing.T) { - type walkedComponent struct { - typeStr string - name string - label string - conf string - } - tests := []struct { - name string - input string - output []walkedComponent - }{ - { - name: "basic components", - input: ` -input: - generate: - mapping: 'root = deleted()' - -pipeline: - processors: - - mutation: 'root = "hm"' - label: fooproc - -output: - reject: lol nah -`, - output: []walkedComponent{ - { - typeStr: "input", - name: "generate", - conf: `label: "" -generate: - mapping: 'root = deleted()'`, - }, - { - typeStr: "buffer", - name: "none", - conf: `none: {}`, - }, - { - typeStr: "processor", - name: "mutation", - label: "fooproc", - conf: `label: fooproc -mutation: 'root = "hm"'`, - }, - { - typeStr: "output", - name: "reject", - conf: `label: "" -reject: lol nah`, - }, - { - typeStr: "metrics", - name: "none", - conf: `none: {} -mapping: ""`, - }, - { - typeStr: "tracer", - name: "none", - conf: `none: {}`, - }, - }, - }, - { - name: "input and output procs", - input: ` -input: - generate: - mapping: 'root = deleted()' - processors: - - mutation: 'root = "hm"' - -output: - reject: lol nah - processors: - - mutation: 'root = "eh"' -`, - output: []walkedComponent{ - { - typeStr: "input", - name: "generate", - conf: `label: "" -generate: - mapping: 'root = deleted()' -processors: - - label: "" - mutation: 'root = "hm"'`, - }, - { - typeStr: "processor", - name: "mutation", - conf: `label: "" -mutation: 'root = "hm"'`, - }, - { - typeStr: "buffer", - name: "none", - conf: `none: {}`, - }, - { - typeStr: "output", - name: "reject", - conf: `label: "" -reject: lol nah -processors: - - label: "" - mutation: 'root = "eh"'`, - }, - { - typeStr: "processor", - name: "mutation", - conf: `label: "" -mutation: 'root = "eh"'`, - }, - { - typeStr: "metrics", - name: "none", - conf: `none: {} -mapping: ""`, - }, - { - typeStr: "tracer", - name: "none", - conf: `none: {}`, - }, - }, - }, - { - name: "nested components", - input: ` -input: - dynamic: - inputs: - foo: - file: - paths: [ aaa.txt ] -`, - output: []walkedComponent{ - { - typeStr: "input", - name: "dynamic", - conf: `label: "" -dynamic: - inputs: - foo: - file: - paths: [aaa.txt]`, - }, - { - typeStr: "input", - name: "file", - conf: `file: - paths: [aaa.txt]`, - }, - { - typeStr: "buffer", - name: "none", - conf: `none: {}`, - }, - { - typeStr: "output", - name: "stdout", - conf: `label: "" -stdout: {}`, - }, - { - typeStr: "metrics", - name: "none", - conf: `none: {} -mapping: ""`, - }, - { - typeStr: "tracer", - name: "none", - conf: `none: {}`, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - b := service.NewStreamBuilder() - require.NoError(t, b.SetYAML(test.input)) - - var results []walkedComponent - require.NoError(t, b.WalkComponents(func(w *service.WalkedComponent) error { - results = append(results, walkedComponent{ - typeStr: w.ComponentType, - name: w.Name, - label: w.Label, - conf: strings.TrimSpace(w.ConfigYAML()), - }) - return nil - })) - - assert.Equal(t, test.output, results) - }) - } -} - -func TestStreamBuilderSetCoreYAML(t *testing.T) { - b := service.NewStreamBuilder() - b.SetThreads(10) - require.NoError(t, b.SetYAML(` -input: - generate: - mapping: 'root = deleted()' - -pipeline: - threads: 5 - processors: - - type: bloblang - - type: jmespath - -output: - drop: {} -`)) - - act, err := b.AsYAML() - require.NoError(t, err) - - exp := []string{ - `input: - label: "" - generate:`, - `buffer: - none: {}`, - `pipeline: - threads: 5 - processors:`, - ` - - label: "" - bloblang: ""`, - ` - - label: "" - jmespath: {}`, - `output: - label: "" - drop:`, - } - - for _, str := range exp { - assert.Contains(t, act, str) - } -} - -func TestStreamBuilderDisabledLinting(t *testing.T) { - lintingErrorConfig := ` -input: - generate: - mapping: 'root = deleted()' - meow: ignore this field - -output: - another: linting error - drop: {} -` - b := service.NewStreamBuilder() - require.Error(t, b.SetYAML(lintingErrorConfig)) - - b = service.NewStreamBuilder() - b.DisableLinting() - require.NoError(t, b.SetYAML(lintingErrorConfig)) -} - -type noopProc struct{} - -func (n noopProc) Process(ctx context.Context, m *service.Message) (service.MessageBatch, error) { - return service.MessageBatch{m}, nil -} - -func (n noopProc) Close(context.Context) error { - return nil -} - -func TestStreamBuilderSecretsSetYAML(t *testing.T) { - var meowValues []string - - env := service.NewEnvironment() - require.NoError(t, env.RegisterProcessor("foo", - service.NewConfigSpec(). - Field(service.NewStringField("meow").Secret()), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - meowValue, _ := conf.FieldString("meow") - meowValues = append(meowValues, meowValue) - return noopProc{}, nil - })) - - b := env.NewStreamBuilder() - require.NoError(t, b.SetYAML(` -input: - generate: - count: 1 - interval: 1ms - mapping: 'root.id = "foo"' - processors: - - foo: - meow: first -output: - drop: {} -`)) - - strm, err := b.Build() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - require.NoError(t, strm.Run(tCtx)) - - assert.Equal(t, []string{"first"}, meowValues) -} - -func TestStreamBuilderSecretsSetField(t *testing.T) { - var meowValues []string - - env := service.NewEnvironment() - require.NoError(t, env.RegisterProcessor("foo", - service.NewConfigSpec(). - Field(service.NewStringField("meow").Secret()), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - meowValue, _ := conf.FieldString("meow") - meowValues = append(meowValues, meowValue) - return noopProc{}, nil - })) - - b := env.NewStreamBuilder() - require.NoError(t, b.SetYAML(` -input: - generate: - count: 1 - interval: 1ms - mapping: 'root.id = "foo"' - processors: - - foo: - meow: ignorethisvalue -output: - drop: {} -`)) - - require.NoError(t, b.SetFields("input.processors.0.foo.meow", "second")) - - strm, err := b.Build() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - require.NoError(t, strm.Run(tCtx)) - - assert.Equal(t, []string{"second"}, meowValues) -} - -type disabledMux struct{} - -func (d disabledMux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { -} - -func BenchmarkStreamRun(b *testing.B) { - config := ` -input: - generate: - count: 5 - interval: "" - mapping: | - root.id = uuid_v4() - -pipeline: - processors: - - bloblang: 'root = this' - -output: - drop: {} - -logger: - level: OFF -` - - strmBuilder := service.NewStreamBuilder() - strmBuilder.SetHTTPMux(disabledMux{}) - require.NoError(b, strmBuilder.SetYAML(config)) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - strm, err := strmBuilder.Build() - require.NoError(b, err) - - require.NoError(b, strm.Run(context.Background())) - } -} - -func BenchmarkStreamRunOutputN1(b *testing.B) { - benchmarkStreamRunOutputNX(b, 1) -} - -func BenchmarkStreamRunOutputN10(b *testing.B) { - benchmarkStreamRunOutputNX(b, 10) -} - -func BenchmarkStreamRunOutputN100(b *testing.B) { - benchmarkStreamRunOutputNX(b, 100) -} - -type noopOutput struct{} - -func (n *noopOutput) Connect(ctx context.Context) error { - return nil -} - -func (n *noopOutput) Write(ctx context.Context, msg *service.Message) error { - return nil -} - -func (n *noopOutput) WriteBatch(ctx context.Context, b service.MessageBatch) error { - return nil -} - -func (n *noopOutput) Close(ctx context.Context) error { - return nil -} - -func benchmarkStreamRunOutputNX(b *testing.B, size int) { - var outputsBuf bytes.Buffer - for i := 0; i < size; i++ { - outputsBuf.WriteString(" - custom: {}\n") - } - - config := fmt.Sprintf(` -input: - generate: - count: 5 - interval: "" - mapping: | - root.id = uuid_v4() - -pipeline: - processors: - - bloblang: 'root = this' - -output: - broker: - outputs: -%v - -logger: - level: OFF -`, outputsBuf.String()) - - env := service.NewEnvironment() - require.NoError(b, env.RegisterOutput( - "custom", - service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - return &noopOutput{}, 1, nil - }, - )) - - strmBuilder := env.NewStreamBuilder() - strmBuilder.SetHTTPMux(disabledMux{}) - require.NoError(b, strmBuilder.SetYAML(config)) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - strm, err := strmBuilder.Build() - require.NoError(b, err) - - require.NoError(b, strm.Run(context.Background())) - } -} - -func TestStreamBuilderLargeNestingSmoke(t *testing.T) { - b := service.NewStreamBuilder() - require.NoError(t, b.SetYAML(` -input: - label: ibroker0 - broker: - inputs: - - label: ifoo - generate: - count: 1 - interval: 1ms - mapping: 'root = "ifoo: " + counter().string()' - processors: - - mutation: 'root = content().string() + " pfoo0"' - - mutation: 'root = content().string() + " pfoo1"' - - label: ibroker1 - broker: - inputs: - - label: ibar - generate: - count: 1 - interval: 1ms - mapping: 'root = "ibar: " + counter().string()' - processors: - - mutation: 'root = content().string() + " pbar0"' - - label: ibaz - generate: - count: 1 - interval: 1ms - mapping: 'root = "ibaz: " + counter().string()' - processors: - - mutation: 'root = content().string() + " pbaz0"' - processors: - - mutation: 'root = content().string() + " pibroker10"' - processors: - - mutation: 'root = content().string() + " pibroker00"' - -pipeline: - processors: - - try: - - mutation: 'root = content().string() + " pquack0"' - - for_each: - - mutation: 'root = content().string() + " pwoof0"' - -output: - label: obroker0 - broker: - outputs: - - label: ofoo - drop: {} - processors: - - mutation: 'root = content().string() + " pofoo0"' - - label: obroker1 - broker: - outputs: - - label: obar - drop: {} - - label: obaz - drop: {} - processors: - - mutation: 'root = content().string() + " pobaz0"' - processors: - - mutation: 'root = content().string() + " pobroker10"' - processors: - - mutation: 'root = content().string() + " pobroker00"' -`)) - - strm, tracSum, err := b.BuildTraced() - require.NoError(t, err) - - tCtx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - require.NoError(t, strm.Run(tCtx)) - - eventKeys := map[string]map[string]struct{}{} - for k, v := range tracSum.InputEvents() { - eMap := map[string]struct{}{} - for _, e := range v { - eMap[e.Content] = struct{}{} - } - eventKeys[k] = eMap - } - - assert.Equal(t, map[string]map[string]struct{}{ - "ifoo": {"ifoo: 1 pfoo0 pfoo1": struct{}{}}, - "ibar": {"ibar: 1 pbar0": struct{}{}}, - "ibaz": {"ibaz: 1 pbaz0": struct{}{}}, - "ibroker0": { - "ifoo: 1 pfoo0 pfoo1 pibroker00": struct{}{}, - "ibar: 1 pbar0 pibroker10 pibroker00": struct{}{}, - "ibaz: 1 pbaz0 pibroker10 pibroker00": struct{}{}, - }, - "ibroker1": { - "ibar: 1 pbar0 pibroker10": struct{}{}, - "ibaz: 1 pbaz0 pibroker10": struct{}{}, - }, - }, eventKeys) - - eventKeys = map[string]map[string]struct{}{} - for k, v := range tracSum.OutputEvents() { - eMap := map[string]struct{}{} - for _, e := range v { - eMap[e.Content] = struct{}{} - } - eventKeys[k] = eMap - } - - assert.Equal(t, map[string]map[string]struct{}{ - "ofoo": { - "ifoo: 1 pfoo0 pfoo1 pibroker00 pquack0 pwoof0 pobroker00 pofoo0": struct{}{}, - "ibar: 1 pbar0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pofoo0": struct{}{}, - "ibaz: 1 pbaz0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pofoo0": struct{}{}, - }, - "obar": { - "ifoo: 1 pfoo0 pfoo1 pibroker00 pquack0 pwoof0 pobroker00 pobroker10": struct{}{}, - "ibar: 1 pbar0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pobroker10": struct{}{}, - "ibaz: 1 pbaz0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pobroker10": struct{}{}, - }, - "obaz": { - "ifoo: 1 pfoo0 pfoo1 pibroker00 pquack0 pwoof0 pobroker00 pobroker10 pobaz0": struct{}{}, - "ibar: 1 pbar0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pobroker10 pobaz0": struct{}{}, - "ibaz: 1 pbaz0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pobroker10 pobaz0": struct{}{}, - }, - "obroker0": { - "ifoo: 1 pfoo0 pfoo1 pibroker00 pquack0 pwoof0 pobroker00": struct{}{}, - "ibar: 1 pbar0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00": struct{}{}, - "ibaz: 1 pbaz0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00": struct{}{}, - }, - "obroker1": { - "ifoo: 1 pfoo0 pfoo1 pibroker00 pquack0 pwoof0 pobroker00 pobroker10": struct{}{}, - "ibar: 1 pbar0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pobroker10": struct{}{}, - "ibaz: 1 pbaz0 pibroker10 pibroker00 pquack0 pwoof0 pobroker00 pobroker10": struct{}{}, - }, - }, eventKeys) -} diff --git a/public/service/stream_config_linter.go b/public/service/stream_config_linter.go deleted file mode 100644 index 4d694fb89a..0000000000 --- a/public/service/stream_config_linter.go +++ /dev/null @@ -1,109 +0,0 @@ -package service - -import ( - "bytes" - "context" - "errors" - "os" - "unicode/utf8" - - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// StreamConfigLinter provides utilities for linting stream configs. -type StreamConfigLinter struct { - env *Environment - spec docs.FieldSpecs - lintConf docs.LintConfig - skipEnvVarLint bool - envVarLookupFn func(string) (string, bool) -} - -// NewStreamConfigLinter creates a component for marshalling stream configs, -// allowing you to print sanitised, redacted or hydrated configs in various -// formats. -func (s *ConfigSchema) NewStreamConfigLinter() *StreamConfigLinter { - lintConf := docs.NewLintConfig(s.env.internal) - lintConf.BloblangEnv = s.env.bloblangEnv.Deactivated() - return &StreamConfigLinter{ - env: s.env, - spec: s.fields, - lintConf: lintConf, - envVarLookupFn: os.LookupEnv, - } -} - -// SetRejectDeprecated sets whether deprecated fields should trigger linting -// errors. -func (s *StreamConfigLinter) SetRejectDeprecated(v bool) *StreamConfigLinter { - s.lintConf.RejectDeprecated = v - return s -} - -// SetRequireLabels sets whether labels must be present for all components that -// support them. -func (s *StreamConfigLinter) SetRequireLabels(v bool) *StreamConfigLinter { - s.lintConf.RequireLabels = v - return s -} - -// SetSkipEnvVarCheck sets whether the linter should ignore cases where -// environment variables are referenced and do not exist. -func (s *StreamConfigLinter) SetSkipEnvVarCheck(v bool) *StreamConfigLinter { - s.skipEnvVarLint = v - return s -} - -// SetEnvVarLookupFunc overrides the default environment variable lookup so that -// interpolations within a config are resolved by the provided closure function. -func (s *StreamConfigLinter) SetEnvVarLookupFunc(fn func(context.Context, string) (string, bool)) *StreamConfigLinter { - s.envVarLookupFn = func(s string) (string, bool) { - return fn(context.Background(), s) - } - return s -} - -// LintYAML attempts to parse a config in YAML format and, if successful, -// returns a slice of linting errors, or an error is the parsing failed. -func (s *StreamConfigLinter) LintYAML(yamlBytes []byte) (lints []Lint, err error) { - if !utf8.Valid(yamlBytes) { - lints = append(lints, Lint{ - Line: 1, - Type: LintFailedRead, - What: "detected invalid utf-8 encoding in config, this may result in interpolation functions not working as expected", - }) - } - - if yamlBytes, err = config.ReplaceEnvVariables(yamlBytes, s.envVarLookupFn); err != nil { - var errEnvMissing *config.ErrMissingEnvVars - if !errors.As(err, &errEnvMissing) { - return - } - yamlBytes = errEnvMissing.BestAttempt - if !s.skipEnvVarLint { - lints = append(lints, Lint{Line: 1, Type: LintMissingEnvVar, What: err.Error()}) - } - } - - if bytes.HasPrefix(yamlBytes, []byte("# BENTHOS LINT DISABLE")) { - return - } - - var cNode *yaml.Node - if cNode, err = docs.UnmarshalYAML(yamlBytes); err != nil { - return - } - - for _, l := range s.spec.LintYAML(docs.NewLintContext(s.lintConf), cNode) { - lints = append(lints, Lint{ - Column: l.Column, - Line: l.Line, - Type: convertDocsLintType(l.Type), - What: l.What, - }) - } - return -} diff --git a/public/service/stream_config_linter_test.go b/public/service/stream_config_linter_test.go deleted file mode 100644 index 96ad300eb1..0000000000 --- a/public/service/stream_config_linter_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package service_test - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestStreamLinter(t *testing.T) { - blobl := bloblang.NewEmptyEnvironment() - - require.NoError(t, blobl.RegisterFunction("cow", func(args ...any) (bloblang.Function, error) { - return nil, errors.New("nope") - })) - - env := service.NewEmptyEnvironment() - env.UseBloblangEnvironment(blobl) - - require.NoError(t, env.RegisterInput("dog", service.NewConfigSpec().Fields( - service.NewStringField("woof").Example("WOOF"), - ), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterBatchBuffer("none", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterProcessor("testprocessor", service.NewConfigSpec().Field(service.NewBloblangField("mapfield")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterOutput("stdout", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - err = errors.New("nope") - return - })) - - schema := env.CoreConfigSchema("", "") - - tests := []struct { - name string - config string - lintContains []string - errContains string - linter *service.StreamConfigLinter - }{ - { - name: "basic config no lints", - config: ` -input: - dog: - woof: wooooowooof -`, - }, - { - name: "good bloblang", - config: ` -pipeline: - processors: - - testprocessor: - mapfield: 'root = cow("test")' -`, - }, - { - name: "bad bloblang", - config: ` -pipeline: - processors: - - testprocessor: - mapfield: 'root = meow("test")' -`, - lintContains: []string{ - "unrecognised function", - }, - }, - { - name: "unknown field lint", - config: ` -input: - dog: - woof: wooooowooof - huh: whats this? -`, - lintContains: []string{ - "field huh not recognised", - }, - }, - { - name: "invalid yaml", - config: ` this !!!! isn't valid: yaml dog`, - errContains: "found character", - }, - { - name: "env var defined", - config: ` -input: - dog: - woof: ${WOOF}`, - linter: schema.NewStreamConfigLinter(). - SetEnvVarLookupFunc(func(ctx context.Context, s string) (string, bool) { - return "meow", true - }), - }, - { - name: "env var missing with default", - config: ` -input: - dog: - woof: ${WOOF:defaultvalue}`, - linter: schema.NewStreamConfigLinter(). - SetEnvVarLookupFunc(func(ctx context.Context, s string) (string, bool) { - return "", false - }), - }, - { - name: "env var missing with lint disabled", - config: ` -input: - dog: - woof: ${WOOF}`, - linter: schema.NewStreamConfigLinter(). - SetSkipEnvVarCheck(true). - SetEnvVarLookupFunc(func(ctx context.Context, s string) (string, bool) { - return "", false - }), - }, - { - name: "env var missing and linted", - config: ` -input: - dog: - woof: ${WOOF}`, - linter: schema.NewStreamConfigLinter(). - SetEnvVarLookupFunc(func(ctx context.Context, s string) (string, bool) { - return "", false - }), - lintContains: []string{ - "required environment variables were not set", - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - if test.linter == nil { - test.linter = schema.NewStreamConfigLinter() - } - - lints, err := test.linter.LintYAML([]byte(test.config)) - if test.errContains != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errContains) - return - } - - require.NoError(t, err) - require.Len(t, lints, len(test.lintContains)) - for i, lc := range test.lintContains { - assert.Contains(t, lints[i].Error(), lc) - } - }) - } -} diff --git a/public/service/stream_config_marshaller.go b/public/service/stream_config_marshaller.go deleted file mode 100644 index f55c0eb43d..0000000000 --- a/public/service/stream_config_marshaller.go +++ /dev/null @@ -1,94 +0,0 @@ -package service - -import ( - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" -) - -// StreamConfigMarshaller provides utilities for marshalling stream configs, -// allowing you to print sanitised, redacted or hydrated configs. -type StreamConfigMarshaller struct { - env *Environment - spec docs.FieldSpecs - sanitConf docs.SanitiseConfig -} - -// NewStreamConfigMarshaller creates a component for marshalling stream configs, -// allowing you to print sanitised, redacted or hydrated configs in various -// formats. -func (s *ConfigSchema) NewStreamConfigMarshaller() *StreamConfigMarshaller { - sanitConf := docs.NewSanitiseConfig(s.env.internal) - sanitConf.RemoveTypeField = true - - return &StreamConfigMarshaller{ - env: s.env, - spec: s.fields, - sanitConf: sanitConf, - } -} - -// SetScrubSecrets sets whether fields that contain secrets should be scrubbed -// when they contain raw values. -func (s *StreamConfigMarshaller) SetScrubSecrets(v bool) *StreamConfigMarshaller { - s.sanitConf.ScrubSecrets = v - return s -} - -// SetHydrateExamples sets whether to recurse and hydrate the config with -// default or example values when marshalling. This is useful for generating -// example configs. -func (s *StreamConfigMarshaller) SetHydrateExamples(v bool) *StreamConfigMarshaller { - s.sanitConf.ForExample = v - return s -} - -// SetOmitDeprecated sets whether deprecated fields should be omitted from the -// marshalled result. -func (s *StreamConfigMarshaller) SetOmitDeprecated(v bool) *StreamConfigMarshaller { - s.sanitConf.RemoveDeprecated = v - return s -} - -// FieldView provides a way to analyse a config field. -type FieldView struct { - f docs.FieldSpec -} - -// IsAdvanced returns true if the field is advanced. -func (f *FieldView) IsAdvanced() bool { - return f.f.IsAdvanced -} - -// SetFieldFilter sets a closure function to be used when preparing the -// marshalled config which determines whether the field is kept in the config, -// otherwise it is removed. -func (s *StreamConfigMarshaller) SetFieldFilter(fn func(view *FieldView, value any) bool) *StreamConfigMarshaller { - s.sanitConf.Filter = func(spec docs.FieldSpec, v any) bool { - return fn(&FieldView{f: spec}, v) - } - return s -} - -// AnyToYAML attempts to parse a config expressed as a structured value -// according to a stream config schema and then marshals it into a YAML string. -func (s *StreamConfigMarshaller) AnyToYAML(v any) (yamlStr string, err error) { - var pConf *docs.ParsedConfig - if pConf, err = s.spec.ParsedConfigFromAny(v); err != nil { - return - } - - var node yaml.Node - if err = node.Encode(pConf.Raw()); err != nil { - return - } - if err = s.spec.SanitiseYAML(&node, s.sanitConf); err != nil { - return - } - var configYAML []byte - if configYAML, err = docs.MarshalYAML(node); err != nil { - return - } - yamlStr = string(configYAML) - return -} diff --git a/public/service/stream_config_marshaller_test.go b/public/service/stream_config_marshaller_test.go deleted file mode 100644 index 9db97ccb17..0000000000 --- a/public/service/stream_config_marshaller_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package service_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/public/service" -) - -func TestStreamMarshallerMinimal(t *testing.T) { - env := service.NewEmptyEnvironment() - - require.NoError(t, env.RegisterInput("dog", service.NewConfigSpec().Field(service.NewStringField("woof").Example("WOOF")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterBatchBuffer("none", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterOutput("cat", service.NewConfigSpec().Field(service.NewStringField("meow").Example("MEOW")), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - err = errors.New("nope") - return - })) - - yamlStr, err := env.CoreConfigSchema("aaa", "bbb").NewStreamConfigMarshaller(). - AnyToYAML(map[string]any{ - "input": map[string]any{ - "type": "dog", - }, - "output": map[string]any{ - "type": "cat", - }, - }) - require.NoError(t, err) - - assert.Equal(t, `input: - dog: {} # No default (required) -buffer: - none: {} -pipeline: - threads: -1 - processors: [] -output: - cat: {} # No default (required) -`, yamlStr) -} - -func TestStreamMarshallerExamples(t *testing.T) { - env := service.NewEmptyEnvironment() - - require.NoError(t, env.RegisterInput("dog", service.NewConfigSpec().Field(service.NewStringField("woof").Example("WOOF")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterBatchBuffer("none", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterOutput("cat", service.NewConfigSpec().Field(service.NewStringField("meow").Example("MEOW")), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - err = errors.New("nope") - return - })) - - yamlStr, err := env.CoreConfigSchema("aaa", "bbb").NewStreamConfigMarshaller(). - SetHydrateExamples(true). - AnyToYAML(map[string]any{ - "input": map[string]any{ - "type": "dog", - }, - "output": map[string]any{ - "type": "cat", - }, - }) - require.NoError(t, err) - - assert.Equal(t, `input: - dog: - woof: WOOF # No default (required) -buffer: - none: {} -pipeline: - threads: -1 - processors: [] -output: - cat: - meow: MEOW # No default (required) -`, yamlStr) -} - -func TestStreamMarshallerFieldFilter(t *testing.T) { - env := service.NewEmptyEnvironment() - - require.NoError(t, env.RegisterInput("dog", service.NewConfigSpec().Fields( - service.NewStringField("basicfield").Example("WOOF"), - service.NewStringField("advancedfield").Advanced().Example("WOOF"), - ), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterBatchBuffer("none", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterOutput("cat", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - err = errors.New("nope") - return - })) - - yamlStr, err := env.CoreConfigSchema("aaa", "bbb").NewStreamConfigMarshaller(). - SetHydrateExamples(true). - SetFieldFilter(func(view *service.FieldView, value any) bool { - return !view.IsAdvanced() - }). - AnyToYAML(map[string]any{ - "input": map[string]any{ - "type": "dog", - }, - "output": map[string]any{ - "type": "cat", - }, - }) - require.NoError(t, err) - - assert.Equal(t, `input: - dog: - basicfield: WOOF # No default (required) -buffer: - none: {} -pipeline: - threads: -1 - processors: [] -output: - cat: null # No default (required) -`, yamlStr) -} diff --git a/public/service/stream_schema.go b/public/service/stream_schema.go deleted file mode 100644 index 78653e7004..0000000000 --- a/public/service/stream_schema.go +++ /dev/null @@ -1,419 +0,0 @@ -package service - -import ( - "encoding/json" - "errors" - "fmt" - - "go.opentelemetry.io/otel/trace" - - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/config/schema" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" - "github.com/benthosdev/benthos/v4/internal/stream" - "github.com/benthosdev/benthos/v4/public/bloblang" -) - -// ConfigSchema contains the definitions of all config fields for the overall -// Benthos config as well as all component plugins. A schema can be used in -// order to analyse, export and import the schemas of varying distributions and -// versions of Benthos. -type ConfigSchema struct { - fields docs.FieldSpecs - env *Environment - version, dateBuilt string -} - -// FullConfigSchema returns a config schema containing all the standard config -// fields and all plugin definitions from the environment. -func (e *Environment) FullConfigSchema(version, dateBuilt string) *ConfigSchema { - return &ConfigSchema{ - fields: config.Spec(), - env: e, - version: version, - dateBuilt: dateBuilt, - } -} - -// CoreConfigSchema returns a config schema containing only the core Benthos -// pipeline fields (input, buffer, pipeline, output), and all plugin definitions -// from the environment. -func (e *Environment) CoreConfigSchema(version, dateBuilt string) *ConfigSchema { - return &ConfigSchema{ - fields: stream.Spec(), - env: e, - version: version, - dateBuilt: dateBuilt, - } -} - -// Environment provides access to the environment referenced by this schema. -func (s *ConfigSchema) Environment() *Environment { - return s.env -} - -// ConfigSchemaFromJSONV0 attempts to parse a JSON serialised definition of an -// entire schema. Any plugins defined in the schema will be registered with the -// config schema environment and can be used for config linting and marshalling. -// -// However, the environment cannot be used for instantiating a runnable pipeline -// as the constructors will be disabled. This allows applications to lint -// against plugin definitions that they themselves haven't imported. -func ConfigSchemaFromJSONV0(jBytes []byte) (*ConfigSchema, error) { - emptyEnvironment := &Environment{ - internal: bundle.NewEnvironment(), - bloblangEnv: bloblang.NewEmptyEnvironment().WithDisabledImports(), - fs: ifs.OS(), // TODO: Isolate this as well? - } - - var tmpSchema rawMessageSchema - if err := json.Unmarshal(jBytes, &tmpSchema); err != nil { - return nil, err - } - - if err := expandEnvWithSchema(&tmpSchema, emptyEnvironment); err != nil { - return nil, err - } - if err := expandBloblEnvWithSchema(&tmpSchema, emptyEnvironment.bloblangEnv); err != nil { - return nil, err - } - return &ConfigSchema{ - version: tmpSchema.Version, - dateBuilt: tmpSchema.Date, - fields: tmpSchema.Config, - env: emptyEnvironment, - }, nil -} - -// MarshalJSONV0 attempts to marshal a JSON document containing the entire -// config and plugin ecosystem schema such that other applications can -// potentially execute their own linting and generation tools with it. -func (s *ConfigSchema) MarshalJSONV0() ([]byte, error) { - bEnv := s.env.getBloblangParserEnv() - - var functionDocs []query.FunctionSpec - bEnv.WalkFunctions(func(name string, spec query.FunctionSpec) { - functionDocs = append(functionDocs, spec) - }) - - var methodDocs []query.MethodSpec - bEnv.WalkMethods(func(name string, spec query.MethodSpec) { - methodDocs = append(methodDocs, spec) - }) - - iSchema := schema.Full{ - Version: s.version, - Date: s.dateBuilt, - Config: s.fields, - Buffers: s.env.internal.BufferDocs(), - Caches: s.env.internal.CacheDocs(), - Inputs: s.env.internal.InputDocs(), - Outputs: s.env.internal.OutputDocs(), - Processors: s.env.internal.ProcessorDocs(), - RateLimits: s.env.internal.RateLimitDocs(), - Metrics: s.env.internal.MetricsDocs(), - Tracers: s.env.internal.TracersDocs(), - Scanners: s.env.internal.ScannerDocs(), - BloblangFunctions: functionDocs, - BloblangMethods: methodDocs, - } - - return json.Marshal(iSchema) -} - -func compSpecsToDefinition(specs []docs.ComponentSpec, typeFields map[string]docs.FieldSpec) map[string]any { - generalFields := map[string]any{} - for k, v := range typeFields { - generalFields[k] = v.JSONSchema() - } - - var componentDefs []any - for _, s := range specs { - componentDefs = append(componentDefs, map[string]any{ - "type": "object", - "properties": map[string]any{ - s.Name: s.Config.JSONSchema(), - }, - }) - } - - return map[string]any{ - "allOf": []any{ - map[string]any{ - "anyOf": componentDefs, // TODO: Convert this to oneOf once issues are resolved. - }, - map[string]any{ - "type": "object", - "properties": generalFields, - }, - }, - } -} - -// MarshalJSONSchema attempts to marshal a JSON Schema definition containing the -// entire config and plugin ecosystem such that other applications can -// potentially execute their own linting and generation tools with it. -func (s *ConfigSchema) MarshalJSONSchema() ([]byte, error) { - s.env.internal.BufferDocs() - defs := map[string]any{ - "input": compSpecsToDefinition(s.env.internal.InputDocs(), docs.ReservedFieldsByType(docs.TypeInput)), - "buffer": compSpecsToDefinition(s.env.internal.BufferDocs(), docs.ReservedFieldsByType(docs.TypeBuffer)), - "cache": compSpecsToDefinition(s.env.internal.CacheDocs(), docs.ReservedFieldsByType(docs.TypeCache)), - "processor": compSpecsToDefinition(s.env.internal.ProcessorDocs(), docs.ReservedFieldsByType(docs.TypeProcessor)), - "rate_limit": compSpecsToDefinition(s.env.internal.RateLimitDocs(), docs.ReservedFieldsByType(docs.TypeRateLimit)), - "output": compSpecsToDefinition(s.env.internal.OutputDocs(), docs.ReservedFieldsByType(docs.TypeOutput)), - "metrics": compSpecsToDefinition(s.env.internal.MetricsDocs(), docs.ReservedFieldsByType(docs.TypeMetrics)), - "tracer": compSpecsToDefinition(s.env.internal.TracersDocs(), docs.ReservedFieldsByType(docs.TypeTracer)), - "scanner": compSpecsToDefinition(s.env.internal.ScannerDocs(), docs.ReservedFieldsByType(docs.TypeScanner)), - } - - schemaObj := map[string]any{ - "properties": s.fields.JSONSchema(), - "definitions": defs, - } - - return json.Marshal(schemaObj) -} - -// SetVersion sets the version and date-built stamp associated with the schema. -func (s *ConfigSchema) SetVersion(version, dateBuilt string) *ConfigSchema { - s.version = version - s.dateBuilt = dateBuilt - return s -} - -// Field adds a field to the main config of a schema. -func (s *ConfigSchema) Field(f *ConfigField) *ConfigSchema { - s.fields = append(s.fields, f.field) - return s -} - -// Fields adds multiple fields to the main config of a schema. -func (s *ConfigSchema) Fields(fs ...*ConfigField) *ConfigSchema { - spec := s - for _, f := range fs { - spec = s.Field(f) - } - return spec -} - -//------------------------------------------------------------------------------ - -type rawMessageSchema struct { - Version string `json:"version"` - Date string `json:"date"` - Config docs.FieldSpecs `json:"config,omitempty"` - Buffers []json.RawMessage `json:"buffers,omitempty"` - Caches []json.RawMessage `json:"caches,omitempty"` - Inputs []json.RawMessage `json:"inputs,omitempty"` - Outputs []json.RawMessage `json:"outputs,omitempty"` - Processors []json.RawMessage `json:"processors,omitempty"` - RateLimits []json.RawMessage `json:"rate-limits,omitempty"` - Metrics []json.RawMessage `json:"metrics,omitempty"` - Tracers []json.RawMessage `json:"tracers,omitempty"` - Scanners []json.RawMessage `json:"scanners,omitempty"` - BloblangFunctions []json.RawMessage `json:"bloblang-functions,omitempty"` - BloblangMethods []json.RawMessage `json:"bloblang-methods,omitempty"` -} - -func nameAndBloblSpec(data []byte) (string, *bloblang.PluginSpec, error) { - var nameData struct { - Name string `json:"name"` - } - if err := json.Unmarshal(data, &nameData); err != nil { - return "", nil, err - } - - pluginSpec := bloblang.NewPluginSpec() - if err := pluginSpec.EncodeJSON(data); err != nil { - return "", nil, err - } - return nameData.Name, pluginSpec, nil -} - -func expandBloblEnvWithSchema(schema *rawMessageSchema, bEnv *bloblang.Environment) error { - hasPlug := map[string]struct{}{} - bEnv.WalkFunctions(func(name string, spec *bloblang.FunctionView) { - hasPlug[name] = struct{}{} - }) - for _, spec := range schema.BloblangFunctions { - name, pluginSpec, err := nameAndBloblSpec(spec) - if err != nil { - return err - } - - if _, exists := hasPlug[name]; exists { - continue - } - if err = bEnv.RegisterFunctionV2(name, pluginSpec, func(args *bloblang.ParsedParams) (bloblang.Function, error) { - return func() (interface{}, error) { - return nil, fmt.Errorf("function %v not enabled", name) - }, nil - }); err != nil { - return err - } - } - - hasPlug = map[string]struct{}{} - for _, spec := range schema.BloblangMethods { - name, pluginSpec, err := nameAndBloblSpec(spec) - if err != nil { - return err - } - - if _, exists := hasPlug[name]; exists { - continue - } - if err = bEnv.RegisterMethodV2(name, pluginSpec, func(args *bloblang.ParsedParams) (bloblang.Method, error) { - return func(v interface{}) (interface{}, error) { - return nil, fmt.Errorf("method %v not enabled", name) - }, nil - }); err != nil { - return err - } - } - return nil -} - -var errComponentDisabled = errors.New("component not enabled") - -func expandEnvWithSchema(schema *rawMessageSchema, env *Environment) error { - for _, spec := range schema.Buffers { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeBuffer); exists { - continue - } - _ = env.RegisterBatchBuffer( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (BatchBuffer, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Caches { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeCache); exists { - continue - } - _ = env.RegisterCache( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (Cache, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Inputs { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeInput); exists { - continue - } - _ = env.RegisterInput( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (Input, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Processors { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeProcessor); exists { - continue - } - _ = env.RegisterProcessor( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (Processor, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Outputs { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeOutput); exists { - continue - } - _ = env.RegisterBatchOutput( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (BatchOutput, BatchPolicy, int, error) { - return nil, BatchPolicy{}, 0, errComponentDisabled - }) - } - - for _, spec := range schema.RateLimits { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeRateLimit); exists { - continue - } - _ = env.RegisterRateLimit( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (RateLimit, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Metrics { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeMetrics); exists { - continue - } - _ = env.RegisterMetricsExporter( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, log *Logger) (MetricsExporter, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Tracers { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeTracer); exists { - continue - } - _ = env.RegisterOtelTracerProvider( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig) (trace.TracerProvider, error) { - return nil, errComponentDisabled - }) - } - - for _, spec := range schema.Scanners { - pluginSpec := NewConfigSpec() - if err := pluginSpec.EncodeJSON(spec); err != nil { - return err - } - if _, exists := env.internal.GetDocs(pluginSpec.component.Name, docs.TypeScanner); exists { - continue - } - _ = env.RegisterBatchScannerCreator( - pluginSpec.component.Name, pluginSpec, - func(conf *ParsedConfig, mgr *Resources) (BatchScannerCreator, error) { - return nil, errComponentDisabled - }) - } - return nil -} diff --git a/public/service/stream_schema_test.go b/public/service/stream_schema_test.go deleted file mode 100644 index a4ef9dfcda..0000000000 --- a/public/service/stream_schema_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package service_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" - - jsonschema "github.com/xeipuuv/gojsonschema" - - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" -) - -func testEnvWithPlugins(t testing.TB) *service.Environment { - t.Helper() - - env := service.NewEmptyEnvironment() - - require.NoError(t, env.RegisterInput("testinput", service.NewConfigSpec().Field(service.NewStringField("woof").Example("WOOF")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterInput("anothertestinput", service.NewConfigSpec().Field(service.NewStringField("moo").Example("MOO")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterBatchBuffer("testbuffer", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchBuffer, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterProcessor("testprocessor", service.NewConfigSpec().Field(service.NewBloblangField("mapfield")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterOutput("testoutput", service.NewConfigSpec().Field(service.NewStringField("meow").Example("MEOW")), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - err = errors.New("nope") - return - })) - - require.NoError(t, env.RegisterCache("testcache", service.NewConfigSpec().Field(service.NewStringField("cachefield")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Cache, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterRateLimit("testratelimit", service.NewConfigSpec().Field(service.NewStringField("ratelimitfield")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.RateLimit, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterMetricsExporter("testmetrics", service.NewConfigSpec().Field(service.NewStringField("metricsfield")), - func(conf *service.ParsedConfig, log *service.Logger) (service.MetricsExporter, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterOtelTracerProvider("testtracer", service.NewConfigSpec().Field(service.NewStringField("tracerfield")), - func(conf *service.ParsedConfig) (trace.TracerProvider, error) { - return nil, errors.New("nope") - })) - - require.NoError(t, env.RegisterBatchScannerCreator("testscanner", service.NewConfigSpec().Field(service.NewStringField("scannerfield")), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchScannerCreator, error) { - return nil, errors.New("nope") - })) - - return env -} - -func TestStreamSchemaInteropCore(t *testing.T) { - bEnv := bloblang.NewEmptyEnvironment() - - require.NoError(t, bEnv.RegisterFunction("cow", func(args ...any) (bloblang.Function, error) { - return nil, errors.New("nope") - })) - require.NoError(t, bEnv.RegisterMethod("sheep", func(args ...any) (bloblang.Method, error) { - return nil, errors.New("nope") - })) - - env := testEnvWithPlugins(t) - env.UseBloblangEnvironment(bEnv) - - schemaSource := env.FullConfigSchema("aaa", "bbb").Field(service.NewStringField("foo").Default("test default")) - - schemaBytes, err := schemaSource.MarshalJSONV0() - require.NoError(t, err) - - schemaSink, err := service.ConfigSchemaFromJSONV0(schemaBytes) - require.NoError(t, err) - - yamlStr, err := schemaSink.NewStreamConfigMarshaller(). - SetHydrateExamples(true). - AnyToYAML(map[string]any{ - "input": map[string]any{"type": "testinput"}, - "buffer": map[string]any{"type": "testbuffer"}, - "pipeline": map[string]any{ - "processors": []any{ - map[string]any{ - "testprocessor": map[string]any{ - "mapfield": "root = cow().sheep()", - }, - }, - }, - }, - "output": map[string]any{"type": "testoutput"}, - "cache_resources": []any{ - map[string]any{"label": "acache", "type": "testcache"}, - }, - "rate_limit_resources": []any{ - map[string]any{"label": "aratelimit", "type": "testratelimit"}, - }, - "metrics": map[string]any{"type": "testmetrics"}, - "tracer": map[string]any{"type": "testtracer"}, - }) - require.NoError(t, err) - - for _, k := range []string{ - ` -input: - testinput: - woof: WOOF # No default (required)`, - ` -output: - testoutput: - meow: MEOW # No default (required)`, - ` -buffer: - testbuffer: null # No default (required)`, - ` - - testprocessor: - mapfield: root = cow().sheep()`, - ` -cache_resources: - - label: acache - testcache: - cachefield: "" # No default (required)`, - ` -rate_limit_resources: - - label: aratelimit - testratelimit: - ratelimitfield: "" # No default (required)`, - ` -metrics: - testmetrics: - metricsfield: "" # No default (required) -`, - ` -tracer: - testtracer: - tracerfield: "" # No default (required)`, - `foo: test default`, - } { - assert.Contains(t, yamlStr, k) - } -} - -func TestStreamSchemaInteropLinter(t *testing.T) { - bEnv := bloblang.NewEmptyEnvironment() - - require.NoError(t, bEnv.RegisterFunction("cow", func(args ...any) (bloblang.Function, error) { - return nil, errors.New("nope") - })) - require.NoError(t, bEnv.RegisterMethod("sheep", func(args ...any) (bloblang.Method, error) { - return nil, errors.New("nope") - })) - - env := testEnvWithPlugins(t) - env.UseBloblangEnvironment(bEnv) - - schemaSource := env.FullConfigSchema("aaa", "bbb").Field(service.NewStringField("foo").Default("test default")) - - schemaBytes, err := schemaSource.MarshalJSONV0() - require.NoError(t, err) - - schemaSink, err := service.ConfigSchemaFromJSONV0(schemaBytes) - require.NoError(t, err) - - lints, err := schemaSink.NewStreamConfigLinter().LintYAML([]byte(` -input: - testinput: - woof: WOOF - -pipeline: - processors: - - testprocessor: - mapfield: root = cow().sheep() - -output: - testoutput: - meow: MEOW # No default (required) -`)) - require.NoError(t, err) - assert.Empty(t, lints) -} - -func TestJSONSchema(t *testing.T) { - env := testEnvWithPlugins(t) - - testSchema, err := env.FullConfigSchema("xxx", "yyy").MarshalJSONSchema() - require.NoError(t, err) - - schema, err := jsonschema.NewSchema(jsonschema.NewStringLoader(string(testSchema))) - require.NoError(t, err) - - res, err := schema.Validate(jsonschema.NewGoLoader(map[string]any{ - "input": map[string]any{ - "testinput": map[string]any{ - "woof": "uhhhhh, woof!", - }, - "processors": []any{ - map[string]any{ - "testprocessor": map[string]any{ - "mapfield": "hello world", - }, - }, - }, - }, - })) - require.NoError(t, err) - require.Empty(t, res.Errors()) -} diff --git a/public/service/stream_template_tester.go b/public/service/stream_template_tester.go deleted file mode 100644 index ad4daead8f..0000000000 --- a/public/service/stream_template_tester.go +++ /dev/null @@ -1,60 +0,0 @@ -package service - -import ( - "gopkg.in/yaml.v3" - - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/template" -) - -// StreamTemplateTester provides utilities for testing templates. -type StreamTemplateTester struct { - env *Environment -} - -// NewStreamTemplateTester creates a component for marshalling stream configs, -// allowing you to print sanitised, redacted or hydrated configs in various -// formats. -func (e *Environment) NewStreamTemplateTester() *StreamTemplateTester { - return &StreamTemplateTester{ - env: e, - } -} - -// LintYAML attempts to read a template defined in YAML format and lints it. -func (s *StreamTemplateTester) LintYAML(yamlBytes []byte) (lints []Lint, err error) { - var node yaml.Node - if err = yaml.Unmarshal(yamlBytes, &node); err != nil { - return - } - - for _, l := range template.ConfigSpec().LintYAML(docs.NewLintContext(docs.NewLintConfig(s.env.internal)), &node) { - lints = append(lints, Lint{ - Line: l.Line, - Column: l.Column, - Type: convertDocsLintType(l.Type), - What: l.What, - }) - } - return -} - -// RunYAML attempts to read a template defined in YAML format and runs any tests -// defined within the template. -func (s *StreamTemplateTester) RunYAML(yamlBytes []byte) (lints []Lint, err error) { - var conf template.Config - if err = yaml.Unmarshal(yamlBytes, &conf); err != nil { - return - } - - testErrors, err := conf.Test() - if err != nil { - lints = append(lints, Lint{Line: 1, Type: LintFailedRead, What: err.Error()}) - return - } - - for _, tErr := range testErrors { - lints = append(lints, Lint{Line: 1, Type: LintFailedRead, What: tErr}) - } - return -} diff --git a/public/service/tracing.go b/public/service/tracing.go deleted file mode 100644 index 98d679525f..0000000000 --- a/public/service/tracing.go +++ /dev/null @@ -1,137 +0,0 @@ -package service - -import ( - "sync/atomic" - - "github.com/benthosdev/benthos/v4/internal/bundle/tracing" -) - -// TracingEventType describes the type of tracing event a component might -// experience during a config run. -// -// Experimental: This type may change outside of major version releases. -type TracingEventType string - -// Various tracing event types. -// -// Experimental: This type may change outside of major version releases. -var ( - // Note: must match up with ./internal/bundle/tracing/events.go. - TracingEventProduce TracingEventType = "PRODUCE" - TracingEventConsume TracingEventType = "CONSUME" - TracingEventDelete TracingEventType = "DELETE" - TracingEventError TracingEventType = "ERROR" - TracingEventUnknown TracingEventType = "UNKNOWN" -) - -func convertTracingEventType(t tracing.EventType) TracingEventType { - switch t { - case tracing.EventProduce: - return TracingEventProduce - case tracing.EventConsume: - return TracingEventConsume - case tracing.EventDelete: - return TracingEventDelete - case tracing.EventError: - return TracingEventError - } - return TracingEventUnknown -} - -// TracingEvent represents a single event that occurred within the stream. -// -// Experimental: This type may change outside of major version releases. -type TracingEvent struct { - Type TracingEventType - Content string - Meta map[string]any -} - -// TracingSummary is a high level description of all traced events. When tracing -// a stream this should only be queried once the stream has ended. -// -// Experimental: This type may change outside of major version releases. -type TracingSummary struct { - summary *tracing.Summary -} - -// TotalInput returns the total traced input messages received. -// -// Experimental: This method may change outside of major version releases. -func (s *TracingSummary) TotalInput() uint64 { - return atomic.LoadUint64(&s.summary.Input) -} - -// TotalProcessorErrors returns the total traced processor errors occurred. -// -// Experimental: This method may change outside of major version releases. -func (s *TracingSummary) TotalProcessorErrors() uint64 { - return atomic.LoadUint64(&s.summary.ProcessorErrors) -} - -// TotalOutput returns the total traced output messages received. -// -// Experimental: This method may change outside of major version releases. -func (s *TracingSummary) TotalOutput() uint64 { - return atomic.LoadUint64(&s.summary.Output) -} - -// InputEvents returns a map of input labels to events traced during the -// execution of a stream pipeline. -// -// Experimental: This method may change outside of major version releases. -func (s *TracingSummary) InputEvents() map[string][]TracingEvent { - m := map[string][]TracingEvent{} - for k, v := range s.summary.InputEvents(false) { - events := make([]TracingEvent, len(v)) - for i, e := range v { - events[i] = TracingEvent{ - Type: convertTracingEventType(e.Type), - Content: e.Content, - Meta: e.Meta, - } - } - m[k] = events - } - return m -} - -// ProcessorEvents returns a map of processor labels to events traced during the -// execution of a stream pipeline. -// -// Experimental: This method may change outside of major version releases. -func (s *TracingSummary) ProcessorEvents() map[string][]TracingEvent { - m := map[string][]TracingEvent{} - for k, v := range s.summary.ProcessorEvents(false) { - events := make([]TracingEvent, len(v)) - for i, e := range v { - events[i] = TracingEvent{ - Type: convertTracingEventType(e.Type), - Content: e.Content, - Meta: e.Meta, - } - } - m[k] = events - } - return m -} - -// OutputEvents returns a map of output labels to events traced during the -// execution of a stream pipeline. -// -// Experimental: This method may change outside of major version releases. -func (s *TracingSummary) OutputEvents() map[string][]TracingEvent { - m := map[string][]TracingEvent{} - for k, v := range s.summary.OutputEvents(false) { - events := make([]TracingEvent, len(v)) - for i, e := range v { - events[i] = TracingEvent{ - Type: convertTracingEventType(e.Type), - Content: e.Content, - Meta: e.Meta, - } - } - m[k] = events - } - return m -} diff --git a/public/service/tracing_test.go b/public/service/tracing_test.go deleted file mode 100644 index b63b6e76bf..0000000000 --- a/public/service/tracing_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package service_test - -import ( - "bytes" - "context" - "fmt" - "testing" - "time" - - "github.com/gofrs/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/public/components/pure" -) - -func TestOtelTracingPlugin(t *testing.T) { - env := service.NewEnvironment() - confSpec := service.NewConfigSpec().Field(service.NewStringField("foo")) - - var testValue string - - require.NoError(t, env.RegisterOtelTracerProvider( - "meow", confSpec, - func(conf *service.ParsedConfig) (trace.TracerProvider, error) { - testStr, err := conf.FieldString("foo") - if err != nil { - return nil, err - } - testValue = testStr - return noop.NewTracerProvider(), nil - })) - - builder := env.NewStreamBuilder() - require.NoError(t, builder.SetYAML(` -input: - label: fooinput - generate: - count: 2 - interval: 1ns - mapping: 'root.id = uuid_v4()' - -output: - label: foooutput - drop: {} - -tracer: - meow: - foo: foo value from config -`)) - - strm, err := builder.Build() - require.NoError(t, err) - - ctx, done := context.WithTimeout(context.Background(), time.Second) - defer done() - - require.NoError(t, strm.Run(ctx)) - - assert.Equal(t, "foo value from config", testValue) -} - -func TestTracing(t *testing.T) { - u, err := uuid.NewV4() - require.NoError(t, err) - - config := ` -input: - generate: - count: 5 - interval: 1us - mapping: | - root.id = count("` + u.String() + `") - -pipeline: - threads: 1 - processors: - - bloblang: | - root.count = if this.id % 2 == 0 { throw("nah %v".format(this.id)) } else { this.id } - meta foo = this.id - -output: - drop: {} - -logger: - level: OFF -` - - strmBuilder := service.NewStreamBuilder() - require.NoError(t, strmBuilder.SetYAML(config)) - - strm, trace, err := strmBuilder.BuildTraced() - require.NoError(t, err) - - require.NoError(t, strm.Run(context.Background())) - - assert.Equal(t, 5, int(trace.TotalInput())) - assert.Equal(t, 5, int(trace.TotalOutput())) - assert.Equal(t, 2, int(trace.TotalProcessorErrors())) - - type tMap = map[string]any - - assert.Equal(t, map[string][]service.TracingEvent{ - "root.input": { - {Type: service.TracingEventProduce, Content: `{"id":1}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"id":2}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"id":3}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"id":4}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"id":5}`, Meta: tMap{}}, - }, - }, trace.InputEvents()) - - assert.Equal(t, map[string][]service.TracingEvent{ - "root.pipeline.processors.0": { - {Type: service.TracingEventConsume, Content: `{"id":1}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"count":1}`, Meta: tMap{"foo": int64(1)}}, - {Type: service.TracingEventConsume, Content: `{"id":2}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"id":2}`, Meta: tMap{}}, - {Type: service.TracingEventError, Content: `failed assignment (line 1): nah 2`}, - {Type: service.TracingEventConsume, Content: `{"id":3}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"count":3}`, Meta: tMap{"foo": int64(3)}}, - {Type: service.TracingEventConsume, Content: `{"id":4}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"id":4}`, Meta: tMap{}}, - {Type: service.TracingEventError, Content: `failed assignment (line 1): nah 4`}, - {Type: service.TracingEventConsume, Content: `{"id":5}`, Meta: tMap{}}, - {Type: service.TracingEventProduce, Content: `{"count":5}`, Meta: tMap{"foo": int64(5)}}, - }, - }, trace.ProcessorEvents()) - - assert.Equal(t, map[string][]service.TracingEvent{ - "root.output": { - {Type: service.TracingEventConsume, Content: `{"count":1}`, Meta: tMap{"foo": int64(1)}}, - {Type: service.TracingEventConsume, Content: `{"id":2}`, Meta: tMap{}}, - {Type: service.TracingEventConsume, Content: `{"count":3}`, Meta: tMap{"foo": int64(3)}}, - {Type: service.TracingEventConsume, Content: `{"id":4}`, Meta: tMap{}}, - {Type: service.TracingEventConsume, Content: `{"count":5}`, Meta: tMap{"foo": int64(5)}}, - }, - }, trace.OutputEvents()) -} - -func BenchmarkStreamTracing(b *testing.B) { - config := ` -input: - generate: - count: 5 - interval: "" - mapping: | - root.id = uuid_v4() - -pipeline: - threads: 1 - processors: - - bloblang: 'root = this' - -output: - drop: {} - -logger: - level: OFF -` - - strmBuilder := service.NewStreamBuilder() - strmBuilder.SetHTTPMux(disabledMux{}) - require.NoError(b, strmBuilder.SetYAML(config)) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - strm, trace, err := strmBuilder.BuildTraced() - require.NoError(b, err) - - require.NoError(b, strm.Run(context.Background())) - - assert.Equal(b, 5, int(trace.TotalInput())) - assert.Equal(b, 5, int(trace.TotalOutput())) - assert.Equal(b, 0, int(trace.TotalProcessorErrors())) - } -} - -func BenchmarkStreamTracingOutputN1(b *testing.B) { - benchmarkStreamTracingOutputNX(b, 1) -} - -func BenchmarkStreamTracingOutputN10(b *testing.B) { - benchmarkStreamTracingOutputNX(b, 10) -} - -func BenchmarkStreamTracingOutputN100(b *testing.B) { - benchmarkStreamTracingOutputNX(b, 100) -} - -func benchmarkStreamTracingOutputNX(b *testing.B, size int) { - var outputsBuf bytes.Buffer - for i := 0; i < size; i++ { - outputsBuf.WriteString(" - custom: {}\n") - } - - config := fmt.Sprintf(` -input: - generate: - count: 5 - interval: "" - mapping: | - root.id = uuid_v4() - -pipeline: - threads: 1 - processors: - - bloblang: 'root = this' - -output: - broker: - outputs: -%v - -logger: - level: OFF -`, outputsBuf.String()) - - env := service.NewEnvironment() - require.NoError(b, env.RegisterOutput( - "custom", - service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (out service.Output, maxInFlight int, err error) { - return &noopOutput{}, 1, nil - }, - )) - - strmBuilder := env.NewStreamBuilder() - strmBuilder.SetHTTPMux(disabledMux{}) - require.NoError(b, strmBuilder.SetYAML(config)) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - strm, trace, err := strmBuilder.BuildTraced() - require.NoError(b, err) - - require.NoError(b, strm.Run(context.Background())) - - assert.Equal(b, 5, int(trace.TotalInput())) - } -} diff --git a/public/service/util.go b/public/service/util.go deleted file mode 100644 index d4bf037e3d..0000000000 --- a/public/service/util.go +++ /dev/null @@ -1,45 +0,0 @@ -package service - -import ( - "io" - "io/fs" - "os" - - "github.com/benthosdev/benthos/v4/internal/filepath" - "github.com/benthosdev/benthos/v4/internal/filepath/ifs" -) - -// ReadFile opens a file from an fs.FS and reads all bytes. When the OpenFile -// method is available this will be used instead of Open with the RDONLY flag. -func ReadFile(f fs.FS, name string) ([]byte, error) { - var i fs.File - var err error - if ef, ok := f.(ifs.FS); ok { - i, err = ef.OpenFile(name, os.O_RDONLY, 0) - } else { - i, err = f.Open(name) - } - if err != nil { - return nil, err - } - return io.ReadAll(i) -} - -// Globs attempts to expand the glob patterns within of a series of paths and -// returns the resulting expanded slice or an error. -func Globs(f fs.FS, paths ...string) ([]string, error) { - return filepath.Globs(f, paths) -} - -// GlobsAndSuperPaths attempts to expand a list of paths, which may include glob -// patterns and super paths (the ... thing) to a list of explicit file paths. -// Extensions must be provided, and limit the file types that are captured with -// a super path. -func GlobsAndSuperPaths(f fs.FS, paths []string, extensions ...string) ([]string, error) { - return filepath.GlobsAndSuperPaths(f, paths) -} - -// OSFS provides an fs.FS implementation that simply calls into the os. -func OSFS() fs.FS { - return ifs.OS() -} diff --git a/public/wasm/README.md b/public/wasm/README.md deleted file mode 100644 index 4dfafb8a3e..0000000000 --- a/public/wasm/README.md +++ /dev/null @@ -1,10 +0,0 @@ -Benthos WASM Plugins -==================== - -In this directory are libraries and examples tailored to help developers create WASM plugins that can be run by the Benthos [`wasm` processor][processor.wasm]. It's possible to write WASM plugins in any language that compiles to a WASM module. However, given the complexity in passing allocated memory between the module and the host process it's much easier to use the libraries provided here as the basis for your plugin. - -Most of these are adapted from the fantastic range of examples provided by [the Wazero library][wazero_examples]. Our goal is to eventually provide libraries and examples for all popular languages and we'll be tackling them one at a time based on demand. Please be patient but also make [yourself heard][community]. - -[processor.wasm]: https://www.benthos.dev/docs/components/processors/wasm -[wazero_examples]: https://github.com/tetratelabs/wazero/tree/main/examples -[community]: https://www.benthos.dev/community diff --git a/public/wasm/examples/rust/.gitignore b/public/wasm/examples/rust/.gitignore deleted file mode 100644 index f2f9e58ec3..0000000000 --- a/public/wasm/examples/rust/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -Cargo.lock \ No newline at end of file diff --git a/public/wasm/examples/rust/Cargo.toml b/public/wasm/examples/rust/Cargo.toml deleted file mode 100644 index 0c30d4fcfc..0000000000 --- a/public/wasm/examples/rust/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "louder" -version = "0.1.0" -edition = "2021" - -[lib] -# cdylib builds a %.wasm file with `cargo build --release --target wasm32-unknown-unknown` -crate-type = ["cdylib"] -name = "louder" -path = "louder.rs" - -[dependencies] -# wee_aloc is a WebAssembly optimized allocator, which is needed to use non-numeric types like strings. -# See https://docs.rs/wee_alloc/latest/wee_alloc/ -wee_alloc = "0.4.5" - -# Below settings dramatically reduce wasm output size -# See https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-sizewasm-opt -Oz -o -# See https://doc.rust-lang.org/cargo/reference/profiles.html#codegen-units -[profile.release] -opt-level = "z" -lto = true -codegen-units = 1 diff --git a/public/wasm/examples/rust/louder.rs b/public/wasm/examples/rust/louder.rs deleted file mode 100644 index 8510992d69..0000000000 --- a/public/wasm/examples/rust/louder.rs +++ /dev/null @@ -1,105 +0,0 @@ -extern crate alloc; -extern crate core; -extern crate wee_alloc; - -use alloc::vec::Vec; -use std::mem::MaybeUninit; -use std::slice; - -/// Makes a message louder. -fn process(message: &String) -> String { - return[&message, "!!!!111!!11!"].concat() ; -} - -/// WebAssembly export that is called for each message being processed. -#[cfg_attr(all(target_arch = "wasm32"), export_name = "process")] -#[no_mangle] -pub unsafe extern "C" fn _process() { - let ptr_size = _msg_as_bytes(); - - let content_ptr = (ptr_size >> 32) as u32; - let content_size = ptr_size as u32; - let message = &ptr_to_string(content_ptr, content_size); - - let processed = process(&message); - let (ptr, len) = string_to_ptr(&processed); - std::mem::forget(processed); - - _msg_set_bytes(ptr, len); -} - -//------------------------------------------------------------------------------ - -#[link(wasm_import_module = "benthos_wasm")] -extern "C" { - /// WebAssembly import for mutating Benthos messages. - /// - /// Note: This is not an ownership transfer: Rust still owns the pointer - /// and ensures it isn't deallocated during this call. - #[link_name = "v0_msg_set_bytes"] - fn _msg_set_bytes(ptr: u32, size: u32); - - /// WebAssembly import for accessing Benthos messages. - /// - /// Note: This is not an ownership transfer: Rust still owns the pointer - /// and ensures it isn't deallocated during this call. - #[link_name = "v0_msg_as_bytes"] - fn _msg_as_bytes() -> u64; -} - -//------------------------------------------------------------------------------ - -/// Returns a string from WebAssembly compatible numeric types representing -/// its pointer and length. -unsafe fn ptr_to_string(ptr: u32, len: u32) -> String { - let slice = slice::from_raw_parts_mut(ptr as *mut u8, len as usize); - let utf8 = std::str::from_utf8_unchecked_mut(slice); - return String::from(utf8); -} - -/// Returns a pointer and size pair for the given string in a way compatible -/// with WebAssembly numeric types. -/// -/// Note: This doesn't change the ownership of the String. To intentionally -/// leak it, use [`std::mem::forget`] on the input after calling this. -unsafe fn string_to_ptr(s: &String) -> (u32, u32) { - return (s.as_ptr() as u32, s.len() as u32); -} - -/// Set the global allocator to the WebAssembly optimized one. -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - -/// WebAssembly export that allocates a pointer (linear memory offset) that can -/// be used for a string. -/// -/// This is an ownership transfer, which means the caller must call -/// [`deallocate`] when finished. -#[cfg_attr(all(target_arch = "wasm32"), export_name = "allocate")] -#[no_mangle] -pub extern "C" fn _allocate(size: u32) -> *mut u8 { - allocate(size as usize) -} - -/// Allocates size bytes and leaks the pointer where they start. -fn allocate(size: usize) -> *mut u8 { - // Allocate the amount of bytes needed. - let vec: Vec> = Vec::with_capacity(size); - - // into_raw leaks the memory to the caller. - Box::into_raw(vec.into_boxed_slice()) as *mut u8 -} - - -/// WebAssembly export that deallocates a pointer of the given size (linear -/// memory offset, byteCount) allocated by [`allocate`]. -#[cfg_attr(all(target_arch = "wasm32"), export_name = "deallocate")] -#[no_mangle] -pub unsafe extern "C" fn _deallocate(ptr: u32, size: u32) { - deallocate(ptr as *mut u8, size as usize); -} - -/// Retakes the pointer which allows its memory to be freed. -unsafe fn deallocate(ptr: *mut u8, size: usize) { - let _ = Vec::from_raw_parts(ptr, 0, size); -} \ No newline at end of file diff --git a/public/wasm/examples/tinygo/README.md b/public/wasm/examples/tinygo/README.md deleted file mode 100644 index 0692213815..0000000000 --- a/public/wasm/examples/tinygo/README.md +++ /dev/null @@ -1,20 +0,0 @@ -TinyGo Benthos WASM Module -========================== - -This example builds a Benthos plugin as a WASM module written in Go and can be compiled using [TinyGo][tinygo] with the following command: - -```sh -tinygo build -scheduler=none -target=wasi -o uppercase.wasm . -``` - -You can then run the compiled module using the [`wasm` processor][processor.wasm], configured like so: - -```yaml -pipeline: - processors: - - wasm: - module_path: ./uppercase.wasm -``` - -[TinyGo]: https://tinygo.org/ -[processor.wasm]: https://www.benthos.dev/docs/components/processors/wasm diff --git a/public/wasm/examples/tinygo/main.go b/public/wasm/examples/tinygo/main.go deleted file mode 100644 index a0dcb3aa6c..0000000000 --- a/public/wasm/examples/tinygo/main.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build tinygo - -package main - -import ( - "bytes" - - "github.com/benthosdev/benthos/v4/public/wasm/tinygo" -) - -// main is required for TinyGo to compile to Wasm. -func main() {} - -// _process is a WebAssembly export without arguments that triggers processing -// of a Benthos message. The message data is accessed and mutated by functions -// imported from Benthos and are accessible via the ./public/wasm packages (in -// this case tinygo). -// -//export process -func _process() { - mBytes, err := tinygo.GetMsgAsBytes() - if err != nil { - panic(err) - } - if err := tinygo.SetMsgBytes(bytes.ToUpper(mBytes)); err != nil { - panic(err) - } -} diff --git a/public/wasm/tinygo/package.go b/public/wasm/tinygo/package.go deleted file mode 100644 index f79262bb68..0000000000 --- a/public/wasm/tinygo/package.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package tinygo provides entry points that allow WASM modules compiled with -// TinyGo to be executed by Benthos using the `wasm` processor. -// -// Check out https://github.com/benthosdev/benthos/tree/main/public/wasm/examples/tinygo -// for an example of how to use this package. -package tinygo diff --git a/public/wasm/tinygo/tinygo.go b/public/wasm/tinygo/tinygo.go deleted file mode 100644 index fc7d82c5e6..0000000000 --- a/public/wasm/tinygo/tinygo.go +++ /dev/null @@ -1,66 +0,0 @@ -//go:build tinygo - -package tinygo - -import ( - "reflect" - "unsafe" -) - -// _msg_set_bytes is a WebAssembly import which writes the contents of the -// message being processed to a byte array (linear memory offset, byteCount). -// -// Note: In TinyGo "//export" on a func is actually an import! -// -//go:wasm-module benthos_wasm -//export v0_msg_set_bytes -func _v0_msg_set_bytes(ptr, size uint32) - -// SetMsgBytes sets the contents of the message currently being processed to the -// value provided. -func SetMsgBytes(b []byte) error { - resP, resS := bytesToPtr(b) - _v0_msg_set_bytes(resP, resS) - return nil -} - -// _msg_as_bytes is a WebAssembly import which obtains the contents of the -// message being processed as a byte array (linear memory offset, byteCount). -// -// Note: This uses a uint64 instead of two result values for compatibility with -// WebAssembly 1.0. -// -// Note: In TinyGo "//export" on a func is actually an import! -// -//go:wasm-module benthos_wasm -//export v0_msg_as_bytes -func _v0_msg_as_bytes() (ptrSize uint64) - -// GetMsgAsBytes returns the contents of the message currently being processed -// as bytes. -func GetMsgAsBytes() ([]byte, error) { - ptrSize := _v0_msg_as_bytes() - contentPtr := uint32(ptrSize >> 32) - contentSize := uint32(ptrSize) - return ptrToBytes(contentPtr, contentSize), nil -} - -// ptrToBytes returns a byte slice from WebAssembly compatible numeric types -// representing its pointer and length. -func ptrToBytes(ptr, size uint32) []byte { - // Get a slice view of the underlying bytes in the stream. We use SliceHeader, not StringHeader - // as it allows us to fix the capacity to what was allocated. - return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(ptr), - Len: uintptr(size), // Tinygo requires these as uintptrs even if they are int fields. - Cap: uintptr(size), // ^^ See https://github.com/tinygo-org/tinygo/issues/1284 - })) -} - -// bytesToPtr returns a pointer and size pair for the given byte slice in a way -// compatible with WebAssembly numeric types. -func bytesToPtr(buf []byte) (uint32, uint32) { - ptr := &buf[0] - unsafePtr := uintptr(unsafe.Pointer(ptr)) - return uint32(unsafePtr), uint32(len(buf)) -} diff --git a/website/.gitignore b/website/.gitignore deleted file mode 100755 index b2d6de3062..0000000000 --- a/website/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/website/README.md b/website/README.md deleted file mode 100755 index ee0ccc9d18..0000000000 --- a/website/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Website - -This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -``` -$ GIT_USER= USE_SSH=true yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/website/blog/2019-05-27-compiling-benthos-to-wasm.md b/website/blog/2019-05-27-compiling-benthos-to-wasm.md deleted file mode 100644 index e673c0e9d0..0000000000 --- a/website/blog/2019-05-27-compiling-benthos-to-wasm.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: "Compiling Benthos to Web Assembly" -author: "Ashley Jeffs" -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: "Don't worry about why" -keywords: [ - "benthos", - "go", - "golang", - "web assembly", - "wasm", - "gowasm", -] -tags: [ "Benthos Lab" ] ---- - -Web assembly won't fix seasons 7 and 8, but it's still pretty cool. At a -[Meltwater hackathon](https://underthehood.meltwater.com/blog/2019/06/17/benthos-lab-a-case-study-of-hackathon-innovation/) I had a project in mind (details soon to -follow) that would benefit hugely from Benthos running directly in the browser. -I therefore set out to compile it in wasm, this is my short and sweet journey. - - - -## The Build - -The first thing I did and the first thing you ought to do if you are targeting -wasm yourself is skim through [this section of the Go wiki][wasm-go-wiki]. - -In short, I wrote a Go file: - -``` go -package main - -import ( - "syscall/js" - - "github.com/Jeffail/benthos/lib/config" - "gopkg.in/yaml.v3" -) - -func normalise(this js.Value, args []js.Value) interface{} { - var configStr string - if len(args) > 0 { - configStr = args[0].String() - } - - conf := config.New() - - // Ignoring errors for brevity - yaml.Unmarshal([]byte(configStr), &conf) - - sanit, _ := conf.Sanitised() - sanitBytes, _ := yaml.Marshal(sanit) - - return string(sanitBytes) -} - -func main() { - c := make(chan struct{}, 0) - js.Global().Set("benthosNormaliseConfig", js.FuncOf(normalise)) - <-c -} -``` - -And compiled it: - -``` sh -GOOS=js GOARCH=wasm go build -o main.wasm -``` - -I was pretty sure that this would be the end of the road for me. Benthos uses a -vast swathe of dependencies for its various connectors and so I was sure that I -would be immobilised with errors. However, to my surprise there were only three -(formatted for brevity): - -``` text -lib/util/disk/check.go:29:11: undefined: syscall.Statfs_t -github.com/edsrzf/mmap-go@v1.0.0/mmap.go:77:9: undefined: mmap -github.com/lib/pq@v1.0.0/conn.go:321:13: undefined: userCurrent -``` - -Which involved some calls for a buffer implementation using a memory-mapped file -library and the PostgreSQL driver for the SQL package. The errors themselves are -basically "this thing doesn't exist in Web Assembly", which usually means the -library has a feature behind build constraints but doesn't support wasm yet. - -The solution for these problems in my case was as simple as to not to do the -call, and perhaps document that the feature doesn't work with a wasm build. - -Obviously, we only want to disable these calls specifically when targeting wasm. -In Go that's easy, stick a cheeky -[build constraint on there][go-build-constraint]. Here's the actual commit: -[9903b3d5d8519fcf7ecbce94c336e7f054a75942][wasm-commit], note that you can't -just constrain the feature, you also need to add an empty stub that has the -opposite constraint in order to satisfy your build. - -## Executing Go From JavaScript - -The [Go Wiki][wasm-go-wiki] shows you how to actually execute your wasm build -and I won't repeat it here, but I followed the steps and it was pretty straight -forward. - -There was, however, one issue I came across. Some functions that I was calling -from JavaScript were causing my wasm runtime to panic and stop. The functions -all had channel blocking in common, something like this: - -``` go -func ashHasACoolBlog(this js.Value, args []js.Value) interface{} { - someChan <- args[0].String() - return <-someOtherChanIHateNamingThings -} -``` - -The function would sometimes execute successfully. Other times, specifically for -longer running calls, I would get a deadlock panic: - -``` text -fatal error: all goroutines are asleep - deadlock! wasm_exec.js:47:6 -wasm_exec.js:47:6 -goroutine 1 [chan receive]: wasm_exec.js:47:6 -main.main() wasm_exec.js:47:6 - /home/ash/tmp/wasm/main.go:20 +0x7 -``` - -Which was odd as they would be occasions where I would not expect a real -deadlock. I then found the relevant docs in the [`syscall/js`][syscall-js-func] -package: - -> Blocking operations in the wrapped function will block the event loop. As a -> consequence, if one wrapped function blocks, other wrapped funcs will not be -> processed. A blocking function should therefore explicitly start a new -> goroutine. - -The consequences of blocking sound pretty harmless here, but in reality it -seemed to be the cause of my deadlock crash. I assume the odd error message is a -result of some nuanced mechanics within the wasm runtime. - -I didn't investigate this crash any further as I was a lazy idiot back in those -dark days. I simply stopped writing blocking functions, and instead spawned -goroutines everywhere like they were losers at a Nickelback concert: - -``` go -func iJustWantToClarify(this js.Value, args []js.Value) interface{} { - go func() { - someChan <- args[0].String() - otherThing := <-someOtherChanIHateNamingThings - - js.Global().Get("thatActually").Set( - "textContent", - "I quite enjoy and respect Knickelback as artists... " + otherThing, - ) - }() - return nil -} -``` - -## Other Issues - -There weren't any. - -## Final Words - -It took a day for me to get a working application together and soon I'll be -blogging about the resulting product. Web assembly with Go is yummy. - -Kudos to both the W3C and the Go team for taking their time to build something -to completion without rushing the conclusion. Yes, I'm still bitter about Game -of Thrones. - -[meltwater]: https://underthehood.meltwater.com/blog/2019/06/17/benthos-lab-a-case-study-of-hackathon-innovation/ -[Benthos]: https://www.benthos.dev/ -[wasm-go-wiki]: https://github.com/golang/go/wiki/WebAssembly -[syscall-js-func]: https://godoc.org/syscall/js#Func -[go-build-constraint]: https://golang.org/pkg/go/build/#hdr-Build_Constraints -[wasm-commit]: https://github.com/Jeffail/benthos/commit/9903b3d5d8519fcf7ecbce94c336e7f054a75942#diff-146b6fd87106d7f70f56facf7b1e7d98 \ No newline at end of file diff --git a/website/blog/2019-06-17-introducing-benthos-lab.md b/website/blog/2019-06-17-introducing-benthos-lab.md deleted file mode 100644 index 5de9840b4c..0000000000 --- a/website/blog/2019-06-17-introducing-benthos-lab.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Introducing Benthos Lab -author: "Ashley Jeffs" -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: "Where you can build your very own monstrosities" -keywords: [ - "benthos", - "benthos lab", - "web assembly", - "wasm", - "go", - "golang", - "stream processor", -] -tags: [ "Benthos Lab" ] ---- - -After experimenting with innovative new ways to choke your browser to death I am -pleased to announce Benthos Lab, which lives at -[https://lab.benthos.dev](https://lab.benthos.dev). - - - -Benthos Lab is a website where users of the [Benthos stream processor][benthos] -can write, format, execute and share their pipeline configurations. This was -made possible by compiling the entire service (written in Go) into Web Assembly -so that it can run directly in your browser. - -Here's a video of it in action: [https://youtu.be/1ZN-42A0sJU][lab-video]. - -Some technical details about how this was achieved can be found in -[a previous post of mine][wasm-blog]. The repo can be found at: -[https://github.com/benthosdev/benthos-lab][lab-repo], feel free to clone it, -hack it and host your own version. - -[![benthos-lab](/img/introducing-benthos-lab/banner.svg)][benthos-lab] - -Like every good laboratory it is scrappy and sometimes explodes. You need a -modern browser version and it doesn't currently support mobile. Regardless, I -think it's a good time to invite you try it out. Don't worry, I'll be watching -from a safe distance. - -Also, if it does break on you then please remember that you don't pay me for -this, you vile parasite. - -## Why - -At Meltwater we have many distributed teams using Benthos for a wide and ever -increasing list of use cases, which results in a lot of remote collaboration. - -For the slower moving, asynchronous types of work we are usually fine with ye -olde git. However, sometimes it's nice to quickly hash stuff out, at which point -things would suddenly devolve into a barrage of files getting chaotically thrown -at a slack channel. - -![slamming-slack](/img/introducing-benthos-lab/slamslack.jpg) - -By contrast, the lab is a civilised place where one can quickly and easily -create a pipeline concept, including sample input data, and share it directly -with a group. Opening up a shared lab provides all that same context, allows you -to make your own revisions to it, and even lets you execute it in order to see -the results it produces. - -![genteel-lab](/img/introducing-benthos-lab/genteel.jpg) - -For our teams this app dramatically reduced the time taken to create, prove and -demonstrate a pipeline concept. It's also much quicker for me to help teams that -have issues with their configs as they can make sure I have all the information -needed up front with a single URL. - -### Use it to build your own web app - -There's also plenty of unintended use cases for Benthos Lab as it basically -allows you to build your own custom web applications. Here's a session that -lower cases and normalises a JSON document, computes its sha256 hash and then -hex encodes the hash: -[https://lab.benthos.dev/l/N-3sss3WPjj#input](https://lab.benthos.dev/l/N-3sss3WPjj#input). - -Pro tip: if you add the anchor `#input` to the end of the session URL then it -opens up at the input tab for quickly inserting stuff. - -Since there's such a vast catalogue of Benthos -[processors available][benthos-procs] I've already found myself bookmarking a -few lab sessions as general utilities. - -## Next Steps - -With this running within the sandbox of your browser there's lots of missing -functionality. For example, you can't create TCP/UDP connections (as of right -now) and you can't access a file system. - -However, I don't plan to address this any time soon as the intention of the app -is to test snippets of config, not to execute your whole damn streaming -pipeline. You ought to learn to manage your expectations. - -What I will do though is continue to improve the UI. If a feature you want is -missing (or broken, obviously) then please [open an issue][lab-issues]. - -[benthos-lab]: https://lab.benthos.dev -[lab-video]: https://youtu.be/1ZN-42A0sJU -[wasm-blog]: /blog/2019/05/27/compiling-benthos-to-wasm/ -[lab-repo]: https://github.com/benthosdev/benthos-lab -[lab-issues]: https://github.com/benthosdev/benthos-lab/issues -[benthos]: https://www.benthos.dev -[under-the-hood]: https://underthehood.meltwater.com/ -[benthos-procs]: https://benthos.dev/docs/components/processors/about \ No newline at end of file diff --git a/website/blog/2019-08-20-write-a-benthos-plugin.md b/website/blog/2019-08-20-write-a-benthos-plugin.md deleted file mode 100644 index 3a8374507d..0000000000 --- a/website/blog/2019-08-20-write-a-benthos-plugin.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: Write a Benthos Plugin -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: I made it difficult for our job security -keywords: [ - "benthos", - "plugin", - "go", - "golang", - "stream processor", -] -tags: [ "Plugins" ] ---- - -I'm going to walk you through writing a Benthos plugin from scratch in Go. - - - -Too lazy to read? You can find a video equivalent of this post at: [https://youtu.be/Ilah_Y0uMk4](https://youtu.be/Ilah_Y0uMk4). If you prefer to dig straight into code then you should check out the [benthos-plugin-example][plugin-repo] repo. - -![benthos-plugged](/img/write-a-benthos-plugin/benthos-plugged.png) - -Plugins allow you to embed your code within Benthos as a component. [Processors][benthos-proc] are the most common type of component to get plugged, which is what we're going to do in this post. If you want to run non-Go code from Benthos then you still have options, such as the [`subprocess`][subprocess-proc], [`http`][http-proc] or [`lambda`][lambda-proc] processors. - -## Roleplay - -Imagine you are a competent engineer. You wrote a function to detect sarcasm in internet posts on a linear scale of 0 to 100: - -```go -// HowSarcastic TOTALLY detects sarcasm EVERY time. -func HowSarcastic(content []byte) float64 { - if bytes.Contains(content, []byte("/s")) { - return 100 - } - return 0 -} -``` - -You are confident that `HowSarcastic` is 100% accurate and wish to apply it to a continuous stream of data by deploying it within a stream processing solution. - -You want this service to be resilient with at-least-once delivery guarantees, scalable both horizontally and vertically, and able to expose various metrics about the health of the data stream. - -You have decided to use Benthos for this service because you love the logo. - -![charming-benthos-logo](/img/write-a-benthos-plugin/blobfish.jpg) - -### Stuff you don't need to care about yet - -Since you are using Benthos you don't need to choose a queue system, metrics aggregator or deployment platform yet, those items can be configured. - -You don't even need to know what format the data comes in or how it needs to look when it leaves your service, as Benthos [has plenty of processors][processors] for configuring that stuff on the fly. - -## Getting Started - -You're going to use Go modules for this one, make a directory and create a `go.mod` file: - -```sh -mkdir foo && cd foo -go mod init github.com/bar/foo -``` - -Next, you need to pull in your only dependency, [Benthos][benthos-repo]: - -```sh -go get github.com/Jeffail/benthos/v3 -# Look! Now you have more dependencies than friends! -``` - -That'll automatically add the dep to your `go.mod` file at the latest v3 tag. Next, you're going to write your stream processor service. Write this to the file `main.go`: - -```go -package main - -import ( - "github.com/Jeffail/benthos/v3/lib/service" -) - -func main() { - service.Run() -} -``` - -That's it, you've got a full Benthos. If you want to verify then you can run it: - -```sh -go run ./main.go --help -``` - -## Write Your Plugin - -Now you will write the actual plugin that executes your function. Processor plugins implement [`types.Processor`][types-processor] and have the signature: - -```go -func ProcessMessage(msg types.Message) ([]types.Message, types.Response) -``` - -A message can have multiple parts (synonymous with a batch) and we are allowed to return either one or more messages or a response which is either an ack or noack. - -A message part has both content and any number of metadata key/value pairs. It is therefore up to you as to whether you modify the contents of messages or whether the sarcasm level is added as metadata. - -Thankfully you don't need to make that decision now. Instead, you're going to expose it as a config field and support both. The config field will be called `metadata_key`, and if left empty the contents of messages will be replaced entirely with the sarcasm level. - -There won't be much code needed so for brevity you are going to write this straight into your `main.go` file: - -```go -// SarcasmProc applies our sarcasm detector to messages. -type SarcasmProc struct { - MetadataKey string `json:"metadata_key" yaml:"metadata_key"` -} - -// ProcessMessage returns messages mutated with their sarcasm level. -func (s *SarcasmProc) ProcessMessage(msg types.Message) ([]types.Message, types.Response) { - newMsg := msg.Copy() - - newMsg.Iter(func(i int, p types.Part) error { - sarcasm := HowSarcastic(p.Get()) - sarcasmStr := strconv.FormatFloat(sarcasm, 'f', -1, 64) - - if len(s.MetadataKey) > 0 { - p.Metadata().Set(s.MetadataKey, sarcasmStr) - } else { - p.Set([]byte(sarcasmStr)) - } - return nil - }) - - return []types.Message{newMsg}, nil -} - -// CloseAsync does nothing. -func (s *SarcasmProc) CloseAsync() {} - -// WaitForClose does nothing. -func (s *SarcasmProc) WaitForClose(timeout time.Duration) error { - return nil -} -``` - -Let's break this down. You have a struct called `SarcasmProc`, which contains a configuration field `MetadataKey`. The functions `CloseAsync` and `WaitForClose` can be ignored as your processor doesn't contain any state that requires termination. - -Within your function `ProcessMessage` you iterate all the payloads within the message batch and calculate the sarcasm level with your function `HowSarcastic`. The result is converted into a string and, depending on whether a metadata key has been set, replaces the contents with the result or sets a new metadata value on the payload. - -That's your processor completed. Now you need to register the plugin before calling `service.Run`. Since this is a processor plugin you're going to call [`processor.RegisterPlugin`][proc-register-plugin]: - -```go -func main() { - processor.RegisterPlugin( - "how_sarcastic", - func() interface{} { - s := SarcasmProc{} - return &s - }, - func( - iconf interface{}, - mgr types.Manager, - logger log.Modular, - stats metrics.Type, - ) (types.Processor, error) { - return iconf.(*SarcasmProc), nil - }, - ) - - service.Run() -} -``` - -The first argument is a string that identifies the type of this plugin, that's the string used to specify it within a Benthos config file. - -The second argument is a function that creates our config structure, this will be embedded within the Benthos config specification. In this case our processor implementation is the same type as the configuration struct, but you can separate them if you prefer. - -The third argument is the generic function that constructs our processor. In this case we've already constructed it as our configuration type and so we can simply cast it and return it. - -Now you're going to build your custom Benthos with: - -```sh -go build -o benthos -``` - -## Run Your Plugin - -In order to execute your plugin with Benthos you need a config. Write the following to a file `config.yaml`: - -```yaml -pipeline: - processors: - - type: how_sarcastic -``` - -And run it: - -```sh -./benthos -c ./config.yaml -``` - -Your config hasn't specified an input or output so they will default to `stdin` and `stdout`. Write the line `'this is not sarcastic'`, followed by the line `'this is sarcastic /s'`. Benthos should print `0` and `100` respectively. - -Cool, but this config is pretty useless, good job idiot. Now you're going to fix your mistake. Let's imagine you are processing a stream of JSON documents of the form `{"id":"fooid","content":"this is the content"}` and you want to add a field `sarcasm` containing the sarcasm level of `content`. You can do that purely through config by using the [`json`][json-proc] and [`process_field`][process-field-proc] processors: - -```yaml -pipeline: - processors: - - json: - operator: copy - path: content - value: sarcasm - - process_field: - path: sarcasm - result_type: float - processors: - - type: how_sarcastic -``` - -Run that config with some JSON documents: - -```sh -echo '{"id":"foo1","content":"this is totally sarcastic /s"} - {"id":"foo2","content":"but this isnt sarcastic at all"}' | - ./benthos -c ./config.yaml -``` - -You'll see some log events but also you should see your two modified documents: - -```text -{"content":"this is totally sarcastic /s","id":"foo1","sarcasm":100} -{"content":"but this isnt sarcastic at all","id":"foo2","sarcasm":0} -``` - -That's much more useful, but this is just barely scratching the surface of what Benthos can do. For example, here's a config that calculates sarcasm with your processor and removes anything with a sarcasm level at or above 80: - -```yaml -pipeline: - processors: - - type: how_sarcastic - plugin: - metadata_key: sarcasm - - filter_parts: - metadata: - operator: less_than - key: sarcasm - arg: 80 -``` - -Note that it makes use of your `metadata_key` field in order to filter the documents without changing their content. - -Try experimenting with other Benthos processors, you can find the documentation at [benthos.dev/docs/components/processors/about][processors]. - -## Next Steps - -After playing around with Benthos processors you should check out the various [inputs][inputs], [outputs][outputs], [metrics aggregators][metrics] and [tracers][tracers] that it's able to hook up with. - -For example, here's a modified version of the previous config where we write from Kafka to an S3 bucket, sending our metrics to Prometheus: - -```yaml -http: - address: 0.0.0.0:4195 - -input: - kafka: - addresses: - - localhost:9092 - consumer_group: foo_consumer_group - topics: - - foo_stream - -pipeline: - processors: - - type: how_sarcastic - plugin: - metadata_key: sarcasm - - filter_parts: - metadata: - operator: less_than - key: sarcasm - arg: 80 - -output: - s3: - bucket: foo_bucket - content_type: application/json - path: ${!metadata:kafka_key}-${!timestamp_unix_nano}-${!count:files}.json - -metrics: - # Endpoint hosted at both :4195/stats and :4195/metrics - type: prometheus -``` - -I'm sure you'll make great use of Benthos plugins with your extremely important work /s. - -[benthos]: https://www.benthos.dev -[benthos-repo]: https://github.com/Jeffail/benthos -[plugin-repo]: https://github.com/benthosdev/benthos-plugin-example -[processors]: https://benthos.dev/docs/components/processors/about -[inputs]: https://benthos.dev/docs/components/inputs/about -[outputs]: https://benthos.dev/docs/components/outputs/about -[metrics]: https://benthos.dev/docs/components/metrics/about -[tracers]: https://benthos.dev/docs/components/tracers/about -[benthos-proc]: https://benthos.dev/docs/components/processors/about -[json-proc]: https://benthos.dev/docs/components/processors/json -[subprocess-proc]: https://benthos.dev/docs/components/processors/subprocess -[http-proc]: https://benthos.dev/docs/components/processors/http -[lambda-proc]: https://benthos.dev/docs/components/processors/lambda -[process-field-proc]: https://benthos.dev/docs/components/processors/process_field -[types-processor]: https://godoc.org/github.com/Jeffail/benthos/lib/types#Processor -[proc-register-plugin]: https://godoc.org/github.com/Jeffail/benthos/lib/processor#RegisterPlugin diff --git a/website/blog/2020-04-18-sneak-peek-at-bloblang.md b/website/blog/2020-04-18-sneak-peek-at-bloblang.md deleted file mode 100644 index 66d5dba30c..0000000000 --- a/website/blog/2020-04-18-sneak-peek-at-bloblang.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: Sneak Peek at Bloblang -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: An experiment in mapping languages -keywords: [ - "benthos", - "bloblang", - "go", - "golang", - "stream processor", - "mapping", -] -tags: [ "Bloblang" ] ---- - -For the last few weekends I've been dipping my toes in a mapping language design that I'm calling Bloblang. Bloblang is specifically designed for data queries and (eventually) structural data mappings. In Benthos version 3.12, which I'm planning to release today, you can play around with a limited feature set of Bloblang by using it in [function interpolations](/docs/configuration/interpolation). - - - -## Why - -My life has no meaning. Also, mapping is one of the most common boring tasks in stream and event processing. Given Benthos is meant to specialise in the boring and mundane it makes sense to treat mapping as a first class citizen. - -Up until now the story for mapping documents in Benthos has been to use [JMESPath][processor.jmespath], [AWK][processor.awk] or a string of the general purpose JSON processors. Time and time again it has been made apparent that it ain't good enough for many use cases. - -I should mention at this point that there's also the option of [IDML][idml], and although Benthos hasn't supported it internally there is a solution to [running it in your pipeline][processor.subprocess]. - -For the last few years I've been helping users adopt these options and each time they fall short I've taken note of where the gaps are. This is an important part of the "research" phase for a language, but I also don't want to dwell on it. Here's an insultingly terse summary of what we currently have within Benthos. - -### JMESPath - -The spiritual cousin of [jq][jq], [JMESPath][jmespath] is a great spec for mapping JSON documents, especially so when your intention is to outright replace the original document. - -However, when our goal is to preserve the majority of the existing document, and we only wish to express isolated mutations within the structure, it becomes ugly and risky. For example, changing just `foo.bar.baz` to `this value` looks like this: - -``` -merge(@, { - "foo": merge(foo, { - "bar": merge(bar, { - "baz": "this value" - }) - }) -}) -``` - -Hopefully you don't add a typo there or miss on a `merge`, otherwise you're scrapping a large chunk of your original document! - -Expressing your entire map in one single object also scales pretty poorly as the mapping grows in complexity. - -A final and Benthos specific issue is that JMESPath only supports mapping the content of Benthos messages, without the ability to modify or reference the metadata of a message or other messages of a batch, which would be great for [windowed processing][windowed-processing]. - -### AWK - -Benthos has an [AWK processor][processor.awk], and since this is a proper programming language it has uses far beyond mapping. However, this also makes it riskier to use for large and complex maps. More opportunities to write bugs, more opportunities to break your program, more opportunities to regress. - -A simpler language specifically designed for mappings is a much more scalable solution as it reduces the opportunities for mistakes as both maps and teams grow. Although, risk aside, the major problem with using AWK within Benthos is the performance hit. - -### JSON Processor - -The JSON processor is pretty flexible and would be the highest performer of all options here. However, beyond one or two mutations a mapping becomes an absolute mess of YAML, and if we need to add conditional maps into the mix it becomes much worse. - -It has been clear to me for a while that this processor is so quickly and easily outgrown by a typical user config that it perhaps ought to be entirely replaced with a real mapping solution. - -### IDML - -If I could run [IDML][idml] natively from Benthos then Bloblang wouldn't be happening. In my opinion [IDML][idml] is a criminally underused technology and absolutely nails the issue of mapping data at scale. - -Similar to JMESPath the language itself doesn't have a concept of metadata, or querying across multiple documents (a batch). The issue I had here was that if I were going to go through the trouble of implementing IDML in Go I might as well add metadata and cross-batch querying, making it a different language anyway. - -However, I'm definitely writing Bloblang with IDML in mind, and if I manage to reach feature parity with IDML then I intend to break it out into its own lib and offer it to the org, with my Bloblang extensions as Benthos specific plugins. - -## Features - -So with that in mind what does Bloblang look like? Right now we only have queries, which is the "right hand side" of a mapping. These queries support literals: - -``` -"string literal" -true -93435.45 -``` - -And arithmetic: - -``` -50 + 34 -("this" == "that") || ("that" == "that") -``` - -And functions: - -``` -json("foo.bar.baz") -meta("kafka_key") -timestamp_unix() -``` - -And methods, which are attached to a function or value: - -``` -json("foo.bar.baz").from_all().sum() -``` - -And path literals with coalescing: - -``` -json().foo.(bar | something_else).baz -``` - -## Next Steps - -In terms of core syntaxes Bloblang is basically complete. It's implemented using parser combinators, and is very easy for me to extend with new functions and methods. Soon I'll expand Bloblang to support left hand query targets, which is when it really becomes a mapping language. It'll look something like this: - -```yaml -pipeline: - processors: - - bloblang: - mapping: | - json.foo.bar = json().(something + another.thing) - json.and_this = meta("kafka_key").base64() -``` - -And I'll also add a `condition` type for expressing logic as a Bloblang query: - -```yaml -pipeline: - processors: - - filter_parts: - bloblang: - query: | - (meta("kafka_topic") == "junk") && - json().foo.(bar | baz.quz).id.contains("blah") -``` - -Until I'm allowed to practice with my professional rock paper scissors team again I'm sure each weekend will deliver something new to the world of Bloblang. - -[function-interpolations]: /docs/configuration/interpolation -[windowed-processing]: /docs/configuration/windowed_processing -[processor.jmespath]: /docs/components/processors/jmespath -[processor.awk]: /docs/components/processors/awk -[idml]: https://idml.io/ -[processor.subprocess]: /docs/components/processors/subprocess -[jq]: https://stedolan.github.io/jq/ -[jmespath]: https://jmespath.org/ diff --git a/website/blog/2020-05-10-bloblang-beta.md b/website/blog/2020-05-10-bloblang-beta.md deleted file mode 100644 index f2ff7daf2d..0000000000 --- a/website/blog/2020-05-10-bloblang-beta.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Bloblang Beta -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: Available in v3.13 -keywords: [ - "benthos", - "bloblang", - "go", - "golang", - "stream processor", - "mapping", -] -tags: [ "Bloblang" ] ---- - -As of this weekend (and [Benthos v3.13](https://github.com/Jeffail/benthos/releases/tag/v3.13.0)) you can now use a [`bloblang` processor](/docs/components/processors/bloblang) and complementary condition. These components are in a beta phase, which means that based on feedback the mapping language might change in minor ways in upcoming minor releases. - - - -## The Motivation - -[In the last post][post.sneak_peek] I outlined my motivations for experimenting with a mapping language. Words are stupid and boring and so to illustrate why a mapping language kicks ass here's a config example using the old processors compared to the new one. Keep in mind that the new version is simpler _and_ performs better. - -Using old processors: - -```yaml -pipeline: - processors: - - metadata: - operator: set - key: bar - value: ${!json_field:foo.bar} - - - json: - operator: delete - path: foo.bar - - - json: - operator: set - path: foo.topic - value: ${!metadata:topic} - - - metadata: - operator: delete - key: topic - - - conditional: - condition: - jmespath: - query: "foo.baz == 'thing'" - processors: - - json: - operator: set - path: foo.thing_id - value: ${!uuid_v4} -``` - -Using Bloblang: - -```yaml -pipeline: - processors: - - bloblang: | - root = this - - foo.topic = meta("topic") - meta topic = deleted() - - meta bar = foo.bar - foo.bar = deleted() - - foo.thing_id = match { - foo.baz == "thing" => uuid_v4() - } -``` - -My ultimate intention is to completely eradicate the need for a `json`, `metadata` and `text` processor, as well as a range of others. However, I'll need as much help as possible to get the language right, so please consider testing and feeding back on [Github][gh.issues], the [Gitter channel][gitter], or event @ me [on Twitter][twitter] for the good of blobkind. - -[processor.bloblang]: /docs/components/processors/bloblang -[post.sneak_peek]: /blog/2020/04/18/sneak-peek-at-bloblang -[gh.issues]: https://github.com/Jeffail/benthos/issues/439/ -[gitter]: https://gitter.im/jeffail-benthos/community -[twitter]: https://twitter.com/Jeffail diff --git a/website/blog/2020-08-30-improved-workflows.md b/website/blog/2020-08-30-improved-workflows.md deleted file mode 100644 index 8f1168d988..0000000000 --- a/website/blog/2020-08-30-improved-workflows.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Powered Up Workflows -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: Available in v3.26.0 -keywords: [ - "benthos", - "workflows", - "go", - "golang", - "stream processor", - "enrichments", -] -tags: [ "Workflows" ] ---- - -For the last few weeks I've been working on improving the workflow story in Benthos. That means reducing the number of processors, simplifying them, and at the same time making them more powerful than before. The new functionality outlined here can be used in the latest release [v3.26.0](https://github.com/Jeffail/benthos/releases/tag/v3.26.0). - - - -## The Motivation - -After similar efforts to [improve the mapping story][post.bloblang-beta] in Benthos it seemed sensible to target workflows. Specifically, I've added a new [`branch` processor][processor.branch] for wrapping child processors in request/result maps, and have reworked the [`workflow` processor][processor.workflow] to use them. - -If you haven't used workflows in Benthos then there's a section in the new [`workflow` processor][processor.workflow.why] page outlining why they're useful. In short, when performing multiple integrations within a pipeline such as hitting HTTP services, lambdas, caches, etc, it's best to perform them in parallel when possible in order to reduce the processing latency of messages, organizing these integrations into a topology with a workflow makes it easier to manage their interdependencies and ensure they're executed in the right order. - -In the old world you could use the `process_dag` processor which has child `process_map` processors, where the mappings were a series of clunky to/from [dot paths][configuration.field_paths], separated into optional and non-optional mappings. There was no way to manually specify the dependency tree, and conditional flows required a separate list of conditions which didn't factor into dependency resolution. - -Having such complex and brittle mapping capabilities meant these processors were difficult to document and more so to understand and use. - -## Leaning into Bloblang - -Thankfully, with [Bloblang][guides.bloblang] now finished it was pretty easy to replace most of the complexity of the workflow mappings for the language itself. - -For example, when mapping the request payload for an integration you can express a bunch of different patterns... - -Empty request body: - -```yaml -request_map: root = "" -``` - -Sub-object (`foo`) as request body, if the sub-object doesn't exist (or is null) the integration is abandoned: - -```yaml -request_map: root = this.foo.not_null() -``` - -Sub-object as request body which can be obtained from one of a number of possible paths: - -```yaml -request_map: root = this.(foo | bar | baz).doc.not_null() -``` - -Conditional integration applies when the `type` is `foo`, with an unmodified message as request body: - -```yaml -request_map: | - root = if this.type != "foo" { - deleted() - } -``` - -Conditional integration applies when the `type` is `foo`, with a sub-object as the request body: - -```yaml -request_map: | - root = if this.type == "foo" { - this.foo.not_null() - } else { - deleted() - } -``` - -Similarly, it's possible to express a bunch of things in the result mapping... - -Discard the result (the original message is unchanged): - -```yaml -result_map: "" -``` - -Place the entire result at a path: - -```yaml -result_map: root.foo = this -``` - -Place the result in a metadata field: - -```yaml -result_map: meta foo = this -``` - -If you want to see what it looks like there is an [enrichment cookbook][cookbook.enrichment] that demonstrates workflows in action, but there are also smaller examples on the [workflow page][processor.workflow.examples] such as the following snippet: - -```yaml -pipeline: - processors: - - workflow: - meta_path: meta.workflow - branches: - foo: - request_map: 'root = ""' - processors: - - http: - url: TODO - result_map: 'root.foo = this' - - bar: - request_map: 'root = this.body' - processors: - - lambda: - function: TODO - result_map: 'root.bar = this' - - baz: - request_map: | - root.fooid = this.foo.id - root.barstuff = this.bar.content - processors: - - cache: - resource: TODO - operator: set - key: ${! json("fooid") } - value: ${! json("barstuff") } -``` - -## Conclusion - -The docs have been updated to use these new goodies. Obviously the old processors are still being maintained but in a mostly dormant state. The workflow and branch processors are currently labelled as `beta`, but their general behavior is stable with the only exceptions being odd edge cases that might arise. - -With the behavior of these processors being dramatically simplified I've also been able to simplify the documentation for them, which also means using more space on the page for example configs. - -If you have feedback then [get the absolute heck in the chat you utter recluse][community]. - -[processor.workflow]: /docs/components/processors/workflow/ -[processor.branch]: /docs/components/processors/branch/ -[processor.workflow.why]: /docs/components/processors/workflow/#why-use-a-workflow -[processor.workflow.examples]: /docs/components/processors/workflow/#examples -[post.bloblang-beta]: /blog/2020/05/10/bloblang-beta/ -[configuration.field_paths]: /docs/configuration/field_paths/ -[cookbook.enrichment]: /cookbooks/enrichments/ -[guides.bloblang]: /docs/guides/bloblang/about/ -[community]: /community/ diff --git a/website/blog/2021-01-04-v4-roadmap.md b/website/blog/2021-01-04-v4-roadmap.md deleted file mode 100644 index e87579b31d..0000000000 --- a/website/blog/2021-01-04-v4-roadmap.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: First Look at the V4 Roadmap -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: Well, it's roadmapish -keywords: [ - "v4", - "roadmap", - "go", - "golang", - "stream processor", - "ETL", -] -tags: [ "v4" ] ---- - -Benthos has been at major version 3 for over a year now, and I consider it to be a pretty cool achievement that given [all the great features added](https://github.com/Jeffail/benthos/blob/master/CHANGELOG.md) we've managed to keep both the Benthos config spec and APIs fully backwards compatible. - -However, eventually it would be nice to cut a new major release and prune all of the dead weight that has accumulated during this time. Since major version releases don't come often I wanted to be sure that we've considered and planned any other potential breaking changes that could be bundled along with it. - - - -Up until now Benthos has never had a roadmap or really any plan beyond just building what we want to use or want to build, this is known in the industry as attention-span-driven development. Alas, if we're going to get mileage out of version 4 then _some_ planning is necessary, and I figured we might as well put together our very first roadmap. - -A few months ago I [asked for feedback][feedback-thread], I already had my own wish list of things to change in the next major release but I wanted to give you all an opportunity to factor in your own use cases. I've attempted to capture all of the feedback and create issues for the stuff that's achievable, then I marked the issues that require breaking changes and added them to my roadmap plans. I think it's currently in a state that works for me and is something deliverable, therefore I think it's now worth sharing and allowing you all to help shape it further. - -Benthos is blessed with a decent and growing number of contributors. However, it's still clear that if I personally were to burn out then the project would pretty much grind to a temporary halt, and therefore my sanity is a higher priority than committing to a rigid plan. Here's a few things to clarify about this roadmap before you get too excited: - -1. This isn't final, it's going to mutate over time in order to flex around "everything else" going on. -2. This isn't everything. The only items included in this roadmap are items that I consider required to have ready for v4. Any features that can definitely be implemented without breaking changes are not included and can be worked on at any time, including right now. -3. There is no timeline or estimate for this work (by design). If you are blocked on any of the items on this roadmap and aren't able to contribute then please still make sure I'm aware and I'll factor that in, but do not expect promises or commitments (unless you're paying for them). - -With that made clear and everyone sufficiently bored let's get into the planned work _as it currently stands_. I've created an issue for every item here where you can read more details beyond my elevator pitch. - -### Improved plugin APIs - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/501) - -This is by far the biggest item of work I want to establish _before_ v4. The plugin APIs are currently heavily tied into the same component interfaces that are used internally. This means that it's not possible for me to modify the signatures of internal components without breaking the plugin APIs. This has historically put us in awkward positions where in order to make a change that's backwards compatible with both our configuration spec and the plugin APIs we have to implement nasty tricks. - -If we're instead able to isolate the plugin APIs with an air gap then it will allow us to iterate on the internal components without impacting the APIs used for plugins. - -The plan is to fully implement an isolated (and nicer) plugin API, give everyone a lot of time to try it out, provide feedback, and migrate, all within good time _before_ v4 so that I don't pull the rug out from under current plugin users. - -### Streams Mode API for Resources - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/566) - -This one's pretty simple, we want to expand the streams mode APIs to allow the mutation of resources. This is blocked behind a breaking change (to the plugin APIs) as it would require sweeping changes to how resources are accessed. - -### Input Scheduling Capabilities - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/580) - -Sometimes it's nice to slow things down, this issue would allow us to configure inputs that are triggered in scheduled bursts rather than realtime streams in order to have them behave similar to batch processors. Implementing this will require a minor review of the input initialization flow, which could potentially lead to breaking changes to the internal API. - -### Configuration Templating - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/590) - -This would allow you to create reusable, parameterized, configuration templates and have them natively supported within Benthos. This issue is pretty great but also a significant amount of work, it could easily result in breaking changes being required and so I'd like to have this at least planned out and understood before v4. - -### Improved Logging - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/589) - -As Benthos has evolved it has gained a few oddities in how logging works. This issue adjusts logging to lean more into structured logging fields and update the configuration defaults to be more sensible. This will mostly impact internal components that create logs, and therefore depends on having the isolated plugin APIs. - -### Improved Metrics - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/510) - -Similar to the logging issue, metrics in Benthos are a bit wonky due to the collision between targets that do and don't support labels/tagging. Since Prometheus and other tag based metrics types seem to be winning out nowadays I think we can flip the defaults to favour tags over long metric names. - -### Configuration File Reloading - -[Click here to access the issue.](https://github.com/Jeffail/benthos/issues/338) - -Pretty much self explanatory. I believe this can be implemented without any breaking changes, but it would be good to have it understood (or finished) before v4 just in case. - -## Tracking these Features - -There's a [project on Github][v4-project] containing all of these issues, but the way that I've configured it is unique as issues aren't necessarily tracked by their progress. Issues in the "Blocked" column are unable to progress without a breaking change and therefore are blocked on v4. Issues in the "Unblocked" column are features that can be worked on, and will either become done if they were able to be completed without breaking changes, or will be put back into "Blocked" once they reach a point where breaking changes are needed. - -Once the "Unblocked" column has been emptied, and all of our v4 issues are either blocked or done, that will indicate that we are ready to commit to a new major version release, at which point a v4 branch will be created and that work can be started. - -I'm hoping that this will make it easier for me to minimize disruption. Ideally, I want the process of implementing Benthos v4 to be a simple case of deleting old deprecated stuff, and then removing flags/feature toggles in order to make new breaking features the default, having already been implemented and tested. There should be no green field work as part of the new v4 branch. - -## What's Next - -Make sure you get your thoughts and opinions added to the issues you're interested in. I'm also going to try and open up mini forums over [our Discord server][community] to get feedback on the plans. If any of these issues are something you'd personally like then please add a thumbs up emoji to it, as that helps me prioritize them. - -If you're interested in getting involved then make sure you've joined one or more of our [glorious community spaces][community]. - -[changelog]: https://github.com/Jeffail/benthos/blob/master/CHANGELOG.md -[community]: /community -[v4-project]: https://github.com/Jeffail/benthos/projects/2 -[feedback-thread]: https://github.com/Jeffail/benthos/issues/503 \ No newline at end of file diff --git a/website/blog/2021-03-09-redpanda.md b/website/blog/2021-03-09-redpanda.md deleted file mode 100644 index e815dcc372..0000000000 --- a/website/blog/2021-03-09-redpanda.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: "Cross Post: We're Bringing Simple Back (to Streaming)" -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -keywords: [ - "redpanda", - "stream processing", - "kafka", -] -tags: [] ---- - -(Cross-posted with [https://vectorized.io/blog/benthos/](https://vectorized.io/blog/benthos/)) - -Combining the power of Redpanda and Benthos for your streaming needs is so simple that this blog post is almost over already. - - - -[Benthos](https://www.benthos.dev/) is an open source stream processor that provides data mapping, filtering, hydration and enrichment capabilities across a wide range of connectors. It is driven by a minimal, declarative configuration spec, and with a transaction based architecture it eliminates the development effort of building resilient stream processing pipelines. - -Likewise, with its simplicity and high performance, Redpanda eliminates the operational effort of data persistence and availability by providing a Kafka-compatible streaming platform without the moving parts. - -With so much taken care of you're well in for a boring, uneventful time when you combine the two. Make sure you've grabbed a copy of both services, full instructions can be found in the [getting started guide for Benthos](https://www.benthos.dev/docs/guides/getting_started) and [the Redpanda docs](https://vectorized.io/docs). In this post we'll be running them with Docker so we'll start by pulling both images: - -``` -docker pull vectorized/redpanda:latest -docker pull jeffail/benthos:latest -``` - -We can then create a new network for the services to connect with: - -``` -docker network create -d bridge redpandanet -``` - -Next, run Redpanda in the background, we'll go with a single node for now: - -``` -docker run -d \ - --network redpandanet \ - --name redpanda \ - -p 9092:9092 \ - vectorized/redpanda redpanda start \ - --reserve-memory 0M \ - --overprovisioned \ - --smp 1 \ - --memory 1G \ - --advertise-kafka-addr redpanda:9092 -``` - -In order to send data to Redpanda with Benthos we'll need to create a config, starting off with a simple Stdin to Kafka pipeline, copy the following config into a file `producer.yaml`: - -```yaml -input: - stdin: {} - -output: - kafka: - addresses: [ redpanda:9092 ] - topic: topic_A -``` - -Pro tip: You can also use Benthos itself to generate a config like this with `docker run --rm jeffail/benthos create stdin//kafka > ./producer.yaml`. - -And now run Benthos by adding the config as a Docker volume, along with a pseudo-TTY for writing our messages: - -``` -docker run --rm -it \ - --network redpandanet \ - -v $(pwd)/producer.yaml:/benthos.yaml \ - jeffail/benthos -``` - -This will open an interactive shell where you can write in some data to send. Benthos will gobble up anything you throw at it, try mixing structured and unstructured messages, ending each message with a newline: - -``` -{"id":"1","data":"a structured message"} -but this here ain't structured at all! -[{"id":"2"},"also structured in a different (but totally valid) way"] -``` - -When you're finished hit CTRL+C and it'll exit. - -Next, let's try reading that data back out from Redpanda, this time let's also add a [processor](https://www.benthos.dev/docs/components/processors/about) in order to mutate our data, copy the following into a file `consumer.yaml`: - -```yaml -input: - kafka: - addresses: [ redpanda:9092 ] - topics: [ topic_A ] - consumer_group: example_group - -pipeline: - processors: - - bloblang: | - root.doc = this | content().string() - root.length = content().length() - root.topic = meta("kafka_topic") - -output: - stdout: {} -``` - -And run it with our new config, and without the pseudo-TTY this time: - -``` -docker run --rm \ - --network redpandanet \ - -v $(pwd)/consumer.yaml:/benthos.yaml \ - jeffail/benthos -``` - -Now you should see it print mutated versions of the messages you sent to Stdout: - -```json -{"doc":{"data":"a structured message","id":"1"},"length":40,"topic":"topic_A"} -{"doc":"but this here ain't structured at all!","length":38,"topic":"topic_A"} -{"doc":[{"id":"2"},"also structured in a different (but totally valid) way"],"length":69,"topic":"topic_A"} -``` - -The [Bloblang processor](https://www.benthos.dev/docs/components/processors/bloblang) in our consumer config has remapped the original message to a new field `doc`, first attempting to extract it as a structured document, but falling back to a stringified version of it when it's unstructured. We've also added a field `length` which contains the length of the original message, and `topic` which contains the Kafka topic the message was consumed from. - -That's it for now, if you're still hungry for more then check out the Benthos website at [https://www.benthos.dev](https://www.benthos.dev/), and you can learn more about the Benthos mapping language Bloblang [in this guide](https://www.benthos.dev/docs/guides/bloblang/about). diff --git a/website/blog/2021-06-02-new-plugins-and-templates.md b/website/blog/2021-06-02-new-plugins-and-templates.md deleted file mode 100644 index def96f1499..0000000000 --- a/website/blog/2021-06-02-new-plugins-and-templates.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: 'Preview: Go Plugins V2 and Config Templates' -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: It's ready, now we need test subjects -keywords: [ - "go", - "golang", - "stream processor", - "ETL", -] -tags: [ "v4", "plugins", "templates", "roadmap" ] ---- - -I need help, attention and affirmation, and therefore it's time for a development update. Around five months ago I posted a [roadmap for Benthos v4](/blog/2021/01/04/v4-roadmap) that included some utterly unattainable goals that only a super human could achieve. - -Now that most of those features are ready to test, namely a new plugins API and config templating, I'm looking for people to try them out and give feedback. Please read on if that sounds like fun to you, or also if it doesn't sound fun but you intend to do it anyway. - - - -## Config Templates - -The new config templates functionality allows you to define parameterised templates for Benthos configuration snippets. These templates can then be imported with a cli flag and used in Benthos configs like native Benthos components. - -This is going to be super useful in situations where you have commonly used configuration patterns with small differences that prevent you from using resources. - -The current state of templates is that they'll be included in the next release as an experimental feature, meaning any aspect of this functionality is subject to change outside of major version releases. This includes the config spec of templates, how they work, and so on. - -Defining a template looks roughly like this: - -```yaml -name: log_message -type: processor -summary: Print a log line that shows the contents of a message. - -fields: - - name: level - description: The level to log at. - type: string - default: INFO - -mapping: | - root.log.level = this.level - root.log.message = "${! content() }" - root.log.fields.metadata = "${! meta() }" - root.log.fields.error = "${! error() }" -``` - -And you're able to import templates with the `-t` flag: - -```sh -benthos -t ./templates/foo.yaml -c ./config.yaml -``` - -And using it in a config looks like any other component: - -```yaml -pipeline: - processors: - - log_message: - level: ERROR -``` - -To find out more about configuration templates, including how to try them out, check out [the new templates page][configuration.templating]. More importantly, you can give feedback on them [in this Github discussion][templates-feedback-thread]. - -## The V2 Go Plugins API - -Benthos has had Go plugins for a while now and they're fairly well received. However, they can sometimes be confusing as they expose Benthos internals that aren't necessary to understand as plugin authors. - -It was also an issue for me as a maintainer that the current plugin APIs hook directly into Benthos packages that have no business being public. This makes it extra difficult to improve the service without introducing breaking changes. - -The new APIs are simpler, more powerful (in the ways that matter), add milk after the water, and most importantly are air-gapped from Benthos internals so that they can evolve independently. Here's a sneaky glance of what a processor plugin looks like: - -```go -type ReverseProcessor struct { - logger *service.Logger -} - -func (r *ReverseProcessor) Process(ctx context.Context, m *service.Message) ([]*service.Message, error) { - bytesContent, err := m.AsBytes() - if err != nil { - return nil, err - } - - newBytes := make([]byte, len(bytesContent)) - for i, b := range bytesContent { - newBytes[len(newBytes)-i-1] = b - } - - if bytes.Equal(newBytes, bytesContent) { - r.logger.Infof("Woah! This is like totally a palindrome: %s", bytesContent) - } - - m.SetBytes(newBytes) - return []*service.Message{m}, nil -} - -func (r *ReverseProcessor) Close(ctx context.Context) error { - return nil -} - -func main() { - err := service.RegisterProcessor( - "reverse", service.NewConfigSpec(), - func(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) { - return &ReverseProcessor{logger: mgr.Logger()}, nil - }) - if err != nil { - panic(err) - } - - service.RunCLI() -} -``` - -You can play around with these APIs right now by pulling the latest commit with: - -```sh -go get -u github.com/Jeffail/benthos/v3@master -``` - -And you can find more examples along with the API docs at [pkg.go.dev][plugins.api]. - -The package will remain in an experimental state under `public/x/service` for a month or so, and once it's "ready" (I'm personally happy with it) then it'll be moved to `public/service` and will be considered stable. - -The goal is to allow everyone to migrate to the new APIs whilst still supporting the old ones, and then when Benthos V4 is tagged the old ones will vanish and we're no longer blocked on them. - -Similar to the templates there is [a Github discussion open for feedback][plugins-feedback-thread]. Be honest, be brutal. - -## Join the Community - -I've been babbling on for months so if this stuff is news to you then you're clearly out of the loop. Worry not, for you can remedy the situation by joining one or more of our [glorious community spaces][community]. - -[community]: /community -[configuration.templating]: /docs/configuration/templating -[plugins.api]: https://pkg.go.dev/github.com/Jeffail/benthos/v3/public/service -[templates-feedback-thread]: https://github.com/Jeffail/benthos/discussions/785 -[plugins-feedback-thread]: https://github.com/Jeffail/benthos/discussions/754 diff --git a/website/blog/2021-10-12-new-plugins-stable.md b/website/blog/2021-10-12-new-plugins-stable.md deleted file mode 100644 index 1664cad910..0000000000 --- a/website/blog/2021-10-12-new-plugins-stable.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: 'Go Plugins V2 are Ready' -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: In case you didn't see the Tweets, Discord posts and Github activity -keywords: [ - "go", - "golang", - "stream processor", - "ETL", -] -tags: [ "v4", "plugins" ] ---- - -The [new plugin APIs](https://pkg.go.dev/github.com/Jeffail/benthos/v3/public/service) are ready to use, are being used, and [here's a video of them in action](https://youtu.be/uH6mKw-Ly0g). - -import ReactPlayer from 'react-player/youtube'; - -
-
- -
-
- -The full API docs can be found at [pkg.go.dev/github.com/Jeffail/benthos/v3/public](https://pkg.go.dev/github.com/Jeffail/benthos/v3/public), and there's an example repository demonstrating a few different component plugin types at [github.com/benthosdev/benthos-plugin-example](https://github.com/benthosdev/benthos-plugin-example). diff --git a/website/blog/2022-03-03-v4-coming.md b/website/blog/2022-03-03-v4-coming.md deleted file mode 100644 index 600e014539..0000000000 --- a/website/blog/2022-03-03-v4-coming.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: V4 Coming Up -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: How to prepare -keywords: [ - "v4", - "roadmap", - "go", - "golang", - "stream processor", - "ETL", -] -tags: [ "v4" ] ---- - -The [v4 roadmap](/blog/2021/01/04/v4-roadmap) was outlined more than a year ago, in that time all the major features planned have been completed and released in backwards compatible ways into v3, leaving only the breaking changes left (the fun stuff). - -We're now also at a point where the breaking changes that we want to include in v4 have been completed on a branch, and a migration guide is ready to use [which describes how to prepare for v4](/docs/guides/migration/v4). Therefore, it seems to me, that we're ready to actually prepare the release. - - - -## Why - -Benthos follows semantic versioning, which means a major version release is not necessarily an indication of significant improvements to the product, but an indication that things have changed and therefore upgrading should be done with care (and likely some effort). As such, all the major improvements that _could_ have been part of this release (config templates, new plugin APIs, etc) are already part of version 3 and being actively used. - -The vast majority of these improvements have been an evolution or consolidation of prior features, which have since become deprecated (and hidden from the documentation). Doing things this way has enabled you to slowly learn about and adopt the nice new goodies whilst also running the old stuff. However, the reality is that the old deprecated features are a maintenance burden. Not only do we need to ensure that a growing number of potentially unused features remain bug free, but these features often frustrate or entirely prevent us from making improvements elsewhere. - -As such, after keeping v3 going for more than two years, it feels like it's about the right time for spring cleaning. The absolute top priority of this release is to remove deprecated components, which for people keeping up with the newest features would in theory mean no breaking changes at all. However, since major releases are a rare event (this could possibly be the last one) it also makes sense to use this as an opportunity to change things about Benthos that we feel aren't intuitive. - -The biggest of these intentionally breaking changes is the way in which metrics work. Benthos exposes a huge amount of metrics but the names can often be confusing and difficult to understand, and some components expose inconsistent or undocumented metrics. For v4 they have been massively simplified and made consistent, leaning on labels/tags for presenting (optionally) granular series, and allowing full customisation for all metric destination types with a generic `mapping` field. - -The full list of changes we've decided to add are outlined in the [the migration guide](/docs/guides/migration/v4), which is an attempt at finding a good balance between keeping to a minimal impact for the majority of users, and tackling the biggest culprits of confusion in Benthos land. It's extremely important to stress that at this stage nothing is absolute, we can pull these changes back if they seem to heavy, and likewise we can push further if there's hunger for more. Ultimately, we need feedback in the coming weeks in order to get this right. - -## Timescales - -None of this is set in stone but it currently looks as though we will have a release candidate available for v4 in the coming weeks. You can expect `edge` tagged docker images to include v4 changes soon, followed by an image tagged `4.0.0-rc1` and so on, at the same time there will be artifacts you can obtain from the github releases page: https://github.com/Jeffail/benthos/releases. - -At this stage we'll be looking for general testing as well as feedback on the migration process. If you find yourself in a situation where a feature you rely upon in v3 is gone and you can't find an alternative in v4 then we want to hear about it as soon as possible. There will be a temporary website up with v4 documentation but the URL will change each time the docs are updated, so we'll post them in several of the [community spaces][community], make sure you're in one if you're interested in testing. - -We'll likely be playing with release candidates for a few weeks, in which time any official releases we put out will be v3 patches for any bug fixes we've added in the meantime. Once we're happy with release candidates the official v4 tag will be pushed, built and the documentation site will be bumped to represent v4. When this happens the domain v3.benthos.dev will be set up to contain the v3 documentation. - -## Preparing - -If you're interested in minimizing the effort to migrate to v4 right now then the first thing worth doing is moving away from any deprecated components in your v3 configs. You can detect them by pulling the latest version of v3 and running the linter with the `--deprecated` flag: - -```sh -benthos lint --deprecated ./configs/*.yaml -``` - -This will report any deprecated components and fields that you still have in your configs. - -Next, if you're using plugins or have a custom build of Benthos then you need to ensure you're no longer using any of the packages within `./lib`, all of those packages are being removed in v4 in favour of the new `./public` packages. The plugin APIs within `./lib` all have a newer (and better) alternative, but if you were using some of the misc packages as helper libraries then copy them from the v3 branch into your codebase. - -The metrics changes are likely to affect almost every deployment, it is recommended that you make the best use of the new labels and rework your dashboards. However, if in your case the dashboards are difficult to change then it is likely easier to use a `mapping` in your new v4 config to reproduce the metrics series your dashboards currently rely upon. In order to prepare for this it's worth making an inventory of each series you use, the labels they have, and preparing the mapping you'll need. The documentation for the new metrics system can be found here: https://github.com/Jeffail/benthos/blob/main/website/docs/components/metrics/about.md - -Finally, it's worth getting familiar with the changes outlined in [the migration guide](/docs/guides/migration/v4). There are breaking changes in some components, mostly default values being changed in order to make them more intuitive, but they should all be understood before upgrading in order to avoid unintended consequences. - -## Please Help - -If you're interested in helping out then we'd love to get you involved and the best way is to read the migration guide, try out the release candidates, and give us feedback in any of the [community spaces][community]. - -The major concern that we're looking for feedback on is whether the breaking changes are manageable for you, if you have identified things that will significantly hinder your migration then it's important we hear about it as soon as possible. This allows us to triage whether it's worth building tooling to help with the transition, or to potentially roll-back those changes if necessary. - -[community]: /community diff --git a/website/blog/2022-11-07-whats-next.md b/website/blog/2022-11-07-whats-next.md deleted file mode 100644 index b02a86db33..0000000000 --- a/website/blog/2022-11-07-whats-next.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: What's Next for Benthos? -author: Ashley Jeffs -author_url: https://github.com/Jeffail -author_image_url: /img/ash.jpg -description: A summary of what's going on and what's coming up -keywords: [ - "v4", - "roadmap", - "go", - "golang", - "stream processor", - "ETL", -] -tags: [ "v4", "studio" ] ---- - -A few months ago it was announced that [v4 was coming](/blog/2022/03/03/v4-coming). Well, that happened... and also [a bunch more releases](https://github.com/benthosdev/benthos/blob/main/CHANGELOG.md) since then. Now that the fundamentals have been tidied up considerably we're adding new features and they're coming in hot and heavy (and a bit sticky). Almost entirely parallel to this effort is the work on the new [Benthos Studio](https://studio.benthos.dev), which is a visual application for creating, modifying and sharing Benthos (and Bloblang) configs. - -Things are certainly moving fast. However, we've clearly been lacking in the blog department. To remedy this here's a summary of all the stuff we would have blogged about if we had more time and bother. - - - -## Community Growth - -Benthos has now surpassed [150 contributors][contributors], and between our [discord and slack communities][community] we're steadily approaching 1,000 gossips, at which point we'll be too mainstream to be considered cool and hip. This site has daily traffic in the thousands and Benthos itself is downloaded around 2,000 times per day. The [Jeffail youtube channel][jeffail-youtube], which features all of our Benthos and stream processing related video content, is also growing steadily in both content and precious subscribers. I'd promise none of this traffic is bots but I actually don't know. - -Another major milestone reached is that I watched witlessly as my wife gave birth to a human child, she's never done that before. In order to support the incoherent squawker (and our new child) I took a month away from the project. If you're a keen follower of the open source work going on in Benthos land then you'll be aware that it has historically been a mostly one-person show (me), but I'm thrilled (perhaps a bit sour) to report that during my absence the project carried on at pretty much the same intense pace. - -These are all signs that our codebase, community support and developer ecosystems are becoming more decentralised. This trend in growth is entirely organic (besides a few stickers sent to conferences) and mostly based on the volunteer effort of professionals that use Benthos in their daily work. We're also blessed with a number of hobbyists getting involved just because they enjoy it, we've tried sapping the fun out of the project but they just won't go away. For now I'm still retaining absolute control over what gets merged but the bus factor is certainly becoming less significant. - -The trends I'm outlining here should be soothing for Benthos fans, we're growing, we're managing the growth just fine, and you can safely bet on the project continuing on that path. Over the years some users (active or prospective) have expressed concern that Benthos does not have an organisation around it with a pot of money, their worry being the longevity of the project is at risk. I've obviously not agreed as I haven't yet sought after such a pot of money (more on that later in this post). Over time it has become easier to shrug those concerns off as we continue to move at a pace that'd easily beat the expectations of any funded operation. - -## Benthos Studio - -I've been quietly working on [Benthos Studio][benthos-studio] pretty much since the early years of Benthos itself. The idea being a visual editing application to complement the development and running of Benthos configs. What I like about having this exist as an entirely separate application is that the main project is still incentivised to make configuration as easy and intuitive as possible, we can't just rely on visual tooling to plug gaps in ergonomics or observability. This allows Studio to focus on lifting that experience up a few notches as an optional extra. - -If you aren't familiar with it then here's a quick video introduction: - -import ReactPlayer from 'react-player/youtube'; - -
-
- -
-
- -Studio is currently offered as an open beta with new stuff coming in constantly. The main directions we're heading in are: - -- More storage flexibility, including allowing you to use Studio to view and configure streams running in your own local deployments -- More execution visibility, better visuals and ability to dig into what's happening within a Benthos configuration -- A smoother configuration experience for beginners, including wizards for building new stream pipelines - -You may also have noticed that Benthos Studio is not open source. This is because I believe Studio is a good bet at putting together a scalable monetisation strategy that complements the goals of the open source work rather than causing friction against it, a sort of golden path for building a business around an existing open source ecosystem. It will take a while to work out which features should be paid for, but there will always be a free tier that provides all the interesting bits. - -## Monetisation - -Uh oh! He said monetisation, bag him! Monetising an open source project is a hefty topic and gets lots of people mad and sweaty, one far end of the spectrum believing it's entirely counter to the open source movement, the other end refusing to take seriously any project that _isn't_ monetised. - -The reality is obviously that neither extreme holds merit. People need to eat and when projects reach a certain size the time required to maintain them goes well beyond the scope of a fun hobby, but conversely there's nothing preventing a non-funded project from outperforming a funded one even with enterpisey things like support and product stability. A common feedback we get is that our stability and support goes well beyond what people are used to even with paid products. So, given that and all the stuff I outlined at the beginning of this post, do we even need funding? - -Well yes, we do. In fact, as the main driving force behind Benthos I've been funded since day one, just not directly. I get returns from my Benthos work in the form of job opportunities that feed back into the project, support contracts, issue bounties and sponsorships dotted throughout. This is a similar story for many open source maintainers, where if you keep yourself from burning out then a project can get very far indeed without needing to commit to a more scalable business model. - -This set up has been working out just fine but it's far from ideal. I spend a lot of energy keeping these gigs going which could be much better spent growing a team around a paid product. This team would then steward and give back to the open source community and we would all win, the only losers being people that hate teams, products or the abstract concept of winning, or me as a person. - -Many may have jumped straight into the deep end and asked for VC funding up front. If/when we choose to go down that route we're locked in for the whole ride, where in abstract terms if that aforementioned golden path of monetisation gives us problems then we're going to be pressured into following a new path that prioritises the venture and not the open source. Well, that's life, every decision has risks and in reality we'd still do well as a community in any outcome, but for now I've been exploring that golden path and seeing how far I can get on my own. - -So that's how we got to here, we have a ship that seems pretty much sea worthy and without any obligations or milestones to deliver. Dare I remain bootstrapped? Perhaps I should join a fleet? Do we finally rocket boost these salty decks and aim for the stars? Is that a dumb metaphor? Well keep checking this blog to find out a few months after all the Discord and Slack users do. - -## Supporting the Project - -The longer this project remains sustainable without obligation the more we can experiment freely and independently. If you want to help stretch that progress further and maybe help keep us on the golden path then [get involved in the project][open-source], get your organisation to [sponsor my work][sponsor-jeffail], or consider some of the [paid support options][paid-support] we're currently offering. - -[benthos-studio]: https://studio.benthos.dev -[contributors]: https://github.com/benthosdev/benthos/graphs/contributors -[community]: /community -[jeffail-youtube]: https://www.youtube.com/c/Jeffail -[sponsor-jeffail]: https://github.com/sponsors/Jeffail -[open-source]: https://github.com/benthosdev/benthos -[paid-support]: /support#paid-services diff --git a/website/build_plugins.sh b/website/build_plugins.sh deleted file mode 100755 index b98c83ef59..0000000000 --- a/website/build_plugins.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -tsc --project ./src/plugins/cookbooks/tsconfig.json diff --git a/website/cookbooks/custom_metrics.md b/website/cookbooks/custom_metrics.md deleted file mode 100644 index 4679411423..0000000000 --- a/website/cookbooks/custom_metrics.md +++ /dev/null @@ -1,384 +0,0 @@ ---- -slug: custom-metrics -title: Custom Metrics -description: Learn how to emit custom metrics from messages. ---- - -You can't build cool graphs without metrics, and [Benthos emits many][internal-metrics]. However, occasionally you might want to also emit custom metrics that track data extracted from messages being processed. In this cookbook we'll explore how to achieve this by configuring Benthos to pull download stats from Github, Dockerhub and Homebrew and emit them as gauges. - -## The Basics - -Firstly, we need to target an API so let's start with the nice and simple Homebrew API, which we'll poll every 60 seconds. - -We can either do it with an [`http_client` input][inputs.http_client] and a [rate limit][rate_limits] that restricts us to one request per 60 seconds, or we can use a [`generate` input][inputs.generate] to generate a message every 60 seconds that triggers an [`http` processor][processors.http]: - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - - - - -```yaml -input: - generate: - interval: 60s - mapping: root = "" - -pipeline: - processors: - - http: - url: https://formulae.brew.sh/api/formula/benthos.json - verb: GET -``` - - - - - -```yaml -input: - http_client: - url: https://formulae.brew.sh/api/formula/benthos.json - verb: GET - rate_limit: brewlimit - -rate_limit_resources: - - label: brewlimit - local: - count: 1 - interval: 60s -``` - - - - - - -For this cookbook we'll continue with the processor option as it makes it easier to deploy it as a [scheduled lambda function][serverless.lambda] later on, which is how I'm currently doing it in real life. - -The homebrew formula API gives us a JSON blob that looks like this (removing fields we're not interested in, and with numbers inflated relative to my ego): - -```json -{ - "name":"benthos", - "desc":"Stream processor for mundane tasks written in Go", - "analytics":{"install":{"30d":{"benthos":78978979},"90d":{"benthos":253339124},"365d":{"benthos":681356871}}} -} -``` - -This format makes it fairly easy to emit the value of `analytics.install.30d.benthos` as a gauge with the [`metric` processor][processors.metric]: - -```yaml -http: - address: 0.0.0.0:4195 - -input: - generate: - interval: 60s - mapping: root = "" - -pipeline: - processors: - - http: - url: https://formulae.brew.sh/api/formula/benthos.json - verb: GET - - - metric: - type: gauge - name: downloads - labels: - source: homebrew - value: ${! json("analytics.install.30d.benthos") } - - - mapping: root = deleted() - -metrics: - mapping: if this != "downloads" { deleted() } - prometheus: {} -``` - -With the above config we have selected the [`prometheus` metrics type][metrics.prometheus], which allows us to use [Prometheus][prometheus] to scrape metrics from Benthos by polling its HTTP API at the url `http://localhost:4195/stats`. - -We have also specified a [`path_mapping`][metrics.prometheus.path_mapping] that deletes any internal metrics usually emitted by Benthos by filtering on our custom metric name. - -Finally, there's also a [`mapping` processor][processors.mapping] added to the end of our pipeline that deletes all messages since we're not interested in sending the raw data anywhere after this point anyway. - -While running this config you can verify that our custom metric is emitted with `curl`: - -```sh -curl -s http://localhost:4195/stats | grep downloads -``` - -Giving something like: - -```text -# HELP benthos_downloads Benthos Gauge metric -# TYPE benthos_downloads gauge -benthos_downloads{source="homebrew"} 78978979 -``` - -Easy! The Dockerhub API is also pretty simple, and adding it to our pipeline is just: - - - - - -```diff - source: homebrew - value: ${! json("analytics.install.30d.benthos") } - -+ - mapping: root = "" -+ -+ - http: -+ url: https://hub.docker.com/v2/repositories/jeffail/benthos/ -+ verb: GET -+ headers: -+ Content-Type: application/json -+ -+ - metric: -+ type: gauge -+ name: downloads -+ labels: -+ source: dockerhub -+ value: ${! json("pull_count") } -+ - - mapping: root = deleted() -``` - - - - -```yaml -http: - address: 0.0.0.0:4195 - -input: - generate: - interval: 60s - mapping: root = "" - -pipeline: - processors: - - http: - url: https://formulae.brew.sh/api/formula/benthos.json - verb: GET - - - metric: - type: gauge - name: downloads - labels: - source: homebrew - value: ${! json("analytics.install.30d.benthos") } - - - mapping: root = "" - - - http: - url: https://hub.docker.com/v2/repositories/jeffail/benthos/ - verb: GET - headers: - Content-Type: application/json - - - metric: - type: gauge - name: downloads - labels: - source: dockerhub - value: ${! json("pull_count") } - - - mapping: root = deleted() - -metrics: - mapping: if this != "downloads" { deleted() } - prometheus: {} -``` - - - - - -## Harder Example - -So that's the basics covered. Next, we're going to target the Github releases API which gives a slightly more complex payload that looks something like this: - -```json -[ - { - "tag_name": "X.XX.X", - "assets":[ - {"name":"benthos-lambda_X.XX.X_linux_amd64.zip","download_count":543534545}, - {"name":"benthos_X.XX.X_darwin_amd64.tar.gz","download_count":43242342}, - {"name":"benthos_X.XX.X_freebsd_amd64.tar.gz","download_count":534565656}, - {"name":"benthos_X.XX.X_linux_amd64.tar.gz","download_count":743282474324} - ] - } -] -``` - -It's an array of objects, one for each tagged release, with a field `assets` which is an array of objects representing each release asset, of which we want to emit a separate download gauge. In order to do this we're going to use a [`mapping` processor][processors.mapping] to remap the payload from Github into an array of objects of the following form: - -```json -[ - {"source":"github","dist":"lambda_linux_amd64","download_count":543534545,"version":"X.XX.X"}, - {"source":"github","dist":"darwin_amd64","download_count":43242342,"version":"X.XX.X"}, - {"source":"github","dist":"freebsd_amd64","download_count":534565656,"version":"X.XX.X"}, - {"source":"github","dist":"linux_amd64","download_count":743282474324,"version":"X.XX.X"} -] -``` - -Then we can use an [`unarchive` processor][processors.unarchive] with the format `json_array` to expand this array into N individual messages, one for each asset. Finally, we will follow up with a [`metric` processor][processors.metric] that dynamically sets labels following the fields `source`, `dist` and `version` so that we have a separate metrics series for each asset type for each tagged version. - -A simple pipeline of these steps would look like this (please forgive the regexp): - -```yaml -http: - address: 0.0.0.0:4195 - -input: - generate: - interval: 60s - mapping: root = "" - -pipeline: - processors: - - http: - url: https://api.github.com/repos/benthosdev/benthos/releases - verb: GET - - - mapping: | - root = this.map_each(release -> release.assets.map_each(asset -> { - "source": "github", - "dist": asset.name.re_replace_all("^benthos-?((lambda_)|_)[0-9\\.]+(-rc[0-9]+)?_([^\\.]+).*", "$2$4"), - "download_count": asset.download_count, - "version": release.tag_name.trim("v"), - }).filter(asset -> asset.dist != "checksums")).flatten() - - - unarchive: - format: json_array - - - metric: - type: gauge - name: downloads - labels: - dist: ${! json("dist") } - source: ${! json("source") } - value: ${! json("download_count") } - - - mapping: root = deleted() - -metrics: - mapping: if this != "downloads" { deleted() } - prometheus: {} -``` - -Finally, let's combine all the custom metrics into one pipeline. - -## Combining into a Workflow - -Okay I'm getting bored now so let's wrap this up. The following config expands on the previous examples by configuring each API poll as a [`branch` processor][processors.branch], which allows us to run them within a [`workflow` processor][processors.workflow] that can execute all three branches in parallel. - -The [`metric` processors][processors.metric] have also been combined into a single reusable resource by updating the other API calls to format their payloads into the same structure as our Github remap. - -```yaml -http: - address: 0.0.0.0:4195 - -input: - generate: - interval: 60s - mapping: root = {} - -pipeline: - processors: - - workflow: - meta_path: results - order: [ [ dockerhub, github, homebrew ] ] - -processor_resources: - - label: dockerhub - branch: - request_map: 'root = ""' - processors: - - try: - - http: - url: https://hub.docker.com/v2/repositories/jeffail/benthos/ - verb: GET - headers: - Content-Type: application/json - - mapping: | - root.source = "docker" - root.dist = "docker" - root.download_count = this.pull_count - root.version = "all" - - resource: metric_gauge - - - label: github - branch: - request_map: 'root = ""' - processors: - - try: - - http: - url: https://api.github.com/repos/benthosdev/benthos/releases - verb: GET - - mapping: | - root = this.map_each(release -> release.assets.map_each(asset -> { - "source": "github", - "dist": asset.name.re_replace_all("^benthos-?((lambda_)|_)[0-9\\.]+(-rc[0-9]+)?_([^\\.]+).*", "$2$4"), - "download_count": asset.download_count, - "version": release.tag_name.trim("v"), - }).filter(asset -> asset.dist != "checksums")).flatten() - - unarchive: - format: json_array - - resource: metric_gauge - - mapping: 'root = if batch_index() != 0 { deleted() }' - - - label: homebrew - branch: - request_map: 'root = ""' - processors: - - try: - - http: - url: https://formulae.brew.sh/api/formula/benthos.json - verb: GET - - mapping: | - root.source = "homebrew" - root.dist = "homebrew" - root.download_count = this.analytics.install.30d.benthos - root.version = "all" - - resource: metric_gauge - - - label: metric_gauge - metric: - type: gauge - name: downloads - labels: - dist: ${! json("dist") } - source: ${! json("source") } - version: ${! json("version") } - value: ${! json("download_count") } - -metrics: - mapping: if this != "downloads" { deleted() } - prometheus: {} -``` - -[serverless.lambda]: /docs/guides/serverless/lambda -[internal-metrics]: /docs/components/metrics/about -[inputs.http_client]: /docs/components/inputs/http_client -[inputs.generate]: /docs/components/inputs/generate -[processors.workflow]: /docs/components/processors/workflow -[processors.branch]: /docs/components/processors/branch -[processors.unarchive]: /docs/components/processors/unarchive -[processors.mapping]: /docs/components/processors/mapping -[processors.http]: /docs/components/processors/http -[processors.metric]: /docs/components/processors/metric -[rate_limits]: /docs/components/rate_limits/about -[metrics.prometheus]: /docs/components/metrics/prometheus -[metrics.about.mapping]: /docs/components/metrics/about#metric-mapping -[prometheus]: https://prometheus.io/ diff --git a/website/cookbooks/discord_bot.md b/website/cookbooks/discord_bot.md deleted file mode 100644 index 223a461428..0000000000 --- a/website/cookbooks/discord_bot.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -slug: discord-bot -title: Create a Discord Bot -description: Learn how to use Benthos to create a vanity chat bot. ---- - -Stream processing is stupid and boring, and so it's important to re-purpose tools like Benthos for fun things occasionally. This cookbook outlines how Benthos can be used to create a Discord bot for important tasks such as providing insults and bad jokes to your chat. If you're a member of the [Benthos Discord server][discord-link] then you're likely already familiar with Blob Bot which is the resulting product. - -import ReactPlayer from 'react-player/youtube'; - -
-
- -
-
- -## Consuming Messages - -Before you start messing with Benthos you need to register a new bot with the [Discord Developer Portal][discord-applications]. Start by building an Application, then use the build-a-bot page to choose a bot name and avatar. You should end up with a token generated for the bot, and you'll also need to add it to your server. - -As soon as your bot is added to your server and you have a token you can immediately begin consuming messages from a channel with the [`discord` input][inputs.discord]: - -```yaml -input: - discord: - channel_id: ${DISCORD_CHANNEL} - bot_token: ${DISCORD_BOT_TOKEN} - cache: request_tracking - -cache_resources: - - label: request_tracking - file: - directory: /tmp/discord_bot -``` - -> If you aren't sure how to access the ID of a channel try [this tutorial][discord-channel-id]. - -The `poll_period` shouldn't be too short as it'll exhaust your rate limits. If you plan to use the bot for hitting multiple Discord APIs then give it a fair few seconds between each poll. It's also necessary to point the input to a [cache resource][caches], and this will be used to store the ID of the latest message received for paginating the messages endpoint. - -The `limit` is the maximum number of messages to consume from the channel when we haven't got a message to track and are consuming a backlog. The first time we run our bot we will pull a maximum of 10 of the latest messages in the channel, the maximum you can set here is 100. - -If you were to run this config (setting the channel and bot token as env vars in a file called `testing.env`) you'll see it print messages from the channel to stdout in JSON form: - -```sh -$ benthos -e testing.env -c ./config.yaml -{"content":"so i like totally just tripped over my own network cables","author":{"id":"1234"}} -{"content":"like omg that is SO you!!!","author":{"id":"4321"}} -{"content":"yas totally","author":{"id":"1234"}} -{"content":"yeah","author":{"id":"4321"}} -``` - -It might be tempting to leave your silent surveillance bot running indefinitely but that's creepy and weird, so instead let's add the ability to respond to messages. - -## Writing Messages - -Writing messages to a Discord channel is pretty easy. You can feed the [`discord` output][outputs.discord] either a JSON object following the [Message Object structure][discord-message-object], or just a raw string and the structure will be created for you. Therefore we can write a hypothetical uppercasing echo bot with a simple [Bloblang mapping][bloblang]: - -```yaml -pipeline: - processors: - - mapping: | - root = if !this.content.has_prefix("SHOUTS BACK") { - "SHOUTS BACK BOT SAYS " + this.content.uppercase() - } else { - deleted() - } - -output: - discord: - channel_id: ${DISCORD_CHANNEL} - bot_token: ${DISCORD_BOT_TOKEN} -``` - -If we add that to the end of the first config you should see the bot respond to messages in the channel by posting an uppercase version of it with a prefix. Note that we also delete the message in our mapping if it has the same prefix that we're adding ourselves, which is a quick and dirty way of ensuring the bot doesn't echo its own messages. - -## Custom Commands - -Shout bot is clearly an absolute riot and a true fan favourite. However, it will get old fast. Let's make our bot more elegant by introducing some commands by swapping our plain mapping with a [`switch` processor][processors.switch]: - -```yaml -pipeline: - processors: - - switch: - - check: this.type == 7 - processors: - - mapping: 'root = "Welcome to the server <@%v>!".format(this.author.id)' - - - processors: - - mapping: 'root = deleted()' -``` - -By changing our mapping out to this switch we can add specialised commands for different message types, and if none of the cases match then we don't respond. Technically, we can do all of this within a single Bloblang mapping by using a match expression, but having a switch processor would also allow us to add cases where we do cool things like hit other APIs, etc. - -The only case we've added here is one that activates when the message type is a specific one sent when a new person joins, and in response we give them a warm welcome. The welcome mentions the new user by injecting the user id into the welcome string with `.format(this.author.id)`, which replaces the `%v` placeholder with the author ID (the user that joined and therefore created the join message). - -This response is cool but not very interactive, let's add a few commands that people can play with: - -```yaml -pipeline: - processors: - - switch: - - check: this.type == 7 - processors: - - mapping: 'root = "Welcome to the server <@%v>!".format(this.author.id)' - - - check: this.content == "/joke" - processors: - - mapping: | - let jokes = [ - "What do you call a belt made of watches? A waist of time.", - "What does a clock do when it’s hungry? It goes back four seconds.", - "A company is making glass coffins. Whether they’re successful remains to be seen.", - ] - root = $jokes.index(timestamp_unix_nano() % $jokes.length()) - - - check: this.content == "/roast" - processors: - - mapping: | - let roasts = [ - "If <@%v>'s brain was dynamite, there wouldn’t be enough to blow their hat off.", - "Someday you’ll go far <@%v>, and I really hope you stay there.", - "I’d give you a nasty look, but you’ve already got one <@%v>.", - ] - root = $roasts.index(timestamp_unix_nano() % $roasts.length()).format(this.author.id) - - - processors: - - mapping: 'root = deleted()' -``` - -Here we have two new commands. If someone posts a message "/joke" then we respond by selecting one of several exceptionally funny jokes from a static list in the mapping. - -The second new command is "/roast" and is exclusively for brave souls as the responses can be cruel and personal. The command works similarly to "/joke" with the exception being the ID of the user that made the command will be injected into the roast, as mentioning the target of the roast makes it significantly more heartbreaking (as intended). - -## Hitting Other APIs - -Clicking websites and browsing the internet is very difficult and most people are simply too busy for it, it'd therefore be useful if we could have our bot do some browsing for us occasionally. - -The final command we're going to add to our bot is "/release", where it will hit the Github API and find out for us what the latest Benthos release is: - -```yaml -pipeline: - processors: - - switch: - # Other cases omitted for brevity - - check: this.content == "/release" - processors: - - mapping: 'root = ""' - - try: - - http: - url: https://api.github.com/repos/benthosdev/benthos/releases/latest - verb: GET - - mapping: 'root = "The latest release of Benthos is %v: %v".format(this.tag_name, this.html_url)' - - - catch: - - log: - fields_mapping: 'root.error = error()' - message: "Failed to process message" - - mapping: 'root = "Sorry, my circuits are all bent from twerking and I must have malfunctioned."' -``` - -Here we've added a switch case that clears the contents of the message, hits the Github API to obtain the latest Benthos release as a JSON object, and finally maps the tag name and the URL of the release to a useful message. - -> We're hitting the Github API with the [generic `http` processor][processors.http], which can be configured to work with most HTTP based APIs. In fact, the Discord input and output are actually [configuration templates][templates] that use the generic HTTP components [under the hood][templates.discord]. - -Since this command is networked and therefore has a chance of failure we've added some [error handling][error-handling] mechanisms after the switch processor so that it'd capture errors from this new case and any new cases we add later. - -Within the catch block we simply log the error for the admin to peruse and change the response message out for a generic "whoopsie daisy" apology. - -## Final Words - -The full config for Blob Bot (with some super secret responses redacted) can be found [in the Github repo][full-config]. To find out more about Bloblang check out [the guide page][bloblang]. To find out more about config templates check out the [templates documentation page][templates]. - -If you want to play with Blob Bot then [join our Discord][discord-link]. There are also some humans in there that will help you manage your disappointment when you see Blob Bot in action. - -[discord-link]: https://discord.gg/6VaWjzP -[discord-applications]: https://discord.com/developers/applications -[discord-channel-id]: https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID- -[discord-message-object]: https://discord.com/developers/docs/resources/channel#message-object -[inputs.discord]: /docs/components/inputs/discord -[outputs.discord]: /docs/components/outputs/discord -[caches]: /docs/components/caches/about -[processors.switch]: /docs/components/processors/switch -[processors.http]: /docs/components/processors/http -[bloblang]: /docs/guides/bloblang/about -[full-config]: https://github.com/benthosdev/benthos/blob/master/config/examples/discord_bot.yaml -[error-handling]: /docs/configuration/error_handling -[templates]: /docs/configuration/templating -[templates.discord]: https://github.com/benthosdev/benthos/blob/master/template/outputs/discord.yaml diff --git a/website/cookbooks/enrichments.md b/website/cookbooks/enrichments.md deleted file mode 100644 index 72689eb549..0000000000 --- a/website/cookbooks/enrichments.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -slug: enrichments -title: Enrichment Workflows -description: How to configure Benthos to process a workflow of enrichment services. ---- - -This cookbook demonstrates how to enrich a stream of JSON documents with HTTP services. This method also works with [AWS Lambda functions][processor.lambda], [subprocesses][processor.subprocess], etc. - -We will start off by configuring a single enrichment, then we will move onto a workflow of enrichments with a network of dependencies using the [`workflow` processor][processor.workflow]. - -Each enrichment will be performed in parallel across a [pre-batched][batching] stream of documents. Workflow enrichments that do not depend on each other will also be performed in parallel, making this orchestration method very efficient. - -The imaginary problem we are going to solve is applying a set of NLP based enrichments to a feed of articles in order to detect fake news. We will be consuming and writing to Kafka, but the example works with any [input][inputs] and [output][outputs] combination. - -Articles are received over the topic `articles` and look like this: - -```json -{ - "type": "article", - "article": { - "id": "123foo", - "title": "Dogs Stop Barking", - "content": "The world was shocked this morning to find that all dogs have stopped barking." - } -} -``` - -## Meet the Enrichments - -### Claims Detector - -To start us off we will configure a single enrichment, which is an imaginary 'claims detector' service. This is an HTTP service that wraps a trained machine learning model to extract claims that are made within a body of text. - -The service expects a `POST` request with JSON payload of the form: - -```json -{ - "text": "The world was shocked this morning to find that all dogs have stopped barking." -} -``` - -And returns a JSON payload of the form: - -```json -{ - "claims": [ - { - "entity": "world", - "claim": "shocked" - }, - { - "entity": "dogs", - "claim": "NOT barking" - } - ] -} -``` - -Since each request only applies to a single document we will make this enrichment scale by deploying multiple HTTP services and hitting those instances in parallel across our document batches. - -In order to send a mapped request and map the response back into the original document we will use the [`branch` processor][processor.branch], with a child [`http`][processor.http] processor. - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ articles ] - consumer_group: benthos_articles_group - batching: - count: 20 # Tune this to set the size of our document batches. - period: 1s - -pipeline: - processors: - - branch: - request_map: 'root.text = this.article.content' - processors: - - http: - url: http://localhost:4197/claims - verb: POST - result_map: 'root.tmp.claims = this.claims' - -output: - kafka: - addresses: [ TODO ] - topic: comments_hydrated -``` - -With this pipeline our documents will come out looking something like this: - -```json -{ - "type": "article", - "article": { - "id": "123foo", - "title": "Dogs Stop Barking", - "content": "The world was shocked this morning to find that all dogs have stopped barking." - }, - "tmp": { - "claims": [ - { - "entity": "world", - "claim": "shocked" - }, - { - "entity": "dogs", - "claim": "NOT barking" - } - ] - } -} -``` - -### Hyperbole Detector - -Next up is a 'hyperbole detector' that takes a `POST` request containing the article contents and returns a hyperbole score between 0 and 1. This time the format is array-based and therefore supports calculating multiple documents in a single request, making better use of the host machines GPU. - -A request should take the following form: - -```json -[ - { - "text": "The world was shocked this morning to find that all dogs have stopped barking." - } -] -``` - -And the response looks like this: - -```json -[ - { - "hyperbole_rank": 0.73 - } -] -``` - -In order to create a single request from a batch of documents, and subsequently map the result back into our batch, we will use the [`archive`][processor.archive] and [`unarchive`][processor.unarchive] processors in our [`branch`][processor.branch] flow, like this: - -```yaml -pipeline: - processors: - - branch: - request_map: 'root.text = this.article.content' - processors: - - archive: - format: json_array - - http: - url: http://localhost:4198/hyperbole - verb: POST - - unarchive: - format: json_array - result_map: 'root.tmp.hyperbole_rank = this.hyperbole_rank' -``` - -The purpose of the `json_array` format `archive` processor is to take a batch of JSON documents and place them into a single document as an array. Subsequently, we then send one single request for each batch. - -After the request is made we do the opposite with the `unarchive` processor in order to convert it back into a batch of the original size. - -### Fake News Detector - -Finally, we are going to use a 'fake news detector' that takes the article contents as well as the output of the previous two enrichments and calculates a fake news rank between 0 and 1. - -This service behaves similarly to the claims detector service and takes a document of the form: - -```json -{ - "text": "The world was shocked this morning to find that all dogs have stopped barking.", - "hyperbole_rank": 0.73, - "claims": [ - { - "entity": "world", - "claim": "shocked" - }, - { - "entity": "dogs", - "claim": "NOT barking" - } - ] -} -``` - -And returns an object of the form: - -```json -{ - "fake_news_rank": 0.893 -} -``` - -We then wish to map the field `fake_news_rank` from that result into the original document at the path `article.fake_news_score`. Our [`branch`][processor.branch] block for this enrichment would look like this: - -```yaml -pipeline: - processors: - - branch: - request_map: | - root.text = this.article.content - root.claims = this.tmp.claims - root.hyperbole_rank = this.tmp.hyperbole_rank - processors: - - http: - url: http://localhost:4199/fakenews - verb: POST - result_map: 'root.article.fake_news_score = this.fake_news_rank' -``` - -Note that in our `request_map` we are targeting fields that are populated from the previous two enrichments. - -If we were to execute all three enrichments in a sequence we'll end up with a document looking like this: - -```json -{ - "type": "article", - "article": { - "id": "123foo", - "title": "Dogs Stop Barking", - "content": "The world was shocked this morning to find that all dogs have stopped barking.", - "fake_news_rank": 0.76 - }, - "tmp": { - "hyperbole_rank": 0.34, - "claims": [ - { - "entity": "world", - "claim": "shocked" - }, - { - "entity": "dogs", - "claim": "NOT barking" - } - ] - } -} -``` - -Great! However, as a streaming pipeline this set up isn't ideal as our first two enrichments are independent and could potentially be executed in parallel in order to reduce processing latency. - -## Combining into a Workflow - -If we configure our enrichments within a [`workflow` processor][processor.workflow] we can use Benthos to automatically detect our dependency graph, giving us two key benefits: - -1. Enrichments at the same level of a dependency graph (claims and hyperbole) will be executed in parallel. -2. When introducing more enrichments to our pipeline the added complexity of resolving the dependency graph is handled automatically by Benthos. - -Placing our branches within a [`workflow` processor][processor.workflow] makes our final pipeline configuration look like this: - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ articles ] - consumer_group: benthos_articles_group - batching: - count: 20 # Tune this to set the size of our document batches. - period: 1s - -pipeline: - processors: - - workflow: - meta_path: '' # Don't bother storing branch metadata. - branches: - claims: - request_map: 'root.text = this.article.content' - processors: - - http: - url: http://localhost:4197/claims - verb: POST - result_map: 'root.tmp.claims = this.claims' - - hyperbole: - request_map: 'root.text = this.article.content' - processors: - - archive: - format: json_array - - http: - url: http://localhost:4198/hyperbole - verb: POST - - unarchive: - format: json_array - result_map: 'root.tmp.hyperbole_rank = this.hyperbole_rank' - - fake_news: - request_map: | - root.text = this.article.content - root.claims = this.tmp.claims - root.hyperbole_rank = this.tmp.hyperbole_rank - processors: - - http: - url: http://localhost:4199/fakenews - verb: POST - result_map: 'root.article.fake_news_score = this.fake_news_rank' - - - catch: - - log: - fields_mapping: 'root.content = content().string()' - message: "Enrichments failed due to: ${!error()}" - - - mapping: | - root = this - root.tmp = deleted() - -output: - kafka: - addresses: [ TODO ] - topic: comments_hydrated -``` - -Since the contents of `tmp` won't be required downstream we remove it after our enrichments using a [`mapping` processor][processor.mapping]. - -A [`catch`][processor.catch] processor was added at the end of the pipeline which catches documents that failed enrichment. You can replace the log event with a wide range of recovery actions such as sending to a dead-letter/retry queue, dropping the message entirely, etc. You can read more about error handling [in this article][error-handling]. - -[inputs]: /docs/components/inputs/about -[outputs]: /docs/components/outputs/about -[error-handling]: /docs/configuration/error_handling -[batching]: /docs/configuration/batching -[processor.catch]: /docs/components/processors/catch -[processor.archive]: /docs/components/processors/archive -[processor.unarchive]: /docs/components/processors/unarchive -[processor.mapping]: /docs/components/processors/mapping -[processor.subprocess]: /docs/components/processors/subprocess -[processor.lambda]: /docs/components/processors/aws_lambda -[processor.http]: /docs/components/processors/http -[processor.branch]: /docs/components/processors/branch -[processor.workflow]: /docs/components/processors/workflow \ No newline at end of file diff --git a/website/cookbooks/filtering.md b/website/cookbooks/filtering.md deleted file mode 100644 index 387deeab93..0000000000 --- a/website/cookbooks/filtering.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -slug: filtering -title: Filtering and Sampling -description: Configure Benthos to conditionally drop messages. ---- - -Events are like eyebrows, sometimes it's best to just get rid of them. Filtering events in Benthos is both easy and flexible, this cookbook demonstrates a few different types of filtering you can do. All of these examples make use of the [`mapping` processor][processors.mapping] but shouldn't require any prior knowledge. - -## The Basic Filter - -Dropping events with [Bloblang][guides.bloblang] is done by mapping the function `deleted()` to the `root` of the mapped document. To remove all events indiscriminately you can simply do: - -```yaml -pipeline: - processors: - - mapping: root = deleted() -``` - -But that's most likely not what you want. We can instead only delete an event under certain conditions with a [`match`][bloblang.match] or [`if`][bloblang.if] expression: - -```yaml -pipeline: - processors: - - mapping: | - root = if @topic.or("") == "foo" || - this.doc.type == "bar" || - this.doc.urls.contains("https://www.benthos.dev/").catch(false) { - deleted() - } -``` - -The above config removes any events where: - -- The metadata field `topic` is equal to `foo` -- The event field `doc.type` (a string) is equal to `bar` -- The event field `doc.urls` (an array) contains the string `https://www.benthos.dev/` - -Events that do not match any of these conditions will remain unchanged. - -## Sample Events - -Another type of filter we might want is a sampling filter, we can do that with a random number generator: - -```yaml -pipeline: - processors: - - mapping: | - # Drop 50% of documents randomly - root = if random_int() % 2 == 0 { deleted() } -``` - -We can also do this in a deterministic way by hashing events and filtering by that hash value: - -```yaml -pipeline: - processors: - - mapping: | - # Drop ~10% of documents deterministically (same docs filtered each run) - root = if content().hash("xxhash64").slice(-8).number() % 10 == 0 { - deleted() - } -``` - -[processors.mapping]: /docs/components/processors/mapping -[bloblang.match]: /docs/guides/bloblang/about#pattern-matching -[bloblang.if]: /docs/guides/bloblang/about#conditional-mapping -[guides.bloblang]: /docs/guides/bloblang/about diff --git a/website/cookbooks/joining_streams.md b/website/cookbooks/joining_streams.md deleted file mode 100644 index 956c77cd7e..0000000000 --- a/website/cookbooks/joining_streams.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -slug: joining-streams -title: Joining Streams -description: How to hydrate documents by joining multiple streams. ---- - -This cookbook demonstrates how to merge JSON events from parallel streams using content based rules and a [cache][caches] of your choice. - -The imaginary problem we are going to solve is hydrating a feed of article comments with information from their parent articles. We will be consuming and writing to Kafka, but the example works with any [input][inputs] and [output][outputs] combination. - -Articles are received over the topic `articles` and look like this: - -```json -{ - "type": "article", - "article": { - "id": "123foo", - "title": "Good article", - "content": "this is a totally good article" - }, - "user": { - "id": "user1" - } -} -``` - -Comments can either be posted on an article or a parent comment, are received over the topic `comments`, and look like this: - -```json -{ - "type": "comment", - "comment": { - "id": "456bar", - "parent_id": "123foo", - "content": "this article is bad" - }, - "user": { - "id": "user2" - } -} -``` - -Our goal is to end up with a single stream of comments, where information about the root article of the comment is attached to the event. The above comment should exit our pipeline looking like this: - -```json -{ - "type": "comment", - "comment": { - "id": "456bar", - "parent_id": "123foo", - "content": "this article is bad" - }, - "article": { - "title": "Good article", - "content": "this is a totally good article" - }, - "user": { - "id": "user2" - } -} -``` - -In order to achieve this we will need to cache articles as they pass through our pipelines and then retrieve them for each comment passing through. Since the parent of a comment might be another comment we will also need to cache and retrieve comments in the same way. - -## Caching Articles - -Our first pipeline is very simple, we just consume articles, reduce them to only the fields we wish to cache, and then cache them. If we receive the same article multiple times we're going to assume it's okay to overwrite the old article in the cache. - -In this example I'm targeting Redis, but you can choose any of the supported [cache targets][caches]. The TTL of cached articles is set to one week. - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ articles ] - consumer_group: benthos_articles_group - -pipeline: - processors: - # Reduce document into only fields we wish to cache. - - mapping: 'article = article' - - # Store reduced articles into our cache. - - cache: - operator: set - resource: hydration_cache - key: '${!json("article.id")}' - value: '${!content()}' - -# Drop all articles after they are cached. -output: - drop: {} - -cache_resources: - - label: hydration_cache - redis: - url: TODO - default_ttl: 168h -``` - -## Hydrating Comments - -Our second pipeline consumes comments, caches them in case a subsequent comment references them, obtains its parent (article or comment), and attaches the root article to the event before sending it to our output topic `comments_hydrated`. - -In this config we make use of the [`branch`][processor.branch] processor as it allows us to reduce documents into smaller maps for caching and gives us greater control over how results are mapped back into the document. - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ comments ] - consumer_group: benthos_comments_group - -pipeline: - processors: - # Perform both hydration and caching within a for_each block as this ensures - # that a given message of a batch is cached before the next message is - # hydrated, ensuring that when a message of the batch has a parent within - # the same batch hydration can still work. - - for_each: - # Attempt to obtain parent event from cache (if the ID exists). - - branch: - request_map: 'root = this.comment.parent_id | deleted()' - processors: - - cache: - operator: get - resource: hydration_cache - key: '${!content()}' - # And if successful copy it into the field `article`. - result_map: 'root.article = this.article' - - # Reduce comment into only fields we wish to cache. - - branch: - request_map: | - root.comment.id = this.comment.id - root.article = this.article - processors: - # Store reduced comment into our cache. - - cache: - operator: set - resource: hydration_cache - key: '${!json("comment.id")}' - value: '${!content()}' - # No `result_map` since we don't need to map into the original message. - -# Send resulting documents to our hydrated topic. -output: - kafka: - addresses: [ TODO ] - topic: comments_hydrated - -cache_resources: - - label: hydration_cache - redis: - url: TODO - default_ttl: 168h -``` - -This pipeline satisfies our basic needs but errors aren't handled at all, meaning intermittent cache connectivity problems that span beyond our cache retries will result in failed documents entering our `comments_hydrated` topic. This is also the case if a comment arrives in our pipeline before its parent. - -There are [many patterns for error handling][error-handling] to choose from in Benthos. In this example we're going to introduce a delayed retry queue as it enables us to reprocess failed documents after a grace period, which is isolated from our main pipeline. - -## Adding a Retry Queue - -Our retry queue is going to be another topic called `comments_retried`. Since most errors are related to time we will delay retry attempts by storing the current timestamp after a failed request as a metadata field. - -We will use an input [`broker`][input.broker] so that we can consume both the `comments` and `comments_retry` topics in the same pipeline. - -Our config (omitting the caching sections for brevity) now looks like this: - -```yaml -input: - broker: - inputs: - - kafka: - addresses: [ TODO ] - topics: [ comments ] - consumer_group: benthos_comments_group - - - kafka: - addresses: [ TODO ] - topics: [ comments_retry ] - consumer_group: benthos_comments_group - - processors: - - for_each: - # Calculate time until next retry attempt and sleep for that duration. - # This sleep blocks the topic 'comments_retry' but NOT 'comments', - # because both topics are consumed independently and these processors - # only apply to the 'comments_retry' input. - - sleep: - duration: '${! 3600 - ( timestamp_unix() - meta("last_attempted").number() ) }s' - -pipeline: - processors: - - try: - - for_each: - # Attempt to obtain parent event from cache. - - branch: - {} # Omitted - - # Reduce document into only fields we wish to cache. - - branch: - {} # Omitted - - # If we've reached this point then both processors succeeded. - - mapping: 'meta output_topic = "comments_hydrated"' - - - catch: - # If we reach here then a processing stage failed. - - mapping: | - meta output_topic = "comments_retry" - meta last_attempted = timestamp_unix() - -# Send resulting documents either to our hydrated topic or the retry topic. -output: - kafka: - addresses: [ TODO ] - topic: '${!meta("output_topic")}' - -cache_resources: - - label: hydration_cache - redis: - url: TODO - default_ttl: 168h -``` - -You can find a full example [in the project repo][full-example], and with this config we can deploy as many instances of Benthos as we need as the partitions will be balanced across the consumers. - -[caches]: /docs/components/caches/about -[inputs]: /docs/components/inputs/about -[input.broker]: /docs/components/inputs/broker -[outputs]: /docs/components/outputs/about -[error-handling]: /docs/configuration/error_handling -[processor.branch]: /docs/components/processors/branch -[full-example]: https://github.com/benthosdev/benthos/blob/master/config/examples/joining_streams.yaml \ No newline at end of file diff --git a/website/docs/about.md b/website/docs/about.md deleted file mode 100644 index 6c3515afae..0000000000 --- a/website/docs/about.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: What is Benthos for? -sidebar_label: About -hide_title: false ---- - -
- -Benthos is a declarative data streaming service that solves a wide range of data engineering problems with simple, chained, stateless [processing steps][docs.processors]. It implements transaction based resiliency with back pressure, so when connecting to at-least-once sources and sinks it's able to guarantee at-least-once delivery without needing to persist messages during transit. - -import ReactPlayer from 'react-player/youtube'; - -
-
- -
-
- -It's [simple to deploy][docs.guides.getting_started], comes with a wide range of [connectors](#components), and is totally data agnostic, making it easy to drop into your existing infrastructure. Benthos has functionality that overlaps with integration frameworks, log aggregators and ETL workflow engines, and can therefore be used to complement these traditional data engineering tools or act as a simpler alternative. - -Benthos is ready to commit to this relationship, are you? - -import Link from '@docusaurus/Link'; - -Get Started - -## Components - -import ComponentsByCategory from '@theme/ComponentsByCategory'; - -### Inputs - - - ---- - -### Processors - - - ---- - -### Outputs - - - -[guides]: /cookbooks -[docs.guides.getting_started]: /docs/guides/getting_started -[docs.processors]: /docs/components/processors/about diff --git a/website/docs/components/about.md b/website/docs/components/about.md deleted file mode 100644 index 4c403f77d2..0000000000 --- a/website/docs/components/about.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: Components -sidebar_label: About -description: Learn about Benthos components ---- - -A good ninja gets clued up on its gear. - -
- -## Core Components - -Every Benthos pipeline has at least one [input][inputs], an optional [buffer][buffers], an [output][outputs] and any number of [processors][processors]: - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - -buffer: - type: none - -pipeline: - processors: - - mapping: | - message = this - meta.link_count = links.length() - -output: - aws_s3: - bucket: TODO - path: '${! meta("kafka_topic") }/${! json("message.id") }.json' -``` - -These are the main components within Benthos and they provide the majority of useful behaviour. - -## Observability Components - -There are also the observability components [http][http], [logger][logger], [metrics][metrics], and [tracing][tracers], which allow you to specify how Benthos exposes observability data: - -```yaml -http: - address: 0.0.0.0:4195 - enabled: true - debug_endpoints: false - -logger: - format: json - level: WARN - -metrics: - statsd: - address: localhost:8125 - flush_period: 100ms - -tracer: - jaeger: - agent_address: localhost:6831 -``` - -## Resource Components - -Finally, there are [caches][caches] and [rate limits][rate_limits]. These are components that are referenced by core components and can be shared. - -```yaml -input: - http_client: # This is an input - url: TODO - rate_limit: foo_ratelimit # This is a reference to a rate limit - -pipeline: - processors: - - cache: # This is a processor - resource: baz_cache # This is a reference to a cache - operator: add - key: '${! json("id") }' - value: "x" - - mapping: root = if errored() { deleted() } - -rate_limit_resources: - - label: foo_ratelimit - local: - count: 500 - interval: 1s - -cache_resources: - - label: baz_cache - memcached: - addresses: [ localhost:11211 ] -``` - -It's also possible to configure inputs, outputs and processors as resources which allows them to be reused throughout a configuration with the [`resource` input][inputs.resource], [`resource` output][outputs.resource] and [`resource` processor][processors.resource] respectively. - -For more information about any of these component types check out their sections: - -- [inputs][inputs] -- [processors][processors] -- [outputs][outputs] -- [buffers][buffers] -- [metrics][metrics] -- [tracers][tracers] -- [logger][logger] -- [caches][caches] -- [rate limits][rate_limits] - -[inputs]: /docs/components/inputs/about -[inputs.resource]: /docs/components/inputs/resource -[processors]: /docs/components/processors/about -[processors.resource]: /docs/components/processors/resource -[outputs]: /docs/components/outputs/about -[outputs.resource]: /docs/components/outputs/resource -[buffers]: /docs/components/buffers/about -[metrics]: /docs/components/metrics/about -[tracers]: /docs/components/tracers/about -[logger]: /docs/components/logger/about -[http]: /docs/components/http/about -[caches]: /docs/components/caches/about -[rate_limits]: /docs/components/rate_limits/about \ No newline at end of file diff --git a/website/docs/components/buffers/about.md b/website/docs/components/buffers/about.md deleted file mode 100644 index 4dee7c7f01..0000000000 --- a/website/docs/components/buffers/about.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Buffers -sidebar_label: About ---- - -Benthos uses a transaction model internally for guaranteeing delivery of messages, this means that a message from an input is not acknowledged (or its offset committed, etc) until that message has been processed and either intentionally deleted or successfully delivered to all outputs. This transaction model makes Benthos safe to deploy in scenarios where data loss is unacceptable. However, sometimes it's useful to customize the way in which messages are delivered, and this is where buffers come in. - -A buffer is an optional component type that comes immediately after the input layer and can be used as a way of decoupling the transaction model from components downstream such as the processing layer and outputs. This is considered an advanced component as most users will likely not benefit from a buffer, but they enable you to do things like group messages using window algorithms or intentionally weaken the delivery guarantees of the pipeline depending on the buffer you choose. - -Since buffers are able to modify (or disable) the transaction model within Benthos it is important that when you choose a buffer you read its documentation to understand the implication it will have on delivery guarantees. - -import ComponentsByCategory from '@theme/ComponentsByCategory'; - -## Categories - - - -import ComponentSelect from '@theme/ComponentSelect'; - - \ No newline at end of file diff --git a/website/docs/components/buffers/memory.md b/website/docs/components/buffers/memory.md deleted file mode 100644 index 736cbe2947..0000000000 --- a/website/docs/components/buffers/memory.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -title: memory -slug: memory -type: buffer -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores consumed messages in memory and acknowledges them at the input level. During shutdown Benthos will make a best attempt at flushing all remaining messages before exiting cleanly. - - - - - - -```yml -# Common config fields, showing default values -buffer: - memory: - limit: 524288000 - batch_policy: - enabled: false - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -buffer: - memory: - limit: 524288000 - batch_policy: - enabled: false - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -This buffer is appropriate when consuming messages from inputs that do not gracefully handle back pressure and where delivery guarantees aren't critical. - -This buffer has a configurable limit, where consumption will be stopped with back pressure upstream if the total size of messages in the buffer reaches this amount. Since this calculation is only an estimate, and the real size of messages in RAM is always higher, it is recommended to set the limit significantly below the amount of RAM available. - -## Delivery Guarantees - -This buffer intentionally weakens the delivery guarantees of the pipeline and therefore should never be used in places where data loss is unacceptable. - -## Batching - -It is possible to batch up messages sent from this buffer using a [batch policy](/docs/configuration/batching#batch-policy). - -## Fields - -### `limit` - -The maximum buffer size (in bytes) to allow before applying backpressure upstream. - - -Type: `int` -Default: `524288000` - -### `batch_policy` - -Optionally configure a policy to flush buffered messages in batches. - - -Type: `object` - -### `batch_policy.enabled` - -Whether to batch messages as they are flushed. - - -Type: `bool` -Default: `false` - -### `batch_policy.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batch_policy.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batch_policy.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batch_policy.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batch_policy.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/buffers/none.md b/website/docs/components/buffers/none.md deleted file mode 100644 index c2cccf32bc..0000000000 --- a/website/docs/components/buffers/none.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: none -slug: none -type: buffer -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Do not buffer messages. This is the default and most resilient configuration. - -```yml -# Config fields, showing default values -buffer: - none: {} -``` - -Selecting no buffer means the output layer is directly coupled with the input layer. This is the safest and lowest latency option since acknowledgements from at-least-once protocols can be propagated all the way from the output protocol to the input protocol. - -If the output layer is hit with back pressure it will propagate all the way to the input layer, and further up the data stream. If you need to relieve your pipeline of this back pressure consider using a more robust buffering solution such as Kafka before resorting to alternatives. - - diff --git a/website/docs/components/buffers/sqlite.md b/website/docs/components/buffers/sqlite.md deleted file mode 100644 index 5d1c8af6ac..0000000000 --- a/website/docs/components/buffers/sqlite.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: sqlite -slug: sqlite -type: buffer -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores messages in an SQLite database and acknowledges them at the input level. - -```yml -# Config fields, showing default values -buffer: - sqlite: - path: "" # No default (required) - pre_processors: [] # No default (optional) - post_processors: [] # No default (optional) -``` - -Stored messages are then consumed as a stream from the database and deleted only once they are successfully sent at the output level. If the service is restarted Benthos will make a best attempt to finish delivering messages that are already read from the database, and when it starts again it will consume from the oldest message that has not yet been delivered. - -## Delivery Guarantees - -Messages are not acknowledged at the input level until they have been added to the SQLite database, and they are not removed from the SQLite database until they have been successfully delivered. This means at-least-once delivery guarantees are preserved in cases where the service is shut down unexpectedly. However, since this process relies on interaction with the disk (wherever the SQLite DB is stored) these delivery guarantees are not resilient to disk corruption or loss. - -## Batching - -Messages that are logically batched at the point where they are added to the buffer will continue to be associated with that batch when they are consumed. This buffer is also more efficient when storing messages within batches, and therefore it is recommended to use batching at the input level in high-throughput use cases even if they are not required for processing. - - -## Fields - -### `path` - -The path of the database file, which will be created if it does not already exist. - - -Type: `string` - -### `pre_processors` - -An optional list of processors to apply to messages before they are stored within the buffer. These processors are useful for compressing, archiving or otherwise reducing the data in size before it's stored on disk. - - -Type: `array` - -### `post_processors` - -An optional list of processors to apply to messages after they are consumed from the buffer. These processors are useful for undoing any compression, archiving, etc that may have been done by your `pre_processors`. - - -Type: `array` - -## Examples - - - - - -Batching at the input level greatly increases the throughput of this buffer. If logical batches aren't needed for processing add a [`split` processor](/docs/components/processors/split) to the `post_processors`. - -```yaml -input: - batched: - child: - sql_select: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - table: footable - columns: [ '*' ] - policy: - count: 100 - period: 500ms - -buffer: - sqlite: - path: ./foo.db - post_processors: - - split: {} -``` - - - - - diff --git a/website/docs/components/buffers/system_window.md b/website/docs/components/buffers/system_window.md deleted file mode 100644 index 390990ce08..0000000000 --- a/website/docs/components/buffers/system_window.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: system_window -slug: system_window -type: buffer -status: beta -categories: ["Windowing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Chops a stream of messages into tumbling or sliding windows of fixed temporal size, following the system clock. - -Introduced in version 3.53.0. - -```yml -# Config fields, showing default values -buffer: - system_window: - timestamp_mapping: root = now() - size: 30s # No default (required) - slide: "" - offset: "" - allowed_lateness: "" -``` - -A window is a grouping of messages that fit within a discrete measure of time following the system clock. Messages are allocated to a window either by the processing time (the time at which they're ingested) or by the event time, and this is controlled via the [`timestamp_mapping` field](#timestamp_mapping). - -In tumbling mode (default) the beginning of a window immediately follows the end of a prior window. When the buffer is initialized the first window to be created and populated is aligned against the zeroth minute of the zeroth hour of the day by default, and may therefore be open for a shorter period than the specified size. - -A window is flushed only once the system clock surpasses its scheduled end. If an [`allowed_lateness`](#allowed_lateness) is specified then the window will not be flushed until the scheduled end plus that length of time. - -When a message is added to a window it has a metadata field `window_end_timestamp` added to it containing the timestamp of the end of the window as an RFC3339 string. - -## Sliding Windows - -Sliding windows begin from an offset of the prior windows' beginning rather than its end, and therefore messages may belong to multiple windows. In order to produce sliding windows specify a [`slide` duration](#slide). - -## Back Pressure - -If back pressure is applied to this buffer either due to output services being unavailable or resources being saturated, windows older than the current and last according to the system clock will be dropped in order to prevent unbounded resource usage. This means you should ensure that under the worst case scenario you have enough system memory to store two windows' worth of data at a given time (plus extra for redundancy and other services). - -If messages could potentially arrive with event timestamps in the future (according to the system clock) then you should also factor in these extra messages in memory usage estimates. - -## Delivery Guarantees - -This buffer honours the transaction model within Benthos in order to ensure that messages are not acknowledged until they are either intentionally dropped or successfully delivered to outputs. However, since messages belonging to an expired window are intentionally dropped there are circumstances where not all messages entering the system will be delivered. - -When this buffer is configured with a slide duration it is possible for messages to belong to multiple windows, and therefore be delivered multiple times. In this case the first time the message is delivered it will be acked (or nacked) and subsequent deliveries of the same message will be a "best attempt". - -During graceful termination if the current window is partially populated with messages they will be nacked such that they are re-consumed the next time the service starts. - - -## Examples - - - - - -Given a stream of messages relating to cars passing through various traffic lights of the form: - -```json -{ - "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", - "created_at": "2021-08-07T09:49:35Z", - "registration_plate": "AB1C DEF", - "passengers": 3 -} -``` - -We can use a window buffer in order to create periodic messages summarising the traffic for a period of time of this form: - -```json -{ - "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", - "created_at": "2021-08-07T10:00:00Z", - "total_cars": 15, - "passengers": 43 -} -``` - -With the following config: - -```yaml -buffer: - system_window: - timestamp_mapping: root = this.created_at - size: 1h - -pipeline: - processors: - # Group messages of the window into batches of common traffic light IDs - - group_by_value: - value: '${! json("traffic_light") }' - - # Reduce each batch to a single message by deleting indexes > 0, and - # aggregate the car and passenger counts. - - mapping: | - root = if batch_index() == 0 { - { - "traffic_light": this.traffic_light, - "created_at": meta("window_end_timestamp"), - "total_cars": json("registration_plate").from_all().unique().length(), - "passengers": json("passengers").from_all().sum(), - } - } else { deleted() } -``` - - - - -## Fields - -### `timestamp_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) applied to each message during ingestion that provides the timestamp to use for allocating it a window. By default the function `now()` is used in order to generate a fresh timestamp at the time of ingestion (the processing time), whereas this mapping can instead extract a timestamp from the message itself (the event time). - -The timestamp value assigned to `root` must either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in ISO 8601 format. If the mapping fails or provides an invalid result the message will be dropped (with logging to describe the problem). - - -Type: `string` -Default: `"root = now()"` - -```yml -# Examples - -timestamp_mapping: root = this.created_at - -timestamp_mapping: root = meta("kafka_timestamp_unix").number() -``` - -### `size` - -A duration string describing the size of each window. By default windows are aligned to the zeroth minute and zeroth hour on the UTC clock, meaning windows of 1 hour duration will match the turn of each hour in the day, this can be adjusted with the `offset` field. - - -Type: `string` - -```yml -# Examples - -size: 30s - -size: 10m -``` - -### `slide` - -An optional duration string describing by how much time the beginning of each window should be offset from the beginning of the previous, and therefore creates sliding windows instead of tumbling. When specified this duration must be smaller than the `size` of the window. - - -Type: `string` -Default: `""` - -```yml -# Examples - -slide: 30s - -slide: 10m -``` - -### `offset` - -An optional duration string to offset the beginning of each window by, otherwise they are aligned to the zeroth minute and zeroth hour on the UTC clock. The offset cannot be a larger or equal measure to the window size or the slide. - - -Type: `string` -Default: `""` - -```yml -# Examples - -offset: -6h - -offset: 30m -``` - -### `allowed_lateness` - -An optional duration string describing the length of time to wait after a window has ended before flushing it, allowing late arrivals to be included. Since this windowing buffer uses the system clock an allowed lateness can improve the matching of messages when using event time. - - -Type: `string` -Default: `""` - -```yml -# Examples - -allowed_lateness: 10s - -allowed_lateness: 1m -``` - - diff --git a/website/docs/components/caches/about.md b/website/docs/components/caches/about.md deleted file mode 100644 index a85b889227..0000000000 --- a/website/docs/components/caches/about.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Caches -sidebar_label: About ---- - -A cache is a key/value store which can be used by certain components for applications such as deduplication or data joins. Caches are configured as a named resource: - -```yaml -cache_resources: - - label: foobar - memcached: - addresses: - - localhost:11211 - default_ttl: 60s -``` - -> It's possible to layer caches with read-through and write-through behaviour using the [`multilevel` cache][cache.multilevel]. - -And then any components that use caches have a field `resource` that specifies the cache resource: - -```yaml -pipeline: - processors: - - cache: - resource: foobar - operator: add - key: '${! json("message.id") }' - value: "storeme" - - mapping: root = if errored() { deleted() } -``` - -For the simple case where you wish to store messages in a cache as an output destination for your pipeline check out the [`cache` output][output.cache]. To see examples of more advanced uses of caches such as hydration and deduplication check out the [`cache` processor][processor.cache]. - -You can find out more about resources [in this document.][config.resources] - -import ComponentSelect from '@theme/ComponentSelect'; - - - -[cache.multilevel]: /docs/components/caches/multilevel -[processor.cache]: /docs/components/processors/cache -[output.cache]: /docs/components/outputs/cache -[config.resources]: /docs/configuration/resources \ No newline at end of file diff --git a/website/docs/components/caches/aws_dynamodb.md b/website/docs/components/caches/aws_dynamodb.md deleted file mode 100644 index 2f9a0e0ff9..0000000000 --- a/website/docs/components/caches/aws_dynamodb.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: aws_dynamodb -slug: aws_dynamodb -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores key/value pairs as a single document in a DynamoDB table. The key is stored as a string value and used as the table hash key. The value is stored as -a binary value using the `data_key` field name. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -aws_dynamodb: - table: "" # No default (required) - hash_key: "" # No default (required) - data_key: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -aws_dynamodb: - table: "" # No default (required) - hash_key: "" # No default (required) - data_key: "" # No default (required) - consistent_read: false - default_ttl: "" # No default (optional) - ttl_key: "" # No default (optional) - retries: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -A prefix can be specified to allow multiple cache types to share a single DynamoDB table. An optional TTL duration (`ttl`) and field -(`ttl_key`) can be specified if the backing table has TTL enabled. - -Strong read consistency can be enabled using the `consistent_read` configuration field. - -## Fields - -### `table` - -The table to store items in. - - -Type: `string` - -### `hash_key` - -The key of the table column to store item keys within. - - -Type: `string` - -### `data_key` - -The key of the table column to store item values within. - - -Type: `string` - -### `consistent_read` - -Whether to use strongly consistent reads on Get commands. - - -Type: `bool` -Default: `false` - -### `default_ttl` - -An optional default TTL to set for items, calculated from the moment the item is cached. A `ttl_key` must be specified in order to set item TTLs. - - -Type: `string` - -### `ttl_key` - -The column key to place the TTL value within. - - -Type: `string` - -### `retries` - -Determine time intervals and cut offs for retry attempts. - - -Type: `object` - -### `retries.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `retries.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"5s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `retries.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. - - -Type: `string` -Default: `"30s"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/caches/aws_s3.md b/website/docs/components/caches/aws_s3.md deleted file mode 100644 index ab39e0e5c2..0000000000 --- a/website/docs/components/caches/aws_s3.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -title: aws_s3 -slug: aws_s3 -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores each item in an S3 bucket as a file, where an item ID is the path of the item within the bucket. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -aws_s3: - bucket: "" # No default (required) - content_type: application/octet-stream -``` - - - - -```yml -# All config fields, showing default values -label: "" -aws_s3: - bucket: "" # No default (required) - content_type: application/octet-stream - force_path_style_urls: false - retries: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -It is not possible to atomically upload S3 objects exclusively when the target does not already exist, therefore this cache is not suitable for deduplication. - -## Fields - -### `bucket` - -The S3 bucket to store items in. - - -Type: `string` - -### `content_type` - -The content type to set for each item. - - -Type: `string` -Default: `"application/octet-stream"` - -### `force_path_style_urls` - -Forces the client API to use path style URLs, which helps when connecting to custom endpoints. - - -Type: `bool` -Default: `false` - -### `retries` - -Determine time intervals and cut offs for retry attempts. - - -Type: `object` - -### `retries.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `retries.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"5s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `retries.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. - - -Type: `string` -Default: `"30s"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/caches/couchbase.md b/website/docs/components/caches/couchbase.md deleted file mode 100644 index ee6e9e08be..0000000000 --- a/website/docs/components/caches/couchbase.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: couchbase -slug: couchbase -type: cache -status: experimental ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Use a Couchbase instance as a cache. - -Introduced in version 4.12.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -couchbase: - url: couchbase://localhost:11210 # No default (required) - username: "" # No default (optional) - password: "" # No default (optional) - bucket: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -couchbase: - url: couchbase://localhost:11210 # No default (required) - username: "" # No default (optional) - password: "" # No default (optional) - bucket: "" # No default (required) - collection: _default - transcoder: legacy - timeout: 15s - default_ttl: "" # No default (optional) -``` - - - - -## Fields - -### `url` - -Couchbase connection string. - - -Type: `string` - -```yml -# Examples - -url: couchbase://localhost:11210 -``` - -### `username` - -Username to connect to the cluster. - - -Type: `string` - -### `password` - -Password to connect to the cluster. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `bucket` - -Couchbase bucket. - - -Type: `string` - -### `collection` - -Bucket collection. - - -Type: `string` -Default: `"_default"` - -### `transcoder` - -Couchbase transcoder to use. - - -Type: `string` -Default: `"legacy"` - -| Option | Summary | -|---|---| -| `json` | JSONTranscoder implements the default transcoding behavior and applies JSON transcoding to all values. This will apply the following behavior to the value: binary ([]byte) -> error. default -> JSON value, JSON Flags. | -| `legacy` | LegacyTranscoder implements the behaviour for a backward-compatible transcoder. This transcoder implements behaviour matching that of gocb v1.This will apply the following behavior to the value: binary ([]byte) -> binary bytes, Binary expectedFlags. string -> string bytes, String expectedFlags. default -> JSON value, JSON expectedFlags. | -| `raw` | RawBinaryTranscoder implements passthrough behavior of raw binary data. This transcoder does not apply any serialization. This will apply the following behavior to the value: binary ([]byte) -> binary bytes, binary expectedFlags. default -> error. | -| `rawjson` | RawJSONTranscoder implements passthrough behavior of JSON data. This transcoder does not apply any serialization. It will forward data across the network without incurring unnecessary parsing costs. This will apply the following behavior to the value: binary ([]byte) -> JSON bytes, JSON expectedFlags. string -> JSON bytes, JSON expectedFlags. default -> error. | -| `rawstring` | RawStringTranscoder implements passthrough behavior of raw string data. This transcoder does not apply any serialization. This will apply the following behavior to the value: string -> string bytes, string expectedFlags. default -> error. | - - -### `timeout` - -Operation timeout. - - -Type: `string` -Default: `"15s"` - -### `default_ttl` - -An optional default TTL to set for items, calculated from the moment the item is cached. - - -Type: `string` - - diff --git a/website/docs/components/caches/file.md b/website/docs/components/caches/file.md deleted file mode 100644 index f4df43ba21..0000000000 --- a/website/docs/components/caches/file.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: file -slug: file -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores each item in a directory as a file, where an item ID is the path relative to the configured directory. - -```yml -# Config fields, showing default values -label: "" -file: - directory: "" # No default (required) -``` - -This type currently offers no form of item expiry or garbage collection, and is intended to be used for development and debugging purposes only. - -## Fields - -### `directory` - -The directory within which to store items. - - -Type: `string` - - diff --git a/website/docs/components/caches/gcp_cloud_storage.md b/website/docs/components/caches/gcp_cloud_storage.md deleted file mode 100644 index 0402f9e65f..0000000000 --- a/website/docs/components/caches/gcp_cloud_storage.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: gcp_cloud_storage -slug: gcp_cloud_storage -type: cache -status: beta ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Use a Google Cloud Storage bucket as a cache. - -```yml -# Config fields, showing default values -label: "" -gcp_cloud_storage: - bucket: "" # No default (required) - content_type: "" # No default (optional) -``` - -It is not possible to atomically upload cloud storage objects exclusively when the target does not already exist, therefore this cache is not suitable for deduplication. - -## Fields - -### `bucket` - -The Google Cloud Storage bucket to store items in. - - -Type: `string` - -### `content_type` - -Optional field to explicitly set the Content-Type. - - -Type: `string` - - diff --git a/website/docs/components/caches/lru.md b/website/docs/components/caches/lru.md deleted file mode 100644 index 3b68897065..0000000000 --- a/website/docs/components/caches/lru.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: lru -slug: lru -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores key/value pairs in a lru in-memory cache. This cache is therefore reset every time the service restarts. - - - - - - -```yml -# Common config fields, showing default values -label: "" -lru: - cap: 1000 - init_values: {} -``` - - - - -```yml -# All config fields, showing default values -label: "" -lru: - cap: 1000 - init_values: {} - algorithm: standard - two_queues_recent_ratio: 0.25 - two_queues_ghost_ratio: 0.5 - optimistic: false -``` - - - - -This provides the lru package which implements a fixed-size thread safe LRU cache. - -It uses the package [`lru`](https://github.com/hashicorp/golang-lru/v2) - -The field init_values can be used to pre-populate the memory cache with any number of key/value pairs: - -```yaml -cache_resources: - - label: foocache - lru: - cap: 1024 - init_values: - foo: bar -``` - -These values can be overridden during execution. - -## Fields - -### `cap` - -The cache maximum capacity (number of entries) - - -Type: `int` -Default: `1000` - -### `init_values` - -A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables. - - -Type: `object` -Default: `{}` - -```yml -# Examples - -init_values: - Nickelback: "1995" - Spice Girls: "1994" - The Human League: "1977" -``` - -### `algorithm` - -the lru cache implementation - - -Type: `string` -Default: `"standard"` - -| Option | Summary | -|---|---| -| `arc` | is an adaptive replacement cache. It tracks recent evictions as well as recent usage in both the frequent and recent caches. Its computational overhead is comparable to two_queues, but the memory overhead is linear with the size of the cache. ARC has been patented by IBM. | -| `standard` | is a simple LRU cache. It is based on the LRU implementation in groupcache | -| `two_queues` | tracks frequently used and recently used entries separately. This avoids a burst of accesses from taking out frequently used entries, at the cost of about 2x computational overhead and some extra bookkeeping. | - - -### `two_queues_recent_ratio` - -is the ratio of the two_queues cache dedicated to recently added entries that have only been accessed once. - - -Type: `float` -Default: `0.25` - -### `two_queues_ghost_ratio` - -is the default ratio of ghost entries kept to track entries recently evicted on two_queues cache. - - -Type: `float` -Default: `0.5` - -### `optimistic` - -If true, we do not lock on read/write events. The lru package is thread-safe, however the ADD operation is not atomic. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/caches/memcached.md b/website/docs/components/caches/memcached.md deleted file mode 100644 index 5033499a32..0000000000 --- a/website/docs/components/caches/memcached.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: memcached -slug: memcached -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to a cluster of memcached services, a prefix can be specified to allow multiple cache types to share a memcached cluster under different namespaces. - - - - - - -```yml -# Common config fields, showing default values -label: "" -memcached: - addresses: [] # No default (required) - prefix: "" # No default (optional) - default_ttl: 300s -``` - - - - -```yml -# All config fields, showing default values -label: "" -memcached: - addresses: [] # No default (required) - prefix: "" # No default (optional) - default_ttl: 300s - retries: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - - - - -## Fields - -### `addresses` - -A list of addresses of memcached servers to use. - - -Type: `array` - -### `prefix` - -An optional string to prefix item keys with in order to prevent collisions with similar services. - - -Type: `string` - -### `default_ttl` - -A default TTL to set for items, calculated from the moment the item is cached. - - -Type: `string` -Default: `"300s"` - -### `retries` - -Determine time intervals and cut offs for retry attempts. - - -Type: `object` - -### `retries.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `retries.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"5s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `retries.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. - - -Type: `string` -Default: `"30s"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - - diff --git a/website/docs/components/caches/memory.md b/website/docs/components/caches/memory.md deleted file mode 100644 index 28f220f3c5..0000000000 --- a/website/docs/components/caches/memory.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: memory -slug: memory -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores key/value pairs in a map held in memory. This cache is therefore reset every time the service restarts. Each item in the cache has a TTL set from the moment it was last edited, after which it will be removed during the next compaction. - - - - - - -```yml -# Common config fields, showing default values -label: "" -memory: - default_ttl: 5m - compaction_interval: 60s - init_values: {} -``` - - - - -```yml -# All config fields, showing default values -label: "" -memory: - default_ttl: 5m - compaction_interval: 60s - init_values: {} - shards: 1 -``` - - - - -The compaction interval determines how often the cache is cleared of expired items, and this process is only triggered on writes to the cache. Access to the cache is blocked during this process. - -Item expiry can be disabled entirely by setting the `compaction_interval` to an empty string. - -The field `init_values` can be used to prepopulate the memory cache with any number of key/value pairs which are exempt from TTLs: - -```yaml -cache_resources: - - label: foocache - memory: - default_ttl: 60s - init_values: - foo: bar -``` - -These values can be overridden during execution, at which point the configured TTL is respected as usual. - -## Fields - -### `default_ttl` - -The default TTL of each item. After this period an item will be eligible for removal during the next compaction. - - -Type: `string` -Default: `"5m"` - -### `compaction_interval` - -The period of time to wait before each compaction, at which point expired items are removed. This field can be set to an empty string in order to disable compactions/expiry entirely. - - -Type: `string` -Default: `"60s"` - -### `init_values` - -A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables. - - -Type: `object` -Default: `{}` - -```yml -# Examples - -init_values: - Nickelback: "1995" - Spice Girls: "1994" - The Human League: "1977" -``` - -### `shards` - -A number of logical shards to spread keys across, increasing the shards can have a performance benefit when processing a large number of keys. - - -Type: `int` -Default: `1` - - diff --git a/website/docs/components/caches/mongodb.md b/website/docs/components/caches/mongodb.md deleted file mode 100644 index 39bf992000..0000000000 --- a/website/docs/components/caches/mongodb.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: mongodb -slug: mongodb -type: cache -status: experimental ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Use a MongoDB instance as a cache. - -Introduced in version 3.43.0. - -```yml -# Config fields, showing default values -label: "" -mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - key_field: "" # No default (required) - value_field: "" # No default (required) -``` - -## Fields - -### `url` - -The URL of the target MongoDB server. - - -Type: `string` - -```yml -# Examples - -url: mongodb://localhost:27017 -``` - -### `database` - -The name of the target MongoDB database. - - -Type: `string` - -### `username` - -The username to connect to the database. - - -Type: `string` -Default: `""` - -### `password` - -The password to connect to the database. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `collection` - -The name of the target collection. - - -Type: `string` - -### `key_field` - -The field in the document that is used as the key. - - -Type: `string` - -### `value_field` - -The field in the document that is used as the value. - - -Type: `string` - - diff --git a/website/docs/components/caches/multilevel.md b/website/docs/components/caches/multilevel.md deleted file mode 100644 index 09af85acc1..0000000000 --- a/website/docs/components/caches/multilevel.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: multilevel -slug: multilevel -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Combines multiple caches as levels, performing read-through and write-through operations across them. - -```yml -# Config fields, showing default values -label: "" -multilevel: [] # No default (required) -``` - -## Examples - - - - - -The multilevel cache is useful for reducing traffic against a remote cache by routing it through a local cache. In the following example requests will only go through to the memcached server if the local memory cache is missing the key. - -```yaml -pipeline: - processors: - - branch: - processors: - - cache: - resource: leveled - operator: get - key: ${! json("key") } - - catch: - - mapping: 'root = {"err":error()}' - result_map: 'root.result = this' - -cache_resources: - - label: leveled - multilevel: [ hot, cold ] - - - label: hot - memory: - default_ttl: 60s - - - label: cold - memcached: - addresses: [ TODO:11211 ] - default_ttl: 60s -``` - - - - - diff --git a/website/docs/components/caches/nats_kv.md b/website/docs/components/caches/nats_kv.md deleted file mode 100644 index ff79e1cd70..0000000000 --- a/website/docs/components/caches/nats_kv.md +++ /dev/null @@ -1,330 +0,0 @@ ---- -title: nats_kv -slug: nats_kv -type: cache -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Cache key/values in a NATS key-value bucket. - -Introduced in version 4.27.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) -``` - - - - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `bucket` - -The name of the KV bucket. - - -Type: `string` - -```yml -# Examples - -bucket: my_kv_bucket -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - - diff --git a/website/docs/components/caches/noop.md b/website/docs/components/caches/noop.md deleted file mode 100644 index 4d6f1fd05f..0000000000 --- a/website/docs/components/caches/noop.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: noop -slug: noop -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Noop is a cache that stores nothing, all gets returns not found. Why? Sometimes doing nothing is the braver option. - -Introduced in version 4.27.0. - -```yml -# Config fields, showing default values -label: "" -noop: {} -``` - - diff --git a/website/docs/components/caches/redis.md b/website/docs/components/caches/redis.md deleted file mode 100644 index 4ee3d8b74c..0000000000 --- a/website/docs/components/caches/redis.md +++ /dev/null @@ -1,324 +0,0 @@ ---- -title: redis -slug: redis -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Use a Redis instance as a cache. The expiration can be set to zero or an empty string in order to set no expiration. - - - - - - -```yml -# Common config fields, showing default values -label: "" -redis: - url: redis://:6397 # No default (required) - prefix: "" # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -label: "" -redis: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - prefix: "" # No default (optional) - default_ttl: "" # No default (optional) - retries: - initial_interval: 500ms - max_interval: 1s - max_elapsed_time: 5s -``` - - - - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `prefix` - -An optional string to prefix item keys with in order to prevent collisions with similar services. - - -Type: `string` - -### `default_ttl` - -An optional default TTL to set for items, calculated from the moment the item is cached. - - -Type: `string` - -### `retries` - -Determine time intervals and cut offs for retry attempts. - - -Type: `object` - -### `retries.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"500ms"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `retries.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `retries.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. - - -Type: `string` -Default: `"5s"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - - diff --git a/website/docs/components/caches/ristretto.md b/website/docs/components/caches/ristretto.md deleted file mode 100644 index 21709a5915..0000000000 --- a/website/docs/components/caches/ristretto.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: ristretto -slug: ristretto -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores key/value pairs in a map held in the memory-bound [Ristretto cache](https://github.com/dgraph-io/ristretto). - - - - - - -```yml -# Common config fields, showing default values -label: "" -ristretto: - default_ttl: "" -``` - - - - -```yml -# All config fields, showing default values -label: "" -ristretto: - default_ttl: "" - get_retries: - enabled: false - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - - - - -This cache is more efficient and appropriate for high-volume use cases than the standard memory cache. However, the add command is non-atomic, and therefore this cache is not suitable for deduplication. - -## Fields - -### `default_ttl` - -A default TTL to set for items, calculated from the moment the item is cached. Set to an empty string or zero duration to disable TTLs. - - -Type: `string` -Default: `""` - -```yml -# Examples - -default_ttl: 5m - -default_ttl: 60s -``` - -### `get_retries` - -Determines how and whether get attempts should be retried if the key is not found. Ristretto is a concurrent cache that does not immediately reflect writes, and so it can sometimes be useful to enable retries at the cost of speed in cases where the key is expected to exist. - - -Type: `object` - -### `get_retries.enabled` - -Whether retries should be enabled. - - -Type: `bool` -Default: `false` - -### `get_retries.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `get_retries.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"5s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `get_retries.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. - - -Type: `string` -Default: `"30s"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - - diff --git a/website/docs/components/caches/sql.md b/website/docs/components/caches/sql.md deleted file mode 100644 index bbebf3cd2e..0000000000 --- a/website/docs/components/caches/sql.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -title: sql -slug: sql -type: cache -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Uses an SQL database table as a destination for storing cache key/value items. - -Introduced in version 4.26.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -sql: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - key_column: foo # No default (required) - value_column: bar # No default (required) - set_suffix: ON DUPLICATE KEY UPDATE bar=VALUES(bar) # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -label: "" -sql: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - key_column: foo # No default (required) - value_column: bar # No default (required) - set_suffix: ON DUPLICATE KEY UPDATE bar=VALUES(bar) # No default (optional) - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) -``` - - - - -Each cache key/value pair will exist as a row within the specified table. Currently only the key and value columns are set, and therefore any other columns present within the target table must allow NULL values if this cache is going to be used for set and add operations. - -Cache operations are translated into SQL statements as follows: - -### Get - -All `get` operations are performed with a traditional `select` statement. - -### Delete - -All `delete` operations are performed with a traditional `delete` statement. - -### Set - -The `set` operation is performed with a traditional `insert` statement. - -This will behave as an `add` operation by default, and so ideally needs to be adapted in order to provide updates instead of failing on collision s. Since different SQL engines implement upserts differently it is necessary to specify a `set_suffix` that modifies an `insert` statement in order to perform updates on conflict. - -### Add - -The `add` operation is performed with a traditional `insert` statement. - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `table` - -The table to insert/read/delete cache items. - - -Type: `string` - -```yml -# Examples - -table: foo -``` - -### `key_column` - -The name of a column to be used for storing cache item keys. This column should support strings of arbitrary size. - - -Type: `string` - -```yml -# Examples - -key_column: foo -``` - -### `value_column` - -The name of a column to be used for storing cache item values. This column should support strings of arbitrary size. - - -Type: `string` - -```yml -# Examples - -value_column: bar -``` - -### `set_suffix` - -An optional suffix to append to each insert query for a cache `set` operation. This should modify an insert statement into an upsert appropriate for the given SQL engine. - - -Type: `string` - -```yml -# Examples - -set_suffix: ON DUPLICATE KEY UPDATE bar=VALUES(bar) - -set_suffix: ON CONFLICT (foo) DO UPDATE SET bar=excluded.bar - -set_suffix: ON CONFLICT (foo) DO NOTHING -``` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - - diff --git a/website/docs/components/caches/ttlru.md b/website/docs/components/caches/ttlru.md deleted file mode 100644 index 2272e4b680..0000000000 --- a/website/docs/components/caches/ttlru.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: ttlru -slug: ttlru -type: cache -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores key/value pairs in a ttlru in-memory cache. This cache is therefore reset every time the service restarts. - - - - - - -```yml -# Common config fields, showing default values -label: "" -ttlru: - cap: 1024 - default_ttl: 5m0s - init_values: {} -``` - - - - -```yml -# All config fields, showing default values -label: "" -ttlru: - cap: 1024 - default_ttl: 5m0s - ttl: "" # No default (optional) - init_values: {} - optimistic: false -``` - - - - -The cache ttlru provides a simple, goroutine safe, cache with a fixed number of entries. Each entry has a per-cache defined TTL. - -This TTL is reset on both modification and access of the value. As a result, if the cache is full, and no items have expired, when adding a new item, the item with the soonest expiration will be evicted. - -It uses the package [`expirable`](https://github.com/hashicorp/golang-lru/v2/expirable) - -The field init_values can be used to pre-populate the memory cache with any number of key/value pairs: - -```yaml -cache_resources: - - label: foocache - ttlru: - default_ttl: '5m' - cap: 1024 - init_values: - foo: bar -``` - -These values can be overridden during execution. - -## Fields - -### `cap` - -The cache maximum capacity (number of entries) - - -Type: `int` -Default: `1024` - -### `default_ttl` - -The cache ttl of each element - - -Type: `string` -Default: `"5m0s"` -Requires version 4.21.0 or newer - -### `ttl` - -Deprecated. Please use `default_ttl` field - - -Type: `string` - -### `init_values` - -A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables. - - -Type: `object` -Default: `{}` - -```yml -# Examples - -init_values: - Nickelback: "1995" - Spice Girls: "1994" - The Human League: "1977" -``` - -### `optimistic` - -If true, we do not lock on read/write events. The ttlru package is thread-safe, however the ADD operation is not atomic. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/http/about.md b/website/docs/components/http/about.md deleted file mode 100644 index 1600cde221..0000000000 --- a/website/docs/components/http/about.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -title: HTTP ---- - - - -When Benthos runs it kicks off an HTTP server that provides a few generally useful endpoints and is also where configured components such as the [`http_server` input][inputs.http_server] [and output][outputs.http_server] can register their own endpoints if they don't require their own host/port. - -The configuration for this server lives under the `http` namespace, with the following default values: - - -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - - - -```yaml -# Common config fields, showing default values - -http: - address: 0.0.0.0:4195 - enabled: true - root_path: /benthos - debug_endpoints: false -``` - - - - -```yaml -# All config fields, showing default values - -http: - address: 0.0.0.0:4195 - enabled: true - root_path: /benthos - debug_endpoints: false - cert_file: "" - key_file: "" - cors: - enabled: false - allowed_origins: [] - basic_auth: - enabled: false - username: "" - password_hash: "" - algorithm: "sha256" - salt: "" -``` - - - -The field `enabled` can be set to `false` in order to disable the server. - -The field `root_path` specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a `root_path` set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`. - -## Enabling HTTPS - -By default Benthos will serve traffic over HTTP. In order to enforce TLS and serve traffic exclusively over HTTPS you must provide a `cert_file` and `key_file` path in your config, which point to a file containing a certificate and a matching private key for the server respectively. - -If the certificate is signed by a certificate authority, the `cert_file` should be the concatenation of the server's certificate, any intermediates, and the CA's certificate. - -## Enabling Basic Authentication - -By default Benthos does not do any sort of authentication for the service-wide HTTP server. However, it's possible to configure basic authentication with the [`basic_auth`](#basic_auth) field. Passwords configured must be hashed according to the specified algorithm and base64 encoded, for some hashing algorithms you can do this using Benthos itself: - -```sh -echo mynewpassword | benthos blobl 'root = content().hash("sha256").encode("base64")' -``` - -## Endpoints - -The following endpoints will be generally available when the HTTP server is enabled: - -- `/version` provides version info. -- `/ping` can be used as a liveness probe as it always returns a 200. -- `/ready` can be used as a readiness probe as it serves a 200 only when both the input and output are connected, otherwise a 503 is returned. -- `/metrics`, `/stats` both provide metrics when the metrics type is either [`json_api`][metrics.json_api] or [`prometheus`][metrics.prometheus]. -- `/endpoints` provides a JSON object containing a list of available endpoints, including those registered by configured components. - -## CORS - -In order to serve Cross-Origin Resource Sharing headers, which instruct browsers to allow CORS requests, set the subfield `cors.enabled` to `true`. - -### allowed_origins - -A list of allowed origins to connect from. The literal value `*` can be specified as a wildcard. Note `cors.enabled` must be set to `true` for this list to take effect. - -## Debug Endpoints - -The field `debug_endpoints` when set to `true` prompts Benthos to register a few extra endpoints that can be useful for debugging performance or behavioral problems: - -- `/debug/config/json` returns the loaded config as JSON. -- `/debug/config/yaml` returns the loaded config as YAML. -- `/debug/pprof/block` responds with a pprof-formatted block profile. -- `/debug/pprof/heap` responds with a pprof-formatted heap profile. -- `/debug/pprof/mutex` responds with a pprof-formatted mutex profile. -- `/debug/pprof/profile` responds with a pprof-formatted cpu profile. -- `/debug/pprof/goroutine` responds with a pprof-formatted goroutine profile. -- `/debug/pprof/symbol` looks up the program counters listed in the request, responding with a table mapping program counters to function names. -- `/debug/pprof/trace` responds with the execution trace in binary form. Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified. -- `/debug/stack` returns a snapshot of the current service stack trace. - -## Fields - -The schema of the `http` section is as follows: - -### `enabled` - -Whether to enable to HTTP server. - - -Type: `bool` -Default: `true` - -### `address` - -The address to bind to. - - -Type: `string` -Default: `"0.0.0.0:4195"` - -### `root_path` - -Specifies a general prefix for all endpoints, this can help isolate the service endpoints when using a reverse proxy with other shared services. All endpoints will still be registered at the root as well as behind the prefix, e.g. with a root_path set to `/foo` the endpoint `/version` will be accessible from both `/version` and `/foo/version`. - - -Type: `string` -Default: `"/benthos"` - -### `debug_endpoints` - -Whether to register a few extra endpoints that can be useful for debugging performance or behavioral problems. - - -Type: `bool` -Default: `false` - -### `cert_file` - -An optional certificate file for enabling TLS. - - -Type: `string` -Default: `""` - -### `key_file` - -An optional key file for enabling TLS. - - -Type: `string` -Default: `""` - -### `cors` - -Adds Cross-Origin Resource Sharing headers. - - -Type: `object` -Requires version 3.63.0 or newer - -### `cors.enabled` - -Whether to allow CORS requests. - - -Type: `bool` -Default: `false` - -### `cors.allowed_origins` - -An explicit list of origins that are allowed for CORS requests. - - -Type: list of `string` -Default: `[]` - -### `basic_auth` - -Allows you to enforce and customise basic authentication for requests to the HTTP server. - - -Type: `object` - -### `basic_auth.enabled` - -Enable basic authentication - - -Type: `bool` -Default: `false` - -### `basic_auth.realm` - -Custom realm name - - -Type: `string` -Default: `"restricted"` - -### `basic_auth.username` - -Username required to authenticate. - - -Type: `string` -Default: `""` - -### `basic_auth.password_hash` - -Hashed password required to authenticate. (base64 encoded) - - -Type: `string` -Default: `""` - -### `basic_auth.algorithm` - -Encryption algorithm used to generate `password_hash`. - - -Type: `string` -Default: `"sha256"` - -```yml -# Examples - -algorithm: md5 - -algorithm: sha256 - -algorithm: bcrypt - -algorithm: scrypt -``` - -### `basic_auth.salt` - -Salt for scrypt algorithm. (base64 encoded) - - -Type: `string` -Default: `""` - -[inputs.http_server]: /docs/components/inputs/http_server -[outputs.http_server]: /docs/components/outputs/http_server -[metrics.json_api]: /docs/components/metrics/json_api -[metrics.prometheus]: /docs/components/metrics/prometheus diff --git a/website/docs/components/inputs/about.md b/website/docs/components/inputs/about.md deleted file mode 100644 index eb0d7d84bc..0000000000 --- a/website/docs/components/inputs/about.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Inputs -sidebar_label: About ---- - -An input is a source of data piped through an array of optional [processors][processors]: - -```yaml -input: - label: my_redis_input - - redis_streams: - url: tcp://localhost:6379 - streams: - - benthos_stream - body_key: body - consumer_group: benthos_group - - # Optional list of processing steps - processors: - - mapping: | - root.document = this.without("links") - root.link_count = this.links.length() -``` - -Some inputs have a logical end, for example a [`csv` input][input.csv] ends once the last row is consumed, when this happens the input gracefully terminates and Benthos will shut itself down once all messages have been processed fully. - -It's also possible to specify a logical end for an input that otherwise doesn't have one with the [`read_until` input][input.read_until], which checks a condition against each consumed message in order to determine whether it should be the last. - -## Brokering - -Only one input is configured at the root of a Benthos config. However, the root input can be a [broker][input.broker] which combines multiple inputs and merges the streams: - -```yaml -input: - broker: - inputs: - - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - - - redis_streams: - url: tcp://localhost:6379 - streams: - - benthos_stream - body_key: body - consumer_group: benthos_group -``` - -## Labels - -Inputs have an optional field `label` that can uniquely identify them in observability data such as metrics and logs. This can be useful when running configs with multiple inputs, otherwise their metrics labels will be generated based on their composition. For more information check out the [metrics documentation][metrics.about]. - -### Sequential Reads - -Sometimes it's useful to consume a sequence of inputs, where an input is only consumed once its predecessor is drained fully, you can achieve this with the [`sequence` input][input.sequence]. - -## Generating Messages - -It's possible to generate data with Benthos using the [`generate` input][input.generate], which is also a convenient way to trigger scheduled pipelines. - -import ComponentsByCategory from '@theme/ComponentsByCategory'; - -## Categories - - - -import ComponentSelect from '@theme/ComponentSelect'; - - - -[processors]: /docs/components/processors/about -[input.broker]: /docs/components/inputs/broker -[input.generate]: /docs/components/inputs/generate -[input.csv]: /docs/components/inputs/csv -[input.sequence]: /docs/components/inputs/sequence -[input.read_until]: /docs/components/inputs/read_until -[metrics.about]: /docs/components/metrics/about \ No newline at end of file diff --git a/website/docs/components/inputs/amqp_0_9.md b/website/docs/components/inputs/amqp_0_9.md deleted file mode 100644 index 3cf5c058ea..0000000000 --- a/website/docs/components/inputs/amqp_0_9.md +++ /dev/null @@ -1,381 +0,0 @@ ---- -title: amqp_0_9 -slug: amqp_0_9 -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to an AMQP (0.91) queue. AMQP is a messaging protocol used by various message brokers, including RabbitMQ. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - amqp_0_9: - urls: [] # No default (required) - queue: "" # No default (required) - consumer_tag: "" - prefetch_count: 10 -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - amqp_0_9: - urls: [] # No default (required) - queue: "" # No default (required) - queue_declare: - enabled: false - durable: true - auto_delete: false - bindings_declare: [] # No default (optional) - consumer_tag: "" - auto_ack: false - nack_reject_patterns: [] - prefetch_count: 10 - prefetch_size: 0 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] -``` - - - - -TLS is automatic when connecting to an `amqps` URL, but custom settings can be enabled in the `tls` section. - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- amqp_content_type -- amqp_content_encoding -- amqp_delivery_mode -- amqp_priority -- amqp_correlation_id -- amqp_reply_to -- amqp_expiration -- amqp_message_id -- amqp_timestamp -- amqp_type -- amqp_user_id -- amqp_app_id -- amqp_consumer_tag -- amqp_delivery_tag -- amqp_redelivered -- amqp_exchange -- amqp_routing_key -- All existing message headers, including nested headers prefixed with the key of their respective parent. -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `urls` - -A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` -Requires version 3.58.0 or newer - -```yml -# Examples - -urls: - - amqp://guest:guest@127.0.0.1:5672/ - -urls: - - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ - -urls: - - amqp://127.0.0.1:5672/ - - amqp://127.0.0.2:5672/ -``` - -### `queue` - -An AMQP queue to consume from. - - -Type: `string` - -### `queue_declare` - -Allows you to passively declare the target queue. If the queue already exists then the declaration passively verifies that they match the target fields. - - -Type: `object` - -### `queue_declare.enabled` - -Whether to enable queue declaration. - - -Type: `bool` -Default: `false` - -### `queue_declare.durable` - -Whether the declared queue is durable. - - -Type: `bool` -Default: `true` - -### `queue_declare.auto_delete` - -Whether the declared queue will auto-delete. - - -Type: `bool` -Default: `false` - -### `bindings_declare` - -Allows you to passively declare bindings for the target queue. - - -Type: `array` - -```yml -# Examples - -bindings_declare: - - exchange: foo - key: bar -``` - -### `bindings_declare[].exchange` - -The exchange of the declared binding. - - -Type: `string` -Default: `""` - -### `bindings_declare[].key` - -The key of the declared binding. - - -Type: `string` -Default: `""` - -### `consumer_tag` - -A consumer tag. - - -Type: `string` -Default: `""` - -### `auto_ack` - -Acknowledge messages automatically as they are consumed rather than waiting for acknowledgments from downstream. This can improve throughput and prevent the pipeline from blocking but at the cost of eliminating delivery guarantees. - - -Type: `bool` -Default: `false` - -### `nack_reject_patterns` - -A list of regular expression patterns whereby if a message that has failed to be delivered by Benthos has an error that matches it will be dropped (or delivered to a dead-letter queue if one exists). By default failed messages are nacked with requeue enabled. - - -Type: `array` -Default: `[]` -Requires version 3.64.0 or newer - -```yml -# Examples - -nack_reject_patterns: - - ^reject me please:.+$ -``` - -### `prefetch_count` - -The maximum number of pending messages to have consumed at a time. - - -Type: `int` -Default: `10` - -### `prefetch_size` - -The maximum amount of pending messages measured in bytes to have consumed at a time. - - -Type: `int` -Default: `0` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - - diff --git a/website/docs/components/inputs/amqp_1.md b/website/docs/components/inputs/amqp_1.md deleted file mode 100644 index 6feda933d1..0000000000 --- a/website/docs/components/inputs/amqp_1.md +++ /dev/null @@ -1,356 +0,0 @@ ---- -title: amqp_1 -slug: amqp_1 -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Reads messages from an AMQP (1.0) server. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - amqp_1: - urls: [] # No default (optional) - source_address: /foo # No default (required) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - amqp_1: - urls: [] # No default (optional) - source_address: /foo # No default (required) - azure_renew_lock: false - read_header: false - credit: 64 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - sasl: - mechanism: none - user: "" - password: "" -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- amqp_content_type -- amqp_content_encoding -- amqp_creation_time -- All string typed message annotations -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -By setting `read_header` to `true`, additional message header properties will be added to each message: - -``` text -- amqp_durable -- amqp_priority -- amqp_ttl -- amqp_first_acquirer -- amqp_delivery_count -``` - -## Performance - -This input benefits from receiving multiple messages in flight in parallel for improved performance. -You can tune the max number of in flight messages with the field `credit`. - - -## Fields - -### `urls` - -A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` -Requires version 4.23.0 or newer - -```yml -# Examples - -urls: - - amqp://guest:guest@127.0.0.1:5672/ - -urls: - - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ - -urls: - - amqp://127.0.0.1:5672/ - - amqp://127.0.0.2:5672/ -``` - -### `source_address` - -The source address to consume from. - - -Type: `string` - -```yml -# Examples - -source_address: /foo - -source_address: queue:/bar - -source_address: topic:/baz -``` - -### `azure_renew_lock` - -Experimental: Azure service bus specific option to renew lock if processing takes more then configured lock time - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `read_header` - -Read additional message header fields into `amqp_*` metadata properties. - - -Type: `bool` -Default: `false` -Requires version 4.25.0 or newer - -### `credit` - -Specifies the maximum number of unacknowledged messages the sender can transmit. Once this limit is reached, no more messages will arrive until messages are acknowledged and settled. - - -Type: `int` -Default: `64` -Requires version 4.26.0 or newer - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `sasl` - -Enables SASL authentication. - - -Type: `object` - -### `sasl.mechanism` - -The SASL authentication mechanism to use. - - -Type: `string` -Default: `"none"` - -| Option | Summary | -|---|---| -| `anonymous` | Anonymous SASL authentication. | -| `none` | No SASL based authentication. | -| `plain` | Plain text SASL authentication. | - - -### `sasl.user` - -A SASL plain text username. It is recommended that you use environment variables to populate this field. - - -Type: `string` -Default: `""` - -```yml -# Examples - -user: ${USER} -``` - -### `sasl.password` - -A SASL plain text password. It is recommended that you use environment variables to populate this field. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: ${PASSWORD} -``` - - diff --git a/website/docs/components/inputs/aws_kinesis.md b/website/docs/components/inputs/aws_kinesis.md deleted file mode 100644 index 8b0dd2e690..0000000000 --- a/website/docs/components/inputs/aws_kinesis.md +++ /dev/null @@ -1,401 +0,0 @@ ---- -title: aws_kinesis -slug: aws_kinesis -type: input -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Receive messages from one or more Kinesis streams. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - aws_kinesis: - streams: [] # No default (required) - dynamodb: - table: "" - create: false - checkpoint_limit: 1024 - auto_replay_nacks: true - commit_period: 5s - start_from_oldest: true - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - aws_kinesis: - streams: [] # No default (required) - dynamodb: - table: "" - create: false - billing_mode: PAY_PER_REQUEST - read_capacity_units: 0 - write_capacity_units: 0 - checkpoint_limit: 1024 - auto_replay_nacks: true - commit_period: 5s - rebalance_period: 30s - lease_period: 30s - start_from_oldest: true - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Consumes messages from one or more Kinesis streams either by automatically balancing shards across other instances of this input, or by consuming shards listed explicitly. The latest message sequence consumed by this input is stored within a [DynamoDB table](#table-schema), which allows it to resume at the correct sequence of the shard during restarts. This table is also used for coordination across distributed inputs when shard balancing. - -Benthos will not store a consumed sequence unless it is acknowledged at the output level, which ensures at-least-once delivery guarantees. - -### Ordering - -By default messages of a shard can be processed in parallel, up to a limit determined by the field `checkpoint_limit`. However, if strict ordered processing is required then this value must be set to 1 in order to process shard messages in lock-step. When doing so it is recommended that you perform batching at this component for performance as it will not be possible to batch lock-stepped messages at the output level. - -### Table Schema - -It's possible to configure Benthos to create the DynamoDB table required for coordination if it does not already exist. However, if you wish to create this yourself (recommended) then create a table with a string HASH key `StreamID` and a string RANGE key `ShardID`. - -### Batching - -Use the `batching` fields to configure an optional [batching policy](/docs/configuration/batching#batch-policy). Each stream shard will be batched separately in order to ensure that acknowledgements aren't contaminated. - - -## Fields - -### `streams` - -One or more Kinesis data streams to consume from. Streams can either be specified by their name or full ARN. Shards of a stream are automatically balanced across consumers by coordinating through the provided DynamoDB table. Multiple comma separated streams can be listed in a single element. Shards are automatically distributed across consumers of a stream by coordinating through the provided DynamoDB table. Alternatively, it's possible to specify an explicit shard to consume from with a colon after the stream name, e.g. `foo:0` would consume the shard `0` of the stream `foo`. - - -Type: `array` - -```yml -# Examples - -streams: - - foo - - arn:aws:kinesis:*:111122223333:stream/my-stream -``` - -### `dynamodb` - -Determines the table used for storing and accessing the latest consumed sequence for shards, and for coordinating balanced consumers of streams. - - -Type: `object` - -### `dynamodb.table` - -The name of the table to access. - - -Type: `string` -Default: `""` - -### `dynamodb.create` - -Whether, if the table does not exist, it should be created. - - -Type: `bool` -Default: `false` - -### `dynamodb.billing_mode` - -When creating the table determines the billing mode. - - -Type: `string` -Default: `"PAY_PER_REQUEST"` -Options: `PROVISIONED`, `PAY_PER_REQUEST`. - -### `dynamodb.read_capacity_units` - -Set the provisioned read capacity when creating the table with a `billing_mode` of `PROVISIONED`. - - -Type: `int` -Default: `0` - -### `dynamodb.write_capacity_units` - -Set the provisioned write capacity when creating the table with a `billing_mode` of `PROVISIONED`. - - -Type: `int` -Default: `0` - -### `checkpoint_limit` - -The maximum gap between the in flight sequence versus the latest acknowledged sequence at a given time. Increasing this limit enables parallel processing and batching at the output level to work on individual shards. Any given sequence will not be committed unless all messages under that offset are delivered in order to preserve at least once delivery guarantees. - - -Type: `int` -Default: `1024` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `commit_period` - -The period of time between each update to the checkpoint table. - - -Type: `string` -Default: `"5s"` - -### `rebalance_period` - -The period of time between each attempt to rebalance shards across clients. - - -Type: `string` -Default: `"30s"` - -### `lease_period` - -The period of time after which a client that has failed to update a shard checkpoint is assumed to be inactive. - - -Type: `string` -Default: `"30s"` - -### `start_from_oldest` - -Whether to consume from the oldest message when a sequence does not yet exist for the stream. - - -Type: `bool` -Default: `true` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/inputs/aws_s3.md b/website/docs/components/inputs/aws_s3.md deleted file mode 100644 index 004adfb00d..0000000000 --- a/website/docs/components/inputs/aws_s3.md +++ /dev/null @@ -1,328 +0,0 @@ ---- -title: aws_s3 -slug: aws_s3 -type: input -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Downloads objects within an Amazon S3 bucket, optionally filtered by a prefix, either by walking the items in the bucket or by streaming upload notifications in realtime. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - aws_s3: - bucket: "" - prefix: "" - scanner: - to_the_end: {} - sqs: - url: "" - key_path: Records.*.s3.object.key - bucket_path: Records.*.s3.bucket.name - envelope_path: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - aws_s3: - bucket: "" - prefix: "" - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - force_path_style_urls: false - delete_objects: false - scanner: - to_the_end: {} - sqs: - url: "" - endpoint: "" - key_path: Records.*.s3.object.key - bucket_path: Records.*.s3.bucket.name - envelope_path: "" - delay_period: "" - max_messages: 10 - wait_time_seconds: 0 -``` - - - - -## Streaming Objects on Upload with SQS - -A common pattern for consuming S3 objects is to emit upload notification events from the bucket either directly to an SQS queue, or to an SNS topic that is consumed by an SQS queue, and then have your consumer listen for events which prompt it to download the newly uploaded objects. More information about this pattern and how to set it up can be found at: https://docs.aws.amazon.com/AmazonS3/latest/dev/ways-to-add-notification-config-to-bucket.html. - -Benthos is able to follow this pattern when you configure an `sqs.url`, where it consumes events from SQS and only downloads object keys received within those events. In order for this to work Benthos needs to know where within the event the key and bucket names can be found, specified as [dot paths](/docs/configuration/field_paths) with the fields `sqs.key_path` and `sqs.bucket_path`. The default values for these fields should already be correct when following the guide above. - -If your notification events are being routed to SQS via an SNS topic then the events will be enveloped by SNS, in which case you also need to specify the field `sqs.envelope_path`, which in the case of SNS to SQS will usually be `Message`. - -When using SQS please make sure you have sensible values for `sqs.max_messages` and also the visibility timeout of the queue itself. When Benthos consumes an S3 object the SQS message that triggered it is not deleted until the S3 object has been sent onwards. This ensures at-least-once crash resiliency, but also means that if the S3 object takes longer to process than the visibility timeout of your queue then the same objects might be processed multiple times. - -## Downloading Large Files - -When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a [`codec`](#codec) can be specified that determines how to break the input into smaller individual messages. - -## Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Metadata - -This input adds the following metadata fields to each message: - -``` -- s3_key -- s3_bucket -- s3_last_modified_unix -- s3_last_modified (RFC3339) -- s3_content_type -- s3_content_encoding -- s3_version_id -- All user defined metadata -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). Note that user defined metadata is case insensitive within AWS, and it is likely that the keys will be received in a capitalized form, if you wish to make them consistent you can map all metadata keys to lower or uppercase using a Bloblang mapping such as `meta = meta().map_each_key(key -> key.lowercase())`. - -## Fields - -### `bucket` - -The bucket to consume from. If the field `sqs.url` is specified this field is optional. - - -Type: `string` -Default: `""` - -### `prefix` - -An optional path prefix, if set only objects with the prefix are consumed when walking a bucket. - - -Type: `string` -Default: `""` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `force_path_style_urls` - -Forces the client API to use path style URLs for downloading keys, which is often required when connecting to custom endpoints. - - -Type: `bool` -Default: `false` - -### `delete_objects` - -Whether to delete downloaded objects from the bucket once they are processed. - - -Type: `bool` -Default: `false` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"to_the_end":{}}` -Requires version 4.25.0 or newer - -### `sqs` - -Consume SQS messages in order to trigger key downloads. - - -Type: `object` - -### `sqs.url` - -An optional SQS URL to connect to. When specified this queue will control which objects are downloaded. - - -Type: `string` -Default: `""` - -### `sqs.endpoint` - -A custom endpoint to use when connecting to SQS. - - -Type: `string` -Default: `""` - -### `sqs.key_path` - -A [dot path](/docs/configuration/field_paths) whereby object keys are found in SQS messages. - - -Type: `string` -Default: `"Records.*.s3.object.key"` - -### `sqs.bucket_path` - -A [dot path](/docs/configuration/field_paths) whereby the bucket name can be found in SQS messages. - - -Type: `string` -Default: `"Records.*.s3.bucket.name"` - -### `sqs.envelope_path` - -A [dot path](/docs/configuration/field_paths) of a field to extract an enveloped JSON payload for further extracting the key and bucket from SQS messages. This is specifically useful when subscribing an SQS queue to an SNS topic that receives bucket events. - - -Type: `string` -Default: `""` - -```yml -# Examples - -envelope_path: Message -``` - -### `sqs.delay_period` - -An optional period of time to wait from when a notification was originally sent to when the target key download is attempted. - - -Type: `string` -Default: `""` - -```yml -# Examples - -delay_period: 10s - -delay_period: 5m -``` - -### `sqs.max_messages` - -The maximum number of SQS messages to consume from each request. - - -Type: `int` -Default: `10` - -### `sqs.wait_time_seconds` - -Whether to set the wait time. Enabling this activates long-polling. Valid values: 0 to 20. - - -Type: `int` -Default: `0` - - diff --git a/website/docs/components/inputs/aws_sqs.md b/website/docs/components/inputs/aws_sqs.md deleted file mode 100644 index ed1a2a4243..0000000000 --- a/website/docs/components/inputs/aws_sqs.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: aws_sqs -slug: aws_sqs -type: input -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consume messages from an AWS SQS URL. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - aws_sqs: - url: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - aws_sqs: - url: "" # No default (required) - delete_message: true - reset_visibility: true - max_number_of_messages: 10 - wait_time_seconds: 0 - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS -services. It's also possible to set them explicitly at the component level, -allowing you to transfer data across accounts. You can find out more -[in this document](/docs/guides/cloud/aws). - -### Metadata - -This input adds the following metadata fields to each message: - -```text -- sqs_message_id -- sqs_receipt_handle -- sqs_approximate_receive_count -- All message attributes -``` - -You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `url` - -The SQS URL to consume from. - - -Type: `string` - -### `delete_message` - -Whether to delete the consumed message once it is acked. Disabling allows you to handle the deletion using a different mechanism. - - -Type: `bool` -Default: `true` - -### `reset_visibility` - -Whether to set the visibility timeout of the consumed message to zero once it is nacked. Disabling honors the preset visibility timeout specified for the queue. - - -Type: `bool` -Default: `true` -Requires version 3.58.0 or newer - -### `max_number_of_messages` - -The maximum number of messages to return on one poll. Valid values: 1 to 10. - - -Type: `int` -Default: `10` - -### `wait_time_seconds` - -Whether to set the wait time. Enabling this activates long-polling. Valid values: 0 to 20. - - -Type: `int` -Default: `0` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/inputs/azure_blob_storage.md b/website/docs/components/inputs/azure_blob_storage.md deleted file mode 100644 index c6f4914049..0000000000 --- a/website/docs/components/inputs/azure_blob_storage.md +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: azure_blob_storage -slug: azure_blob_storage -type: input -status: beta -categories: ["Services","Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Downloads objects within an Azure Blob Storage container, optionally filtered by a prefix. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - azure_blob_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - container: "" # No default (required) - prefix: "" - scanner: - to_the_end: {} - targets_input: null # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - azure_blob_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - container: "" # No default (required) - prefix: "" - scanner: - to_the_end: {} - delete_objects: false - targets_input: null # No default (optional) -``` - - - - -Supports multiple authentication methods but only one of the following is required: -- `storage_connection_string` -- `storage_account` and `storage_access_key` -- `storage_account` and `storage_sas_token` -- `storage_account` to access via [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) - -If multiple are set then the `storage_connection_string` is given priority. - -If the `storage_connection_string` does not contain the `AccountName` parameter, please specify it in the -`storage_account` field. - -## Downloading Large Files - -When downloading large files it's often necessary to process it in streamed parts in order to avoid loading the entire file in memory at a given time. In order to do this a [`scanner`](#scanner) can be specified that determines how to break the input into smaller individual messages. - -## Streaming New Files - -By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as [an Event Grid source](https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage) and then use this as a [`targets_input`](#targetsinput), in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. - -## Metadata - -This input adds the following metadata fields to each message: - -``` -- blob_storage_key -- blob_storage_container -- blob_storage_last_modified -- blob_storage_last_modified_unix -- blob_storage_content_type -- blob_storage_content_encoding -- All user defined metadata -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `storage_account` - -The storage account to access. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_access_key` - -The storage account access key. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_connection_string` - -A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. - - -Type: `string` -Default: `""` - -### `storage_sas_token` - -The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. - - -Type: `string` -Default: `""` - -### `container` - -The name of the container from which to download blobs. - - -Type: `string` - -### `prefix` - -An optional path prefix, if set only objects with the prefix are consumed. - - -Type: `string` -Default: `""` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"to_the_end":{}}` -Requires version 4.25.0 or newer - -### `delete_objects` - -Whether to delete downloaded objects from the blob once they are processed. - - -Type: `bool` -Default: `false` - -### `targets_input` - -EXPERIMENTAL: An optional source of download targets, configured as a [regular Benthos input](/docs/components/inputs/about). Each message yielded by this input should be a single structured object containing a field `name`, which represents the blob to be downloaded. - - -Type: `input` -Requires version 4.27.0 or newer - -```yml -# Examples - -targets_input: - mqtt: - topics: - - some-topic - urls: - - example.westeurope-1.ts.eventgrid.azure.net:8883 - processors: - - unarchive: - format: json_array - - mapping: |- - if this.eventType == "Microsoft.Storage.BlobCreated" { - root.name = this.data.url.parse_url().path.trim_prefix("/foocontainer/") - } else { - root = deleted() - } -``` - - diff --git a/website/docs/components/inputs/azure_cosmosdb.md b/website/docs/components/inputs/azure_cosmosdb.md deleted file mode 100644 index 1f0848605d..0000000000 --- a/website/docs/components/inputs/azure_cosmosdb.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: azure_cosmosdb -slug: azure_cosmosdb -type: input -status: experimental -categories: ["Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a SQL query against [Azure CosmosDB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) and creates a batch of messages from each page of items. - -Introduced in version v4.25.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - azure_cosmosdb: - endpoint: https://localhost:8081 # No default (optional) - account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) - connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) - database: testdb # No default (required) - container: testcontainer # No default (required) - partition_keys_map: root = "blobfish" # No default (required) - query: SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp # No default (required) - args_mapping: |- # No default (optional) - root = [ - { "Name": "@name", "Value": "benthos" }, - ] - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - azure_cosmosdb: - endpoint: https://localhost:8081 # No default (optional) - account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) - connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) - database: testdb # No default (required) - container: testcontainer # No default (required) - partition_keys_map: root = "blobfish" # No default (required) - query: SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp # No default (required) - args_mapping: |- # No default (optional) - root = [ - { "Name": "@name", "Value": "benthos" }, - ] - batch_count: -1 - auto_replay_nacks: true -``` - - - - -## Cross-partition Queries - -Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey value(s) must be known in advance and specified in the config. See details [here](https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989). - - -## Credentials - -You can use one of the following authentication mechanisms: - -- Set the `endpoint` field and the `account_key` field -- Set only the `endpoint` field to use [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) -- Set the `connection_string` field - - -## Metadata - -This component adds the following metadata fields to each message: -``` -- activity_id -- request_charge -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - - -## Examples - - - - - -Execute a parametrized SQL query to select documents from a container. - -```yaml -input: - azure_cosmosdb: - endpoint: http://localhost:8080 - account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== - database: blobbase - container: blobfish - partition_keys_map: root = "AbyssalPlain" - query: SELECT * FROM blobfish AS b WHERE b.species = @species - args_mapping: | - root = [ - { "Name": "@species", "Value": "smooth-head" }, - ] -``` - - - - -## Fields - -### `endpoint` - -CosmosDB endpoint. - - -Type: `string` - -```yml -# Examples - -endpoint: https://localhost:8081 -``` - -### `account_key` - -Account key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -```yml -# Examples - -account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== -``` - -### `connection_string` - -Connection string. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -```yml -# Examples - -connection_string: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; -``` - -### `database` - -Database. - - -Type: `string` - -```yml -# Examples - -database: testdb -``` - -### `container` - -Container. - - -Type: `string` - -```yml -# Examples - -container: testcontainer -``` - -### `partition_keys_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided. - - -Type: `string` - -```yml -# Examples - -partition_keys_map: root = "blobfish" - -partition_keys_map: root = 41 - -partition_keys_map: root = true - -partition_keys_map: root = null - -partition_keys_map: root = now().ts_format("2006-01-02") -``` - -### `query` - -The query to execute - - -Type: `string` - -```yml -# Examples - -query: SELECT c.foo FROM testcontainer AS c WHERE c.bar = "baz" AND c.timestamp < @timestamp -``` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) that, for each message, creates a list of arguments to use with the query. - - -Type: `string` - -```yml -# Examples - -args_mapping: |- - root = [ - { "Name": "@name", "Value": "benthos" }, - ] -``` - -### `batch_count` - -The maximum number of messages that should be accumulated into each batch. Use '-1' specify dynamic page size. - - -Type: `int` -Default: `-1` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - -## CosmosDB Emulator - -If you wish to run the CosmosDB emulator that is referenced in the documentation [here](https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator), the following Docker command should do the trick: - -```shell -> docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator -``` - -Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. - -Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run [mitmproxy](https://mitmproxy.org/) like so: - -```shell -> mitmproxy -k --mode "reverse:https://localhost:8081" -``` - -Then you can access the CosmosDB UI via `http://localhost:8080/_explorer/index.html` and use `http://localhost:8080` as the CosmosDB endpoint. - - diff --git a/website/docs/components/inputs/azure_queue_storage.md b/website/docs/components/inputs/azure_queue_storage.md deleted file mode 100644 index 6ccd0e47d8..0000000000 --- a/website/docs/components/inputs/azure_queue_storage.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: azure_queue_storage -slug: azure_queue_storage -type: input -status: beta -categories: ["Services","Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Dequeue objects from an Azure Storage Queue. - -Introduced in version 3.42.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - azure_queue_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - queue_name: foo_queue # No default (required) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - azure_queue_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - queue_name: foo_queue # No default (required) - dequeue_visibility_timeout: 30s - max_in_flight: 10 - track_properties: false -``` - - - - -This input adds the following metadata fields to each message: - -``` -- queue_storage_insertion_time -- queue_storage_queue_name -- queue_storage_message_lag (if 'track_properties' set to true) -- All user defined queue metadata -``` - -Only one authentication method is required, `storage_connection_string` or `storage_account` and `storage_access_key`. If both are set then the `storage_connection_string` is given priority. - -## Fields - -### `storage_account` - -The storage account to access. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_access_key` - -The storage account access key. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_connection_string` - -A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. - - -Type: `string` -Default: `""` - -### `queue_name` - -The name of the source storage queue. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -queue_name: foo_queue - -queue_name: ${! env("MESSAGE_TYPE").lowercase() } -``` - -### `dequeue_visibility_timeout` - -The timeout duration until a dequeued message gets visible again, 30s by default - - -Type: `string` -Default: `"30s"` -Requires version 3.45.0 or newer - -### `max_in_flight` - -The maximum number of unprocessed messages to fetch at a given time. - - -Type: `int` -Default: `10` - -### `track_properties` - -If set to `true` the queue is polled on each read request for information such as the queue message lag. These properties are added to consumed messages as metadata, but will also have a negative performance impact. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/inputs/azure_table_storage.md b/website/docs/components/inputs/azure_table_storage.md deleted file mode 100644 index afdc969be5..0000000000 --- a/website/docs/components/inputs/azure_table_storage.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: azure_table_storage -slug: azure_table_storage -type: input -status: beta -categories: ["Services","Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Queries an Azure Storage Account Table, optionally with multiple filters. - -Introduced in version 4.10.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - azure_table_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - table_name: Foo # No default (required) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - azure_table_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - table_name: Foo # No default (required) - filter: "" - select: "" - page_size: 1000 -``` - - - - -Queries an Azure Storage Account Table, optionally with multiple filters. -## Metadata -This input adds the following metadata fields to each message: -``` -- table_storage_name -- row_num -``` -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `storage_account` - -The storage account to access. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_access_key` - -The storage account access key. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_connection_string` - -A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. - - -Type: `string` -Default: `""` - -### `storage_sas_token` - -The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. - - -Type: `string` -Default: `""` - -### `table_name` - -The table to read messages from. - - -Type: `string` - -```yml -# Examples - -table_name: Foo -``` - -### `filter` - -OData filter expression. Is not set all rows are returned. Valid operators are `eq, ne, gt, lt, ge and le` - - -Type: `string` -Default: `""` - -```yml -# Examples - -filter: PartitionKey eq 'foo' and RowKey gt '1000' -``` - -### `select` - -Select expression using OData notation. Limits the columns on each record to just those requested. - - -Type: `string` -Default: `""` - -```yml -# Examples - -select: PartitionKey,RowKey,Foo,Bar,Timestamp -``` - -### `page_size` - -Maximum number of records to return on each page. - - -Type: `int` -Default: `1000` - - diff --git a/website/docs/components/inputs/batched.md b/website/docs/components/inputs/batched.md deleted file mode 100644 index eabfe0d687..0000000000 --- a/website/docs/components/inputs/batched.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: batched -slug: batched -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes data from a child input and applies a batching policy to the stream. - -Introduced in version 4.11.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - batched: - child: null # No default (required) - policy: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - batched: - child: null # No default (required) - policy: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Batching at the input level is sometimes useful for processing across micro-batches, and can also sometimes be a useful performance trick. However, most inputs are fine without it so unless you have a specific plan for batching this component is not worth using. - -## Fields - -### `child` - -The child input. - - -Type: `input` - -### `policy` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -policy: - byte_size: 5000 - count: 0 - period: 1s - -policy: - count: 10 - period: 1s - -policy: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `policy.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `policy.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `policy.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `policy.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `policy.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/inputs/beanstalkd.md b/website/docs/components/inputs/beanstalkd.md deleted file mode 100644 index 6fe57bf7ec..0000000000 --- a/website/docs/components/inputs/beanstalkd.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: beanstalkd -slug: beanstalkd -type: input -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Reads messages from a Beanstalkd queue. - -Introduced in version 4.7.0. - -```yml -# Config fields, showing default values -input: - label: "" - beanstalkd: - address: 127.0.0.1:11300 # No default (required) -``` - -## Fields - -### `address` - -An address to connect to. - - -Type: `string` - -```yml -# Examples - -address: 127.0.0.1:11300 -``` - - diff --git a/website/docs/components/inputs/broker.md b/website/docs/components/inputs/broker.md deleted file mode 100644 index 3067b2236b..0000000000 --- a/website/docs/components/inputs/broker.md +++ /dev/null @@ -1,215 +0,0 @@ ---- -title: broker -slug: broker -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Allows you to combine multiple inputs into a single stream of data, where each input will be read in parallel. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - broker: - inputs: [] # No default (required) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - broker: - copies: 1 - inputs: [] # No default (required) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -A broker type is configured with its own list of input configurations and a field to specify how many copies of the list of inputs should be created. - -Adding more input types allows you to combine streams from multiple sources into one. For example, reading from both RabbitMQ and Kafka: - -```yaml -input: - broker: - copies: 1 - inputs: - - amqp_0_9: - urls: - - amqp://guest:guest@localhost:5672/ - consumer_tag: benthos-consumer - queue: benthos-queue - - # Optional list of input specific processing steps - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() - - - kafka: - addresses: - - localhost:9092 - client_id: benthos_kafka_input - consumer_group: benthos_consumer_group - topics: [ benthos_stream:0 ] -``` - -If the number of copies is greater than zero the list will be copied that number of times. For example, if your inputs were of type foo and bar, with 'copies' set to '2', you would end up with two 'foo' inputs and two 'bar' inputs. - -### Batching - -It's possible to configure a [batch policy](/docs/configuration/batching#batch-policy) with a broker using the `batching` fields. When doing this the feeds from all child inputs are combined. Some inputs do not support broker based batching and specify this in their documentation. - -### Processors - -It is possible to configure [processors](/docs/components/processors/about) at the broker level, where they will be applied to _all_ child inputs, as well as on the individual child inputs. If you have processors at both the broker level _and_ on child inputs then the broker processors will be applied _after_ the child nodes processors. - -## Fields - -### `copies` - -Whatever is specified within `inputs` will be created this many times. - - -Type: `int` -Default: `1` - -### `inputs` - -A list of inputs to create. - - -Type: `array` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/inputs/cassandra.md b/website/docs/components/inputs/cassandra.md deleted file mode 100644 index 75e1e540df..0000000000 --- a/website/docs/components/inputs/cassandra.md +++ /dev/null @@ -1,369 +0,0 @@ ---- -title: cassandra -slug: cassandra -type: input -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a find query and creates a message for each row received. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - cassandra: - addresses: [] # No default (required) - timeout: 600ms - query: "" # No default (required) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - cassandra: - addresses: [] # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - password_authenticator: - enabled: false - username: "" - password: "" - disable_initial_host_lookup: false - max_retries: 3 - backoff: - initial_interval: 1s - max_interval: 5s - timeout: 600ms - query: "" # No default (required) - auto_replay_nacks: true -``` - - - - -## Examples - - - - - - -Let's presume that we have 3 Cassandra nodes, like in this tutorial by Sebastian Sigl from freeCodeCamp: - -https://www.freecodecamp.org/news/the-apache-cassandra-beginner-tutorial/ - -Then if we want to select everything from the table users_by_country, we should use the configuration below. -If we specify the stdin output, the result will look like: - -```json -{"age":23,"country":"UK","first_name":"Bob","last_name":"Sandler","user_email":"bob@email.com"} -``` - -This configuration also works for Scylla. - - -```yaml -input: - cassandra: - addresses: - - 172.17.0.2 - query: - 'SELECT * FROM learn_cassandra.users_by_country' -``` - - - - -## Fields - -### `addresses` - -A list of Cassandra nodes to connect to. Multiple comma separated addresses can be specified on a single line. - - -Type: `array` - -```yml -# Examples - -addresses: - - localhost:9042 - -addresses: - - foo:9042 - - bar:9042 - -addresses: - - foo:9042,bar:9042 -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `password_authenticator` - -Optional configuration of Cassandra authentication parameters. - - -Type: `object` - -### `password_authenticator.enabled` - -Whether to use password authentication - - -Type: `bool` -Default: `false` - -### `password_authenticator.username` - -The username to authenticate as. - - -Type: `string` -Default: `""` - -### `password_authenticator.password` - -The password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `disable_initial_host_lookup` - -If enabled the driver will not attempt to get host info from the system.peers table. This can speed up queries but will mean that data_centre, rack and token information will not be available. - - -Type: `bool` -Default: `false` - -### `max_retries` - -The maximum number of retries before giving up on a request. - - -Type: `int` -Default: `3` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `timeout` - -The client connection timeout. - - -Type: `string` -Default: `"600ms"` - -### `query` - -A query to execute. - - -Type: `string` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/cockroachdb_changefeed.md b/website/docs/components/inputs/cockroachdb_changefeed.md deleted file mode 100644 index 96deee823f..0000000000 --- a/website/docs/components/inputs/cockroachdb_changefeed.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -title: cockroachdb_changefeed -slug: cockroachdb_changefeed -type: input -status: experimental -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Listens to a [CockroachDB Core Changefeed](https://www.cockroachlabs.com/docs/stable/changefeed-examples) and creates a message for each row received. Each message is a json object looking like: -```json -{ - "primary_key": "[\"1a7ff641-3e3b-47ee-94fe-a0cadb56cd8f\", 2]", // stringifed JSON array - "row": "{\"after\": {\"k\": \"1a7ff641-3e3b-47ee-94fe-a0cadb56cd8f\", \"v\": 2}, \"updated\": \"1637953249519902405.0000000000\"}", // stringified JSON object - "table": "strm_2" -} -``` - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - cockroachdb_changefeed: - dsn: postgres://user:password@example.com:26257/defaultdb?sslmode=require # No default (required) - tables: [] # No default (required) - cursor_cache: "" # No default (optional) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - cockroachdb_changefeed: - dsn: postgres://user:password@example.com:26257/defaultdb?sslmode=require # No default (required) - tls: - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - tables: [] # No default (required) - cursor_cache: "" # No default (optional) - options: [] # No default (optional) - auto_replay_nacks: true -``` - - - - -This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered. - -Note: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to [the official CockroachDB documentation.](https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core) - -## Fields - -### `dsn` - -A Data Source Name to identify the target database. - - -Type: `string` - -```yml -# Examples - -dsn: postgres://user:password@example.com:26257/defaultdb?sslmode=require -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `tables` - -CSV of tables to be included in the changefeed - - -Type: `array` - -```yml -# Examples - -tables: - - table1 - - table2 -``` - -### `cursor_cache` - -A [cache resource](https://www.benthos.dev/docs/components/caches/about) to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table. - - -Type: `string` - -### `options` - -A list of options to be included in the changefeed (WITH X, Y...). -**NOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case.** - - -Type: `array` - -```yml -# Examples - -options: - - virtual_columns="omitted" -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/csv.md b/website/docs/components/inputs/csv.md deleted file mode 100644 index 7a26e6f5a0..0000000000 --- a/website/docs/components/inputs/csv.md +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: csv -slug: csv -type: input -status: stable -categories: ["Local"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Reads one or more CSV files as structured records following the format described in RFC 4180. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - csv: - paths: [] # No default (required) - parse_header_row: true - delimiter: ',' - lazy_quotes: false - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - csv: - paths: [] # No default (required) - parse_header_row: true - delimiter: ',' - lazy_quotes: false - delete_on_finish: false - batch_count: 1 - auto_replay_nacks: true -``` - - - - -This input offers more control over CSV parsing than the [`file` input](/docs/components/inputs/file). - -When parsing with a header row each line of the file will be consumed as a structured object, where the key names are determined from the header now. For example, the following CSV file: - -```csv -foo,bar,baz -first foo,first bar,first baz -second foo,second bar,second baz -``` - -Would produce the following messages: - -```json -{"foo":"first foo","bar":"first bar","baz":"first baz"} -{"foo":"second foo","bar":"second bar","baz":"second baz"} -``` - -If, however, the field `parse_header_row` is set to `false` then arrays are produced instead, like follows: - -```json -["first foo","first bar","first baz"] -["second foo","second bar","second baz"] -``` - -### Metadata - -This input adds the following metadata fields to each message: - -```text -- header -- path -- mod_time_unix -- mod_time (RFC3339) -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -Note: The `header` field is only set when `parse_header_row` is `true`. - -### Output CSV column order - -When [creating CSV](/docs/guides/bloblang/advanced#creating-csv) from Benthos messages, the columns must be sorted lexicographically to make the output deterministic. Alternatively, when using the `csv` input, one can leverage the `header` metadata field to retrieve the column order: - -```yaml -input: - csv: - paths: - - ./foo.csv - - ./bar.csv - parse_header_row: true - - processors: - - mapping: | - map escape_csv { - root = if this.re_match("[\"\n,]+") { - "\"" + this.replace_all("\"", "\"\"") + "\"" - } else { - this - } - } - - let header = if count(@path) == 1 { - @header.map_each(c -> c.apply("escape_csv")).join(",") + "\n" - } else { "" } - - root = $header + @header.map_each(c -> this.get(c).string().apply("escape_csv")).join(",") - -output: - file: - path: ./output/${! @path.filepath_split().index(-1) } -``` - - -## Fields - -### `paths` - -A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star). - - -Type: `array` - -```yml -# Examples - -paths: - - /tmp/foo.csv - - /tmp/bar/*.csv - - /tmp/data/**/*.csv -``` - -### `parse_header_row` - -Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, each message will consist of an array of values from the corresponding CSV row. - - -Type: `bool` -Default: `true` - -### `delimiter` - -The delimiter to use for splitting values in each record. It must be a single character. - - -Type: `string` -Default: `","` - -### `lazy_quotes` - -If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field. - - -Type: `bool` -Default: `false` -Requires version 4.1.0 or newer - -### `delete_on_finish` - -Whether to delete input files from the disk once they are fully consumed. - - -Type: `bool` -Default: `false` - -### `batch_count` - -Optionally process records in batches. This can help to speed up the consumption of exceptionally large CSV files. When the end of the file is reached the remaining records are processed as a (potentially smaller) batch. - - -Type: `int` -Default: `1` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -This input is particularly useful when consuming CSV from files too large to parse entirely within memory. However, in cases where CSV is consumed from other input types it's also possible to parse them using the [Bloblang `parse_csv` method](/docs/guides/bloblang/methods#parse_csv). - diff --git a/website/docs/components/inputs/discord.md b/website/docs/components/inputs/discord.md deleted file mode 100644 index f7b2b9f3d8..0000000000 --- a/website/docs/components/inputs/discord.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: discord -slug: discord -type: input -status: experimental -categories: ["Services","Social"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Consumes messages posted in a Discord channel. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - discord: - channel_id: "" # No default (required) - bot_token: "" # No default (required) - cache: "" # No default (required) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - discord: - channel_id: "" # No default (required) - bot_token: "" # No default (required) - cache: "" # No default (required) - cache_key: last_message_id - auto_replay_nacks: true -``` - - - - -This input works by authenticating as a bot using token based authentication. The ID of the newest message consumed and acked is stored in a cache in order to perform a backfill of unread messages each time the input is initialised. Ideally this cache should be persisted across restarts. - -## Fields - -### `channel_id` - -A discord channel ID to consume messages from. - - -Type: `string` - -### `bot_token` - -A bot token used for authentication. - - -Type: `string` - -### `cache` - -A cache resource to use for performing unread message backfills, the ID of the last message received will be stored in this cache and used for subsequent requests. - - -Type: `string` - -### `cache_key` - -The key identifier used when storing the ID of the last message received. - - -Type: `string` -Default: `"last_message_id"` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/dynamic.md b/website/docs/components/inputs/dynamic.md deleted file mode 100644 index 4196169727..0000000000 --- a/website/docs/components/inputs/dynamic.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: dynamic -slug: dynamic -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A special broker type where the inputs are identified by unique labels and can be created, changed and removed during runtime via a REST HTTP interface. - -```yml -# Config fields, showing default values -input: - label: "" - dynamic: - inputs: {} - prefix: "" -``` - -## Fields - -### `inputs` - -A map of inputs to statically create. - - -Type: `object` -Default: `{}` - -### `prefix` - -A path prefix for HTTP endpoints that are registered. - - -Type: `string` -Default: `""` - -## Endpoints - -### GET `/inputs` - -Returns a JSON object detailing all dynamic inputs, providing information such as their current uptime and configuration. - -### GET `/inputs/{id}` - -Returns the configuration of an input. - -### POST `/inputs/{id}` - -Creates or updates an input with a configuration provided in the request body (in YAML or JSON format). - -### DELETE `/inputs/{id}` - -Stops and removes an input. - -### GET `/inputs/{id}/uptime` - -Returns the uptime of an input as a duration string (of the form "72h3m0.5s"), or "stopped" in the case where the input has gracefully terminated. - diff --git a/website/docs/components/inputs/file.md b/website/docs/components/inputs/file.md deleted file mode 100644 index 4f5e09abb1..0000000000 --- a/website/docs/components/inputs/file.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: file -slug: file -type: input -status: stable -categories: ["Local"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes data from files on disk, emitting messages according to a chosen codec. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - file: - paths: [] # No default (required) - scanner: - lines: {} - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - file: - paths: [] # No default (required) - scanner: - lines: {} - delete_on_finish: false - auto_replay_nacks: true -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -```text -- path -- mod_time_unix -- mod_time (RFC3339) -``` - -You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `paths` - -A list of paths to consume sequentially. Glob patterns are supported, including super globs (double star). - - -Type: `array` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"lines":{}}` -Requires version 4.25.0 or newer - -### `delete_on_finish` - -Whether to delete input files from the disk once they are fully consumed. - - -Type: `bool` -Default: `false` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -## Examples - - - - - -If we wished to consume a directory of CSV files as structured documents we can use a glob pattern and the `csv` scanner: - -```yaml -input: - file: - paths: [ ./data/*.csv ] - scanner: - csv: {} -``` - - - - - diff --git a/website/docs/components/inputs/gcp_bigquery_select.md b/website/docs/components/inputs/gcp_bigquery_select.md deleted file mode 100644 index 95342d6626..0000000000 --- a/website/docs/components/inputs/gcp_bigquery_select.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: gcp_bigquery_select -slug: gcp_bigquery_select -type: input -status: beta -categories: ["Services","GCP"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Executes a `SELECT` query against BigQuery and creates a message for each row received. - -Introduced in version 3.63.0. - -```yml -# Config fields, showing default values -input: - label: "" - gcp_bigquery_select: - project: "" # No default (required) - table: bigquery-public-data.samples.shakespeare # No default (required) - columns: [] # No default (required) - where: type = ? and created_at > ? # No default (optional) - auto_replay_nacks: true - job_labels: {} - priority: "" - args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) - prefix: "" # No default (optional) - suffix: "" # No default (optional) -``` - -Once the rows from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute). - -## Examples - - - - - - -Here we query the public corpus of Shakespeare's works to generate a stream of the top 10 words that are 3 or more characters long: - -```yaml -input: - gcp_bigquery_select: - project: sample-project - table: bigquery-public-data.samples.shakespeare - columns: - - word - - sum(word_count) as total_count - where: length(word) >= ? - suffix: | - GROUP BY word - ORDER BY total_count DESC - LIMIT 10 - args_mapping: | - root = [ 3 ] -``` - - - - -## Fields - -### `project` - -GCP project where the query job will execute. - - -Type: `string` - -### `table` - -Fully-qualified BigQuery table name to query. - - -Type: `string` - -```yml -# Examples - -table: bigquery-public-data.samples.shakespeare -``` - -### `columns` - -A list of columns to query. - - -Type: `array` - -### `where` - -An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks (`?`). - - -Type: `string` - -```yml -# Examples - -where: type = ? and created_at > ? - -where: user_id = ? -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `job_labels` - -A list of labels to add to the query job. - - -Type: `object` -Default: `{}` - -### `priority` - -The priority with which to schedule the query. - - -Type: `string` -Default: `""` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ "article", now().ts_format("2006-01-02") ] -``` - -### `prefix` - -An optional prefix to prepend to the select query (before SELECT). - - -Type: `string` - -### `suffix` - -An optional suffix to append to the select query. - - -Type: `string` - - diff --git a/website/docs/components/inputs/gcp_cloud_storage.md b/website/docs/components/inputs/gcp_cloud_storage.md deleted file mode 100644 index d9ec391f8b..0000000000 --- a/website/docs/components/inputs/gcp_cloud_storage.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: gcp_cloud_storage -slug: gcp_cloud_storage -type: input -status: beta -categories: ["Services","GCP"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Downloads objects within a Google Cloud Storage bucket, optionally filtered by a prefix. - -Introduced in version 3.43.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - gcp_cloud_storage: - bucket: "" # No default (required) - prefix: "" - scanner: - to_the_end: {} -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - gcp_cloud_storage: - bucket: "" # No default (required) - prefix: "" - scanner: - to_the_end: {} - delete_objects: false -``` - - - - -## Metadata - -This input adds the following metadata fields to each message: - -``` -- gcs_key -- gcs_bucket -- gcs_last_modified -- gcs_last_modified_unix -- gcs_content_type -- gcs_content_encoding -- All user defined metadata -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -### Credentials - -By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more [in this document](/docs/guides/cloud/gcp). - -## Fields - -### `bucket` - -The name of the bucket from which to download objects. - - -Type: `string` - -### `prefix` - -An optional path prefix, if set only objects with the prefix are consumed. - - -Type: `string` -Default: `""` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"to_the_end":{}}` -Requires version 4.25.0 or newer - -### `delete_objects` - -Whether to delete downloaded objects from the bucket once they are processed. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/inputs/gcp_pubsub.md b/website/docs/components/inputs/gcp_pubsub.md deleted file mode 100644 index 49481340e5..0000000000 --- a/website/docs/components/inputs/gcp_pubsub.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: gcp_pubsub -slug: gcp_pubsub -type: input -status: stable -categories: ["Services","GCP"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes messages from a GCP Cloud Pub/Sub subscription. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - gcp_pubsub: - project: "" # No default (required) - subscription: "" # No default (required) - endpoint: "" - sync: false - max_outstanding_messages: 1000 - max_outstanding_bytes: 1e+09 -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - gcp_pubsub: - project: "" # No default (required) - subscription: "" # No default (required) - endpoint: "" - sync: false - max_outstanding_messages: 1000 - max_outstanding_bytes: 1e+09 - create_subscription: - enabled: false - topic: "" -``` - - - - -For information on how to set up credentials check out [this guide](https://cloud.google.com/docs/authentication/production). - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- gcp_pubsub_publish_time_unix - The time at which the message was published to the topic. -- gcp_pubsub_delivery_attempt - When dead lettering is enabled, this is set to the number of times PubSub has attempted to deliver a message. -- All message attributes -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - - -## Fields - -### `project` - -The project ID of the target subscription. - - -Type: `string` - -### `subscription` - -The target subscription ID. - - -Type: `string` - -### `endpoint` - -An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values check out [this document.](https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints) - - -Type: `string` -Default: `""` - -```yml -# Examples - -endpoint: us-central1-pubsub.googleapis.com:443 - -endpoint: us-west3-pubsub.googleapis.com:443 -``` - -### `sync` - -Enable synchronous pull mode. - - -Type: `bool` -Default: `false` - -### `max_outstanding_messages` - -The maximum number of outstanding pending messages to be consumed at a given time. - - -Type: `int` -Default: `1000` - -### `max_outstanding_bytes` - -The maximum number of outstanding pending messages to be consumed measured in bytes. - - -Type: `int` -Default: `1000000000` - -### `create_subscription` - -Allows you to configure the input subscription and creates if it doesn't exist. - - -Type: `object` - -### `create_subscription.enabled` - -Whether to configure subscription or not. - - -Type: `bool` -Default: `false` - -### `create_subscription.topic` - -Defines the topic that the subscription should be vinculated to. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/inputs/generate.md b/website/docs/components/inputs/generate.md deleted file mode 100644 index ecc1ca87d2..0000000000 --- a/website/docs/components/inputs/generate.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: generate -slug: generate -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Generates messages at a given interval using a [Bloblang](/docs/guides/bloblang/about) mapping executed without a context. This allows you to generate messages for testing your pipeline configs. - -Introduced in version 3.40.0. - -```yml -# Config fields, showing default values -input: - label: "" - generate: - mapping: root = "hello world" # No default (required) - interval: 1s - count: 0 - batch_size: 1 - auto_replay_nacks: true -``` - -## Examples - - - - - -A common use case for the generate input is to trigger processors on a schedule so that the processors themselves can behave similarly to an input. The following configuration reads rows from a PostgreSQL table every 5 minutes. - -```yaml -input: - generate: - interval: '@every 5m' - mapping: 'root = {}' - processors: - - sql_select: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - table: foo - columns: [ "*" ] -``` - - - - -The generate input can be used as a convenient way to generate test data. The following example generates 100 rows of structured data by setting an explicit count. The interval field is set to empty, which means data is generated as fast as the downstream components can consume it. - -```yaml -input: - generate: - count: 100 - interval: "" - mapping: | - root = if random_int() % 2 == 0 { - { - "type": "foo", - "foo": "is yummy" - } - } else { - { - "type": "bar", - "bar": "is gross" - } - } -``` - - - - -## Fields - -### `mapping` - -A [bloblang](/docs/guides/bloblang/about) mapping to use for generating messages. - - -Type: `string` - -```yml -# Examples - -mapping: root = "hello world" - -mapping: root = {"test":"message","id":uuid_v4()} -``` - -### `interval` - -The time interval at which messages should be generated, expressed either as a duration string or as a cron expression. If set to an empty string messages will be generated as fast as downstream services can process them. Cron expressions can specify a timezone by prefixing the expression with `TZ=`, where the location name corresponds to a file within the IANA Time Zone database. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -interval: 5s - -interval: 1m - -interval: 1h - -interval: '@every 1s' - -interval: 0,30 */2 * * * * - -interval: TZ=Europe/London 30 3-6,20-23 * * * -``` - -### `count` - -An optional number of messages to generate, if set above 0 the specified number of messages is generated and then the input will shut down. - - -Type: `int` -Default: `0` - -### `batch_size` - -The number of generated messages that should be accumulated into each batch flushed at the specified interval. - - -Type: `int` -Default: `1` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/hdfs.md b/website/docs/components/inputs/hdfs.md deleted file mode 100644 index 20dca4fb48..0000000000 --- a/website/docs/components/inputs/hdfs.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: hdfs -slug: hdfs -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Reads files from a HDFS directory, where each discrete file will be consumed as a single message payload. - -```yml -# Config fields, showing default values -input: - label: "" - hdfs: - hosts: [] # No default (required) - user: "" - directory: "" # No default (required) -``` - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- hdfs_name -- hdfs_path -``` - -You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `hosts` - -A list of target host addresses to connect to. - - -Type: `array` - -```yml -# Examples - -hosts: localhost:9000 -``` - -### `user` - -A user ID to connect as. - - -Type: `string` -Default: `""` - -### `directory` - -The directory to consume from. - - -Type: `string` - - diff --git a/website/docs/components/inputs/http_client.md b/website/docs/components/inputs/http_client.md deleted file mode 100644 index 48bd26e00f..0000000000 --- a/website/docs/components/inputs/http_client.md +++ /dev/null @@ -1,783 +0,0 @@ ---- -title: http_client -slug: http_client -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to a server and continuously performs requests for a single message. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - http_client: - url: "" # No default (required) - verb: GET - headers: {} - rate_limit: "" # No default (optional) - timeout: 5s - payload: "" # No default (optional) - stream: - enabled: false - reconnect: true - scanner: - lines: {} - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - http_client: - url: "" # No default (required) - verb: GET - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - dump_request_log_level: "" - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - oauth2: - enabled: false - client_key: "" - client_secret: "" - token_url: "" - scopes: [] - endpoint_params: {} - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - extract_headers: - include_prefixes: [] - include_patterns: [] - rate_limit: "" # No default (optional) - timeout: 5s - retry_period: 1s - max_retry_backoff: 300s - retries: 3 - backoff_on: - - 429 - drop_on: [] - successful_on: [] - proxy_url: "" # No default (optional) - payload: "" # No default (optional) - drop_empty_bodies: true - stream: - enabled: false - reconnect: true - scanner: - lines: {} - auto_replay_nacks: true -``` - - - - -The URL and header values of this type can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -### Streaming - -If you enable streaming then Benthos will consume the body of the response as a continuous stream of data, breaking messages out following a chosen scanner. This allows you to consume APIs that provide long lived streamed data feeds (such as Twitter). - -### Pagination - -This input supports interpolation functions in the `url` and `headers` fields where data from the previous successfully consumed message (if there was one) can be referenced. This can be used in order to support basic levels of pagination. However, in cases where pagination depends on logic it is recommended that you use an [`http` processor](/docs/components/processors/http) instead, often combined with a [`generate` input](/docs/components/inputs/generate) in order to schedule the processor. - -## Examples - - - - - -Interpolation functions within the `url` and `headers` fields can be used to reference the previously consumed message, which allows simple pagination. - -```yaml -input: - http_client: - url: >- - https://api.example.com/search?query=allmyfoos&start_time=${! ( - (timestamp_unix()-300).ts_format("2006-01-02T15:04:05Z","UTC").escape_url_query() - ) }${! ("&next_token="+this.meta.next_token.not_null()) | "" } - verb: GET - rate_limit: foo_searches - oauth2: - enabled: true - token_url: https://api.example.com/oauth2/token - client_key: "${EXAMPLE_KEY}" - client_secret: "${EXAMPLE_SECRET}" - -rate_limit_resources: - - label: foo_searches - local: - count: 1 - interval: 30s -``` - - - - -## Fields - -### `url` - -The URL to connect to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `verb` - -A verb to connect with - - -Type: `string` -Default: `"GET"` - -```yml -# Examples - -verb: POST - -verb: GET - -verb: DELETE -``` - -### `headers` - -A map of headers to add to the request. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -headers: - Content-Type: application/octet-stream - traceparent: ${! tracing_span().traceparent } -``` - -### `metadata` - -Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `dump_request_log_level` - -EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged. - - -Type: `string` -Default: `""` -Requires version 4.12.0 or newer -Options: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, ``. - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth2` - -Allows you to specify open authentication via OAuth version 2 using the client credentials token flow. - - -Type: `object` - -### `oauth2.enabled` - -Whether to use OAuth version 2 in requests. - - -Type: `bool` -Default: `false` - -### `oauth2.client_key` - -A value used to identify the client to the token provider. - - -Type: `string` -Default: `""` - -### `oauth2.client_secret` - -A secret used to establish ownership of the client key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth2.token_url` - -The URL of the token provider. - - -Type: `string` -Default: `""` - -### `oauth2.scopes` - -A list of optional requested permissions. - - -Type: `array` -Default: `[]` -Requires version 3.45.0 or newer - -### `oauth2.endpoint_params` - -A list of optional endpoint parameters, values should be arrays of strings. - - -Type: `object` -Default: `{}` -Requires version 4.21.0 or newer - -```yml -# Examples - -endpoint_params: - bar: - - woof - foo: - - meow - - quack -``` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `extract_headers` - -Specify which response headers should be added to resulting messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. - - -Type: `object` - -### `extract_headers.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `extract_headers.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `rate_limit` - -An optional [rate limit](/docs/components/rate_limits/about) to throttle requests by. - - -Type: `string` - -### `timeout` - -A static timeout to apply to requests. - - -Type: `string` -Default: `"5s"` - -### `retry_period` - -The base period to wait between failed requests. - - -Type: `string` -Default: `"1s"` - -### `max_retry_backoff` - -The maximum period to wait between failed requests. - - -Type: `string` -Default: `"300s"` - -### `retries` - -The maximum number of retry attempts to make. - - -Type: `int` -Default: `3` - -### `backoff_on` - -A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually. - - -Type: `array` -Default: `[429]` - -### `drop_on` - -A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped. - - -Type: `array` -Default: `[]` - -### `successful_on` - -A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field. - - -Type: `array` -Default: `[]` - -### `proxy_url` - -An optional HTTP proxy URL. - - -Type: `string` - -### `payload` - -An optional payload to deliver for each request. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `drop_empty_bodies` - -Whether empty payloads received from the target server should be dropped. - - -Type: `bool` -Default: `true` - -### `stream` - -Allows you to set streaming mode, where requests are kept open and messages are processed line-by-line. - - -Type: `object` - -### `stream.enabled` - -Enables streaming mode. - - -Type: `bool` -Default: `false` - -### `stream.reconnect` - -Sets whether to re-establish the connection once it is lost. - - -Type: `bool` -Default: `true` - -### `stream.scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"lines":{}}` -Requires version 4.25.0 or newer - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/http_server.md b/website/docs/components/inputs/http_server.md deleted file mode 100644 index 8a437285ad..0000000000 --- a/website/docs/components/inputs/http_server.md +++ /dev/null @@ -1,394 +0,0 @@ ---- -title: http_server -slug: http_server -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Receive messages POSTed over HTTP(S). HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - http_server: - address: "" - path: /post - ws_path: /post/ws - allowed_verbs: - - POST - timeout: 5s - rate_limit: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - http_server: - address: "" - path: /post - ws_path: /post/ws - ws_welcome_message: "" - ws_rate_limit_message: "" - allowed_verbs: - - POST - timeout: 5s - rate_limit: "" - cert_file: "" - key_file: "" - cors: - enabled: false - allowed_origins: [] - sync_response: - status: "200" - headers: - Content-Type: application/octet-stream - metadata_headers: - include_prefixes: [] - include_patterns: [] -``` - - - - -If the `address` config field is left blank the [service-wide HTTP server](/docs/components/http/about) will be used. - -The field `rate_limit` allows you to specify an optional [`rate_limit` resource](/docs/components/rate_limits/about), which will be applied to each HTTP request made and each websocket payload received. - -When the rate limit is breached HTTP requests will have a 429 response returned with a Retry-After header. Websocket payloads will be dropped and an optional response payload will be sent as per `ws_rate_limit_message`. - -### Responses - -It's possible to return a response for each message received using [synchronous responses](/docs/guides/sync_responses). When doing so you can customise headers with the `sync_response` field `headers`, which can also use [function interpolation](/docs/configuration/interpolation#bloblang-queries) in the value based on the response message contents. - -### Endpoints - -The following fields specify endpoints that are registered for sending messages, and support path parameters of the form `/{foo}`, which are added to ingested messages as metadata. A path ending in `/` will match against all extensions of that path: - -#### `path` (defaults to `/post`) - -This endpoint expects POST requests where the entire request body is consumed as a single message. - -If the request contains a multipart `content-type` header as per [rfc1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. - -#### `ws_path` (defaults to `/post/ws`) - -Creates a websocket connection, where payloads received on the socket are passed through the pipeline as a batch of one message. - -:::caution Endpoint Caveats -Components within a Benthos config will register their respective endpoints in a non-deterministic order. This means that establishing precedence of endpoints that are registered via multiple `http_server` inputs or outputs (either within brokers or from cohabiting streams) is not possible in a predictable way. - -This ambiguity makes it difficult to ensure that paths which are both a subset of a path registered by a separate component, and end in a slash (`/`) and will therefore match against all extensions of that path, do not prevent the more specific path from matching against requests. - -It is therefore recommended that you ensure paths of separate components do not collide unless they are explicitly non-competing. - -For example, if you were to deploy two separate `http_server` inputs, one with a path `/foo/` and the other with a path `/foo/bar`, it would not be possible to ensure that the path `/foo/` does not swallow requests made to `/foo/bar`. -::: - -You may specify an optional `ws_welcome_message`, which is a static payload to be sent to all clients once a websocket connection is first established. - -It's also possible to specify a `ws_rate_limit_message`, which is a static payload to be sent to clients that have triggered the servers rate limit. - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- http_server_user_agent -- http_server_request_path -- http_server_verb -- http_server_remote_ip -- All headers (only first values are taken) -- All query parameters -- All path parameters -- All cookies -``` - -If HTTPS is enabled, the following fields are added as well: -``` text -- http_server_tls_version -- http_server_tls_subject -- http_server_tls_cipher_suite -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Examples - - - - - -This example shows an `http_server` input that captures all requests and processes them by switching on that path: - -```yaml -input: - http_server: - path: / - allowed_verbs: [ GET, POST ] - sync_response: - headers: - Content-Type: application/json - - processors: - - switch: - - check: '@http_server_request_path == "/foo"' - processors: - - mapping: | - root.title = "You Got Fooed!" - root.result = content().string().uppercase() - - - check: '@http_server_request_path == "/bar"' - processors: - - mapping: 'root.title = "Bar Is Slow"' - - sleep: # Simulate a slow endpoint - duration: 1s -``` - - - - -This example shows an `http_server` input that mocks an OAuth 2.0 Client Credentials flow server at the endpoint `/oauth2_test`: - -```yaml -input: - http_server: - path: /oauth2_test - allowed_verbs: [ GET, POST ] - sync_response: - headers: - Content-Type: application/json - - processors: - - log: - message: "Received request" - level: INFO - fields_mapping: | - root = @ - root.body = content().string() - - - mapping: | - root.access_token = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3" - root.token_type = "Bearer" - root.expires_in = 3600 - - - sync_response: {} - - mapping: 'root = deleted()' -``` - - - - -## Fields - -### `address` - -An alternative address to host from. If left empty the service wide address is used. - - -Type: `string` -Default: `""` - -### `path` - -The endpoint path to listen for POST requests. - - -Type: `string` -Default: `"/post"` - -### `ws_path` - -The endpoint path to create websocket connections from. - - -Type: `string` -Default: `"/post/ws"` - -### `ws_welcome_message` - -An optional message to deliver to fresh websocket connections. - - -Type: `string` -Default: `""` - -### `ws_rate_limit_message` - -An optional message to delivery to websocket connections that are rate limited. - - -Type: `string` -Default: `""` - -### `allowed_verbs` - -An array of verbs that are allowed for the `path` endpoint. - - -Type: `array` -Default: `["POST"]` -Requires version 3.33.0 or newer - -### `timeout` - -Timeout for requests. If a consumed messages takes longer than this to be delivered the connection is closed, but the message may still be delivered. - - -Type: `string` -Default: `"5s"` - -### `rate_limit` - -An optional [rate limit](/docs/components/rate_limits/about) to throttle requests by. - - -Type: `string` -Default: `""` - -### `cert_file` - -Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. - - -Type: `string` -Default: `""` - -### `key_file` - -Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. - - -Type: `string` -Default: `""` - -### `cors` - -Adds Cross-Origin Resource Sharing headers. Only valid with a custom `address`. - - -Type: `object` -Requires version 3.63.0 or newer - -### `cors.enabled` - -Whether to allow CORS requests. - - -Type: `bool` -Default: `false` - -### `cors.allowed_origins` - -An explicit list of origins that are allowed for CORS requests. - - -Type: `array` -Default: `[]` - -### `sync_response` - -Customise messages returned via [synchronous responses](/docs/guides/sync_responses). - - -Type: `object` - -### `sync_response.status` - -Specify the status code to return with synchronous responses. This is a string value, which allows you to customize it based on resulting payloads and their metadata. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"200"` - -```yml -# Examples - -status: ${! json("status") } - -status: ${! meta("status") } -``` - -### `sync_response.headers` - -Specify headers to return with synchronous responses. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{"Content-Type":"application/octet-stream"}` - -### `sync_response.metadata_headers` - -Specify criteria for which metadata values are added to the response as headers. - - -Type: `object` - -### `sync_response.metadata_headers.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `sync_response.metadata_headers.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - - diff --git a/website/docs/components/inputs/inproc.md b/website/docs/components/inputs/inproc.md deleted file mode 100644 index 5aa66d45a1..0000000000 --- a/website/docs/components/inputs/inproc.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: inproc -slug: inproc -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - -```yml -# Config fields, showing default values -input: - label: "" - inproc: "" -``` - -Directly connect to an output within a Benthos process by referencing it by a chosen ID. This allows you to hook up isolated streams whilst running Benthos in [streams mode](/docs/guides/streams_mode/about), it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. - -It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs. - - diff --git a/website/docs/components/inputs/kafka.md b/website/docs/components/inputs/kafka.md deleted file mode 100644 index a85819d367..0000000000 --- a/website/docs/components/inputs/kafka.md +++ /dev/null @@ -1,642 +0,0 @@ ---- -title: kafka -slug: kafka -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to Kafka brokers and consumes one or more topics. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - kafka: - addresses: [] # No default (required) - topics: [] # No default (required) - target_version: 2.1.0 # No default (optional) - consumer_group: "" - checkpoint_limit: 1024 - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - kafka: - addresses: [] # No default (required) - topics: [] # No default (required) - target_version: 2.1.0 # No default (optional) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - sasl: - mechanism: none - user: "" - password: "" - access_token: "" - token_cache: "" - token_key: "" - consumer_group: "" - client_id: benthos - rack_id: "" - start_from_oldest: true - checkpoint_limit: 1024 - auto_replay_nacks: true - commit_period: 1s - max_processing_period: 100ms - extract_tracing_map: root = @ # No default (optional) - group: - session_timeout: 10s - heartbeat_interval: 3s - rebalance_timeout: 60s - fetch_buffer_cap: 256 - multi_header: false - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Offsets are managed within Kafka under the specified consumer group, and partitions for each topic are automatically balanced across members of the consumer group. - -The Kafka input allows parallel processing of messages from different topic partitions, and messages of the same topic partition are processed with a maximum parallelism determined by the field [`checkpoint_limit`](#checkpoint_limit). - -In order to enforce ordered processing of partition messages set the [`checkpoint_limit`](#checkpoint_limit) to `1` and this will force partitions to be processed in lock-step, where a message will only be processed once the prior message is delivered. - -Batching messages before processing can be enabled using the [`batching`](#batching) field, and this batching is performed per-partition such that messages of a batch will always originate from the same partition. This batching mechanism is capable of creating batches of greater size than the [`checkpoint_limit`](#checkpoint_limit), in which case the next batch will only be created upon delivery of the current one. - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- kafka_key -- kafka_topic -- kafka_partition -- kafka_offset -- kafka_lag -- kafka_timestamp_unix -- kafka_tombstone_message -- All existing message headers (version 0.11+) -``` - -The field `kafka_lag` is the calculated difference between the high water mark offset of the partition at the time of ingestion and the current message offset. - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -### Ordering - -By default messages of a topic partition can be processed in parallel, up to a limit determined by the field `checkpoint_limit`. However, if strict ordered processing is required then this value must be set to 1 in order to process shard messages in lock-step. When doing so it is recommended that you perform batching at this component for performance as it will not be possible to batch lock-stepped messages at the output level. - -### Troubleshooting - -If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer [`kafka_franz` input](/docs/components/inputs/kafka_franz). - -- I'm seeing logs that report `Failed to connect to kafka: kafka: client has run out of available brokers to talk to (Is your cluster reachable?)`, but the brokers are definitely reachable. - -Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have [enabled TLS](#tlsenabled) if applicable. - -## Fields - -### `addresses` - -A list of broker addresses to connect to. If an item of the list contains commas it will be expanded into multiple addresses. - - -Type: `array` - -```yml -# Examples - -addresses: - - localhost:9092 - -addresses: - - localhost:9041,localhost:9042 - -addresses: - - localhost:9041 - - localhost:9042 -``` - -### `topics` - -A list of topics to consume from. Multiple comma separated topics can be listed in a single element. Partitions are automatically distributed across consumers of a topic. Alternatively, it's possible to specify explicit partitions to consume from with a colon after the topic name, e.g. `foo:0` would consume the partition 0 of the topic foo. This syntax supports ranges, e.g. `foo:0-10` would consume partitions 0 through to 10 inclusive. - - -Type: `array` -Requires version 3.33.0 or newer - -```yml -# Examples - -topics: - - foo - - bar - -topics: - - foo,bar - -topics: - - foo:0 - - bar:1 - - bar:3 - -topics: - - foo:0,bar:1,bar:3 - -topics: - - foo:0-5 -``` - -### `target_version` - -The version of the Kafka protocol to use. This limits the capabilities used by the client and should ideally match the version of your brokers. Defaults to the oldest supported stable version. - - -Type: `string` - -```yml -# Examples - -target_version: 2.1.0 - -target_version: 3.1.0 -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `sasl` - -Enables SASL authentication. - - -Type: `object` - -### `sasl.mechanism` - -The SASL authentication mechanism, if left empty SASL authentication is not used. - - -Type: `string` -Default: `"none"` - -| Option | Summary | -|---|---| -| `OAUTHBEARER` | OAuth Bearer based authentication. | -| `PLAIN` | Plain text authentication. NOTE: When using plain text auth it is extremely likely that you'll also need to [enable TLS](#tlsenabled). | -| `SCRAM-SHA-256` | Authentication using the SCRAM-SHA-256 mechanism. | -| `SCRAM-SHA-512` | Authentication using the SCRAM-SHA-512 mechanism. | -| `none` | Default, no SASL authentication. | - - -### `sasl.user` - -A PLAIN username. It is recommended that you use environment variables to populate this field. - - -Type: `string` -Default: `""` - -```yml -# Examples - -user: ${USER} -``` - -### `sasl.password` - -A PLAIN password. It is recommended that you use environment variables to populate this field. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: ${PASSWORD} -``` - -### `sasl.access_token` - -A static OAUTHBEARER access token - - -Type: `string` -Default: `""` - -### `sasl.token_cache` - -Instead of using a static `access_token` allows you to query a [`cache`](/docs/components/caches/about) resource to fetch OAUTHBEARER tokens from - - -Type: `string` -Default: `""` - -### `sasl.token_key` - -Required when using a `token_cache`, the key to query the cache with for tokens. - - -Type: `string` -Default: `""` - -### `consumer_group` - -An identifier for the consumer group of the connection. This field can be explicitly made empty in order to disable stored offsets for the consumed topic partitions. - - -Type: `string` -Default: `""` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `"benthos"` - -### `rack_id` - -A rack identifier for this client. - - -Type: `string` -Default: `""` - -### `start_from_oldest` - -Determines whether to consume from the oldest available offset, otherwise messages are consumed from the latest offset. The setting is applied when creating a new consumer group or the saved offset no longer exists. - - -Type: `bool` -Default: `true` - -### `checkpoint_limit` - -The maximum number of messages of the same topic and partition that can be processed at a given time. Increasing this limit enables parallel processing and batching at the output level to work on individual partitions. Any given offset will not be committed unless all messages under that offset are delivered in order to preserve at least once delivery guarantees. - - -Type: `int` -Default: `1024` -Requires version 3.33.0 or newer - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `commit_period` - -The period of time between each commit of the current partition offsets. Offsets are always committed during shutdown. - - -Type: `string` -Default: `"1s"` - -### `max_processing_period` - -A maximum estimate for the time taken to process a message, this is used for tuning consumer group synchronization. - - -Type: `string` -Default: `"100ms"` - -### `extract_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. - - -Type: `string` -Requires version 3.45.0 or newer - -```yml -# Examples - -extract_tracing_map: root = @ - -extract_tracing_map: root = this.meta.span -``` - -### `group` - -Tuning parameters for consumer group synchronization. - - -Type: `object` - -### `group.session_timeout` - -A period after which a consumer of the group is kicked after no heartbeats. - - -Type: `string` -Default: `"10s"` - -### `group.heartbeat_interval` - -A period in which heartbeats should be sent out. - - -Type: `string` -Default: `"3s"` - -### `group.rebalance_timeout` - -A period after which rebalancing is abandoned if unresolved. - - -Type: `string` -Default: `"60s"` - -### `fetch_buffer_cap` - -The maximum number of unprocessed messages to fetch at a given time. - - -Type: `int` -Default: `256` - -### `multi_header` - -Decode headers into lists to allow handling of multiple values with the same key - - -Type: `bool` -Default: `false` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/inputs/kafka_franz.md b/website/docs/components/inputs/kafka_franz.md deleted file mode 100644 index 3ccd381d54..0000000000 --- a/website/docs/components/inputs/kafka_franz.md +++ /dev/null @@ -1,625 +0,0 @@ ---- -title: kafka_franz -slug: kafka_franz -type: input -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -A Kafka input using the [Franz Kafka client library](https://github.com/twmb/franz-go). - -Introduced in version 3.61.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - kafka_franz: - seed_brokers: [] # No default (required) - topics: [] # No default (required) - regexp_topics: false - consumer_group: "" # No default (optional) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - kafka_franz: - seed_brokers: [] # No default (required) - topics: [] # No default (required) - regexp_topics: false - consumer_group: "" # No default (optional) - client_id: benthos - rack_id: "" - checkpoint_limit: 1024 - auto_replay_nacks: true - commit_period: 5s - start_from_oldest: true - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - sasl: [] # No default (optional) - multi_header: false - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -When a consumer group is specified this input consumes one or more topics where partitions will automatically balance across any other connected clients with the same consumer group. When a consumer group is not specified topics can either be consumed in their entirety or with explicit partitions. - -This input often out-performs the traditional `kafka` input as well as providing more useful logs and error messages. - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- kafka_key -- kafka_topic -- kafka_partition -- kafka_offset -- kafka_timestamp_unix -- kafka_tombstone_message -- All record headers -``` - - -## Fields - -### `seed_brokers` - -A list of broker addresses to connect to in order to establish connections. If an item of the list contains commas it will be expanded into multiple addresses. - - -Type: `array` - -```yml -# Examples - -seed_brokers: - - localhost:9092 - -seed_brokers: - - foo:9092 - - bar:9092 - -seed_brokers: - - foo:9092,bar:9092 -``` - -### `topics` - -A list of topics to consume from. Multiple comma separated topics can be listed in a single element. When a `consumer_group` is specified partitions are automatically distributed across consumers of a topic, otherwise all partitions are consumed. - -Alternatively, it's possible to specify explicit partitions to consume from with a colon after the topic name, e.g. `foo:0` would consume the partition 0 of the topic foo. This syntax supports ranges, e.g. `foo:0-10` would consume partitions 0 through to 10 inclusive. - -Finally, it's also possible to specify an explicit offset to consume from by adding another colon after the partition, e.g. `foo:0:10` would consume the partition 0 of the topic foo starting from the offset 10. If the offset is not present (or remains unspecified) then the field `start_from_oldest` determines which offset to start from. - - -Type: `array` - -```yml -# Examples - -topics: - - foo - - bar - -topics: - - things.* - -topics: - - foo,bar - -topics: - - foo:0 - - bar:1 - - bar:3 - -topics: - - foo:0,bar:1,bar:3 - -topics: - - foo:0-5 -``` - -### `regexp_topics` - -Whether listed topics should be interpreted as regular expression patterns for matching multiple topics. When topics are specified with explicit partitions this field must remain set to `false`. - - -Type: `bool` -Default: `false` - -### `consumer_group` - -An optional consumer group to consume as. When specified the partitions of specified topics are automatically distributed across consumers sharing a consumer group, and partition offsets are automatically committed and resumed under this name. Consumer groups are not supported when specifying explicit partitions to consume from in the `topics` field. - - -Type: `string` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `"benthos"` - -### `rack_id` - -A rack identifier for this client. - - -Type: `string` -Default: `""` - -### `checkpoint_limit` - -Determines how many messages of the same partition can be processed in parallel before applying back pressure. When a message of a given offset is delivered to the output the offset is only allowed to be committed when all messages of prior offsets have also been delivered, this ensures at-least-once delivery guarantees. However, this mechanism also increases the likelihood of duplicates in the event of crashes or server faults, reducing the checkpoint limit will mitigate this. - - -Type: `int` -Default: `1024` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `commit_period` - -The period of time between each commit of the current partition offsets. Offsets are always committed during shutdown. - - -Type: `string` -Default: `"5s"` - -### `start_from_oldest` - -Determines whether to consume from the oldest available offset, otherwise messages are consumed from the latest offset. The setting is applied when creating a new consumer group or the saved offset no longer exists. - - -Type: `bool` -Default: `true` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `sasl` - -Specify one or more methods of SASL authentication. SASL is tried in order; if the broker supports the first mechanism, all connections will use that mechanism. If the first mechanism fails, the client will pick the first supported mechanism. If the broker does not support any client mechanisms, connections will fail. - - -Type: `array` - -```yml -# Examples - -sasl: - - mechanism: SCRAM-SHA-512 - password: bar - username: foo -``` - -### `sasl[].mechanism` - -The SASL mechanism to use. - - -Type: `string` - -| Option | Summary | -|---|---| -| `AWS_MSK_IAM` | AWS IAM based authentication as specified by the 'aws-msk-iam-auth' java library. | -| `OAUTHBEARER` | OAuth Bearer based authentication. | -| `PLAIN` | Plain text authentication. | -| `SCRAM-SHA-256` | SCRAM based authentication as specified in RFC5802. | -| `SCRAM-SHA-512` | SCRAM based authentication as specified in RFC5802. | -| `none` | Disable sasl authentication | - - -### `sasl[].username` - -A username to provide for PLAIN or SCRAM-* authentication. - - -Type: `string` -Default: `""` - -### `sasl[].password` - -A password to provide for PLAIN or SCRAM-* authentication. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `sasl[].token` - -The token to use for a single session's OAUTHBEARER authentication. - - -Type: `string` -Default: `""` - -### `sasl[].extensions` - -Key/value pairs to add to OAUTHBEARER authentication requests. - - -Type: `object` - -### `sasl[].aws` - -Contains AWS specific fields for when the `mechanism` is set to `AWS_MSK_IAM`. - - -Type: `object` - -### `sasl[].aws.region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `sasl[].aws.endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `sasl[].aws.credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `sasl[].aws.credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `multi_header` - -Decode headers into lists to allow handling of multiple values with the same key - - -Type: `bool` -Default: `false` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching) that applies to individual topic partitions in order to batch messages together before flushing them for processing. Batching can be beneficial for performance as well as useful for windowed processing, and doing so this way preserves the ordering of topic partitions. - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/inputs/mongodb.md b/website/docs/components/inputs/mongodb.md deleted file mode 100644 index aec1b6c7f9..0000000000 --- a/website/docs/components/inputs/mongodb.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -title: mongodb -slug: mongodb -type: input -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a query and creates a message for each document received. - -Introduced in version 3.64.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - query: |2 # No default (required) - root.from = {"$lte": timestamp_unix()} - root.to = {"$gte": timestamp_unix()} - auto_replay_nacks: true - batch_size: 1000 # No default (optional) - sort: {} # No default (optional) - limit: 0 # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - operation: find - json_marshal_mode: canonical - query: |2 # No default (required) - root.from = {"$lte": timestamp_unix()} - root.to = {"$gte": timestamp_unix()} - auto_replay_nacks: true - batch_size: 1000 # No default (optional) - sort: {} # No default (optional) - limit: 0 # No default (optional) -``` - - - - -Once the documents from the query are exhausted, this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute). - -## Fields - -### `url` - -The URL of the target MongoDB server. - - -Type: `string` - -```yml -# Examples - -url: mongodb://localhost:27017 -``` - -### `database` - -The name of the target MongoDB database. - - -Type: `string` - -### `username` - -The username to connect to the database. - - -Type: `string` -Default: `""` - -### `password` - -The password to connect to the database. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `collection` - -The collection to select from. - - -Type: `string` - -### `operation` - -The mongodb operation to perform. - - -Type: `string` -Default: `"find"` -Requires version 4.2.0 or newer -Options: `find`, `aggregate`. - -### `json_marshal_mode` - -The json_marshal_mode setting is optional and controls the format of the output message. - - -Type: `string` -Default: `"canonical"` -Requires version 4.7.0 or newer - -| Option | Summary | -|---|---| -| `canonical` | A string format that emphasizes type preservation at the expense of readability and interoperability. That is, conversion from canonical to BSON will generally preserve type information except in certain specific cases. | -| `relaxed` | A string format that emphasizes readability and interoperability at the expense of type preservation.That is, conversion from relaxed format to BSON can lose type information. | - - -### `query` - -Bloblang expression describing MongoDB query. - - -Type: `string` - -```yml -# Examples - -query: |2 - root.from = {"$lte": timestamp_unix()} - root.to = {"$gte": timestamp_unix()} -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `batch_size` - -A explicit number of documents to batch up before flushing them for processing. Must be greater than `0`. Operations: `find`, `aggregate` - - -Type: `int` -Requires version 4.26.0 or newer - -```yml -# Examples - -batch_size: 1000 -``` - -### `sort` - -An object specifying fields to sort by, and the respective sort order (`1` ascending, `-1` descending). Note: The driver currently appears to support only one sorting key. Operations: `find` - - -Type: `object` -Requires version 4.26.0 or newer - -```yml -# Examples - -sort: - name: 1 - -sort: - age: -1 -``` - -### `limit` - -An explicit maximum number of documents to return. Operations: `find` - - -Type: `int` -Requires version 4.26.0 or newer - - diff --git a/website/docs/components/inputs/mqtt.md b/website/docs/components/inputs/mqtt.md deleted file mode 100644 index 4e48b90989..0000000000 --- a/website/docs/components/inputs/mqtt.md +++ /dev/null @@ -1,389 +0,0 @@ ---- -title: mqtt -slug: mqtt -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Subscribe to topics on MQTT brokers. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - mqtt: - urls: [] # No default (required) - client_id: "" - connect_timeout: 30s - topics: [] # No default (required) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - mqtt: - urls: [] # No default (required) - client_id: "" - dynamic_client_id_suffix: "" # No default (optional) - connect_timeout: 30s - will: - enabled: false - qos: 0 - retained: false - topic: "" - payload: "" - user: "" - password: "" - keepalive: 30 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - topics: [] # No default (required) - qos: 1 - clean_session: true - auto_replay_nacks: true -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- mqtt_duplicate -- mqtt_qos -- mqtt_retained -- mqtt_topic -- mqtt_message_id -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - tcp://localhost:1883 -``` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `""` - -### `dynamic_client_id_suffix` - -Append a dynamically generated suffix to the specified `client_id` on each run of the pipeline. This can be useful when clustering Benthos producers. - - -Type: `string` - -| Option | Summary | -|---|---| -| `nanoid` | append a nanoid of length 21 characters | - - -### `connect_timeout` - -The maximum amount of time to wait in order to establish a connection before the attempt is abandoned. - - -Type: `string` -Default: `"30s"` -Requires version 3.58.0 or newer - -```yml -# Examples - -connect_timeout: 1s - -connect_timeout: 500ms -``` - -### `will` - -Set last will message in case of Benthos failure - - -Type: `object` - -### `will.enabled` - -Whether to enable last will messages. - - -Type: `bool` -Default: `false` - -### `will.qos` - -Set QoS for last will message. Valid values are: 0, 1, 2. - - -Type: `int` -Default: `0` - -### `will.retained` - -Set retained for last will message. - - -Type: `bool` -Default: `false` - -### `will.topic` - -Set topic for last will message. - - -Type: `string` -Default: `""` - -### `will.payload` - -Set payload for last will message. - - -Type: `string` -Default: `""` - -### `user` - -A username to connect with. - - -Type: `string` -Default: `""` - -### `password` - -A password to connect with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `keepalive` - -Max seconds of inactivity before a keepalive message is sent. - - -Type: `int` -Default: `30` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `topics` - -A list of topics to consume from. - - -Type: `array` - -### `qos` - -The level of delivery guarantee to enforce. Has options 0, 1, 2. - - -Type: `int` -Default: `1` - -### `clean_session` - -Set whether the connection is non-persistent. - - -Type: `bool` -Default: `true` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/nanomsg.md b/website/docs/components/inputs/nanomsg.md deleted file mode 100644 index a54981b6ca..0000000000 --- a/website/docs/components/inputs/nanomsg.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: nanomsg -slug: nanomsg -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes messages via Nanomsg sockets (scalability protocols). - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - nanomsg: - urls: [] # No default (required) - bind: true - socket_type: PULL - auto_replay_nacks: true - sub_filters: [] -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - nanomsg: - urls: [] # No default (required) - bind: true - socket_type: PULL - auto_replay_nacks: true - sub_filters: [] - poll_timeout: 5s -``` - - - - -Currently only PULL and SUB sockets are supported. - -## Fields - -### `urls` - -A list of URLs to connect to (or as). If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -### `bind` - -Whether the URLs provided should be connected to, or bound as. - - -Type: `bool` -Default: `true` - -### `socket_type` - -The socket type to use. - - -Type: `string` -Default: `"PULL"` -Options: `PULL`, `SUB`. - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `sub_filters` - -A list of subscription topic filters to use when consuming from a SUB socket. Specifying a single sub_filter of `''` will subscribe to everything. - - -Type: `array` -Default: `[]` - -### `poll_timeout` - -The period to wait until a poll is abandoned and reattempted. - - -Type: `string` -Default: `"5s"` - - diff --git a/website/docs/components/inputs/nats.md b/website/docs/components/inputs/nats.md deleted file mode 100644 index de3f8ea576..0000000000 --- a/website/docs/components/inputs/nats.md +++ /dev/null @@ -1,404 +0,0 @@ ---- -title: nats -slug: nats -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Subscribe to a NATS subject. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - nats: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - queue: "" # No default (optional) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - nats: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - queue: "" # No default (optional) - auto_replay_nacks: true - nak_delay: 1m # No default (optional) - prefetch_count: 524288 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) - extract_tracing_map: root = @ # No default (optional) -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- nats_subject -- nats_reply_subject -- All message headers (when supported by the connection) -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `subject` - -A subject to consume from. Supports wildcards for consuming multiple subjects. Either a subject or stream must be specified. - - -Type: `string` - -```yml -# Examples - -subject: foo.bar.baz - -subject: foo.*.baz - -subject: foo.bar.* - -subject: foo.> -``` - -### `queue` - -An optional queue group to consume as. - - -Type: `string` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `nak_delay` - -An optional delay duration on redelivering a message when negatively acknowledged. - - -Type: `string` - -```yml -# Examples - -nak_delay: 1m -``` - -### `prefetch_count` - -The maximum number of messages to pull at a time. - - -Type: `int` -Default: `524288` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `extract_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. - - -Type: `string` -Requires version 4.23.0 or newer - -```yml -# Examples - -extract_tracing_map: root = @ - -extract_tracing_map: root = this.meta.span -``` - - diff --git a/website/docs/components/inputs/nats_jetstream.md b/website/docs/components/inputs/nats_jetstream.md deleted file mode 100644 index 73d113b5a2..0000000000 --- a/website/docs/components/inputs/nats_jetstream.md +++ /dev/null @@ -1,453 +0,0 @@ ---- -title: nats_jetstream -slug: nats_jetstream -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Reads messages from NATS JetStream subjects. - -Introduced in version 3.46.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - nats_jetstream: - urls: [] # No default (required) - queue: "" # No default (optional) - subject: foo.bar.baz # No default (optional) - durable: "" # No default (optional) - stream: "" # No default (optional) - bind: false # No default (optional) - deliver: all -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - nats_jetstream: - urls: [] # No default (required) - queue: "" # No default (optional) - subject: foo.bar.baz # No default (optional) - durable: "" # No default (optional) - stream: "" # No default (optional) - bind: false # No default (optional) - deliver: all - ack_wait: 30s - max_ack_pending: 1024 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) - extract_tracing_map: root = @ # No default (optional) -``` - - - - -### Consuming Mirrored Streams - -In the case where a stream being consumed is mirrored from a different JetStream domain the stream cannot be resolved from the subject name alone, and so the stream name as well as the subject (if applicable) must both be specified. - -### Metadata - -This input adds the following metadata fields to each message: - -```text -- nats_subject -- nats_sequence_stream -- nats_sequence_consumer -- nats_num_delivered -- nats_num_pending -- nats_domain -- nats_timestamp_unix_nano -``` - -You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `queue` - -An optional queue group to consume as. - - -Type: `string` - -### `subject` - -A subject to consume from. Supports wildcards for consuming multiple subjects. Either a subject or stream must be specified. - - -Type: `string` - -```yml -# Examples - -subject: foo.bar.baz - -subject: foo.*.baz - -subject: foo.bar.* - -subject: foo.> -``` - -### `durable` - -Preserve the state of your consumer under a durable name. - - -Type: `string` - -### `stream` - -A stream to consume from. Either a subject or stream must be specified. - - -Type: `string` - -### `bind` - -Indicates that the subscription should use an existing consumer. - - -Type: `bool` - -### `deliver` - -Determines which messages to deliver when consuming without a durable subscriber. - - -Type: `string` -Default: `"all"` - -| Option | Summary | -|---|---| -| `all` | Deliver all available messages. | -| `last` | Deliver starting with the last published messages. | -| `last_per_subject` | Deliver starting with the last published message per subject. | -| `new` | Deliver starting from now, not taking into account any previous messages. | - - -### `ack_wait` - -The maximum amount of time NATS server should wait for an ack from consumer. - - -Type: `string` -Default: `"30s"` - -```yml -# Examples - -ack_wait: 100ms - -ack_wait: 5m -``` - -### `max_ack_pending` - -The maximum number of outstanding acks to be allowed before consuming is halted. - - -Type: `int` -Default: `1024` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `extract_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. - - -Type: `string` -Requires version 4.23.0 or newer - -```yml -# Examples - -extract_tracing_map: root = @ - -extract_tracing_map: root = this.meta.span -``` - - diff --git a/website/docs/components/inputs/nats_kv.md b/website/docs/components/inputs/nats_kv.md deleted file mode 100644 index 71bb30e9f2..0000000000 --- a/website/docs/components/inputs/nats_kv.md +++ /dev/null @@ -1,404 +0,0 @@ ---- -title: nats_kv -slug: nats_kv -type: input -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Watches for updates in a NATS key-value bucket. - -Introduced in version 4.12.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - key: '>' - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - key: '>' - auto_replay_nacks: true - ignore_deletes: false - include_history: false - meta_only: false - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- nats_kv_key -- nats_kv_bucket -- nats_kv_revision -- nats_kv_delta -- nats_kv_operation -- nats_kv_created -``` - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `bucket` - -The name of the KV bucket. - - -Type: `string` - -```yml -# Examples - -bucket: my_kv_bucket -``` - -### `key` - -Key to watch for updates, can include wildcards. - - -Type: `string` -Default: `"\u003e"` - -```yml -# Examples - -key: foo.bar.baz - -key: foo.*.baz - -key: foo.bar.* - -key: foo.> -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `ignore_deletes` - -Do not send delete markers as messages. - - -Type: `bool` -Default: `false` - -### `include_history` - -Include all the history per key, not just the last one. - - -Type: `bool` -Default: `false` - -### `meta_only` - -Retrieve only the metadata of the entry - - -Type: `bool` -Default: `false` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - - diff --git a/website/docs/components/inputs/nats_stream.md b/website/docs/components/inputs/nats_stream.md deleted file mode 100644 index 970e60f406..0000000000 --- a/website/docs/components/inputs/nats_stream.md +++ /dev/null @@ -1,426 +0,0 @@ ---- -title: nats_stream -slug: nats_stream -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Subscribe to a NATS Stream subject. Joining a queue is optional and allows multiple clients of a subject to consume using queue semantics. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - nats_stream: - urls: [] # No default (required) - cluster_id: "" # No default (required) - client_id: "" - queue: "" - subject: "" - durable_name: "" - unsubscribe_on_close: false -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - nats_stream: - urls: [] # No default (required) - cluster_id: "" # No default (required) - client_id: "" - queue: "" - subject: "" - durable_name: "" - unsubscribe_on_close: false - start_from_oldest: true - max_inflight: 1024 - ack_wait: 30s - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) - extract_tracing_map: root = @ # No default (optional) -``` - - - - -:::caution Deprecation Notice -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use [JetStream](https://docs.nats.io/nats-concepts/jetstream). -::: - -Tracking and persisting offsets through a durable name is also optional and works with or without a queue. If a durable name is not provided then subjects are consumed from the most recently published message. - -When a consumer closes its connection it unsubscribes, when all consumers of a durable queue do this the offsets are deleted. In order to avoid this you can stop the consumers from unsubscribing by setting the field `unsubscribe_on_close` to `false`. - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- nats_stream_subject -- nats_stream_sequence -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `cluster_id` - -The ID of the cluster to consume from. - - -Type: `string` - -### `client_id` - -A client ID to connect as. - - -Type: `string` -Default: `""` - -### `queue` - -The queue to consume from. - - -Type: `string` -Default: `""` - -### `subject` - -A subject to consume from. - - -Type: `string` -Default: `""` - -### `durable_name` - -Preserve the state of your consumer under a durable name. - - -Type: `string` -Default: `""` - -### `unsubscribe_on_close` - -Whether the subscription should be destroyed when this client disconnects. - - -Type: `bool` -Default: `false` - -### `start_from_oldest` - -If a position is not found for a queue, determines whether to consume from the oldest available message, otherwise messages are consumed from the latest. - - -Type: `bool` -Default: `true` - -### `max_inflight` - -The maximum number of unprocessed messages to fetch at a given time. - - -Type: `int` -Default: `1024` - -### `ack_wait` - -An optional duration to specify at which a message that is yet to be acked will be automatically retried. - - -Type: `string` -Default: `"30s"` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `extract_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) that attempts to extract an object containing tracing propagation information, which will then be used as the root tracing span for the message. The specification of the extracted fields must match the format used by the service wide tracer. - - -Type: `string` -Requires version 4.23.0 or newer - -```yml -# Examples - -extract_tracing_map: root = @ - -extract_tracing_map: root = this.meta.span -``` - - diff --git a/website/docs/components/inputs/nsq.md b/website/docs/components/inputs/nsq.md deleted file mode 100644 index a7f00895a0..0000000000 --- a/website/docs/components/inputs/nsq.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: nsq -slug: nsq -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Subscribe to an NSQ instance topic and channel. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - nsq: - nsqd_tcp_addresses: [] # No default (required) - lookupd_http_addresses: [] # No default (required) - topic: "" # No default (required) - channel: "" # No default (required) - user_agent: "" # No default (optional) - max_in_flight: 100 - max_attempts: 5 -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - nsq: - nsqd_tcp_addresses: [] # No default (required) - lookupd_http_addresses: [] # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - topic: "" # No default (required) - channel: "" # No default (required) - user_agent: "" # No default (optional) - max_in_flight: 100 - max_attempts: 5 -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -``` text -- nsq_attempts -- nsq_id -- nsq_nsqd_address -- nsq_timestamp -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - - -## Fields - -### `nsqd_tcp_addresses` - -A list of nsqd addresses to connect to. - - -Type: `array` - -### `lookupd_http_addresses` - -A list of nsqlookupd addresses to connect to. - - -Type: `array` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `topic` - -The topic to consume from. - - -Type: `string` - -### `channel` - -The channel to consume from. - - -Type: `string` - -### `user_agent` - -A user agent to assume when connecting. - - -Type: `string` - -### `max_in_flight` - -The maximum number of pending messages to consume at any given time. - - -Type: `int` -Default: `100` - -### `max_attempts` - -The maximum number of attempts to successfully consume a messages. - - -Type: `int` -Default: `5` - - diff --git a/website/docs/components/inputs/parquet.md b/website/docs/components/inputs/parquet.md deleted file mode 100644 index 273ca860f3..0000000000 --- a/website/docs/components/inputs/parquet.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: parquet -slug: parquet -type: input -status: experimental -categories: ["Local"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Reads and decodes [Parquet files](https://parquet.apache.org/docs/) into a stream of structured messages. - -Introduced in version 4.8.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - parquet: - paths: [] # No default (required) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - parquet: - paths: [] # No default (required) - batch_count: 1 - auto_replay_nacks: true -``` - - - - -This input uses [https://github.com/parquet-go/parquet-go](https://github.com/parquet-go/parquet-go), which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. - -By default any BYTE_ARRAY or FIXED_LEN_BYTE_ARRAY value will be extracted as a byte slice (`[]byte`) unless the logical type is UTF8, in which case they are extracted as a string (`string`). - -When a value extracted as a byte slice exists within a document which is later JSON serialized by default it will be base 64 encoded into strings, which is the default for arbitrary data fields. It is possible to convert these binary values to strings (or other data types) using Bloblang transformations such as `root.foo = this.foo.string()` or `root.foo = this.foo.encode("hex")`, etc. - -## Fields - -### `paths` - -A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star). - - -Type: `array` - -```yml -# Examples - -paths: /tmp/foo.parquet - -paths: /tmp/bar/*.parquet - -paths: /tmp/data/**/*.parquet -``` - -### `batch_count` - -Optionally process records in batches. This can help to speed up the consumption of exceptionally large files. When the end of the file is reached the remaining records are processed as a (potentially smaller) batch. - - -Type: `int` -Default: `1` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/pulsar.md b/website/docs/components/inputs/pulsar.md deleted file mode 100644 index d57b9f23eb..0000000000 --- a/website/docs/components/inputs/pulsar.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: pulsar -slug: pulsar -type: input -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Reads messages from an Apache Pulsar server. - -Introduced in version 3.43.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - pulsar: - url: pulsar://localhost:6650 # No default (required) - topics: [] # No default (optional) - topics_pattern: "" # No default (optional) - subscription_name: "" # No default (required) - subscription_type: shared - tls: - root_cas_file: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - pulsar: - url: pulsar://localhost:6650 # No default (required) - topics: [] # No default (optional) - topics_pattern: "" # No default (optional) - subscription_name: "" # No default (required) - subscription_type: shared - tls: - root_cas_file: "" - auth: - oauth2: - enabled: false - audience: "" - issuer_url: "" - private_key_file: "" - token: - enabled: false - token: "" -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -```text -- pulsar_message_id -- pulsar_key -- pulsar_ordering_key -- pulsar_event_time_unix -- pulsar_publish_time_unix -- pulsar_topic -- pulsar_producer_name -- pulsar_redelivery_count -- All properties of the message -``` - -You can access these metadata fields using -[function interpolation](/docs/configuration/interpolation#bloblang-queries). - - -## Fields - -### `url` - -A URL to connect to. - - -Type: `string` - -```yml -# Examples - -url: pulsar://localhost:6650 - -url: pulsar://pulsar.us-west.example.com:6650 - -url: pulsar+ssl://pulsar.us-west.example.com:6651 -``` - -### `topics` - -A list of topics to subscribe to. This or topics_pattern must be set. - - -Type: `array` - -### `topics_pattern` - -A regular expression matching the topics to subscribe to. This or topics must be set. - - -Type: `string` - -### `subscription_name` - -Specify the subscription name for this consumer. - - -Type: `string` - -### `subscription_type` - -Specify the subscription type for this consumer. - -> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See [Pulsar documentation](https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement) and [this Github issue](https://github.com/apache/pulsar/issues/12208) for more details. - - -Type: `string` -Default: `"shared"` -Options: `shared`, `key_shared`, `failover`, `exclusive`. - -### `tls` - -Specify the path to a custom CA certificate to trust broker TLS service. - - -Type: `object` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `auth` - -Optional configuration of Pulsar authentication methods. - - -Type: `object` -Requires version 3.60.0 or newer - -### `auth.oauth2` - -Parameters for Pulsar OAuth2 authentication. - - -Type: `object` - -### `auth.oauth2.enabled` - -Whether OAuth2 is enabled. - - -Type: `bool` -Default: `false` - -### `auth.oauth2.audience` - -OAuth2 audience. - - -Type: `string` -Default: `""` - -### `auth.oauth2.issuer_url` - -OAuth2 issuer URL. - - -Type: `string` -Default: `""` - -### `auth.oauth2.private_key_file` - -The path to a file containing a private key. - - -Type: `string` -Default: `""` - -### `auth.token` - -Parameters for Pulsar Token authentication. - - -Type: `object` - -### `auth.token.enabled` - -Whether Token Auth is enabled. - - -Type: `bool` -Default: `false` - -### `auth.token.token` - -Actual base64 encoded token. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/inputs/read_until.md b/website/docs/components/inputs/read_until.md deleted file mode 100644 index 0130eebb05..0000000000 --- a/website/docs/components/inputs/read_until.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -title: read_until -slug: read_until -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Reads messages from a child input until a consumed message passes a [Bloblang query](/docs/guides/bloblang/about/), at which point the input closes. It is also possible to configure a timeout after which the input is closed if no new messages arrive in that period. - -```yml -# Config fields, showing default values -input: - label: "" - read_until: - input: null # No default (required) - check: this.type == "foo" # No default (optional) - idle_timeout: 5s # No default (optional) - restart_input: false -``` - -Messages are read continuously while the query check returns false, when the query returns true the message that triggered the check is sent out and the input is closed. Use this to define inputs where the stream should end once a certain message appears. - -If the idle timeout is configured, the input will be closed if no new messages arrive after that period of time. Use this field if you want to empty out and close an input that doesn't have a logical end. - -Sometimes inputs close themselves. For example, when the `file` input type reaches the end of a file it will shut down. By default this type will also shut down. If you wish for the input type to be restarted every time it shuts down until the query check is met then set `restart_input` to `true`. - -### Metadata - -A metadata key `benthos_read_until` containing the value `final` is added to the first part of the message that triggers the input to stop. - -## Fields - -### `input` - -The child input to consume from. - - -Type: `input` - -### `check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether the input should now be closed. - - -Type: `string` - -```yml -# Examples - -check: this.type == "foo" - -check: count("messages") >= 100 -``` - -### `idle_timeout` - -The maximum amount of time without receiving new messages after which the input is closed. - - -Type: `string` - -```yml -# Examples - -idle_timeout: 5s -``` - -### `restart_input` - -Whether the input should be reopened if it closes itself before the condition has resolved to true. - - -Type: `bool` -Default: `false` - -## Examples - - - - - -A common reason to use this input is to consume only N messages from an input and then stop. This can easily be done with the [`count` function](/docs/guides/bloblang/functions/#count): - -```yaml -# Only read 100 messages, and then exit. -input: - read_until: - check: count("messages") >= 100 - input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup -``` - - - - -A common reason to use this input is a job that consumes all messages and exits once its empty: - -```yaml -# Consumes all messages and exit when the last message was consumed 5s ago. -input: - read_until: - idle_timeout: 5s - input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup -``` - - - - - diff --git a/website/docs/components/inputs/redis_list.md b/website/docs/components/inputs/redis_list.md deleted file mode 100644 index 8dd9aacff6..0000000000 --- a/website/docs/components/inputs/redis_list.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: redis_list -slug: redis_list -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Pops messages from the beginning of a Redis list using the BLPop command. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - redis_list: - url: redis://:6397 # No default (required) - key: "" # No default (required) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - redis_list: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - key: "" # No default (required) - auto_replay_nacks: true - max_in_flight: 0 - timeout: 5s - command: blpop -``` - - - - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `key` - -The key of a list to read from. - - -Type: `string` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `max_in_flight` - -Optionally sets a limit on the number of messages that can be flowing through a Benthos stream pending acknowledgment from the input at any given time. Once a message has been either acknowledged or rejected (nacked) it is no longer considered pending. If the input produces logical batches then each batch is considered a single count against the maximum. **WARNING**: Batching policies at the output level will stall if this field limits the number of messages below the batching threshold. Zero (default) or lower implies no limit. - - -Type: `int` -Default: `0` -Requires version 4.9.0 or newer - -### `timeout` - -The length of time to poll for new messages before reattempting. - - -Type: `string` -Default: `"5s"` - -### `command` - -The command used to pop elements from the Redis list - - -Type: `string` -Default: `"blpop"` -Requires version 4.22.0 or newer -Options: `blpop`, `brpop`. - - diff --git a/website/docs/components/inputs/redis_pubsub.md b/website/docs/components/inputs/redis_pubsub.md deleted file mode 100644 index 2efe5dda5a..0000000000 --- a/website/docs/components/inputs/redis_pubsub.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -title: redis_pubsub -slug: redis_pubsub -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consume from a Redis publish/subscribe channel using either the SUBSCRIBE or PSUBSCRIBE commands. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - redis_pubsub: - url: redis://:6397 # No default (required) - channels: [] # No default (required) - use_patterns: false - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - redis_pubsub: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - channels: [] # No default (required) - use_patterns: false - auto_replay_nacks: true -``` - - - - -In order to subscribe to channels using the `PSUBSCRIBE` command set the field `use_patterns` to `true`, then you can include glob-style patterns in your channel names. For example: - -- `h?llo` subscribes to hello, hallo and hxllo -- `h*llo` subscribes to hllo and heeeello -- `h[ae]llo` subscribes to hello and hallo, but not hillo - -Use `\` to escape special characters if you want to match them verbatim. - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `channels` - -A list of channels to consume from. - - -Type: `array` - -### `use_patterns` - -Whether to use the PSUBSCRIBE command, allowing for glob-style patterns within target channel names. - - -Type: `bool` -Default: `false` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/redis_scan.md b/website/docs/components/inputs/redis_scan.md deleted file mode 100644 index 5384f7893f..0000000000 --- a/website/docs/components/inputs/redis_scan.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -title: redis_scan -slug: redis_scan -type: input -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Scans the set of keys in the current selected database and gets their values, using the Scan and Get commands. - -Introduced in version 4.27.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - redis_scan: - url: redis://:6397 # No default (required) - auto_replay_nacks: true - match: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - redis_scan: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auto_replay_nacks: true - match: "" -``` - - - - -Optionally, iterates only elements matching a blob-style pattern. For example: -- `*foo*` iterates only keys which contain `foo` in it. -- `foo*` iterates only keys starting with `foo`. - -This input generates a message for each key value pair in the following format: - -```json -{"key":"foo","value":"bar"} -``` - - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `match` - -Iterates only elements matching the optional glob-style pattern. By default, it matches all elements. - - -Type: `string` -Default: `""` - -```yml -# Examples - -match: '*' - -match: 1* - -match: foo* - -match: foo - -match: '*4*' -``` - - diff --git a/website/docs/components/inputs/redis_streams.md b/website/docs/components/inputs/redis_streams.md deleted file mode 100644 index 9d72ea90db..0000000000 --- a/website/docs/components/inputs/redis_streams.md +++ /dev/null @@ -1,348 +0,0 @@ ---- -title: redis_streams -slug: redis_streams -type: input -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Pulls messages from Redis (v5.0+) streams with the XREADGROUP command. The `client_id` should be unique for each consumer of a group. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - redis_streams: - url: redis://:6397 # No default (required) - body_key: body - streams: [] # No default (required) - auto_replay_nacks: true - limit: 10 - client_id: "" - consumer_group: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - redis_streams: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - body_key: body - streams: [] # No default (required) - auto_replay_nacks: true - limit: 10 - client_id: "" - consumer_group: "" - create_streams: true - start_from_oldest: true - commit_period: 1s - timeout: 1s -``` - - - - -Redis stream entries are key/value pairs, as such it is necessary to specify the key that contains the body of the message. All other keys/value pairs are saved as metadata fields. - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `body_key` - -The field key to extract the raw message from. All other keys will be stored in the message as metadata. - - -Type: `string` -Default: `"body"` - -### `streams` - -A list of streams to consume from. - - -Type: `array` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `limit` - -The maximum number of messages to consume from a single request. - - -Type: `int` -Default: `10` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `""` - -### `consumer_group` - -An identifier for the consumer group of the stream. - - -Type: `string` -Default: `""` - -### `create_streams` - -Create subscribed streams if they do not exist (MKSTREAM option). - - -Type: `bool` -Default: `true` - -### `start_from_oldest` - -If an offset is not found for a stream, determines whether to consume from the oldest available offset, otherwise messages are consumed from the latest offset. - - -Type: `bool` -Default: `true` - -### `commit_period` - -The period of time between each commit of the current offset. Offsets are always committed during shutdown. - - -Type: `string` -Default: `"1s"` - -### `timeout` - -The length of time to poll for new messages before reattempting. - - -Type: `string` -Default: `"1s"` - - diff --git a/website/docs/components/inputs/resource.md b/website/docs/components/inputs/resource.md deleted file mode 100644 index 3247d338da..0000000000 --- a/website/docs/components/inputs/resource.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: resource -slug: resource -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Resource is an input type that channels messages from a resource input, identified by its name. - -```yml -# Config fields, showing default values -input: - resource: "" -``` - -Resources allow you to tidy up deeply nested configs. For example, the config: - -```yaml -input: - broker: - inputs: - - kafka: - addresses: [ TODO ] - topics: [ foo ] - consumer_group: foogroup - - gcp_pubsub: - project: bar - subscription: baz -``` - -Could also be expressed as: - -```yaml -input: - broker: - inputs: - - resource: foo - - resource: bar - -input_resources: - - label: foo - kafka: - addresses: [ TODO ] - topics: [ foo ] - consumer_group: foogroup - - - label: bar - gcp_pubsub: - project: bar - subscription: baz - ``` - -Resources also allow you to reference a single input in multiple places, such as multiple streams mode configs, or multiple entries in a broker input. However, when a resource is referenced more than once the messages it produces are distributed across those references, so each message will only be directed to a single reference, not all of them. - -You can find out more about resources [in this document.](/docs/configuration/resources) - - diff --git a/website/docs/components/inputs/sequence.md b/website/docs/components/inputs/sequence.md deleted file mode 100644 index 18fd91fda3..0000000000 --- a/website/docs/components/inputs/sequence.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: sequence -slug: sequence -type: input -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Reads messages from a sequence of child inputs, starting with the first and once that input gracefully terminates starts consuming from the next, and so on. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - sequence: - inputs: [] # No default (required) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - sequence: - sharded_join: - type: none - id_path: "" - iterations: 1 - merge_strategy: array - inputs: [] # No default (required) -``` - - - - -This input is useful for consuming from inputs that have an explicit end but must not be consumed in parallel. - -## Examples - - - - - -A common use case for sequence might be to generate a message at the end of our main input. With the following config once the records within `./dataset.csv` are exhausted our final payload `{"status":"finished"}` will be routed through the pipeline. - -```yaml -input: - sequence: - inputs: - - file: - paths: [ ./dataset.csv ] - scanner: - csv: {} - - generate: - count: 1 - mapping: 'root = {"status":"finished"}' -``` - - - - -Benthos can be used to join unordered data from fragmented datasets in memory by specifying a common identifier field and a number of sharded iterations. For example, given two CSV files, the first called "main.csv", which contains rows of user data: - -```csv -uuid,name,age -AAA,Melanie,34 -BBB,Emma,28 -CCC,Geri,45 -``` - -And the second called "hobbies.csv" that, for each user, contains zero or more rows of hobbies: - -```csv -uuid,hobby -CCC,pokemon go -AAA,rowing -AAA,golf -``` - -We can parse and join this data into a single dataset: - -```json -{"uuid":"AAA","name":"Melanie","age":34,"hobbies":["rowing","golf"]} -{"uuid":"BBB","name":"Emma","age":28} -{"uuid":"CCC","name":"Geri","age":45,"hobbies":["pokemon go"]} -``` - -With the following config: - -```yaml -input: - sequence: - sharded_join: - type: full-outer - id_path: uuid - merge_strategy: array - inputs: - - file: - paths: - - ./hobbies.csv - - ./main.csv - scanner: - csv: {} -``` - - - - -In this example we are able to join unordered and fragmented data from a combination of CSV files and newline-delimited JSON documents by specifying multiple sequence inputs with their own processors for extracting the structured data. - -The first file "main.csv" contains straight forward CSV data: - -```csv -uuid,name,age -AAA,Melanie,34 -BBB,Emma,28 -CCC,Geri,45 -``` - -And the second file called "hobbies.ndjson" contains JSON documents, one per line, that associate an identifier with an array of hobbies. However, these data objects are in a nested format: - -```json -{"document":{"uuid":"CCC","hobbies":[{"type":"pokemon go"}]}} -{"document":{"uuid":"AAA","hobbies":[{"type":"rowing"},{"type":"golf"}]}} -``` - -And so we will want to map these into a flattened structure before the join, and then we will end up with a single dataset that looks like this: - -```json -{"uuid":"AAA","name":"Melanie","age":34,"hobbies":["rowing","golf"]} -{"uuid":"BBB","name":"Emma","age":28} -{"uuid":"CCC","name":"Geri","age":45,"hobbies":["pokemon go"]} -``` - -With the following config: - -```yaml -input: - sequence: - sharded_join: - type: full-outer - id_path: uuid - iterations: 10 - merge_strategy: array - inputs: - - file: - paths: [ ./main.csv ] - scanner: - csv: {} - - file: - paths: [ ./hobbies.ndjson ] - scanner: - lines: {} - processors: - - mapping: | - root.uuid = this.document.uuid - root.hobbies = this.document.hobbies.map_each(this.type) -``` - - - - -## Fields - -### `sharded_join` - -EXPERIMENTAL: Provides a way to perform outer joins of arbitrarily structured and unordered data resulting from the input sequence, even when the overall size of the data surpasses the memory available on the machine. - -When configured the sequence of inputs will be consumed one or more times according to the number of iterations, and when more than one iteration is specified each iteration will process an entirely different set of messages by sharding them by the ID field. Increasing the number of iterations reduces the memory consumption at the cost of needing to fully parse the data each time. - -Each message must be structured (JSON or otherwise processed into a structured form) and the fields will be aggregated with those of other messages sharing the ID. At the end of each iteration the joined messages are flushed downstream before the next iteration begins, hence keeping memory usage limited. - - -Type: `object` -Requires version 3.40.0 or newer - -### `sharded_join.type` - -The type of join to perform. A `full-outer` ensures that all identifiers seen in any of the input sequences are sent, and is performed by consuming all input sequences before flushing the joined results. An `outer` join consumes all input sequences but only writes data joined from the last input in the sequence, similar to a left or right outer join. With an `outer` join if an identifier appears multiple times within the final sequence input it will be flushed each time it appears. `full-outter` and `outter` have been deprecated in favour of `full-outer` and `outer`. - - -Type: `string` -Default: `"none"` -Options: `none`, `full-outer`, `outer`, `full-outter`, `outter`. - -### `sharded_join.id_path` - -A [dot path](/docs/configuration/field_paths) that points to a common field within messages of each fragmented data set and can be used to join them. Messages that are not structured or are missing this field will be dropped. This field must be set in order to enable joins. - - -Type: `string` -Default: `""` - -### `sharded_join.iterations` - -The total number of iterations (shards), increasing this number will increase the overall time taken to process the data, but reduces the memory used in the process. The real memory usage required is significantly higher than the real size of the data and therefore the number of iterations should be at least an order of magnitude higher than the available memory divided by the overall size of the dataset. - - -Type: `int` -Default: `1` - -### `sharded_join.merge_strategy` - -The chosen strategy to use when a data join would otherwise result in a collision of field values. The strategy `array` means non-array colliding values are placed into an array and colliding arrays are merged. The strategy `replace` replaces old values with new values. The strategy `keep` keeps the old value. - - -Type: `string` -Default: `"array"` -Options: `array`, `replace`, `keep`. - -### `inputs` - -An array of inputs to read from sequentially. - - -Type: `array` - - diff --git a/website/docs/components/inputs/sftp.md b/website/docs/components/inputs/sftp.md deleted file mode 100644 index 587d333fa5..0000000000 --- a/website/docs/components/inputs/sftp.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: sftp -slug: sftp -type: input -status: beta -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Consumes files from an SFTP server. - -Introduced in version 3.39.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - sftp: - address: "" # No default (required) - credentials: - username: "" - password: "" - private_key_file: "" - private_key_pass: "" - paths: [] # No default (required) - auto_replay_nacks: true - scanner: - to_the_end: {} - watcher: - enabled: false - minimum_age: 1s - poll_interval: 1s - cache: "" -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - sftp: - address: "" # No default (required) - credentials: - username: "" - password: "" - private_key_file: "" - private_key_pass: "" - paths: [] # No default (required) - auto_replay_nacks: true - scanner: - to_the_end: {} - delete_on_finish: false - watcher: - enabled: false - minimum_age: 1s - poll_interval: 1s - cache: "" -``` - - - - -## Metadata - -This input adds the following metadata fields to each message: - -``` -- sftp_path -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `address` - -The address of the server to connect to. - - -Type: `string` - -### `credentials` - -The credentials to use to log into the target server. - - -Type: `object` - -### `credentials.username` - -The username to connect to the SFTP server. - - -Type: `string` -Default: `""` - -### `credentials.password` - -The password for the username to connect to the SFTP server. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.private_key_file` - -The private key for the username to connect to the SFTP server. - - -Type: `string` -Default: `""` - -### `credentials.private_key_pass` - -Optional passphrase for private key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `paths` - -A list of paths to consume sequentially. Glob patterns are supported. - - -Type: `array` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"to_the_end":{}}` -Requires version 4.25.0 or newer - -### `delete_on_finish` - -Whether to delete files from the server once they are processed. - - -Type: `bool` -Default: `false` - -### `watcher` - -An experimental mode whereby the input will periodically scan the target paths for new files and consume them, when all files are consumed the input will continue polling for new files. - - -Type: `object` -Requires version 3.42.0 or newer - -### `watcher.enabled` - -Whether file watching is enabled. - - -Type: `bool` -Default: `false` - -### `watcher.minimum_age` - -The minimum period of time since a file was last updated before attempting to consume it. Increasing this period decreases the likelihood that a file will be consumed whilst it is still being written to. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -minimum_age: 10s - -minimum_age: 1m - -minimum_age: 10m -``` - -### `watcher.poll_interval` - -The interval between each attempt to scan the target paths for new files. - - -Type: `string` -Default: `"1s"` - -```yml -# Examples - -poll_interval: 100ms - -poll_interval: 1s -``` - -### `watcher.cache` - -A [cache resource](/docs/components/caches/about) for storing the paths of files already consumed. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/inputs/socket.md b/website/docs/components/inputs/socket.md deleted file mode 100644 index b12cb55ca9..0000000000 --- a/website/docs/components/inputs/socket.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: socket -slug: socket -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to a tcp or unix socket and consumes a continuous stream of messages. - -```yml -# Config fields, showing default values -input: - label: "" - socket: - network: "" # No default (required) - address: /tmp/benthos.sock # No default (required) - auto_replay_nacks: true - scanner: - lines: {} -``` - -## Fields - -### `network` - -A network type to assume (unix|tcp). - - -Type: `string` -Options: `unix`, `tcp`. - -### `address` - -The address to connect to. - - -Type: `string` - -```yml -# Examples - -address: /tmp/benthos.sock - -address: 127.0.0.1:6000 -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"lines":{}}` -Requires version 4.25.0 or newer - - diff --git a/website/docs/components/inputs/socket_server.md b/website/docs/components/inputs/socket_server.md deleted file mode 100644 index f8739ad757..0000000000 --- a/website/docs/components/inputs/socket_server.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: socket_server -slug: socket_server -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Creates a server that receives a stream of messages over a tcp, udp or unix socket. - -```yml -# Config fields, showing default values -input: - label: "" - socket_server: - network: "" # No default (required) - address: /tmp/benthos.sock # No default (required) - address_cache: "" # No default (optional) - tls: - cert_file: "" # No default (optional) - key_file: "" # No default (optional) - self_signed: false - auto_replay_nacks: true - scanner: - lines: {} -``` - -## Fields - -### `network` - -A network type to accept. - - -Type: `string` -Options: `unix`, `tcp`, `udp`, `tls`. - -### `address` - -The address to listen from. - - -Type: `string` - -```yml -# Examples - -address: /tmp/benthos.sock - -address: 0.0.0.0:6000 -``` - -### `address_cache` - -An optional [`cache`](/docs/components/caches/about) within which this input should write it's bound address once known. The key of the cache item containing the address will be the label of the component suffixed with `_address` (e.g. `foo_address`), or `socket_server_address` when a label has not been provided. This is useful in situations where the address is dynamically allocated by the server (`127.0.0.1:0`) and you want to store the allocated address somewhere for reference by other systems and components. - - -Type: `string` -Requires version 4.25.0 or newer - -### `tls` - -TLS specific configuration, valid when the `network` is set to `tls`. - - -Type: `object` - -### `tls.cert_file` - -PEM encoded certificate for use with TLS. - - -Type: `string` - -### `tls.key_file` - -PEM encoded private key for use with TLS. - - -Type: `string` - -### `tls.self_signed` - -Whether to generate self signed certificates. - - -Type: `bool` -Default: `false` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"lines":{}}` -Requires version 4.25.0 or newer - - diff --git a/website/docs/components/inputs/sql_raw.md b/website/docs/components/inputs/sql_raw.md deleted file mode 100644 index fea086602e..0000000000 --- a/website/docs/components/inputs/sql_raw.md +++ /dev/null @@ -1,278 +0,0 @@ ---- -title: sql_raw -slug: sql_raw -type: input -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Executes a select query and creates a message for each row received. - -Introduced in version 4.10.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - sql_raw: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - query: SELECT * FROM footable WHERE user_id = $1; # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - sql_raw: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - query: SELECT * FROM footable WHERE user_id = $1; # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - auto_replay_nacks: true - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) -``` - - - - -Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute). - -## Examples - - - - - - -Here we preform an aggregate over a list of names in a table that are less than 3600 seconds old. - -```yaml -input: - sql_raw: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - query: "SELECT name, count(*) FROM person WHERE last_updated < $1 GROUP BY name;" - args_mapping: | - root = [ - now().ts_unix() - 3600 - ] -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `query` - -The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: - -| Driver | Placeholder Style | -|---|---| -| `clickhouse` | Dollar sign | -| `mysql` | Question mark | -| `postgres` | Dollar sign | -| `mssql` | Question mark | -| `sqlite` | Question mark | -| `oracle` | Colon | -| `snowflake` | Question mark | -| `trino` | Question mark | -| `gocosmos` | Colon | - - -Type: `string` - -```yml -# Examples - -query: SELECT * FROM footable WHERE user_id = $1; -``` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of columns specified. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - - diff --git a/website/docs/components/inputs/sql_select.md b/website/docs/components/inputs/sql_select.md deleted file mode 100644 index dab09c3e50..0000000000 --- a/website/docs/components/inputs/sql_select.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -title: sql_select -slug: sql_select -type: input -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Executes a select query and creates a message for each row received. - -Introduced in version 3.59.0. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - sql_select: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - where: type = ? and created_at > ? # No default (optional) - args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - sql_select: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - where: type = ? and created_at > ? # No default (optional) - args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) - prefix: "" # No default (optional) - suffix: "" # No default (optional) - auto_replay_nacks: true - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) -``` - - - - -Once the rows from the query are exhausted this input shuts down, allowing the pipeline to gracefully terminate (or the next input in a [sequence](/docs/components/inputs/sequence) to execute). - -## Examples - - - - - - -Here we define a pipeline that will consume all rows from a table created within the last hour by comparing the unix timestamp stored in the row column "created_at": - -```yaml -input: - sql_select: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - table: footable - columns: [ '*' ] - where: created_at >= ? - args_mapping: | - root = [ - now().ts_unix() - 3600 - ] -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `table` - -The table to select from. - - -Type: `string` - -```yml -# Examples - -table: foo -``` - -### `columns` - -A list of columns to select. - - -Type: `array` - -```yml -# Examples - -columns: - - '*' - -columns: - - foo - - bar - - baz -``` - -### `where` - -An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks, and will automatically be converted to dollar syntax when the postgres or clickhouse drivers are used. - - -Type: `string` - -```yml -# Examples - -where: type = ? and created_at > ? - -where: user_id = ? -``` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ "article", now().ts_format("2006-01-02") ] -``` - -### `prefix` - -An optional prefix to prepend to the select query (before SELECT). - - -Type: `string` - -### `suffix` - -An optional suffix to append to the select query. - - -Type: `string` - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - - diff --git a/website/docs/components/inputs/stdin.md b/website/docs/components/inputs/stdin.md deleted file mode 100644 index c961b9bdbd..0000000000 --- a/website/docs/components/inputs/stdin.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: stdin -slug: stdin -type: input -status: stable -categories: ["Local"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes data piped to stdin, chopping it into individual messages according to the specified scanner. - -```yml -# Config fields, showing default values -input: - label: "" - stdin: - scanner: - lines: {} - auto_replay_nacks: true -``` - -## Fields - -### `scanner` - -The [scanner](/docs/components/scanners/about) by which the stream of bytes consumed will be broken out into individual messages. Scanners are useful for processing large sources of data without holding the entirety of it within memory. For example, the `csv` scanner allows you to process individual CSV rows without loading the entire CSV file in memory at once. - - -Type: `scanner` -Default: `{"lines":{}}` -Requires version 4.25.0 or newer - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - - diff --git a/website/docs/components/inputs/subprocess.md b/website/docs/components/inputs/subprocess.md deleted file mode 100644 index 2889ee65e7..0000000000 --- a/website/docs/components/inputs/subprocess.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: subprocess -slug: subprocess -type: input -status: beta -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Executes a command, runs it as a subprocess, and consumes messages from it over stdout. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - subprocess: - name: cat # No default (required) - args: [] - codec: lines - restart_on_exit: false -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - subprocess: - name: cat # No default (required) - args: [] - codec: lines - restart_on_exit: false - max_buffer: 65536 -``` - - - - -Messages are consumed according to a specified codec. The command is executed once and if it terminates the input also closes down gracefully. Alternatively, the field `restart_on_close` can be set to `true` in order to have Benthos re-execute the command each time it stops. - -The field `max_buffer` defines the maximum message size able to be read from the subprocess. This value should be set significantly above the real expected maximum message size. - -The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. - -## Fields - -### `name` - -The command to execute as a subprocess. - - -Type: `string` - -```yml -# Examples - -name: cat - -name: sed - -name: awk -``` - -### `args` - -A list of arguments to provide the command. - - -Type: `array` -Default: `[]` - -### `codec` - -The way in which messages should be consumed from the subprocess. - - -Type: `string` -Default: `"lines"` -Options: `lines`. - -### `restart_on_exit` - -Whether the command should be re-executed each time the subprocess ends. - - -Type: `bool` -Default: `false` - -### `max_buffer` - -The maximum expected size of an individual message. - - -Type: `int` -Default: `65536` - - diff --git a/website/docs/components/inputs/twitter_search.md b/website/docs/components/inputs/twitter_search.md deleted file mode 100644 index 8f794ce684..0000000000 --- a/website/docs/components/inputs/twitter_search.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: twitter_search -slug: twitter_search -type: input -status: experimental -categories: ["Services","Social"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Consumes tweets matching a given search using the Twitter recent search V2 API. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - twitter_search: - query: "" # No default (required) - tweet_fields: [] - poll_period: 1m - backfill_period: 5m - cache: "" # No default (required) - api_key: "" # No default (required) - api_secret: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - twitter_search: - query: "" # No default (required) - tweet_fields: [] - poll_period: 1m - backfill_period: 5m - cache: "" # No default (required) - cache_key: last_tweet_id - rate_limit: "" - api_key: "" # No default (required) - api_secret: "" # No default (required) -``` - - - - -Continuously polls the [Twitter recent search V2 API](https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent) for tweets that match a given search query. - -Each tweet received is emitted as a JSON object message, with a field `id` and `text` by default. Extra fields [can be obtained from the search API](https://developer.twitter.com/en/docs/twitter-api/fields) when listed with the `tweet_fields` field. - -In order to paginate requests that are made the ID of the latest received tweet is stored in a [cache resource](/docs/components/caches/about), which is then used by subsequent requests to ensure only tweets after it are consumed. It is recommended that the cache you use is persistent so that Benthos can resume searches at the correct place on a restart. - -Authentication is done using OAuth 2.0 credentials which can be generated within the [Twitter developer portal](https://developer.twitter.com). - - -## Fields - -### `query` - -A search expression to use. - - -Type: `string` - -### `tweet_fields` - -An optional list of additional fields to obtain for each tweet, by default only the fields `id` and `text` are returned. For more info refer to the [twitter API docs.](https://developer.twitter.com/en/docs/twitter-api/fields) - - -Type: `array` -Default: `[]` - -### `poll_period` - -The length of time (as a duration string) to wait between each search request. This field can be set empty, in which case requests are made at the limit set by the rate limit. This field also supports cron expressions. - - -Type: `string` -Default: `"1m"` - -### `backfill_period` - -A duration string indicating the maximum age of tweets to acquire when starting a search. - - -Type: `string` -Default: `"5m"` - -### `cache` - -A cache resource to use for request pagination. - - -Type: `string` - -### `cache_key` - -The key identifier used when storing the ID of the last tweet received. - - -Type: `string` -Default: `"last_tweet_id"` - -### `rate_limit` - -An optional rate limit resource to restrict API requests with. - - -Type: `string` -Default: `""` - -### `api_key` - -An API key for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). - - -Type: `string` - -### `api_secret` - -An API secret for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). - - -Type: `string` - - diff --git a/website/docs/components/inputs/websocket.md b/website/docs/components/inputs/websocket.md deleted file mode 100644 index bc5c8bb9c2..0000000000 --- a/website/docs/components/inputs/websocket.md +++ /dev/null @@ -1,421 +0,0 @@ ---- -title: websocket -slug: websocket -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to a websocket server and continuously receives messages. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - websocket: - url: ws://localhost:4195/get/ws # No default (required) - auto_replay_nacks: true -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - websocket: - url: ws://localhost:4195/get/ws # No default (required) - open_message: "" # No default (optional) - open_message_type: binary - auto_replay_nacks: true - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - connection: - max_retries: -1 # No default (optional) - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} -``` - - - - -It is possible to configure an `open_message`, which when set to a non-empty string will be sent to the websocket server each time a connection is first established. - -## Fields - -### `url` - -The URL to connect to. - - -Type: `string` - -```yml -# Examples - -url: ws://localhost:4195/get/ws -``` - -### `open_message` - -An optional message to send to the server upon connection. - - -Type: `string` - -### `open_message_type` - -An optional flag to indicate the data type of open_message. - - -Type: `string` -Default: `"binary"` - -| Option | Summary | -|---|---| -| `binary` | Binary data open_message. | -| `text` | Text data open_message. The text message payload is interpreted as UTF-8 encoded text data. | - - -### `auto_replay_nacks` - -Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation. - - -Type: `bool` -Default: `true` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `connection` - -Customise how websocket connection attempts are made. - - -Type: `object` - -### `connection.max_retries` - -An optional limit to the number of consecutive retry attempts that will be made before abandoning the connection altogether and gracefully terminating the input. When all inputs terminate in this way the service (or stream) will shut down. If set to zero connections will never be reattempted upon a failure. If set below zero this field is ignored (effectively unset). - - -Type: `int` - -```yml -# Examples - -max_retries: -1 - -max_retries: 10 -``` - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - - diff --git a/website/docs/components/inputs/zmq4.md b/website/docs/components/inputs/zmq4.md deleted file mode 100644 index e260f97328..0000000000 --- a/website/docs/components/inputs/zmq4.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: zmq4 -type: input -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes messages from a ZeroMQ socket. - - - - - - -```yml -# Common config fields, showing default values -input: - label: "" - zmq4: - urls: [] - bind: false - socket_type: "" - sub_filters: [] -``` - - - - -```yml -# All config fields, showing default values -input: - label: "" - zmq4: - urls: [] - bind: false - socket_type: "" - sub_filters: [] - high_water_mark: 0 - poll_timeout: 5s -``` - - - - -By default Benthos does not build with components that require linking to external libraries. If you wish to build Benthos locally with this component then set the build tag `x_benthos_extra`: - -```shell -# With go -go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest - -# Using make -make TAGS=x_benthos_extra -``` - -There is a specific docker tag postfix `-cgo` for C builds containing this component. - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - tcp://localhost:5555 -``` - -### `bind` - -Whether to bind to the specified URLs (otherwise they are connected to). - - -Type: `bool` -Default: `false` - -### `socket_type` - -The socket type to connect as. - - -Type: `string` -Options: `PULL`, `SUB`. - -### `sub_filters` - -A list of subscription topic filters to use when consuming from a SUB socket. Specifying a single sub_filter of `''` will subscribe to everything. - - -Type: `array` -Default: `[]` - -### `high_water_mark` - -The message high water mark to use. - - -Type: `int` -Default: `0` - -### `poll_timeout` - -The poll timeout to use. - - -Type: `string` -Default: `"5s"` - - diff --git a/website/docs/components/logger/about.md b/website/docs/components/logger/about.md deleted file mode 100644 index a1cac2ac3f..0000000000 --- a/website/docs/components/logger/about.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Logger ---- - - - -Benthos logging prints to stdout (or stderr if your output is stdout) and is formatted as [logfmt](https://brandur.org/logfmt) by default. Use these configuration options to change both the logging formats as well as the destination of logs. - -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - - - -```yaml -logger: - level: INFO - format: logfmt - add_timestamp: false - static_fields: - '@service': benthos -``` - - - - -```yaml -logger: - level: WARN - format: json - file: - path: ./logs/benthos.ndjson - rotate: true -``` - - - - - -## Fields - -### `level` - -Set the minimum severity level for emitting logs. - - -Type: `string` -Default: `"INFO"` -Options: `OFF`, `FATAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`, `ALL`, `NONE`. - -### `format` - -Set the format of emitted logs. - - -Type: `string` -Default: `"logfmt"` -Options: `json`, `logfmt`. - -### `add_timestamp` - -Whether to include timestamps in logs. - - -Type: `bool` -Default: `false` - -### `level_name` - -The name of the level field added to logs when the `format` is `json`. - - -Type: `string` -Default: `"level"` - -### `timestamp_name` - -The name of the timestamp field added to logs when `add_timestamp` is set to `true` and the `format` is `json`. - - -Type: `string` -Default: `"time"` - -### `message_name` - -The name of the message field added to logs when the `format` is `json`. - - -Type: `string` -Default: `"msg"` - -### `static_fields` - -A map of key/value pairs to add to each structured log. - - -Type: map of `string` -Default: `{"@service":"benthos"}` - -### `file` - -Experimental: Specify fields for optionally writing logs to a file. - - -Type: `object` - -### `file.path` - -The file path to write logs to, if the file does not exist it will be created. Leave this field empty or unset to disable file based logging. - - -Type: `string` -Default: `""` - -### `file.rotate` - -Whether to rotate log files automatically. - - -Type: `bool` -Default: `false` - -### `file.rotate_max_age_days` - -The maximum number of days to retain old log files based on the timestamp encoded in their filename, after which they are deleted. Setting to zero disables this mechanism. - - -Type: `int` -Default: `0` - diff --git a/website/docs/components/metrics/about.md b/website/docs/components/metrics/about.md deleted file mode 100644 index 50a736e511..0000000000 --- a/website/docs/components/metrics/about.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: Metrics -sidebar_label: About ---- - -Benthos emits lots of metrics in order to expose how components configured within your pipeline are behaving. You can configure exactly where these metrics end up with the config field `metrics`, which describes a metrics format and destination. For example, if you wished to push them via the StatsD protocol you could use this configuration: - -```yaml -metrics: - statsd: - address: localhost:8125 - flush_period: 100ms -``` - -The default metrics configuration is to expose Prometheus metrics on the [service-wide HTTP endpoint][http.about] at the endpoints `/metrics` and `/stats`. - -### Timings - -It's worth noting that timing metrics within Benthos are measured in nanoseconds and are therefore named with a `_ns` suffix. However, some exporters do not support this level of precision and are downgraded, or have the unit converted for convenience. In these cases the exporter documentation outlines the conversion and why it is made. - -## Metric Names - -Each major Benthos component type emits one or more metrics with the name prefixed by the type. These metrics are intended to provide an overview of behaviour, performance and health. Some specific component implementations may provide their own unique metrics on top of these standardised ones, these extra metrics can be found listed on their respective documentation pages. - -### Inputs - -- `input_received`: A count of the number of messages received by the input. -- `input_latency_ns`: Measures the roundtrip latency in nanoseconds from the point at which a message is read up to the moment the message has either been acknowledged by an output, has been stored within a buffer, or has been rejected (nacked). -- `batch_created`: A count of each time an input-level batch has been created using a batching policy. Includes a label `mechanism` describing the particular mechanism that triggered it, one of; `count`, `size`, `period`, `check`. -- `input_connection_up`: For continuous stream based inputs represents a count of the number of the times the input has successfully established a connection to the target source. For poll based inputs that do not retain an active connection this value will increment once. -- `input_connection_failed`: For continuous stream based inputs represents a count of the number of times the input has failed to establish a connection to the target source. -- `input_connection_lost`: For continuous stream based inputs represents a count of the number of times the input has lost a previously established connection to the target source. - -:::caution -The behaviour of connection metrics may differ based on input type due to certain libraries and protocols obfuscating the concept of a single connection. -::: - -### Buffers - -- `buffer_received`: A count of the number of messages written to the buffer. -- `buffer_batch_received`: A count of the number of message batches written to the buffer. -- `buffer_sent`: A count of the number of messages read from the buffer. -- `buffer_batch_sent`: A count of the number of message batches read from the buffer. -- `buffer_latency_ns`: Measures the roundtrip latency in nanoseconds from the point at which a message is read from the buffer up to the moment it has been acknowledged by the output. -- `batch_created`: A count of each time a buffer-level batch has been created using a batching policy. Includes a label `mechanism` describing the particular mechanism that triggered it, one of; `count`, `size`, `period`, `check`. - -### Processors - -- `processor_received`: A count of the number of messages the processor has been executed upon. -- `processor_batch_received`: A count of the number of message batches the processor has been executed upon. -- `processor_sent`: A count of the number of messages the processor has returned. -- `processor_batch_sent`: A count of the number of message batches the processor has returned. -- `processor_error`: A count of the number of times the processor has errored. In cases where an error is batch-wide the count is incremented by one, and therefore would not match the number of messages. -- `processor_latency_ns`: Latency of message processing in nanoseconds. When a processor acts upon a batch of messages this latency measures the time taken to process all messages of the batch. - -### Outputs - -- `output_sent`: A count of the number of messages sent by the output. -- `output_batch_sent`: A count of the number of message batches sent by the output. -- `output_error`: A count of the number of send attempts that have failed. On failed batched sends this count is incremented once only. -- `output_latency_ns`: Latency of writes in nanoseconds. This metric may not be populated by outputs that are pull-based such as the `http_server`. -- `batch_created`: A count of each time an output-level batch has been created using a batching policy. Includes a label `mechanism` describing the particular mechanism that triggered it, one of; `count`, `size`, `period`, `check`. -- `output_connection_up`: For continuous stream based outputs represents a count of the number of the times the output has successfully established a connection to the target sink. For poll based outputs that do not retain an active connection this value will increment once. -- `output_connection_failed`: For continuous stream based outputs represents a count of the number of times the output has failed to establish a connection to the target sink. -- `output_connection_lost`: For continuous stream based outputs represents a count of the number of times the output has lost a previously established connection to the target sink. - -:::caution -The behaviour of connection metrics may differ based on output type due to certain libraries and protocols obfuscating the concept of a single connection. -::: - -### Caches - -All cache metrics have a label `operation` denoting the operation that triggered the metric series, one of; `add`, `get`, `set` or `delete`. - -- `cache_success`: A count of the number of successful cache operations. -- `cache_error`: A count of the number of cache operations that resulted in an error. -- `cache_latency_ns`: Latency of operations in nanoseconds. -- `cache_not_found`: A count of the number of get operations that yielded no value due to the item not being found. This count is separate from `cache_error`. -- `cache_duplicate`: A count of the number of add operations that were aborted due to the key already existing. This count is separate from `cache_error`. - -### Rate Limits - -- `rate_limit_checked`: A count of the number of times the rate limit has been probed. -- `rate_limit_triggered`: A count of the number of times the rate limit has been triggered by a probe. -- `rate_limit_error`: A count of the number of times the rate limit has errored when probed. - -## Metric Labels - -The standard metric names are unique to the component type, but a benthos config may consist of any number of component instantiations. In order to provide a metrics series that is unique for each instantiation Benthos adds labels (or tags) that uniquely identify the instantiation. These labels are as follows: - -### `path` - -The `path` label contains a string representation of the position of a component instantiation within a config in a format that would locate it within a Bloblang mapping, beginning at `root`. This path is a best attempt and may not exactly represent the source component position in all cases and is intended to be used for assisting observability only. - -This is the highest cardinality label since paths will change as configs are updated and expanded. It is therefore worth removing this label with a [mapping](#metric-mapping) in cases where you wish to restrict the number of unique metric series. - -### `label` - -The `label` label contains the unique label configured for a component emitting the metric series, or is empty for components that do not have a configured label. This is the most useful label for uniquely identifying a series for a component. - -### `stream` - -The `stream` label is present in a metric series emitted from a stream config executed when Benthos is running in [streams mode][streams.about], and is populated with the stream name. - -## Example - -The following Benthos configuration: - -```yaml -input: - label: foo - http_server: {} - -pipeline: - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() - -output: - label: bar - stdout: {} - -metrics: - prometheus: {} -``` - -Would produce the following metrics series: - -```text -input_latency_ns{label="foo",path="root.input"} -input_received{endpoint="post",label="foo",path="root.input"} -input_received{endpoint="websocket",label="foo",path="root.input"} - -processor_batch_received{label="",path="root.pipeline.processors.0"} -processor_batch_sent{label="",path="root.pipeline.processors.0"} -processor_error{label="",path="root.pipeline.processors.0"} -processor_latency_ns{label="",path="root.pipeline.processors.0"} -processor_received{label="",path="root.pipeline.processors.0"} -processor_sent{label="",path="root.pipeline.processors.0"} - -output_batch_sent{label="bar",path="root.output"} -output_connection_failed{label="bar",path="root.output"} -output_connection_lost{label="bar",path="root.output"} -output_connection_up{label="bar",path="root.output"} -output_error{label="bar",path="root.output"} -output_latency_ns{label="bar",path="root.output"} -output_sent{label="bar",path="root.output"} -``` - -## Metric Mapping - -Since Benthos emits a large variety of metrics it is often useful to restrict or modify the metrics that are emitted. This can be done using the [Bloblang mapping language][bloblang.about] in the field `metrics.mapping`. This is a mapping executed for each metric that is registered within the Benthos service and allows you to delete an entire series, modify the series name and delete or modify individual labels. - -Within the mapping the input document (referenced by the keyword `this`) is a string value containing the metric name, and the resulting document (referenced by the keyword `root`) must be a string value containing the resulting name. As is standard in Bloblang mappings, if the value of `root` is not assigned within the mapping then the metric name remains unchanged. If the value of `root` is `deleted()` then the metric series is dropped. - -Labels can be referenced as metadata values with the function `meta`, where if the label does not exist in the series being mapped the value `null` is returned. Labels can be changed by using meta assignments, and can be assigned `deleted()` in order to remove them. - -For example, the following mapping removes all but the `label` label entirely, which reduces the cardinality of each series. It also renames the `label` (for some reason) so that labels containing meows now contain woofs. Finally, the mapping restricts the metrics emitted to only three series; one for the input count, one for processor errors, and one for the output count, it does this by looking up metric names in a static array of allowed names, and if not present the `root` is assigned `deleted()`: - -```yaml -metrics: - mapping: | - # Delete all pre-existing labels - meta = deleted() - - # Re-add the `label` label with meows replaced with woofs - meta label = meta("label").replace("meow", "woof") - - # Delete all metric series that aren't in our list - root = if ![ - "input_received", - "processor_error", - "output_sent", - ].contains(this) { deleted() } - - prometheus: - use_histogram_timing: false -``` - -import ComponentSelect from '@theme/ComponentSelect'; - - - -[bloblang.about]: /docs/guides/bloblang/about -[http.about]: /docs/components/http/about -[streams.about]: /docs/guides/streams_mode/about \ No newline at end of file diff --git a/website/docs/components/metrics/aws_cloudwatch.md b/website/docs/components/metrics/aws_cloudwatch.md deleted file mode 100644 index 9e25b08591..0000000000 --- a/website/docs/components/metrics/aws_cloudwatch.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: aws_cloudwatch -slug: aws_cloudwatch -type: metrics -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Send metrics to AWS CloudWatch using the PutMetricData endpoint. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -metrics: - aws_cloudwatch: - namespace: Benthos - mapping: "" -``` - - - - -```yml -# All config fields, showing default values -metrics: - aws_cloudwatch: - namespace: Benthos - flush_period: 100ms - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - mapping: "" -``` - - - - -### Timing Metrics - -The smallest timing unit that CloudWatch supports is microseconds, therefore timing metrics are automatically downgraded to microseconds (by dividing delta values by 1000). This conversion will also apply to custom timing metrics produced with a `metric` processor. - -### Billing - -AWS bills per metric series exported, it is therefore STRONGLY recommended that you reduce the metrics that are exposed with a `mapping` like this: - -```yaml -metrics: - mapping: | - if ![ - "input_received", - "input_latency", - "output_sent", - ].contains(this) { deleted() } - aws_cloudwatch: - namespace: Foo -``` - -## Fields - -### `namespace` - -The namespace used to distinguish metrics from other services. - - -Type: `string` -Default: `"Benthos"` - -### `flush_period` - -The period of time between PutMetricData requests. - - -Type: `string` -Default: `"100ms"` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/metrics/influxdb.md b/website/docs/components/metrics/influxdb.md deleted file mode 100644 index 636aec2f6c..0000000000 --- a/website/docs/components/metrics/influxdb.md +++ /dev/null @@ -1,349 +0,0 @@ ---- -title: influxdb -slug: influxdb -type: metrics -status: beta ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Send metrics to InfluxDB 1.x using the `/write` endpoint. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -metrics: - influxdb: - url: "" # No default (required) - db: "" # No default (required) - mapping: "" -``` - - - - -```yml -# All config fields, showing default values -metrics: - influxdb: - url: "" # No default (required) - db: "" # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - username: "" - password: "" - include: - runtime: "" - debug_gc: "" - interval: 1m - ping_interval: 20s - precision: s - timeout: 5s - tags: {} - retention_policy: "" # No default (optional) - write_consistency: "" # No default (optional) - mapping: "" -``` - - - - -See https://docs.influxdata.com/influxdb/v1.8/tools/api/#write-http-endpoint for further details on the write API. - -## Fields - -### `url` - -A URL of the format `[https|http|udp]://host:port` to the InfluxDB host. - - -Type: `string` - -### `db` - -The name of the database to use. - - -Type: `string` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `username` - -A username (when applicable). - - -Type: `string` -Default: `""` - -### `password` - -A password (when applicable). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `include` - -Optional additional metrics to collect, enabling these metrics may have some performance implications as it acquires a global semaphore and does `stoptheworld()`. - - -Type: `object` - -### `include.runtime` - -A duration string indicating how often to poll and collect runtime metrics. Leave empty to disable this metric - - -Type: `string` -Default: `""` - -```yml -# Examples - -runtime: 1m -``` - -### `include.debug_gc` - -A duration string indicating how often to poll and collect GC metrics. Leave empty to disable this metric. - - -Type: `string` -Default: `""` - -```yml -# Examples - -debug_gc: 1m -``` - -### `interval` - -A duration string indicating how often metrics should be flushed. - - -Type: `string` -Default: `"1m"` - -### `ping_interval` - -A duration string indicating how often to ping the host. - - -Type: `string` -Default: `"20s"` - -### `precision` - -[ns|us|ms|s] timestamp precision passed to write api. - - -Type: `string` -Default: `"s"` - -### `timeout` - -How long to wait for response for both ping and writing metrics. - - -Type: `string` -Default: `"5s"` - -### `tags` - -Global tags added to each metric. - - -Type: `object` -Default: `{}` - -```yml -# Examples - -tags: - hostname: localhost - zone: danger -``` - -### `retention_policy` - -Sets the retention policy for each write. - - -Type: `string` - -### `write_consistency` - -[any|one|quorum|all] sets write consistency when available. - - -Type: `string` - - diff --git a/website/docs/components/metrics/json_api.md b/website/docs/components/metrics/json_api.md deleted file mode 100644 index 3ec03bc55d..0000000000 --- a/website/docs/components/metrics/json_api.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: json_api -slug: json_api -type: metrics -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Serves metrics as JSON object with the service wide HTTP service at the endpoints `/stats` and `/metrics`. - -```yml -# Config fields, showing default values -metrics: - json_api: {} - mapping: "" -``` - -This metrics type is useful for debugging as it provides a human readable format that you can parse with tools such as `jq` - - diff --git a/website/docs/components/metrics/logger.md b/website/docs/components/metrics/logger.md deleted file mode 100644 index 9eb77b695e..0000000000 --- a/website/docs/components/metrics/logger.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: logger -slug: logger -type: metrics -status: beta ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Prints aggregated metrics through the logger. - -```yml -# Config fields, showing default values -metrics: - logger: - push_interval: "" # No default (optional) - flush_metrics: false - mapping: "" -``` - -Prints each metric produced by Benthos as a log event (level `info` by default) during shutdown, and optionally on an interval. - -This metrics type is useful for debugging pipelines when you only have access to the logger output and not the service-wide server. Otherwise it's recommended that you use either the `prometheus` or `json_api`types. - -## Fields - -### `push_interval` - -An optional period of time to continuously print all metrics. - - -Type: `string` - -### `flush_metrics` - -Whether counters and timing metrics should be reset to 0 each time metrics are printed. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/metrics/none.md b/website/docs/components/metrics/none.md deleted file mode 100644 index a33c0a63cd..0000000000 --- a/website/docs/components/metrics/none.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: none -slug: none -type: metrics -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Disable metrics entirely. - -```yml -# Config fields, showing default values -metrics: - none: {} - mapping: "" -``` - - diff --git a/website/docs/components/metrics/prometheus.md b/website/docs/components/metrics/prometheus.md deleted file mode 100644 index 5563f65bff..0000000000 --- a/website/docs/components/metrics/prometheus.md +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: prometheus -slug: prometheus -type: metrics -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Host endpoints (`/metrics` and `/stats`) for Prometheus scraping. - - - - - - -```yml -# Common config fields, showing default values -metrics: - prometheus: {} - mapping: "" -``` - - - - -```yml -# All config fields, showing default values -metrics: - prometheus: - use_histogram_timing: false - histogram_buckets: [] - summary_quantiles_objectives: - - quantile: 0.5 - error: 0.05 - - quantile: 0.9 - error: 0.01 - - quantile: 0.99 - error: 0.001 - add_process_metrics: false - add_go_metrics: false - push_url: "" # No default (optional) - push_interval: "" # No default (optional) - push_job_name: benthos_push - push_basic_auth: - username: "" - password: "" - file_output_path: "" - mapping: "" -``` - - - - -## Fields - -### `use_histogram_timing` - -Whether to export timing metrics as a histogram, if `false` a summary is used instead. When exporting histogram timings the delta values are converted from nanoseconds into seconds in order to better fit within bucket definitions. For more information on histograms and summaries refer to: https://prometheus.io/docs/practices/histograms/. - - -Type: `bool` -Default: `false` -Requires version 3.63.0 or newer - -### `histogram_buckets` - -Timing metrics histogram buckets (in seconds). If left empty defaults to DefBuckets (https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#pkg-variables). Applicable when `use_histogram_timing` is set to `true`. - - -Type: `array` -Default: `[]` -Requires version 3.63.0 or newer - -### `summary_quantiles_objectives` - -A list of timing metrics summary buckets (as quantiles). Applicable when `use_histogram_timing` is set to `false`. - - -Type: `array` -Default: `[{"error":0.05,"quantile":0.5},{"error":0.01,"quantile":0.9},{"error":0.001,"quantile":0.99}]` -Requires version 4.23.0 or newer - -```yml -# Examples - -summary_quantiles_objectives: - - error: 0.05 - quantile: 0.5 - - error: 0.01 - quantile: 0.9 - - error: 0.001 - quantile: 0.99 -``` - -### `summary_quantiles_objectives[].quantile` - -Quantile value. - - -Type: `float` -Default: `0` - -### `summary_quantiles_objectives[].error` - -Permissible margin of error for quantile calculations. Precise calculations in a streaming context (without prior knowledge of the full dataset) can be resource-intensive. To balance accuracy with computational efficiency, an error margin is introduced. For instance, if the 90th quantile (`0.9`) is determined to be `100ms` with a 1% error margin (`0.01`), the true value will fall within the `[99ms, 101ms]` range.) - - -Type: `float` -Default: `0` - -### `add_process_metrics` - -Whether to export process metrics such as CPU and memory usage in addition to Benthos metrics. - - -Type: `bool` -Default: `false` - -### `add_go_metrics` - -Whether to export Go runtime metrics such as GC pauses in addition to Benthos metrics. - - -Type: `bool` -Default: `false` - -### `push_url` - -An optional [Push Gateway URL](#push-gateway) to push metrics to. - - -Type: `string` - -### `push_interval` - -The period of time between each push when sending metrics to a Push Gateway. - - -Type: `string` - -### `push_job_name` - -An identifier for push jobs. - - -Type: `string` -Default: `"benthos_push"` - -### `push_basic_auth` - -The Basic Authentication credentials. - - -Type: `object` - -### `push_basic_auth.username` - -The Basic Authentication username. - - -Type: `string` -Default: `""` - -### `push_basic_auth.password` - -The Basic Authentication password. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `file_output_path` - -An optional file path to write all prometheus metrics on service shutdown. - - -Type: `string` -Default: `""` - -## Push Gateway - -The field `push_url` is optional and when set will trigger a push of metrics to a [Prometheus Push Gateway](https://prometheus.io/docs/instrumenting/pushing/) once Benthos shuts down. It is also possible to specify a `push_interval` which results in periodic pushes. - -The Push Gateway is useful for when Benthos instances are short lived. Do not include the "/metrics/jobs/..." path in the push URL. - -If the Push Gateway requires HTTP Basic Authentication it can be configured with `push_basic_auth`. - diff --git a/website/docs/components/metrics/statsd.md b/website/docs/components/metrics/statsd.md deleted file mode 100644 index 5268017ff0..0000000000 --- a/website/docs/components/metrics/statsd.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: statsd -slug: statsd -type: metrics -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Pushes metrics using the [StatsD protocol](https://github.com/statsd/statsd). Supported tagging formats are 'none', 'datadog' and 'influxdb'. - -```yml -# Config fields, showing default values -metrics: - statsd: - address: "" # No default (required) - flush_period: 100ms - tag_format: none - mapping: "" -``` - -## Fields - -### `address` - -The address to send metrics to. - - -Type: `string` - -### `flush_period` - -The time interval between metrics flushes. - - -Type: `string` -Default: `"100ms"` - -### `tag_format` - -Metrics tagging is supported in a variety of formats. - - -Type: `string` -Default: `"none"` -Options: `none`, `datadog`, `influxdb`. - - diff --git a/website/docs/components/outputs/about.md b/website/docs/components/outputs/about.md deleted file mode 100644 index 5df766dd66..0000000000 --- a/website/docs/components/outputs/about.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Outputs -sidebar_label: About ---- - -An output is a sink where we wish to send our consumed data after applying an optional array of [processors][processors]. Only one output is configured at the root of a Benthos config. However, the output can be a [broker][output.broker] which combines multiple outputs under a chosen brokering pattern, or a [switch][output.switch] which is used to multiplex against different outputs. - -An output config section looks like this: - -```yaml -output: - label: my_s3_output - - aws_s3: - bucket: TODO - path: '${! meta("kafka_topic") }/${! json("message.id") }.json' - - # Optional list of processing steps - processors: - - mapping: '{"message":this,"meta":{"link_count":this.links.length()}}' -``` - -## Back Pressure - -Benthos outputs apply back pressure to components upstream. This means if your output target starts blocking traffic Benthos will gracefully stop consuming until the issue is resolved. - -## Retries - -When a Benthos output fails to send a message the error is propagated back up to the input, where depending on the protocol it will either be pushed back to the source as a Noack (e.g. AMQP) or will be reattempted indefinitely with the commit withheld until success (e.g. Kafka). - -It's possible to instead have Benthos indefinitely retry an output until success with a [`retry`][output.retry] output. Some other outputs, such as the [`broker`][output.broker], might also retry indefinitely depending on their configuration. - -## Dead Letter Queues - -It's possible to create fallback outputs for when an output target fails using a [`fallback`][output.fallback] output: - -```yaml -output: - fallback: - - aws_sqs: - url: https://sqs.us-west-2.amazonaws.com/TODO/TODO - max_in_flight: 20 - - - http_client: - url: http://backup:1234/dlq - verb: POST -``` - -## Multiplexing Outputs - -There are a few different ways of multiplexing in Benthos, here's a quick run through: - -### Interpolation Multiplexing - -Some output fields support [field interpolation][interpolation], which is a super easy way to multiplex messages based on their contents in situations where you are multiplexing to the same service. - -For example, multiplexing against Kafka topics is a common pattern: - -```yaml -output: - kafka: - addresses: [ TODO:6379 ] - topic: ${! meta("target_topic") } -``` - -Refer to the field documentation for a given output to see if it support interpolation. - -### Switch Multiplexing - -A more advanced form of multiplexing is to route messages to different output configurations based on a query. This is easy with the [`switch` output][output.switch]: - -```yaml -output: - switch: - cases: - - check: this.type == "foo" - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/the_foos - - - check: this.type == "bar" - output: - gcp_pubsub: - project: dealing_with_mike - topic: mikes_bars - - - output: - redis_streams: - url: tcp://localhost:6379 - stream: everything_else - processors: - - mapping: | - root = this - root.type = this.type.not_null() | "unknown" -``` - -## Labels - -Outputs have an optional field `label` that can uniquely identify them in observability data such as metrics and logs. This can be useful when running configs with multiple outputs, otherwise their metrics labels will be generated based on their composition. For more information check out the [metrics documentation][metrics.about]. - -import ComponentsByCategory from '@theme/ComponentsByCategory'; - -## Categories - - - -import ComponentSelect from '@theme/ComponentSelect'; - - - -[processors]: /docs/components/processors/about -[output.broker]: /docs/components/outputs/broker -[output.switch]: /docs/components/outputs/switch -[output.retry]: /docs/components/outputs/retry -[output.fallback]: /docs/components/outputs/fallback -[interpolation]: /docs/configuration/interpolation -[metrics.about]: /docs/components/metrics/about \ No newline at end of file diff --git a/website/docs/components/outputs/amqp_0_9.md b/website/docs/components/outputs/amqp_0_9.md deleted file mode 100644 index bf250f1576..0000000000 --- a/website/docs/components/outputs/amqp_0_9.md +++ /dev/null @@ -1,462 +0,0 @@ ---- -title: amqp_0_9 -slug: amqp_0_9 -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to an AMQP (0.91) exchange. AMQP is a messaging protocol used by various message brokers, including RabbitMQ.Connects to an AMQP (0.91) queue. AMQP is a messaging protocol used by various message brokers, including RabbitMQ. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - amqp_0_9: - urls: [] # No default (required) - exchange: "" # No default (required) - key: "" - type: "" - metadata: - exclude_prefixes: [] - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - amqp_0_9: - urls: [] # No default (required) - exchange: "" # No default (required) - exchange_declare: - enabled: false - type: direct - durable: true - key: "" - type: "" - content_type: application/octet-stream - content_encoding: "" - correlation_id: "" - reply_to: "" - expiration: "" - message_id: "" - user_id: "" - app_id: "" - metadata: - exclude_prefixes: [] - priority: "" - max_in_flight: 64 - persistent: false - mandatory: false - immediate: false - timeout: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] -``` - - - - -The metadata from each message are delivered as headers. - -It's possible for this output type to create the target exchange by setting `exchange_declare.enabled` to `true`, if the exchange already exists then the declaration passively verifies that the settings match. - -TLS is automatic when connecting to an `amqps` URL, but custom settings can be enabled in the `tls` section. - -The fields 'key', 'exchange' and 'type' can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `urls` - -A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` -Requires version 3.58.0 or newer - -```yml -# Examples - -urls: - - amqp://guest:guest@127.0.0.1:5672/ - -urls: - - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ - -urls: - - amqp://127.0.0.1:5672/ - - amqp://127.0.0.2:5672/ -``` - -### `exchange` - -An AMQP exchange to publish to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `exchange_declare` - -Optionally declare the target exchange (passive). - - -Type: `object` - -### `exchange_declare.enabled` - -Whether to declare the exchange. - - -Type: `bool` -Default: `false` - -### `exchange_declare.type` - -The type of the exchange. - - -Type: `string` -Default: `"direct"` -Options: `direct`, `fanout`, `topic`, `x-custom`. - -### `exchange_declare.durable` - -Whether the exchange should be durable. - - -Type: `bool` -Default: `true` - -### `key` - -The binding key to set for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `type` - -The type property to set for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `content_type` - -The content type attribute to set for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"application/octet-stream"` - -### `content_encoding` - -The content encoding attribute to set for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `correlation_id` - -Set the correlation ID of each message with a dynamic interpolated expression. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `reply_to` - -Carries response queue name - set with a dynamic interpolated expression. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `expiration` - -Set the per-message TTL -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `message_id` - -Set the message ID of each message with a dynamic interpolated expression. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `user_id` - -Set the user ID to the name of the publisher. If this property is set by a publisher, its value must be equal to the name of the user used to open the connection. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `app_id` - -Set the application ID of each message with a dynamic interpolated expression. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `metadata` - -Specify criteria for which metadata values are attached to messages as headers. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `priority` - -Set the priority of each message with a dynamic interpolated expression. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -priority: "0" - -priority: ${! meta("amqp_priority") } - -priority: ${! json("doc.priority") } -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `persistent` - -Whether message delivery should be persistent (transient by default). - - -Type: `bool` -Default: `false` - -### `mandatory` - -Whether to set the mandatory flag on published messages. When set if a published message is routed to zero queues it is returned. - - -Type: `bool` -Default: `false` - -### `immediate` - -Whether to set the immediate flag on published messages. When set if there are no ready consumers of a queue then the message is dropped instead of waiting. - - -Type: `bool` -Default: `false` - -### `timeout` - -The maximum period to wait before abandoning it and reattempting. If not set, wait indefinitely. - - -Type: `string` -Default: `""` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - - diff --git a/website/docs/components/outputs/amqp_1.md b/website/docs/components/outputs/amqp_1.md deleted file mode 100644 index 5892432fe4..0000000000 --- a/website/docs/components/outputs/amqp_1.md +++ /dev/null @@ -1,342 +0,0 @@ ---- -title: amqp_1 -slug: amqp_1 -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to an AMQP (1.0) server. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - amqp_1: - urls: [] # No default (optional) - target_address: /foo # No default (required) - max_in_flight: 64 - metadata: - exclude_prefixes: [] -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - amqp_1: - urls: [] # No default (optional) - target_address: /foo # No default (required) - max_in_flight: 64 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - application_properties_map: "" # No default (optional) - sasl: - mechanism: none - user: "" - password: "" - metadata: - exclude_prefixes: [] -``` - - - - -### Metadata - -Message metadata is added to each AMQP message as string annotations. In order to control which metadata keys are added use the `metadata` config field. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `urls` - -A list of URLs to connect to. The first URL to successfully establish a connection will be used until the connection is closed. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` -Requires version 4.23.0 or newer - -```yml -# Examples - -urls: - - amqp://guest:guest@127.0.0.1:5672/ - -urls: - - amqp://127.0.0.1:5672/,amqp://127.0.0.2:5672/ - -urls: - - amqp://127.0.0.1:5672/ - - amqp://127.0.0.2:5672/ -``` - -### `target_address` - -The target address to write to. - - -Type: `string` - -```yml -# Examples - -target_address: /foo - -target_address: queue:/bar - -target_address: topic:/baz -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `application_properties_map` - -An optional Bloblang mapping that can be defined in order to set the `application-properties` on output messages. - - -Type: `string` - -### `sasl` - -Enables SASL authentication. - - -Type: `object` - -### `sasl.mechanism` - -The SASL authentication mechanism to use. - - -Type: `string` -Default: `"none"` - -| Option | Summary | -|---|---| -| `anonymous` | Anonymous SASL authentication. | -| `none` | No SASL based authentication. | -| `plain` | Plain text SASL authentication. | - - -### `sasl.user` - -A SASL plain text username. It is recommended that you use environment variables to populate this field. - - -Type: `string` -Default: `""` - -```yml -# Examples - -user: ${USER} -``` - -### `sasl.password` - -A SASL plain text password. It is recommended that you use environment variables to populate this field. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: ${PASSWORD} -``` - -### `metadata` - -Specify criteria for which metadata values are attached to messages as headers. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - - diff --git a/website/docs/components/outputs/aws_dynamodb.md b/website/docs/components/outputs/aws_dynamodb.md deleted file mode 100644 index 50dd3fc201..0000000000 --- a/website/docs/components/outputs/aws_dynamodb.md +++ /dev/null @@ -1,413 +0,0 @@ ---- -title: aws_dynamodb -slug: aws_dynamodb -type: output -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Inserts items into a DynamoDB table. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - aws_dynamodb: - table: "" # No default (required) - string_columns: {} - json_map_columns: {} - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - aws_dynamodb: - table: "" # No default (required) - string_columns: {} - json_map_columns: {} - ttl: "" - ttl_key: "" - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - max_retries: 3 - backoff: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - - - - -The field `string_columns` is a map of column names to string values, where the values are [function interpolated](/docs/configuration/interpolation#bloblang-queries) per message of a batch. This allows you to populate string columns of an item by extracting fields within the document payload or metadata like follows: - -```yml -string_columns: - id: ${!json("id")} - title: ${!json("body.title")} - topic: ${!meta("kafka_topic")} - full_content: ${!content()} -``` - -The field `json_map_columns` is a map of column names to json paths, where the [dot path](/docs/configuration/field_paths) is extracted from each document and converted into a map value. Both an empty path and the path `.` are interpreted as the root of the document. This allows you to populate map columns of an item like follows: - -```yml -json_map_columns: - user: path.to.user - whole_document: . -``` - -A column name can be empty: - -```yml -json_map_columns: - "": . -``` - -In which case the top level document fields will be written at the root of the item, potentially overwriting previously defined column values. If a path is not found within a document the column will not be populated. - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - - -## Fields - -### `table` - -The table to store messages in. - - -Type: `string` - -### `string_columns` - -A map of column keys to string values to store. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -string_columns: - full_content: ${!content()} - id: ${!json("id")} - title: ${!json("body.title")} - topic: ${!meta("kafka_topic")} -``` - -### `json_map_columns` - -A map of column keys to [field paths](/docs/configuration/field_paths) pointing to value data within messages. - - -Type: `object` -Default: `{}` - -```yml -# Examples - -json_map_columns: - user: path.to.user - whole_document: . - -json_map_columns: - "": . -``` - -### `ttl` - -An optional TTL to set for items, calculated from the moment the message is sent. - - -Type: `string` -Default: `""` - -### `ttl_key` - -The column key to place the TTL value within. - - -Type: `string` -Default: `""` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `3` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `backoff.max_elapsed_time` - -The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. - - -Type: `string` -Default: `"30s"` - - diff --git a/website/docs/components/outputs/aws_kinesis.md b/website/docs/components/outputs/aws_kinesis.md deleted file mode 100644 index 6e35bd1664..0000000000 --- a/website/docs/components/outputs/aws_kinesis.md +++ /dev/null @@ -1,354 +0,0 @@ ---- -title: aws_kinesis -slug: aws_kinesis -type: output -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to a Kinesis stream. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - aws_kinesis: - stream: foo # No default (required) - partition_key: "" # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - aws_kinesis: - stream: foo # No default (required) - partition_key: "" # No default (required) - hash_key: "" # No default (optional) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - max_retries: 0 - backoff: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - - - - -Both the `partition_key`(required) and `hash_key` (optional) fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages the interpolations are performed per message part. - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `stream` - -The stream to publish messages to. Streams can either be specified by their name or full ARN. - - -Type: `string` - -```yml -# Examples - -stream: foo - -stream: arn:aws:kinesis:*:111122223333:stream/my-stream -``` - -### `partition_key` - -A required key for partitioning messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `hash_key` - -A optional hash key for partitioning messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `0` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `backoff.max_elapsed_time` - -The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. - - -Type: `string` -Default: `"30s"` - - diff --git a/website/docs/components/outputs/aws_kinesis_firehose.md b/website/docs/components/outputs/aws_kinesis_firehose.md deleted file mode 100644 index 1f02cb2462..0000000000 --- a/website/docs/components/outputs/aws_kinesis_firehose.md +++ /dev/null @@ -1,326 +0,0 @@ ---- -title: aws_kinesis_firehose -slug: aws_kinesis_firehose -type: output -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to a Kinesis Firehose delivery stream. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - aws_kinesis_firehose: - stream: "" # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - aws_kinesis_firehose: - stream: "" # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - max_retries: 0 - backoff: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - - - - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - - -## Fields - -### `stream` - -The stream to publish messages to. - - -Type: `string` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `0` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `backoff.max_elapsed_time` - -The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. - - -Type: `string` -Default: `"30s"` - - diff --git a/website/docs/components/outputs/aws_s3.md b/website/docs/components/outputs/aws_s3.md deleted file mode 100644 index 69f2887e41..0000000000 --- a/website/docs/components/outputs/aws_s3.md +++ /dev/null @@ -1,502 +0,0 @@ ---- -title: aws_s3 -slug: aws_s3 -type: output -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends message parts as objects to an Amazon S3 bucket. Each object is uploaded with the path specified with the `path` field. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - aws_s3: - bucket: "" # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - tags: {} - content_type: application/octet-stream - metadata: - exclude_prefixes: [] - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - aws_s3: - bucket: "" # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - tags: {} - content_type: application/octet-stream - content_encoding: "" - cache_control: "" - content_disposition: "" - content_language: "" - website_redirect_location: "" - metadata: - exclude_prefixes: [] - storage_class: STANDARD - kms_key_id: "" - server_side_encryption: "" - force_path_style_urls: false - max_in_flight: 64 - timeout: 5s - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -In order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. - -### Metadata - -Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the [metadata docs](/docs/configuration/metadata). - -### Tags - -The tags field allows you to specify key/value pairs to attach to objects as tags, where the values support [interpolation functions](/docs/configuration/interpolation#bloblang-queries): - -```yaml -output: - aws_s3: - bucket: TODO - path: ${!count("files")}-${!timestamp_unix_nano()}.tar.gz - tags: - Key1: Value1 - Timestamp: ${!meta("Timestamp")} -``` - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -### Batching - -It's common to want to upload messages to S3 as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an [`archive`](/docs/components/processors/archive) and/or [`compress`](/docs/components/processors/compress) processor. - -For example, if we wished to upload messages as a .tar.gz archive of documents we could achieve that with the following config: - -```yaml -output: - aws_s3: - bucket: TODO - path: ${!count("files")}-${!timestamp_unix_nano()}.tar.gz - batching: - count: 100 - period: 10s - processors: - - archive: - format: tar - - compress: - algorithm: gzip -``` - -Alternatively, if we wished to upload JSON documents as a single large document containing an array of objects we can do that with: - -```yaml -output: - aws_s3: - bucket: TODO - path: ${!count("files")}-${!timestamp_unix_nano()}.json - batching: - count: 100 - processors: - - archive: - format: json_array -``` - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `bucket` - -The bucket to upload messages to. - - -Type: `string` - -### `path` - -The path of each message to upload. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` - -```yml -# Examples - -path: ${!count("files")}-${!timestamp_unix_nano()}.txt - -path: ${!meta("kafka_key")}.json - -path: ${!json("doc.namespace")}/${!json("doc.id")}.json -``` - -### `tags` - -Key/value pairs to store with the object as tags. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -tags: - Key1: Value1 - Timestamp: ${!meta("Timestamp")} -``` - -### `content_type` - -The content type to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"application/octet-stream"` - -### `content_encoding` - -An optional content encoding to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `cache_control` - -The cache control to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `content_disposition` - -The content disposition to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `content_language` - -The content language to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `website_redirect_location` - -The website redirect location to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `metadata` - -Specify criteria for which metadata values are attached to objects as headers. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `storage_class` - -The storage class to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"STANDARD"` -Options: `STANDARD`, `REDUCED_REDUNDANCY`, `GLACIER`, `STANDARD_IA`, `ONEZONE_IA`, `INTELLIGENT_TIERING`, `DEEP_ARCHIVE`. - -### `kms_key_id` - -An optional server side encryption key. - - -Type: `string` -Default: `""` - -### `server_side_encryption` - -An optional server side encryption algorithm. - - -Type: `string` -Default: `""` -Requires version 3.63.0 or newer - -### `force_path_style_urls` - -Forces the client API to use path style URLs, which helps when connecting to custom endpoints. - - -Type: `bool` -Default: `false` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `timeout` - -The maximum period to wait on an upload before abandoning it and reattempting. - - -Type: `string` -Default: `"5s"` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/outputs/aws_sns.md b/website/docs/components/outputs/aws_sns.md deleted file mode 100644 index 130400e413..0000000000 --- a/website/docs/components/outputs/aws_sns.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -title: aws_sns -slug: aws_sns -type: output -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to an AWS SNS topic. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - aws_sns: - topic_arn: "" # No default (required) - message_group_id: "" # No default (optional) - message_deduplication_id: "" # No default (optional) - max_in_flight: 64 - metadata: - exclude_prefixes: [] -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - aws_sns: - topic_arn: "" # No default (required) - message_group_id: "" # No default (optional) - message_deduplication_id: "" # No default (optional) - max_in_flight: 64 - metadata: - exclude_prefixes: [] - timeout: 5s - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `topic_arn` - -The topic to publish to. - - -Type: `string` - -### `message_group_id` - -An optional group ID to set for messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 3.60.0 or newer - -### `message_deduplication_id` - -An optional deduplication ID to set for messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 3.60.0 or newer - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `metadata` - -Specify criteria for which metadata values are sent as headers. - - -Type: `object` -Requires version 3.60.0 or newer - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `timeout` - -The maximum period to wait on an upload before abandoning it and reattempting. - - -Type: `string` -Default: `"5s"` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/outputs/aws_sqs.md b/website/docs/components/outputs/aws_sqs.md deleted file mode 100644 index 4e14537c62..0000000000 --- a/website/docs/components/outputs/aws_sqs.md +++ /dev/null @@ -1,378 +0,0 @@ ---- -title: aws_sqs -slug: aws_sqs -type: output -status: stable -categories: ["Services","AWS"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to an SQS queue. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - aws_sqs: - url: "" # No default (required) - message_group_id: "" # No default (optional) - message_deduplication_id: "" # No default (optional) - delay_seconds: "" # No default (optional) - max_in_flight: 64 - metadata: - exclude_prefixes: [] - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - aws_sqs: - url: "" # No default (required) - message_group_id: "" # No default (optional) - message_deduplication_id: "" # No default (optional) - delay_seconds: "" # No default (optional) - max_in_flight: 64 - metadata: - exclude_prefixes: [] - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - max_retries: 0 - backoff: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - - - - -Metadata values are sent along with the payload as attributes with the data type String. If the number of metadata values in a message exceeds the message attribute limit (10) then the top ten keys ordered alphabetically will be selected. - -The fields `message_group_id`, `message_deduplication_id` and `delay_seconds` can be set dynamically using [function interpolations](/docs/configuration/interpolation#bloblang-queries), which are resolved individually for each message of a batch. - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `url` - -The URL of the target SQS queue. - - -Type: `string` - -### `message_group_id` - -An optional group ID to set for messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `message_deduplication_id` - -An optional deduplication ID to set for messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `delay_seconds` - -An optional delay time in seconds for message. Value between 0 and 900 -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `64` - -### `metadata` - -Specify criteria for which metadata values are sent as headers. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `0` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `backoff.max_elapsed_time` - -The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. - - -Type: `string` -Default: `"30s"` - - diff --git a/website/docs/components/outputs/azure_blob_storage.md b/website/docs/components/outputs/azure_blob_storage.md deleted file mode 100644 index 8d58b533fc..0000000000 --- a/website/docs/components/outputs/azure_blob_storage.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: azure_blob_storage -slug: azure_blob_storage -type: output -status: beta -categories: ["Services","Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Sends message parts as objects to an Azure Blob Storage Account container. Each object is uploaded with the filename specified with the `container` field. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - azure_blob_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - container: messages-${!timestamp("2006")} # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - azure_blob_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - container: messages-${!timestamp("2006")} # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - blob_type: BLOCK - public_access_level: PRIVATE - max_in_flight: 64 -``` - - - - -In order to have a different path for each object you should use function -interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are -calculated per message of a batch. - -Supports multiple authentication methods but only one of the following is required: -- `storage_connection_string` -- `storage_account` and `storage_access_key` -- `storage_account` and `storage_sas_token` -- `storage_account` to access via [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) - -If multiple are set then the `storage_connection_string` is given priority. - -If the `storage_connection_string` does not contain the `AccountName` parameter, please specify it in the -`storage_account` field. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `storage_account` - -The storage account to access. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_access_key` - -The storage account access key. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_connection_string` - -A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. - - -Type: `string` -Default: `""` - -### `storage_sas_token` - -The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. - - -Type: `string` -Default: `""` - -### `container` - -The container for uploading the messages to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -container: messages-${!timestamp("2006")} -``` - -### `path` - -The path of each message to upload. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` - -```yml -# Examples - -path: ${!count("files")}-${!timestamp_unix_nano()}.json - -path: ${!meta("kafka_key")}.json - -path: ${!json("doc.namespace")}/${!json("doc.id")}.json -``` - -### `blob_type` - -Block and Append blobs are comprised of blocks, and each blob can support up to 50,000 blocks. The default value is `+"`BLOCK`"+`.` -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"BLOCK"` -Options: `BLOCK`, `APPEND`. - -### `public_access_level` - -The container's public access level. The default value is `PRIVATE`. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"PRIVATE"` -Options: `PRIVATE`, `BLOB`, `CONTAINER`. - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/azure_cosmosdb.md b/website/docs/components/outputs/azure_cosmosdb.md deleted file mode 100644 index 3e61e1757d..0000000000 --- a/website/docs/components/outputs/azure_cosmosdb.md +++ /dev/null @@ -1,492 +0,0 @@ ---- -title: azure_cosmosdb -slug: azure_cosmosdb -type: output -status: experimental -categories: ["Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Creates or updates messages as JSON documents in [Azure CosmosDB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction). - -Introduced in version v4.25.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - azure_cosmosdb: - endpoint: https://localhost:8081 # No default (optional) - account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) - connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) - database: testdb # No default (required) - container: testcontainer # No default (required) - partition_keys_map: root = "blobfish" # No default (required) - operation: Create - item_id: ${! json("id") } # No default (optional) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - azure_cosmosdb: - endpoint: https://localhost:8081 # No default (optional) - account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) - connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) - database: testdb # No default (required) - container: testcontainer # No default (required) - partition_keys_map: root = "blobfish" # No default (required) - operation: Create - patch_operations: [] # No default (optional) - patch_condition: from c where not is_defined(c.blobfish) # No default (optional) - auto_id: true - item_id: ${! json("id") } # No default (optional) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - max_in_flight: 64 -``` - - - - -When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. More details can be found [here](https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents). - -The `partition_keys` field must resolve to the same value(s) across the entire message batch. - - -## Credentials - -You can use one of the following authentication mechanisms: - -- Set the `endpoint` field and the `account_key` field -- Set only the `endpoint` field to use [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) -- Set the `connection_string` field - - -## Batching - -CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (details [here](https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits)). - - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Examples - - - - - -Create new documents in the `blobfish` container with partition key `/habitat`. - -```yaml -output: - azure_cosmosdb: - endpoint: http://localhost:8080 - account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== - database: blobbase - container: blobfish - partition_keys_map: root = json("habitat") - operation: Create -``` - - - - -Execute the Patch operation on documents from the `blobfish` container. - -```yaml -output: - azure_cosmosdb: - endpoint: http://localhost:8080 - account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== - database: testdb - container: blobfish - partition_keys_map: root = json("habitat") - item_id: ${! json("id") } - operation: Patch - patch_operations: - # Add a new /diet field - - operation: Add - path: /diet - value_map: root = json("diet") - # Remove the first location from the /locations array field - - operation: Remove - path: /locations/0 - # Add new location at the end of the /locations array field - - operation: Add - path: /locations/- - value_map: root = "Challenger Deep" -``` - - - - -## Fields - -### `endpoint` - -CosmosDB endpoint. - - -Type: `string` - -```yml -# Examples - -endpoint: https://localhost:8081 -``` - -### `account_key` - -Account key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -```yml -# Examples - -account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== -``` - -### `connection_string` - -Connection string. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -```yml -# Examples - -connection_string: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; -``` - -### `database` - -Database. - - -Type: `string` - -```yml -# Examples - -database: testdb -``` - -### `container` - -Container. - - -Type: `string` - -```yml -# Examples - -container: testcontainer -``` - -### `partition_keys_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided. - - -Type: `string` - -```yml -# Examples - -partition_keys_map: root = "blobfish" - -partition_keys_map: root = 41 - -partition_keys_map: root = true - -partition_keys_map: root = null - -partition_keys_map: root = json("blobfish").depth -``` - -### `operation` - -Operation. - - -Type: `string` -Default: `"Create"` - -| Option | Summary | -|---|---| -| `Create` | Create operation. | -| `Delete` | Delete operation. | -| `Patch` | Patch operation. | -| `Replace` | Replace operation. | -| `Upsert` | Upsert operation. | - - -### `patch_operations` - -Patch operations to be performed when `operation: Patch` . - - -Type: `array` - -### `patch_operations[].operation` - -Operation. - - -Type: `string` -Default: `"Add"` - -| Option | Summary | -|---|---| -| `Add` | Add patch operation. | -| `Increment` | Increment patch operation. | -| `Remove` | Remove patch operation. | -| `Replace` | Replace patch operation. | -| `Set` | Set patch operation. | - - -### `patch_operations[].path` - -Path. - - -Type: `string` - -```yml -# Examples - -path: /foo/bar/baz -``` - -### `patch_operations[].value_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a value of any type that is supported by CosmosDB. - - -Type: `string` - -```yml -# Examples - -value_map: root = "blobfish" - -value_map: root = 41 - -value_map: root = true - -value_map: root = json("blobfish").depth - -value_map: root = [1, 2, 3] -``` - -### `patch_condition` - -Patch operation condition. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -patch_condition: from c where not is_defined(c.blobfish) -``` - -### `auto_id` - -Automatically set the item `id` field to a random UUID v4. If the `id` field is already set, then it will not be overwritten. Setting this to `false` can improve performance, since the messages will not have to be parsed. - - -Type: `bool` -Default: `true` - -### `item_id` - -ID of item to replace or delete. Only used by the Replace and Delete operations -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -item_id: ${! json("id") } -``` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - -## CosmosDB Emulator - -If you wish to run the CosmosDB emulator that is referenced in the documentation [here](https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator), the following Docker command should do the trick: - -```shell -> docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator -``` - -Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. - -Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run [mitmproxy](https://mitmproxy.org/) like so: - -```shell -> mitmproxy -k --mode "reverse:https://localhost:8081" -``` - -Then you can access the CosmosDB UI via `http://localhost:8080/_explorer/index.html` and use `http://localhost:8080` as the CosmosDB endpoint. - - diff --git a/website/docs/components/outputs/azure_queue_storage.md b/website/docs/components/outputs/azure_queue_storage.md deleted file mode 100644 index 86d98be8e3..0000000000 --- a/website/docs/components/outputs/azure_queue_storage.md +++ /dev/null @@ -1,252 +0,0 @@ ---- -title: azure_queue_storage -slug: azure_queue_storage -type: output -status: beta -categories: ["Services","Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Sends messages to an Azure Storage Queue. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - azure_queue_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - queue_name: "" # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - azure_queue_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - queue_name: "" # No default (required) - ttl: "" - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Only one authentication method is required, `storage_connection_string` or `storage_account` and `storage_access_key`. If both are set then the `storage_connection_string` is given priority. - -In order to set the `queue_name` you can use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `storage_account` - -The storage account to access. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_access_key` - -The storage account access key. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_connection_string` - -A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. - - -Type: `string` -Default: `""` - -### `storage_sas_token` - -The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. - - -Type: `string` -Default: `""` - -### `queue_name` - -The name of the target Queue Storage queue. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `ttl` - -The TTL of each individual message as a duration string. Defaults to 0, meaning no retention period is set -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -ttl: 60s - -ttl: 5m - -ttl: 36h -``` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/azure_table_storage.md b/website/docs/components/outputs/azure_table_storage.md deleted file mode 100644 index ae269f8f7d..0000000000 --- a/website/docs/components/outputs/azure_table_storage.md +++ /dev/null @@ -1,348 +0,0 @@ ---- -title: azure_table_storage -slug: azure_table_storage -type: output -status: beta -categories: ["Services","Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Stores messages in an Azure Table Storage table. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - azure_table_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - table_name: ${! meta("kafka_topic") } # No default (required) - partition_key: "" - row_key: "" - properties: {} - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - azure_table_storage: - storage_account: "" - storage_access_key: "" - storage_connection_string: "" - storage_sas_token: "" - table_name: ${! meta("kafka_topic") } # No default (required) - partition_key: "" - row_key: "" - properties: {} - transaction_type: INSERT - max_in_flight: 64 - timeout: 5s - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Only one authentication method is required, `storage_connection_string` or `storage_account` and `storage_access_key`. If both are set then the `storage_connection_string` is given priority. - -In order to set the `table_name`, `partition_key` and `row_key` you can use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. - -If the `properties` are not set in the config, all the `json` fields are marshalled and stored in the table, which will be created if it does not exist. - -The `object` and `array` fields are marshaled as strings. e.g.: - -The JSON message: - -```json -{ - "foo": 55, - "bar": { - "baz": "a", - "bez": "b" - }, - "diz": ["a", "b"] -} -``` - -Will store in the table the following properties: - -```yml -foo: '55' -bar: '{ "baz": "a", "bez": "b" }' -diz: '["a", "b"]' -``` - -It's also possible to use function interpolations to get or transform the properties values, e.g.: - -```yml -properties: - device: '${! json("device") }' - timestamp: '${! json("timestamp") }' -``` - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `storage_account` - -The storage account to access. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_access_key` - -The storage account access key. This field is ignored if `storage_connection_string` is set. - - -Type: `string` -Default: `""` - -### `storage_connection_string` - -A storage account connection string. This field is required if `storage_account` and `storage_access_key` / `storage_sas_token` are not set. - - -Type: `string` -Default: `""` - -### `storage_sas_token` - -The storage account SAS token. This field is ignored if `storage_connection_string` or `storage_access_key` are set. - - -Type: `string` -Default: `""` - -### `table_name` - -The table to store messages into. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -table_name: ${! meta("kafka_topic") } - -table_name: ${! json("table") } -``` - -### `partition_key` - -The partition key. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -partition_key: ${! json("date") } -``` - -### `row_key` - -The row key. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -row_key: ${! json("device")}-${!uuid_v4() } -``` - -### `properties` - -A map of properties to store into the table. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -### `transaction_type` - -Type of transaction operation. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"INSERT"` -Options: `INSERT`, `INSERT_MERGE`, `INSERT_REPLACE`, `UPDATE_MERGE`, `UPDATE_REPLACE`, `DELETE`. - -```yml -# Examples - -transaction_type: ${! json("operation") } - -transaction_type: ${! meta("operation") } - -transaction_type: INSERT -``` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `64` - -### `timeout` - -The maximum period to wait on an upload before abandoning it and reattempting. - - -Type: `string` -Default: `"5s"` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/beanstalkd.md b/website/docs/components/outputs/beanstalkd.md deleted file mode 100644 index 87063da34c..0000000000 --- a/website/docs/components/outputs/beanstalkd.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: beanstalkd -slug: beanstalkd -type: output -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Write messages to a Beanstalkd queue. - -Introduced in version 4.7.0. - -```yml -# Config fields, showing default values -output: - label: "" - beanstalkd: - address: 127.0.0.1:11300 # No default (required) - max_in_flight: 64 -``` - -## Fields - -### `address` - -An address to connect to. - - -Type: `string` - -```yml -# Examples - -address: 127.0.0.1:11300 -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/broker.md b/website/docs/components/outputs/broker.md deleted file mode 100644 index 6e4af54adb..0000000000 --- a/website/docs/components/outputs/broker.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: broker -slug: broker -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Allows you to route messages to multiple child outputs using a range of brokering [patterns](#patterns). - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - broker: - pattern: fan_out - outputs: [] # No default (required) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - broker: - copies: 1 - pattern: fan_out - outputs: [] # No default (required) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -[Processors](/docs/components/processors/about) can be listed to apply across individual outputs or all outputs: - -```yaml -output: - broker: - pattern: fan_out - outputs: - - resource: foo - - resource: bar - # Processors only applied to messages sent to bar. - processors: - - resource: bar_processor - - # Processors applied to messages sent to all brokered outputs. - processors: - - resource: general_processor -``` - -## Fields - -### `copies` - -The number of copies of each configured output to spawn. - - -Type: `int` -Default: `1` - -### `pattern` - -The brokering pattern to use. - - -Type: `string` -Default: `"fan_out"` -Options: `fan_out`, `fan_out_fail_fast`, `fan_out_sequential`, `fan_out_sequential_fail_fast`, `round_robin`, `greedy`. - -### `outputs` - -A list of child outputs to broker. - - -Type: `array` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -## Patterns - -The broker pattern determines the way in which messages are allocated and can be chosen from the following: - -### `fan_out` - -With the fan out pattern all outputs will be sent every message that passes through Benthos in parallel. - -If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. - -Sometimes it is useful to disable the back pressure or retries of certain fan out outputs and instead drop messages that have failed or were blocked. In this case you can wrap outputs with a [`drop_on` output](/docs/components/outputs/drop_on). - -### `fan_out_fail_fast` - -The same as the `fan_out` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. - -### `fan_out_sequential` - -Similar to the fan out pattern except outputs are written to sequentially, meaning an output is only written to once the preceding output has confirmed receipt of the same message. - -If an output applies back pressure it will block all subsequent messages, and if an output fails to send a message it will be retried continuously until completion or service shut down. This mechanism is in place in order to prevent one bad output from causing a larger retry loop that results in a good output from receiving unbounded message duplicates. - -### `fan_out_sequential_fail_fast` - -The same as the `fan_out_sequential` pattern, except that output failures will not be automatically retried. This pattern should be used with caution as busy retry loops could result in unlimited duplicates being introduced into the non-failure outputs. - -### `round_robin` - -With the round robin pattern each message will be assigned a single output following their order. If an output applies back pressure it will block all subsequent messages. If an output fails to send a message then the message will be re-attempted with the next input, and so on. - -### `greedy` - -The greedy pattern results in higher output throughput at the cost of potentially disproportionate message allocations to those outputs. Each message is sent to a single output, which is determined by allowing outputs to claim messages as soon as they are able to process them. This results in certain faster outputs potentially processing more messages at the cost of slower outputs. - diff --git a/website/docs/components/outputs/cache.md b/website/docs/components/outputs/cache.md deleted file mode 100644 index 04fb636521..0000000000 --- a/website/docs/components/outputs/cache.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: cache -slug: cache -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Stores each message in a [cache](/docs/components/caches/about). - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - cache: - target: "" # No default (required) - key: ${!count("items")}-${!timestamp_unix_nano()} - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - cache: - target: "" # No default (required) - key: ${!count("items")}-${!timestamp_unix_nano()} - ttl: 60s # No default (optional) - max_in_flight: 64 -``` - - - - -Caches are configured as [resources](/docs/components/caches/about), where there's a wide variety to choose from. - -The `target` field must reference a configured cache resource label like follows: - -```yaml -output: - cache: - target: foo - key: ${!json("document.id")} - -cache_resources: - - label: foo - memcached: - addresses: - - localhost:11211 - default_ttl: 60s -``` - -In order to create a unique `key` value per item you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `target` - -The target cache to store messages in. - - -Type: `string` - -### `key` - -The key to store messages by, function interpolation should be used in order to derive a unique key for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"${!count(\"items\")}-${!timestamp_unix_nano()}"` - -```yml -# Examples - -key: ${!count("items")}-${!timestamp_unix_nano()} - -key: ${!json("doc.id")} - -key: ${!meta("kafka_key")} -``` - -### `ttl` - -The TTL of each individual item as a duration string. After this period an item will be eligible for removal during the next compaction. Not all caches support per-key TTLs, and those that do not will fall back to their generally configured TTL setting. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 3.33.0 or newer - -```yml -# Examples - -ttl: 60s - -ttl: 5m - -ttl: 36h -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/cassandra.md b/website/docs/components/outputs/cassandra.md deleted file mode 100644 index f75186a597..0000000000 --- a/website/docs/components/outputs/cassandra.md +++ /dev/null @@ -1,527 +0,0 @@ ---- -title: cassandra -slug: cassandra -type: output -status: beta ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Runs a query against a Cassandra database for each message in order to insert data. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - cassandra: - addresses: [] # No default (required) - timeout: 600ms - query: "" # No default (required) - args_mapping: "" # No default (optional) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - cassandra: - addresses: [] # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - password_authenticator: - enabled: false - username: "" - password: "" - disable_initial_host_lookup: false - max_retries: 3 - backoff: - initial_interval: 1s - max_interval: 5s - timeout: 600ms - query: "" # No default (required) - args_mapping: "" # No default (optional) - consistency: QUORUM - logged_batch: true - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Query arguments can be set using a bloblang array for the fields using the `args_mapping` field. - -When populating timestamp columns the value must either be a string in ISO 8601 format (2006-01-02T15:04:05Z07:00), or an integer representing unix time in seconds. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Examples - - - - - -If we were to create a table with some basic columns with `CREATE TABLE foo.bar (id int primary key, content text, created_at timestamp);`, and were processing JSON documents of the form `{"id":"342354354","content":"hello world","timestamp":1605219406}` using logged batches, we could populate our table with the following config: - -```yaml -output: - cassandra: - addresses: - - localhost:9042 - query: 'INSERT INTO foo.bar (id, content, created_at) VALUES (?, ?, ?)' - args_mapping: | - root = [ - this.id, - this.content, - this.timestamp - ] - batching: - count: 500 - period: 1s -``` - - - - -The following example inserts JSON documents into the table `footable` of the keyspace `foospace` using INSERT JSON (https://cassandra.apache.org/doc/latest/cql/json.html#insert-json). - -```yaml -output: - cassandra: - addresses: - - localhost:9042 - query: 'INSERT INTO foospace.footable JSON ?' - args_mapping: 'root = [ this ]' - batching: - count: 500 - period: 1s -``` - - - - -## Fields - -### `addresses` - -A list of Cassandra nodes to connect to. Multiple comma separated addresses can be specified on a single line. - - -Type: `array` - -```yml -# Examples - -addresses: - - localhost:9042 - -addresses: - - foo:9042 - - bar:9042 - -addresses: - - foo:9042,bar:9042 -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `password_authenticator` - -Optional configuration of Cassandra authentication parameters. - - -Type: `object` - -### `password_authenticator.enabled` - -Whether to use password authentication - - -Type: `bool` -Default: `false` - -### `password_authenticator.username` - -The username to authenticate as. - - -Type: `string` -Default: `""` - -### `password_authenticator.password` - -The password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `disable_initial_host_lookup` - -If enabled the driver will not attempt to get host info from the system.peers table. This can speed up queries but will mean that data_centre, rack and token information will not be available. - - -Type: `bool` -Default: `false` - -### `max_retries` - -The maximum number of retries before giving up on a request. - - -Type: `int` -Default: `3` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `timeout` - -The client connection timeout. - - -Type: `string` -Default: `"600ms"` - -### `query` - -A query to execute for each message. - - -Type: `string` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) that can be used to provide arguments to Cassandra queries. The result of the query must be an array containing a matching number of elements to the query arguments. - - -Type: `string` -Requires version 3.55.0 or newer - -### `consistency` - -The consistency level to use. - - -Type: `string` -Default: `"QUORUM"` -Options: `ANY`, `ONE`, `TWO`, `THREE`, `QUORUM`, `ALL`, `LOCAL_QUORUM`, `EACH_QUORUM`, `LOCAL_ONE`. - -### `logged_batch` - -If enabled the driver will perform a logged batch. Disabling this prompts unlogged batches to be used instead, which are less efficient but necessary for alternative storages that do not support logged batches. - - -Type: `bool` -Default: `true` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/discord.md b/website/docs/components/outputs/discord.md deleted file mode 100644 index 7724ab8550..0000000000 --- a/website/docs/components/outputs/discord.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: discord -slug: discord -type: output -status: experimental -categories: ["Services","Social"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Writes messages to a Discord channel. - -```yml -# Config fields, showing default values -output: - label: "" - discord: - channel_id: "" # No default (required) - bot_token: "" # No default (required) -``` - -This output POSTs messages to the `/channels/{channel_id}/messages` Discord API endpoint authenticated as a bot using token based authentication. - -If the format of a message is a JSON object matching the [Discord API message type](https://discord.com/developers/docs/resources/channel#message-object) then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. - - -## Fields - -### `channel_id` - -A discord channel ID to write messages to. - - -Type: `string` - -### `bot_token` - -A bot token used for authentication. - - -Type: `string` - - diff --git a/website/docs/components/outputs/drop.md b/website/docs/components/outputs/drop.md deleted file mode 100644 index f10316657b..0000000000 --- a/website/docs/components/outputs/drop.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: drop -slug: drop -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Drops all messages. - -```yml -# Config fields, showing default values -output: - label: "" - drop: {} -``` - - diff --git a/website/docs/components/outputs/drop_on.md b/website/docs/components/outputs/drop_on.md deleted file mode 100644 index 3fdf5c060f..0000000000 --- a/website/docs/components/outputs/drop_on.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: drop_on -slug: drop_on -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Attempts to write messages to a child output and if the write fails for one of a list of configurable reasons the message is dropped (acked) instead of being reattempted (or nacked). - -```yml -# Config fields, showing default values -output: - label: "" - drop_on: - error: false - error_patterns: [] # No default (optional) - back_pressure: 30s # No default (optional) - output: null # No default (required) -``` - -Regular Benthos outputs will apply back pressure when downstream services aren't accessible, and Benthos retries (or nacks) all messages that fail to be delivered. However, in some circumstances, or for certain output types, we instead might want to relax these mechanisms, which is when this output becomes useful. - -## Fields - -### `error` - -Whether messages should be dropped when the child output returns an error of any type. For example, this could be when an `http_client` output gets a 4XX response code. In order to instead drop only on specific error patterns use the `error_matches` field instead. - - -Type: `bool` -Default: `false` - -### `error_patterns` - -A list of regular expressions (re2) where if the child output returns an error that matches any part of any of these patterns the message will be dropped. - - -Type: `array` -Requires version 4.27.0 or newer - -```yml -# Examples - -error_patterns: - - and that was really bad$ - -error_patterns: - - roughly [0-9]+ issues occurred -``` - -### `back_pressure` - -An optional duration string that determines the maximum length of time to wait for a given message to be accepted by the child output before the message should be dropped instead. The most common reason for an output to block is when waiting for a lost connection to be re-established. Once a message has been dropped due to back pressure all subsequent messages are dropped immediately until the output is ready to process them again. Note that if `error` is set to `false` and this field is specified then messages dropped due to back pressure will return an error response (are nacked or reattempted). - - -Type: `string` - -```yml -# Examples - -back_pressure: 30s - -back_pressure: 1m -``` - -### `output` - -A child output to wrap with this drop mechanism. - - -Type: `output` - -## Examples - - - - - -In this example we have a fan_out broker, where we guarantee delivery to our Kafka output, but drop messages if they fail our secondary HTTP client output. - -```yaml -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ foobar:6379 ] - topic: foo - - drop_on: - error: true - output: - http_client: - url: http://example.com/foo/messages - verb: POST -``` - - - - -Most outputs that attempt to establish and long-lived connection will apply back-pressure when the connection is lost. The following example has a websocket output where if it takes longer than 10 seconds to establish a connection, or recover a lost one, pending messages are dropped. - -```yaml -output: - drop_on: - back_pressure: 10s - output: - websocket: - url: ws://example.com/foo/messages -``` - - - - - diff --git a/website/docs/components/outputs/dynamic.md b/website/docs/components/outputs/dynamic.md deleted file mode 100644 index 629fdd1118..0000000000 --- a/website/docs/components/outputs/dynamic.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: dynamic -slug: dynamic -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A special broker type where the outputs are identified by unique labels and can be created, changed and removed during runtime via a REST API. - -```yml -# Config fields, showing default values -output: - label: "" - dynamic: - outputs: {} - prefix: "" -``` - -The broker pattern used is always `fan_out`, meaning each message will be delivered to each dynamic output. - -## Fields - -### `outputs` - -A map of outputs to statically create. - - -Type: `object` -Default: `{}` - -### `prefix` - -A path prefix for HTTP endpoints that are registered. - - -Type: `string` -Default: `""` - -## Endpoints - -### GET `/outputs` - -Returns a JSON object detailing all dynamic outputs, providing information such as their current uptime and configuration. - -### GET `/outputs/{id}` - -Returns the configuration of an output. - -### POST `/outputs/{id}` - -Creates or updates an output with a configuration provided in the request body (in YAML or JSON format). - -### DELETE `/outputs/{id}` - -Stops and removes an output. - -### GET `/outputs/{id}/uptime` - -Returns the uptime of an output as a duration string (of the form "72h3m0.5s"). - diff --git a/website/docs/components/outputs/elasticsearch.md b/website/docs/components/outputs/elasticsearch.md deleted file mode 100644 index d05c4f180a..0000000000 --- a/website/docs/components/outputs/elasticsearch.md +++ /dev/null @@ -1,632 +0,0 @@ ---- -title: elasticsearch -slug: elasticsearch -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Publishes messages into an Elasticsearch index. If the index does not exist then it is created with a dynamic mapping. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - elasticsearch: - urls: [] # No default (required) - index: "" # No default (required) - id: ${!count("elastic_ids")}-${!timestamp_unix()} - type: "" - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - elasticsearch: - urls: [] # No default (required) - index: "" # No default (required) - action: index - pipeline: "" - id: ${!count("elastic_ids")}-${!timestamp_unix()} - type: "" - routing: "" - sniff: true - healthcheck: true - timeout: 5s - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - max_in_flight: 64 - max_retries: 0 - backoff: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s - basic_auth: - enabled: false - username: "" - password: "" - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - aws: - enabled: false - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - gzip_compression: false -``` - - - - -Both the `id` and `index` fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part. - -### AWS - -It's possible to enable AWS connectivity with this output using the `aws` fields. However, you may need to set `sniff` and `healthcheck` to false for connections to succeed. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - http://localhost:9200 -``` - -### `index` - -The index to place messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `action` - -The action to take on the document. This field must resolve to one of the following action types: `create`, `index`, `update`, `upsert` or `delete`. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"index"` - -### `pipeline` - -An optional pipeline id to preprocess incoming documents. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `id` - -The ID for indexed messages. Interpolation should be used in order to create a unique ID for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"${!count(\"elastic_ids\")}-${!timestamp_unix()}"` - -### `type` - -The document mapping type. This field is required for versions of elasticsearch earlier than 6.0.0, but are invalid for versions 7.0.0 or later. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `routing` - -The routing key to use for the document. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `sniff` - -Prompts Benthos to sniff for brokers to connect to when establishing a connection. - - -Type: `bool` -Default: `true` - -### `healthcheck` - -Whether to enable healthchecks. - - -Type: `bool` -Default: `true` - -### `timeout` - -The maximum time to wait before abandoning a request (and trying again). - - -Type: `string` -Default: `"5s"` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `0` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"1s"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"5s"` - -### `backoff.max_elapsed_time` - -The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. - - -Type: `string` -Default: `"30s"` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `aws` - -Enables and customises connectivity to Amazon Elastic Service. - - -Type: `object` - -### `aws.enabled` - -Whether to connect to Amazon Elastic Service. - - -Type: `bool` -Default: `false` - -### `aws.region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `aws.endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `aws.credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `aws.credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `aws.credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `aws.credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `aws.credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `aws.credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `aws.credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `aws.credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `gzip_compression` - -Enable gzip compression on the request side. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/outputs/fallback.md b/website/docs/components/outputs/fallback.md deleted file mode 100644 index 92cedc4404..0000000000 --- a/website/docs/components/outputs/fallback.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: fallback -slug: fallback -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Attempts to send each message to a child output, starting from the first output on the list. If an output attempt fails then the next output in the list is attempted, and so on. - -Introduced in version 3.58.0. - -```yml -# Config fields, showing default values -output: - label: "" - fallback: [] -``` - -This pattern is useful for triggering events in the case where certain output targets have broken. For example, if you had an output type `http_client` but wished to reroute messages whenever the endpoint becomes unreachable you could use this pattern: - -```yaml -output: - fallback: - - http_client: - url: http://foo:4195/post/might/become/unreachable - retries: 3 - retry_period: 1s - - http_client: - url: http://bar:4196/somewhere/else - retries: 3 - retry_period: 1s - processors: - - mapping: 'root = "failed to send this message to foo: " + content()' - - file: - path: /usr/local/benthos/everything_failed.jsonl -``` - -### Metadata - -When a given output fails the message routed to the following output will have a metadata value named `fallback_error` containing a string error message outlining the cause of the failure. The content of this string will depend on the particular output and can be used to enrich the message or provide information used to broker the data to an appropriate output using something like a `switch` output. - -### Batching - -When an output within a fallback sequence uses batching, like so: - -```yaml -output: - fallback: - - aws_dynamodb: - table: foo - string_columns: - id: ${!json("id")} - content: ${!content()} - batching: - count: 10 - period: 1s - - file: - path: /usr/local/benthos/failed_stuff.jsonl -``` - -Benthos makes a best attempt at inferring which specific messages of the batch failed, and only propagates those individual messages to the next fallback tier. - -However, depending on the output and the error returned it is sometimes not possible to determine the individual messages that failed, in which case the whole batch is passed to the next tier in order to preserve at-least-once delivery guarantees. - - diff --git a/website/docs/components/outputs/file.md b/website/docs/components/outputs/file.md deleted file mode 100644 index a0f4755de4..0000000000 --- a/website/docs/components/outputs/file.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: file -slug: file -type: output -status: stable -categories: ["Local"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Writes messages to files on disk based on a chosen codec. - -```yml -# Config fields, showing default values -output: - label: "" - file: - path: /tmp/data.txt # No default (required) - codec: lines -``` - -Messages can be written to different files by using [interpolation functions](/docs/configuration/interpolation#bloblang-queries) in the path field. However, only one file is ever open at a given time, and therefore when the path changes the previously open file is closed. - -## Fields - -### `path` - -The file to write to, if the file does not yet exist it will be created. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 3.33.0 or newer - -```yml -# Examples - -path: /tmp/data.txt - -path: /tmp/${! timestamp_unix() }.txt - -path: /tmp/${! json("document.id") }.json -``` - -### `codec` - -The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. - - -Type: `string` -Default: `"lines"` -Requires version 3.33.0 or newer - -| Option | Summary | -|---|---| -| `all-bytes` | Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. | -| `append` | Append each message to the output stream without any delimiter or special encoding. | -| `lines` | Append each message to the output stream followed by a line break. | -| `delim:x` | Append each message to the output stream followed by a custom delimiter. | - - -```yml -# Examples - -codec: lines - -codec: "delim:\t" - -codec: delim:foobar -``` - - diff --git a/website/docs/components/outputs/gcp_bigquery.md b/website/docs/components/outputs/gcp_bigquery.md deleted file mode 100644 index c1bbe6cab3..0000000000 --- a/website/docs/components/outputs/gcp_bigquery.md +++ /dev/null @@ -1,375 +0,0 @@ ---- -title: gcp_bigquery -slug: gcp_bigquery -type: output -status: beta -categories: ["GCP","Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Sends messages as new rows to a Google Cloud BigQuery table. - -Introduced in version 3.55.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - gcp_bigquery: - project: "" - dataset: "" # No default (required) - table: "" # No default (required) - format: NEWLINE_DELIMITED_JSON - max_in_flight: 64 - job_labels: {} - csv: - header: [] - field_delimiter: ',' - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - gcp_bigquery: - project: "" - dataset: "" # No default (required) - table: "" # No default (required) - format: NEWLINE_DELIMITED_JSON - max_in_flight: 64 - write_disposition: WRITE_APPEND - create_disposition: CREATE_IF_NEEDED - ignore_unknown_values: false - max_bad_records: 0 - auto_detect: false - job_labels: {} - csv: - header: [] - field_delimiter: ',' - allow_jagged_rows: false - allow_quoted_newlines: false - encoding: UTF-8 - skip_leading_rows: 1 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -## Credentials - -By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more [in this document](/docs/guides/cloud/gcp). - -## Format - -This output currently supports only CSV and NEWLINE_DELIMITED_JSON formats. Learn more about how to use GCP BigQuery with them here: -- [`NEWLINE_DELIMITED_JSON`](https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json) -- [`CSV`](https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv) - -Each message may contain multiple elements separated by newlines. For example a single message containing: - -```json -{"key": "1"} -{"key": "2"} -``` - -Is equivalent to two separate messages: - -```json -{"key": "1"} -``` - -And: - -```json -{"key": "2"} -``` - -The same is true for the CSV format. - -### CSV - -For the CSV format when the field `csv.header` is specified a header row will be inserted as the first line of each message batch. If this field is not provided then the first message of each message batch must include a header line. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `project` - -The project ID of the dataset to insert data to. If not set, it will be inferred from the credentials or read from the GOOGLE_CLOUD_PROJECT environment variable. - - -Type: `string` -Default: `""` - -### `dataset` - -The BigQuery Dataset ID. - - -Type: `string` - -### `table` - -The table to insert messages to. - - -Type: `string` - -### `format` - -The format of each incoming message. - - -Type: `string` -Default: `"NEWLINE_DELIMITED_JSON"` -Options: `NEWLINE_DELIMITED_JSON`, `CSV`. - -### `max_in_flight` - -The maximum number of message batches to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `write_disposition` - -Specifies how existing data in a destination table is treated. - - -Type: `string` -Default: `"WRITE_APPEND"` -Options: `WRITE_APPEND`, `WRITE_EMPTY`, `WRITE_TRUNCATE`. - -### `create_disposition` - -Specifies the circumstances under which destination table will be created. If CREATE_IF_NEEDED is used the GCP BigQuery will create the table if it does not already exist and tables are created atomically on successful completion of a job. The CREATE_NEVER option ensures the table must already exist and will not be automatically created. - - -Type: `string` -Default: `"CREATE_IF_NEEDED"` -Options: `CREATE_IF_NEEDED`, `CREATE_NEVER`. - -### `ignore_unknown_values` - -Causes values not matching the schema to be tolerated. Unknown values are ignored. For CSV this ignores extra values at the end of a line. For JSON this ignores named values that do not match any column name. If this field is set to false (the default value), records containing unknown values are treated as bad records. The max_bad_records field can be used to customize how bad records are handled. - - -Type: `bool` -Default: `false` - -### `max_bad_records` - -The maximum number of bad records that will be ignored when reading data. - - -Type: `int` -Default: `0` - -### `auto_detect` - -Indicates if we should automatically infer the options and schema for CSV and JSON sources. If the table doesn't exist and this field is set to `false` the output may not be able to insert data and will throw insertion error. Be careful using this field since it delegates to the GCP BigQuery service the schema detection and values like `"no"` may be treated as booleans for the CSV format. - - -Type: `bool` -Default: `false` - -### `job_labels` - -A list of labels to add to the load job. - - -Type: `object` -Default: `{}` - -### `csv` - -Specify how CSV data should be interpretted. - - -Type: `object` - -### `csv.header` - -A list of values to use as header for each batch of messages. If not specified the first line of each message will be used as header. - - -Type: `array` -Default: `[]` - -### `csv.field_delimiter` - -The separator for fields in a CSV file, used when reading or exporting data. - - -Type: `string` -Default: `","` - -### `csv.allow_jagged_rows` - -Causes missing trailing optional columns to be tolerated when reading CSV data. Missing values are treated as nulls. - - -Type: `bool` -Default: `false` - -### `csv.allow_quoted_newlines` - -Sets whether quoted data sections containing newlines are allowed when reading CSV data. - - -Type: `bool` -Default: `false` - -### `csv.encoding` - -Encoding is the character encoding of data to be read. - - -Type: `string` -Default: `"UTF-8"` -Options: `UTF-8`, `ISO-8859-1`. - -### `csv.skip_leading_rows` - -The number of rows at the top of a CSV file that BigQuery will skip when reading data. The default value is 1 since Benthos will add the specified header in the first line of each batch sent to BigQuery. - - -Type: `int` -Default: `1` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/gcp_cloud_storage.md b/website/docs/components/outputs/gcp_cloud_storage.md deleted file mode 100644 index 5ba6a3f904..0000000000 --- a/website/docs/components/outputs/gcp_cloud_storage.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -title: gcp_cloud_storage -slug: gcp_cloud_storage -type: output -status: beta -categories: ["Services","GCP"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Sends message parts as objects to a Google Cloud Storage bucket. Each object is uploaded with the path specified with the `path` field. - -Introduced in version 3.43.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - gcp_cloud_storage: - bucket: "" # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - content_type: application/octet-stream - collision_mode: overwrite - timeout: 3s - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - gcp_cloud_storage: - bucket: "" # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - content_type: application/octet-stream - content_encoding: "" - collision_mode: overwrite - chunk_size: 16777216 - timeout: 3s - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -In order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries), which are calculated per message of a batch. - -### Metadata - -Metadata fields on messages will be sent as headers, in order to mutate these values (or remove them) check out the [metadata docs](/docs/configuration/metadata). - -### Credentials - -By default Benthos will use a shared credentials file when connecting to GCP services. You can find out more [in this document](/docs/guides/cloud/gcp). - -### Batching - -It's common to want to upload messages to Google Cloud Storage as batched archives, the easiest way to do this is to batch your messages at the output level and join the batch of messages with an [`archive`](/docs/components/processors/archive) and/or [`compress`](/docs/components/processors/compress) processor. - -For example, if we wished to upload messages as a .tar.gz archive of documents we could achieve that with the following config: - -```yaml -output: - gcp_cloud_storage: - bucket: TODO - path: ${!count("files")}-${!timestamp_unix_nano()}.tar.gz - batching: - count: 100 - period: 10s - processors: - - archive: - format: tar - - compress: - algorithm: gzip -``` - -Alternatively, if we wished to upload JSON documents as a single large document containing an array of objects we can do that with: - -```yaml -output: - gcp_cloud_storage: - bucket: TODO - path: ${!count("files")}-${!timestamp_unix_nano()}.json - batching: - count: 100 - processors: - - archive: - format: json_array -``` - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `bucket` - -The bucket to upload messages to. - - -Type: `string` - -### `path` - -The path of each message to upload. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` - -```yml -# Examples - -path: ${!count("files")}-${!timestamp_unix_nano()}.txt - -path: ${!meta("kafka_key")}.json - -path: ${!json("doc.namespace")}/${!json("doc.id")}.json -``` - -### `content_type` - -The content type to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"application/octet-stream"` - -### `content_encoding` - -An optional content encoding to set for each object. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `collision_mode` - -Determines how file path collisions should be dealt with. - - -Type: `string` -Default: `"overwrite"` -Requires version 3.53.0 or newer - -| Option | Summary | -|---|---| -| `append` | Append the message bytes to the original file. | -| `error-if-exists` | Return an error, this is the equivalent of a nack. | -| `ignore` | Do not modify the original file, the new data will be dropped. | -| `overwrite` | Replace the existing file with the new one. | - - -### `chunk_size` - -An optional chunk size which controls the maximum number of bytes of the object that the Writer will attempt to send to the server in a single request. If ChunkSize is set to zero, chunking will be disabled. - - -Type: `int` -Default: `16777216` - -### `timeout` - -The maximum period to wait on an upload before abandoning it and reattempting. - - -Type: `string` -Default: `"3s"` - -```yml -# Examples - -timeout: 1s - -timeout: 500ms -``` - -### `max_in_flight` - -The maximum number of message batches to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/gcp_pubsub.md b/website/docs/components/outputs/gcp_pubsub.md deleted file mode 100644 index b51628fb64..0000000000 --- a/website/docs/components/outputs/gcp_pubsub.md +++ /dev/null @@ -1,340 +0,0 @@ ---- -title: gcp_pubsub -slug: gcp_pubsub -type: output -status: stable -categories: ["Services","GCP"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to a GCP Cloud Pub/Sub topic. [Metadata](/docs/configuration/metadata) from messages are sent as attributes. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - gcp_pubsub: - project: "" # No default (required) - topic: "" # No default (required) - endpoint: "" - max_in_flight: 64 - count_threshold: 100 - delay_threshold: 10ms - byte_threshold: 1000000 - metadata: - exclude_prefixes: [] - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - gcp_pubsub: - project: "" # No default (required) - topic: "" # No default (required) - endpoint: "" - ordering_key: "" # No default (optional) - max_in_flight: 64 - count_threshold: 100 - delay_threshold: 10ms - byte_threshold: 1000000 - publish_timeout: 1m0s - metadata: - exclude_prefixes: [] - flow_control: - max_outstanding_bytes: -1 - max_outstanding_messages: 1000 - limit_exceeded_behavior: block - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -For information on how to set up credentials check out [this guide](https://cloud.google.com/docs/authentication/production). - -### Troubleshooting - -If you're consistently seeing `Failed to send message to gcp_pubsub: context deadline exceeded` error logs without any further information it is possible that you are encountering https://github.com/benthosdev/benthos/issues/1042, which occurs when metadata values contain characters that are not valid utf-8. This can frequently occur when consuming from Kafka as the key metadata field may be populated with an arbitrary binary value, but this issue is not exclusive to Kafka. - -If you are blocked by this issue then a work around is to delete either the specific problematic keys: - -```yaml -pipeline: - processors: - - mapping: | - meta kafka_key = deleted() -``` - -Or delete all keys with: - -```yaml -pipeline: - processors: - - mapping: meta = deleted() -``` - -## Fields - -### `project` - -The project ID of the topic to publish to. - - -Type: `string` - -### `topic` - -The topic to publish to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `endpoint` - -An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values check out [this document.](https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints) - - -Type: `string` -Default: `""` - -```yml -# Examples - -endpoint: us-central1-pubsub.googleapis.com:443 - -endpoint: us-west3-pubsub.googleapis.com:443 -``` - -### `ordering_key` - -The ordering key to use for publishing messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increasing this may improve throughput. - - -Type: `int` -Default: `64` - -### `count_threshold` - -Publish a pubsub buffer when it has this many messages - - -Type: `int` -Default: `100` - -### `delay_threshold` - -Publish a non-empty pubsub buffer after this delay has passed. - - -Type: `string` -Default: `"10ms"` - -### `byte_threshold` - -Publish a batch when its size in bytes reaches this value. - - -Type: `int` -Default: `1000000` - -### `publish_timeout` - -The maximum length of time to wait before abandoning a publish attempt for a message. - - -Type: `string` -Default: `"1m0s"` - -```yml -# Examples - -publish_timeout: 10s - -publish_timeout: 5m - -publish_timeout: 60m -``` - -### `metadata` - -Specify criteria for which metadata values are sent as attributes, all are sent by default. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `flow_control` - -For a given topic, configures the PubSub client's internal buffer for messages to be published. - - -Type: `object` - -### `flow_control.max_outstanding_bytes` - -Maximum size of buffered messages to be published. If less than or equal to zero, this is disabled. - - -Type: `int` -Default: `-1` - -### `flow_control.max_outstanding_messages` - -Maximum number of buffered messages to be published. If less than or equal to zero, this is disabled. - - -Type: `int` -Default: `1000` - -### `flow_control.limit_exceeded_behavior` - -Configures the behavior when trying to publish additional messages while the flow controller is full. The available options are block (default), ignore (disable), and signal_error (publish results will return an error). - - -Type: `string` -Default: `"block"` -Options: `ignore`, `block`, `signal_error`. - -### `batching` - -Configures a batching policy on this output. While the PubSub client maintains its own internal buffering mechanism, preparing larger batches of messages can further trade-off some latency for throughput. - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/hdfs.md b/website/docs/components/outputs/hdfs.md deleted file mode 100644 index e1d45a60b3..0000000000 --- a/website/docs/components/outputs/hdfs.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: hdfs -slug: hdfs -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends message parts as files to a HDFS directory. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - hdfs: - hosts: [] # No default (required) - user: "" - directory: "" # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - hdfs: - hosts: [] # No default (required) - user: "" - directory: "" # No default (required) - path: ${!count("files")}-${!timestamp_unix_nano()}.txt - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -Each file is written with the path specified with the 'path' field, in order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `hosts` - -A list of target host addresses to connect to. - - -Type: `array` - -```yml -# Examples - -hosts: localhost:9000 -``` - -### `user` - -A user ID to connect as. - - -Type: `string` -Default: `""` - -### `directory` - -A directory to store message files within. If the directory does not exist it will be created. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `path` - -The path to upload messages as, interpolation functions should be used in order to generate unique file paths. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `"${!count(\"files\")}-${!timestamp_unix_nano()}.txt"` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/http_client.md b/website/docs/components/outputs/http_client.md deleted file mode 100644 index b7d1b20b2c..0000000000 --- a/website/docs/components/outputs/http_client.md +++ /dev/null @@ -1,873 +0,0 @@ ---- -title: http_client -slug: http_client -type: output -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to an HTTP server. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - http_client: - url: "" # No default (required) - verb: POST - headers: {} - rate_limit: "" # No default (optional) - timeout: 5s - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - http_client: - url: "" # No default (required) - verb: POST - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - dump_request_log_level: "" - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - oauth2: - enabled: false - client_key: "" - client_secret: "" - token_url: "" - scopes: [] - endpoint_params: {} - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - extract_headers: - include_prefixes: [] - include_patterns: [] - rate_limit: "" # No default (optional) - timeout: 5s - retry_period: 1s - max_retry_backoff: 300s - retries: 3 - backoff_on: - - 429 - drop_on: [] - successful_on: [] - proxy_url: "" # No default (optional) - batch_as_multipart: false - propagate_response: false - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - multipart: [] -``` - - - - -When the number of retries expires the output will reject the message, the behaviour after this will depend on the pipeline but usually this simply means the send is attempted again until successful whilst applying back pressure. - -The URL and header values of this type can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). This behaviour can be disabled by setting the field [`batch_as_multipart`](#batch_as_multipart) to `false`. - -### Propagating Responses - -It's possible to propagate the response from each HTTP request back to the input source by setting `propagate_response` to `true`. Only inputs that support [synchronous responses](/docs/guides/sync_responses) are able to make use of these propagated responses. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `url` - -The URL to connect to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `verb` - -A verb to connect with - - -Type: `string` -Default: `"POST"` - -```yml -# Examples - -verb: POST - -verb: GET - -verb: DELETE -``` - -### `headers` - -A map of headers to add to the request. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -headers: - Content-Type: application/octet-stream - traceparent: ${! tracing_span().traceparent } -``` - -### `metadata` - -Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `dump_request_log_level` - -EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged. - - -Type: `string` -Default: `""` -Requires version 4.12.0 or newer -Options: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, ``. - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth2` - -Allows you to specify open authentication via OAuth version 2 using the client credentials token flow. - - -Type: `object` - -### `oauth2.enabled` - -Whether to use OAuth version 2 in requests. - - -Type: `bool` -Default: `false` - -### `oauth2.client_key` - -A value used to identify the client to the token provider. - - -Type: `string` -Default: `""` - -### `oauth2.client_secret` - -A secret used to establish ownership of the client key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth2.token_url` - -The URL of the token provider. - - -Type: `string` -Default: `""` - -### `oauth2.scopes` - -A list of optional requested permissions. - - -Type: `array` -Default: `[]` -Requires version 3.45.0 or newer - -### `oauth2.endpoint_params` - -A list of optional endpoint parameters, values should be arrays of strings. - - -Type: `object` -Default: `{}` -Requires version 4.21.0 or newer - -```yml -# Examples - -endpoint_params: - bar: - - woof - foo: - - meow - - quack -``` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `extract_headers` - -Specify which response headers should be added to resulting synchronous response messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. This field is not applicable unless `propagate_response` is set to `true`. - - -Type: `object` - -### `extract_headers.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `extract_headers.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `rate_limit` - -An optional [rate limit](/docs/components/rate_limits/about) to throttle requests by. - - -Type: `string` - -### `timeout` - -A static timeout to apply to requests. - - -Type: `string` -Default: `"5s"` - -### `retry_period` - -The base period to wait between failed requests. - - -Type: `string` -Default: `"1s"` - -### `max_retry_backoff` - -The maximum period to wait between failed requests. - - -Type: `string` -Default: `"300s"` - -### `retries` - -The maximum number of retry attempts to make. - - -Type: `int` -Default: `3` - -### `backoff_on` - -A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually. - - -Type: `array` -Default: `[429]` - -### `drop_on` - -A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped. - - -Type: `array` -Default: `[]` - -### `successful_on` - -A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field. - - -Type: `array` -Default: `[]` - -### `proxy_url` - -An optional HTTP proxy URL. - - -Type: `string` - -### `batch_as_multipart` - -Send message batches as a single request using [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). If disabled messages in batches will be sent as individual requests. - - -Type: `bool` -Default: `false` - -### `propagate_response` - -Whether responses from the server should be [propagated back](/docs/guides/sync_responses) to the input. - - -Type: `bool` -Default: `false` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `multipart` - -EXPERIMENTAL: Create explicit multipart HTTP requests by specifying an array of parts to add to the request, each part specified consists of content headers and a data field that can be populated dynamically. If this field is populated it will override the default request creation behaviour. - - -Type: `array` -Default: `[]` -Requires version 3.63.0 or newer - -### `multipart[].content_type` - -The content type of the individual message part. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -content_type: application/bin -``` - -### `multipart[].content_disposition` - -The content disposition of the individual message part. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -content_disposition: form-data; name="bin"; filename='${! @AttachmentName } -``` - -### `multipart[].body` - -The body of the individual message part. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -body: ${! this.data.part1 } -``` - - diff --git a/website/docs/components/outputs/http_server.md b/website/docs/components/outputs/http_server.md deleted file mode 100644 index 4f9e25a9ea..0000000000 --- a/website/docs/components/outputs/http_server.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: http_server -slug: http_server -type: output -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sets up an HTTP server that will send messages over HTTP(S) GET requests. HTTP 2.0 is supported when using TLS, which is enabled when key and cert files are specified. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - http_server: - address: "" - path: /get - stream_path: /get/stream - ws_path: /get/ws - allowed_verbs: - - GET -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - http_server: - address: "" - path: /get - stream_path: /get/stream - ws_path: /get/ws - allowed_verbs: - - GET - timeout: 5s - cert_file: "" - key_file: "" - cors: - enabled: false - allowed_origins: [] -``` - - - - -Sets up an HTTP server that will send messages over HTTP(S) GET requests. If the `address` config field is left blank the [service-wide HTTP server](/docs/components/http/about) will be used. - -Three endpoints will be registered at the paths specified by the fields `path`, `stream_path` and `ws_path`. Which allow you to consume a single message batch, a continuous stream of line delimited messages, or a websocket of messages for each request respectively. - -When messages are batched the `path` endpoint encodes the batch according to [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). This behaviour can be overridden by [archiving your batches](/docs/configuration/batching#post-batch-processing). - -Please note, messages are considered delivered as soon as the data is written to the client. There is no concept of at least once delivery on this output. - -:::caution Endpoint Caveats -Components within a Benthos config will register their respective endpoints in a non-deterministic order. This means that establishing precedence of endpoints that are registered via multiple `http_server` inputs or outputs (either within brokers or from cohabiting streams) is not possible in a predictable way. - -This ambiguity makes it difficult to ensure that paths which are both a subset of a path registered by a separate component, and end in a slash (`/`) and will therefore match against all extensions of that path, do not prevent the more specific path from matching against requests. - -It is therefore recommended that you ensure paths of separate components do not collide unless they are explicitly non-competing. - -For example, if you were to deploy two separate `http_server` inputs, one with a path `/foo/` and the other with a path `/foo/bar`, it would not be possible to ensure that the path `/foo/` does not swallow requests made to `/foo/bar`. -::: - - -## Fields - -### `address` - -An alternative address to host from. If left empty the service wide address is used. - - -Type: `string` -Default: `""` - -### `path` - -The path from which discrete messages can be consumed. - - -Type: `string` -Default: `"/get"` - -### `stream_path` - -The path from which a continuous stream of messages can be consumed. - - -Type: `string` -Default: `"/get/stream"` - -### `ws_path` - -The path from which websocket connections can be established. - - -Type: `string` -Default: `"/get/ws"` - -### `allowed_verbs` - -An array of verbs that are allowed for the `path` and `stream_path` HTTP endpoint. - - -Type: `array` -Default: `["GET"]` - -### `timeout` - -The maximum time to wait before a blocking, inactive connection is dropped (only applies to the `path` endpoint). - - -Type: `string` -Default: `"5s"` - -### `cert_file` - -Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. - - -Type: `string` -Default: `""` - -### `key_file` - -Enable TLS by specifying a certificate and key file. Only valid with a custom `address`. - - -Type: `string` -Default: `""` - -### `cors` - -Adds Cross-Origin Resource Sharing headers. Only valid with a custom `address`. - - -Type: `object` -Requires version 3.63.0 or newer - -### `cors.enabled` - -Whether to allow CORS requests. - - -Type: `bool` -Default: `false` - -### `cors.allowed_origins` - -An explicit list of origins that are allowed for CORS requests. - - -Type: `array` -Default: `[]` - - diff --git a/website/docs/components/outputs/inproc.md b/website/docs/components/outputs/inproc.md deleted file mode 100644 index 8f9e27bf5b..0000000000 --- a/website/docs/components/outputs/inproc.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: inproc -slug: inproc -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - -```yml -# Config fields, showing default values -output: - label: "" - inproc: "" -``` - -Sends data directly to Benthos inputs by connecting to a unique ID. This allows you to hook up isolated streams whilst running Benthos in [streams mode](/docs/guides/streams_mode/about), it is NOT recommended that you connect the inputs of a stream with an output of the same stream, as feedback loops can lead to deadlocks in your message flow. - -It is possible to connect multiple inputs to the same inproc ID, resulting in messages dispatching in a round-robin fashion to connected inputs. However, only one output can assume an inproc ID, and will replace existing outputs if a collision occurs. - - diff --git a/website/docs/components/outputs/kafka.md b/website/docs/components/outputs/kafka.md deleted file mode 100644 index df255466f7..0000000000 --- a/website/docs/components/outputs/kafka.md +++ /dev/null @@ -1,740 +0,0 @@ ---- -title: kafka -slug: kafka -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -The kafka output type writes a batch of messages to Kafka brokers and waits for acknowledgement before propagating it back to the input. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - kafka: - addresses: [] # No default (required) - topic: "" # No default (required) - target_version: 2.1.0 # No default (optional) - key: "" - partitioner: fnv1a_hash - compression: none - static_headers: {} # No default (optional) - metadata: - exclude_prefixes: [] - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - kafka: - addresses: [] # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - sasl: - mechanism: none - user: "" - password: "" - access_token: "" - token_cache: "" - token_key: "" - topic: "" # No default (required) - client_id: benthos - target_version: 2.1.0 # No default (optional) - rack_id: "" - key: "" - partitioner: fnv1a_hash - partition: "" - custom_topic_creation: - enabled: false - partitions: -1 - replication_factor: -1 - compression: none - static_headers: {} # No default (optional) - metadata: - exclude_prefixes: [] - inject_tracing_map: meta = @.merge(this) # No default (optional) - max_in_flight: 64 - idempotent_write: false - ack_replicas: false - max_msg_bytes: 1000000 - timeout: 5s - retry_as_batch: false - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - max_retries: 0 - backoff: - initial_interval: 3s - max_interval: 10s - max_elapsed_time: 30s -``` - - - - -The config field `ack_replicas` determines whether we wait for acknowledgement from all replicas or just a single broker. - -Both the `key` and `topic` fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -[Metadata](/docs/configuration/metadata) will be added to each message sent as headers (version 0.11+), but can be restricted using the field [`metadata`](#metadata). - -### Strict Ordering and Retries - -When strict ordering is required for messages written to topic partitions it is important to ensure that both the field `max_in_flight` is set to `1` and that the field `retry_as_batch` is set to `true`. - -You must also ensure that failed batches are never rerouted back to the same output. This can be done by setting the field `max_retries` to `0` and `backoff.max_elapsed_time` to empty, which will apply back pressure indefinitely until the batch is sent successfully. - -However, this also means that manual intervention will eventually be required in cases where the batch cannot be sent due to configuration problems such as an incorrect `max_msg_bytes` estimate. A less strict but automated alternative would be to route failed batches to a dead letter queue using a [`fallback` broker](/docs/components/outputs/fallback), but this would allow subsequent batches to be delivered in the meantime whilst those failed batches are dealt with. - -### Troubleshooting - -If you're seeing issues writing to or reading from Kafka with this component then it's worth trying out the newer [`kafka_franz` output](/docs/components/outputs/kafka_franz). - -- I'm seeing logs that report `Failed to connect to kafka: kafka: client has run out of available brokers to talk to (Is your cluster reachable?)`, but the brokers are definitely reachable. - -Unfortunately this error message will appear for a wide range of connection problems even when the broker endpoint can be reached. Double check your authentication configuration and also ensure that you have [enabled TLS](#tlsenabled) if applicable. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `addresses` - -A list of broker addresses to connect to. If an item of the list contains commas it will be expanded into multiple addresses. - - -Type: `array` - -```yml -# Examples - -addresses: - - localhost:9092 - -addresses: - - localhost:9041,localhost:9042 - -addresses: - - localhost:9041 - - localhost:9042 -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `sasl` - -Enables SASL authentication. - - -Type: `object` - -### `sasl.mechanism` - -The SASL authentication mechanism, if left empty SASL authentication is not used. - - -Type: `string` -Default: `"none"` - -| Option | Summary | -|---|---| -| `OAUTHBEARER` | OAuth Bearer based authentication. | -| `PLAIN` | Plain text authentication. NOTE: When using plain text auth it is extremely likely that you'll also need to [enable TLS](#tlsenabled). | -| `SCRAM-SHA-256` | Authentication using the SCRAM-SHA-256 mechanism. | -| `SCRAM-SHA-512` | Authentication using the SCRAM-SHA-512 mechanism. | -| `none` | Default, no SASL authentication. | - - -### `sasl.user` - -A PLAIN username. It is recommended that you use environment variables to populate this field. - - -Type: `string` -Default: `""` - -```yml -# Examples - -user: ${USER} -``` - -### `sasl.password` - -A PLAIN password. It is recommended that you use environment variables to populate this field. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: ${PASSWORD} -``` - -### `sasl.access_token` - -A static OAUTHBEARER access token - - -Type: `string` -Default: `""` - -### `sasl.token_cache` - -Instead of using a static `access_token` allows you to query a [`cache`](/docs/components/caches/about) resource to fetch OAUTHBEARER tokens from - - -Type: `string` -Default: `""` - -### `sasl.token_key` - -Required when using a `token_cache`, the key to query the cache with for tokens. - - -Type: `string` -Default: `""` - -### `topic` - -The topic to publish messages to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `"benthos"` - -### `target_version` - -The version of the Kafka protocol to use. This limits the capabilities used by the client and should ideally match the version of your brokers. Defaults to the oldest supported stable version. - - -Type: `string` - -```yml -# Examples - -target_version: 2.1.0 - -target_version: 3.1.0 -``` - -### `rack_id` - -A rack identifier for this client. - - -Type: `string` -Default: `""` - -### `key` - -The key to publish messages with. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `partitioner` - -The partitioning algorithm to use. - - -Type: `string` -Default: `"fnv1a_hash"` -Options: `fnv1a_hash`, `murmur2_hash`, `random`, `round_robin`, `manual`. - -### `partition` - -The manually-specified partition to publish messages to, relevant only when the field `partitioner` is set to `manual`. Must be able to parse as a 32-bit integer. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `custom_topic_creation` - -If enabled, topics will be created with the specified number of partitions and replication factor if they do not already exist. - - -Type: `object` - -### `custom_topic_creation.enabled` - -Whether to enable custom topic creation. - - -Type: `bool` -Default: `false` - -### `custom_topic_creation.partitions` - -The number of partitions to create for new topics. Leave at -1 to use the broker configured default. Must be >= 1. - - -Type: `int` -Default: `-1` - -### `custom_topic_creation.replication_factor` - -The replication factor to use for new topics. Leave at -1 to use the broker configured default. Must be an odd number, and less then or equal to the number of brokers. - - -Type: `int` -Default: `-1` - -### `compression` - -The compression algorithm to use. - - -Type: `string` -Default: `"none"` -Options: `none`, `snappy`, `lz4`, `gzip`, `zstd`. - -### `static_headers` - -An optional map of static headers that should be added to messages in addition to metadata. - - -Type: `object` - -```yml -# Examples - -static_headers: - first-static-header: value-1 - second-static-header: value-2 -``` - -### `metadata` - -Specify criteria for which metadata values are sent with messages as headers. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `inject_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. - - -Type: `string` -Requires version 3.45.0 or newer - -```yml -# Examples - -inject_tracing_map: meta = @.merge(this) - -inject_tracing_map: root.meta.span = this -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `idempotent_write` - -Enable the idempotent write producer option. This requires the `IDEMPOTENT_WRITE` permission on `CLUSTER` and can be disabled if this permission is not available. - - -Type: `bool` -Default: `false` - -### `ack_replicas` - -Ensure that messages have been copied across all replicas before acknowledging receipt. - - -Type: `bool` -Default: `false` - -### `max_msg_bytes` - -The maximum size in bytes of messages sent to the target topic. - - -Type: `int` -Default: `1000000` - -### `timeout` - -The maximum period of time to wait for message sends before abandoning the request and retrying. - - -Type: `string` -Default: `"5s"` - -### `retry_as_batch` - -When enabled forces an entire batch of messages to be retried if any individual message fails on a send, otherwise only the individual messages that failed are retried. Disabling this helps to reduce message duplicates during intermittent errors, but also makes it impossible to guarantee strict ordering of messages. - - -Type: `bool` -Default: `false` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `0` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"3s"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"10s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `backoff.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. Setting this value to a zeroed duration (such as `0s`) will result in unbounded retries. - - -Type: `string` -Default: `"30s"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - - diff --git a/website/docs/components/outputs/kafka_franz.md b/website/docs/components/outputs/kafka_franz.md deleted file mode 100644 index b979abf36f..0000000000 --- a/website/docs/components/outputs/kafka_franz.md +++ /dev/null @@ -1,672 +0,0 @@ ---- -title: kafka_franz -slug: kafka_franz -type: output -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -A Kafka output using the [Franz Kafka client library](https://github.com/twmb/franz-go). - -Introduced in version 3.61.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - kafka_franz: - seed_brokers: [] # No default (required) - topic: "" # No default (required) - key: "" # No default (optional) - partition: ${! meta("partition") } # No default (optional) - metadata: - include_prefixes: [] - include_patterns: [] - max_in_flight: 10 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - kafka_franz: - seed_brokers: [] # No default (required) - topic: "" # No default (required) - key: "" # No default (optional) - partitioner: "" # No default (optional) - partition: ${! meta("partition") } # No default (optional) - client_id: benthos - rack_id: "" - idempotent_write: true - metadata: - include_prefixes: [] - include_patterns: [] - max_in_flight: 10 - timeout: 10s - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - max_message_bytes: 1MB - compression: "" # No default (optional) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - sasl: [] # No default (optional) -``` - - - - -Writes a batch of messages to Kafka brokers and waits for acknowledgement before propagating it back to the input. - -This output often out-performs the traditional `kafka` output as well as providing more useful logs and error messages. - - -## Fields - -### `seed_brokers` - -A list of broker addresses to connect to in order to establish connections. If an item of the list contains commas it will be expanded into multiple addresses. - - -Type: `array` - -```yml -# Examples - -seed_brokers: - - localhost:9092 - -seed_brokers: - - foo:9092 - - bar:9092 - -seed_brokers: - - foo:9092,bar:9092 -``` - -### `topic` - -A topic to write messages to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `key` - -An optional key to populate for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `partitioner` - -Override the default murmur2 hashing partitioner. - - -Type: `string` - -| Option | Summary | -|---|---| -| `least_backup` | Chooses the least backed up partition (the partition with the fewest amount of buffered records). Partitions are selected per batch. | -| `manual` | Manually select a partition for each message, requires the field `partition` to be specified. | -| `murmur2_hash` | Kafka's default hash algorithm that uses a 32-bit murmur2 hash of the key to compute which partition the record will be on. | -| `round_robin` | Round-robin's messages through all available partitions. This algorithm has lower throughput and causes higher CPU load on brokers, but can be useful if you want to ensure an even distribution of records to partitions. | - - -### `partition` - -An optional explicit partition to set for each message. This field is only relevant when the `partitioner` is set to `manual`. The provided interpolation string must be a valid integer. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -partition: ${! meta("partition") } -``` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `"benthos"` - -### `rack_id` - -A rack identifier for this client. - - -Type: `string` -Default: `""` - -### `idempotent_write` - -Enable the idempotent write producer option. This requires the `IDEMPOTENT_WRITE` permission on `CLUSTER` and can be disabled if this permission is not available. - - -Type: `bool` -Default: `true` - -### `metadata` - -Determine which (if any) metadata values should be added to messages as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `max_in_flight` - -The maximum number of batches to be sending in parallel at any given time. - - -Type: `int` -Default: `10` - -### `timeout` - -The maximum period of time to wait for message sends before abandoning the request and retrying - - -Type: `string` -Default: `"10s"` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `max_message_bytes` - -The maximum space in bytes than an individual message may take, messages larger than this value will be rejected. This field corresponds to Kafka's `max.message.bytes`. - - -Type: `string` -Default: `"1MB"` - -```yml -# Examples - -max_message_bytes: 100MB - -max_message_bytes: 50mib -``` - -### `compression` - -Optionally set an explicit compression type. The default preference is to use snappy when the broker supports it, and fall back to none if not. - - -Type: `string` -Options: `lz4`, `snappy`, `gzip`, `none`, `zstd`. - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `sasl` - -Specify one or more methods of SASL authentication. SASL is tried in order; if the broker supports the first mechanism, all connections will use that mechanism. If the first mechanism fails, the client will pick the first supported mechanism. If the broker does not support any client mechanisms, connections will fail. - - -Type: `array` - -```yml -# Examples - -sasl: - - mechanism: SCRAM-SHA-512 - password: bar - username: foo -``` - -### `sasl[].mechanism` - -The SASL mechanism to use. - - -Type: `string` - -| Option | Summary | -|---|---| -| `AWS_MSK_IAM` | AWS IAM based authentication as specified by the 'aws-msk-iam-auth' java library. | -| `OAUTHBEARER` | OAuth Bearer based authentication. | -| `PLAIN` | Plain text authentication. | -| `SCRAM-SHA-256` | SCRAM based authentication as specified in RFC5802. | -| `SCRAM-SHA-512` | SCRAM based authentication as specified in RFC5802. | -| `none` | Disable sasl authentication | - - -### `sasl[].username` - -A username to provide for PLAIN or SCRAM-* authentication. - - -Type: `string` -Default: `""` - -### `sasl[].password` - -A password to provide for PLAIN or SCRAM-* authentication. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `sasl[].token` - -The token to use for a single session's OAUTHBEARER authentication. - - -Type: `string` -Default: `""` - -### `sasl[].extensions` - -Key/value pairs to add to OAUTHBEARER authentication requests. - - -Type: `object` - -### `sasl[].aws` - -Contains AWS specific fields for when the `mechanism` is set to `AWS_MSK_IAM`. - - -Type: `object` - -### `sasl[].aws.region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `sasl[].aws.endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `sasl[].aws.credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `sasl[].aws.credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `sasl[].aws.credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/outputs/mongodb.md b/website/docs/components/outputs/mongodb.md deleted file mode 100644 index 63d3db8208..0000000000 --- a/website/docs/components/outputs/mongodb.md +++ /dev/null @@ -1,350 +0,0 @@ ---- -title: mongodb -slug: mongodb -type: output -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Inserts items into a MongoDB collection. - -Introduced in version 3.43.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - operation: update-one - write_concern: - w: "" - j: false - w_timeout: "" - document_map: "" - filter_map: "" - hint_map: "" - upsert: false - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - operation: update-one - write_concern: - w: "" - j: false - w_timeout: "" - document_map: "" - filter_map: "" - hint_map: "" - upsert: false - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `url` - -The URL of the target MongoDB server. - - -Type: `string` - -```yml -# Examples - -url: mongodb://localhost:27017 -``` - -### `database` - -The name of the target MongoDB database. - - -Type: `string` - -### `username` - -The username to connect to the database. - - -Type: `string` -Default: `""` - -### `password` - -The password to connect to the database. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `collection` - -The name of the target collection. - - -Type: `string` - -### `operation` - -The mongodb operation to perform. - - -Type: `string` -Default: `"update-one"` -Options: `insert-one`, `delete-one`, `delete-many`, `replace-one`, `update-one`. - -### `write_concern` - -The write concern settings for the mongo connection. - - -Type: `object` - -### `write_concern.w` - -W requests acknowledgement that write operations propagate to the specified number of mongodb instances. - - -Type: `string` -Default: `""` - -### `write_concern.j` - -J requests acknowledgement from MongoDB that write operations are written to the journal. - - -Type: `bool` -Default: `false` - -### `write_concern.w_timeout` - -The write concern timeout. - - -Type: `string` -Default: `""` - -### `document_map` - -A bloblang map representing a document to store within MongoDB, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). The document map is required for the operations insert-one, replace-one and update-one. - - -Type: `string` -Default: `""` - -```yml -# Examples - -document_map: |- - root.a = this.foo - root.b = this.bar -``` - -### `filter_map` - -A bloblang map representing a filter for a MongoDB command, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. - - -Type: `string` -Default: `""` - -```yml -# Examples - -filter_map: |- - root.a = this.foo - root.b = this.bar -``` - -### `hint_map` - -A bloblang map representing the hint for the MongoDB command, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. - - -Type: `string` -Default: `""` - -```yml -# Examples - -hint_map: |- - root.a = this.foo - root.b = this.bar -``` - -### `upsert` - -The upsert setting is optional and only applies for update-one and replace-one operations. If the filter specified in filter_map matches, the document is updated or replaced accordingly, otherwise it is created. - - -Type: `bool` -Default: `false` -Requires version 3.60.0 or newer - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/mqtt.md b/website/docs/components/outputs/mqtt.md deleted file mode 100644 index b8880aa688..0000000000 --- a/website/docs/components/outputs/mqtt.md +++ /dev/null @@ -1,413 +0,0 @@ ---- -title: mqtt -slug: mqtt -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Pushes messages to an MQTT broker. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - mqtt: - urls: [] # No default (required) - client_id: "" - connect_timeout: 30s - topic: "" # No default (required) - qos: 1 - write_timeout: 3s - retained: false - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - mqtt: - urls: [] # No default (required) - client_id: "" - dynamic_client_id_suffix: "" # No default (optional) - connect_timeout: 30s - will: - enabled: false - qos: 0 - retained: false - topic: "" - payload: "" - user: "" - password: "" - keepalive: 30 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - topic: "" # No default (required) - qos: 1 - write_timeout: 3s - retained: false - retained_interpolated: "" # No default (optional) - max_in_flight: 64 -``` - - - - -The `topic` field can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - tcp://localhost:1883 -``` - -### `client_id` - -An identifier for the client connection. - - -Type: `string` -Default: `""` - -### `dynamic_client_id_suffix` - -Append a dynamically generated suffix to the specified `client_id` on each run of the pipeline. This can be useful when clustering Benthos producers. - - -Type: `string` - -| Option | Summary | -|---|---| -| `nanoid` | append a nanoid of length 21 characters | - - -### `connect_timeout` - -The maximum amount of time to wait in order to establish a connection before the attempt is abandoned. - - -Type: `string` -Default: `"30s"` -Requires version 3.58.0 or newer - -```yml -# Examples - -connect_timeout: 1s - -connect_timeout: 500ms -``` - -### `will` - -Set last will message in case of Benthos failure - - -Type: `object` - -### `will.enabled` - -Whether to enable last will messages. - - -Type: `bool` -Default: `false` - -### `will.qos` - -Set QoS for last will message. Valid values are: 0, 1, 2. - - -Type: `int` -Default: `0` - -### `will.retained` - -Set retained for last will message. - - -Type: `bool` -Default: `false` - -### `will.topic` - -Set topic for last will message. - - -Type: `string` -Default: `""` - -### `will.payload` - -Set payload for last will message. - - -Type: `string` -Default: `""` - -### `user` - -A username to connect with. - - -Type: `string` -Default: `""` - -### `password` - -A password to connect with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `keepalive` - -Max seconds of inactivity before a keepalive message is sent. - - -Type: `int` -Default: `30` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `topic` - -The topic to publish messages to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `qos` - -The QoS value to set for each message. Has options 0, 1, 2. - - -Type: `int` -Default: `1` - -### `write_timeout` - -The maximum amount of time to wait to write data before the attempt is abandoned. - - -Type: `string` -Default: `"3s"` -Requires version 3.58.0 or newer - -```yml -# Examples - -write_timeout: 1s - -write_timeout: 500ms -``` - -### `retained` - -Set message as retained on the topic. - - -Type: `bool` -Default: `false` - -### `retained_interpolated` - -Override the value of `retained` with an interpolable value, this allows it to be dynamically set based on message contents. The value must resolve to either `true` or `false`. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 3.59.0 or newer - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/nanomsg.md b/website/docs/components/outputs/nanomsg.md deleted file mode 100644 index 54a705ab6e..0000000000 --- a/website/docs/components/outputs/nanomsg.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: nanomsg -slug: nanomsg -type: output -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Send messages over a Nanomsg socket. - -```yml -# Config fields, showing default values -output: - label: "" - nanomsg: - urls: [] # No default (required) - bind: false - socket_type: PUSH - poll_timeout: 5s - max_in_flight: 64 -``` - -Currently only PUSH and PUB sockets are supported. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -### `bind` - -Whether the URLs listed should be bind (otherwise they are connected to). - - -Type: `bool` -Default: `false` - -### `socket_type` - -The socket type to send with. - - -Type: `string` -Default: `"PUSH"` -Options: `PUSH`, `PUB`. - -### `poll_timeout` - -The maximum period of time to wait for a message to send before the request is abandoned and reattempted. - - -Type: `string` -Default: `"5s"` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/nats.md b/website/docs/components/outputs/nats.md deleted file mode 100644 index 43b90ca3e8..0000000000 --- a/website/docs/components/outputs/nats.md +++ /dev/null @@ -1,429 +0,0 @@ ---- -title: nats -slug: nats -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Publish to an NATS subject. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - nats: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - nats: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - max_in_flight: 64 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) - inject_tracing_map: meta = @.merge(this) # No default (optional) -``` - - - - -This output will interpolate functions within the subject field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries). - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `subject` - -The subject to publish to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -subject: foo.bar.baz -``` - -### `headers` - -Explicit message headers to add to messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -headers: - Content-Type: application/json - Timestamp: ${!meta("Timestamp")} -``` - -### `metadata` - -Determine which (if any) metadata values should be added to messages as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `inject_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. - - -Type: `string` -Requires version 4.23.0 or newer - -```yml -# Examples - -inject_tracing_map: meta = @.merge(this) - -inject_tracing_map: root.meta.span = this -``` - - diff --git a/website/docs/components/outputs/nats_jetstream.md b/website/docs/components/outputs/nats_jetstream.md deleted file mode 100644 index 9218455ca4..0000000000 --- a/website/docs/components/outputs/nats_jetstream.md +++ /dev/null @@ -1,434 +0,0 @@ ---- -title: nats_jetstream -slug: nats_jetstream -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Write messages to a NATS JetStream subject. - -Introduced in version 3.46.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - nats_jetstream: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - max_in_flight: 1024 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - nats_jetstream: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - max_in_flight: 1024 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) - inject_tracing_map: meta = @.merge(this) # No default (optional) -``` - - - - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `subject` - -A subject to write to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -subject: foo.bar.baz - -subject: ${! meta("kafka_topic") } - -subject: foo.${! json("meta.type") } -``` - -### `headers` - -Explicit message headers to add to messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` -Requires version 4.1.0 or newer - -```yml -# Examples - -headers: - Content-Type: application/json - Timestamp: ${!meta("Timestamp")} -``` - -### `metadata` - -Determine which (if any) metadata values should be added to messages as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `1024` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `inject_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. - - -Type: `string` -Requires version 4.23.0 or newer - -```yml -# Examples - -inject_tracing_map: meta = @.merge(this) - -inject_tracing_map: root.meta.span = this -``` - - diff --git a/website/docs/components/outputs/nats_kv.md b/website/docs/components/outputs/nats_kv.md deleted file mode 100644 index 01b3d3c46e..0000000000 --- a/website/docs/components/outputs/nats_kv.md +++ /dev/null @@ -1,366 +0,0 @@ ---- -title: nats_kv -slug: nats_kv -type: output -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Put messages in a NATS key-value bucket. - -Introduced in version 4.12.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - key: foo # No default (required) - max_in_flight: 1024 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - key: foo # No default (required) - max_in_flight: 1024 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) -``` - - - - -The field `key` supports -[interpolation functions](/docs/configuration/interpolation#bloblang-queries), allowing -you to create a unique key for each message. - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `bucket` - -The name of the KV bucket. - - -Type: `string` - -```yml -# Examples - -bucket: my_kv_bucket -``` - -### `key` - -The key for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -key: foo - -key: foo.bar.baz - -key: foo.${! json("meta.type") } -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `1024` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - - diff --git a/website/docs/components/outputs/nats_stream.md b/website/docs/components/outputs/nats_stream.md deleted file mode 100644 index 4081ab3d7d..0000000000 --- a/website/docs/components/outputs/nats_stream.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -title: nats_stream -slug: nats_stream -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Publish to a NATS Stream subject. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - nats_stream: - urls: [] # No default (required) - cluster_id: "" # No default (required) - subject: "" # No default (required) - client_id: "" - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - nats_stream: - urls: [] # No default (required) - cluster_id: "" # No default (required) - subject: "" # No default (required) - client_id: "" - max_in_flight: 64 - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) - inject_tracing_map: meta = @.merge(this) # No default (optional) -``` - - - - -:::caution Deprecation Notice -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use [JetStream](https://docs.nats.io/nats-concepts/jetstream). -::: - -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `cluster_id` - -The cluster ID to publish to. - - -Type: `string` - -### `subject` - -The subject to publish to. - - -Type: `string` - -### `client_id` - -The client ID to connect with. - - -Type: `string` -Default: `""` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `inject_tracing_map` - -EXPERIMENTAL: A [Bloblang mapping](/docs/guides/bloblang/about) used to inject an object containing tracing propagation information into outbound messages. The specification of the injected fields will match the format used by the service wide tracer. - - -Type: `string` -Requires version 4.23.0 or newer - -```yml -# Examples - -inject_tracing_map: meta = @.merge(this) - -inject_tracing_map: root.meta.span = this -``` - - diff --git a/website/docs/components/outputs/nsq.md b/website/docs/components/outputs/nsq.md deleted file mode 100644 index 6fdd3b5d1a..0000000000 --- a/website/docs/components/outputs/nsq.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: nsq -slug: nsq -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Publish to an NSQ topic. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - nsq: - nsqd_tcp_address: "" # No default (required) - topic: "" # No default (required) - user_agent: "" # No default (optional) - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - nsq: - nsqd_tcp_address: "" # No default (required) - topic: "" # No default (required) - user_agent: "" # No default (optional) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - max_in_flight: 64 -``` - - - - -The `topic` field can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `nsqd_tcp_address` - -The address of the target NSQD server. - - -Type: `string` - -### `topic` - -The topic to publish to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `user_agent` - -A user agent to assume when connecting. - - -Type: `string` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/opensearch.md b/website/docs/components/outputs/opensearch.md deleted file mode 100644 index 38def0f667..0000000000 --- a/website/docs/components/outputs/opensearch.md +++ /dev/null @@ -1,568 +0,0 @@ ---- -title: opensearch -slug: opensearch -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Publishes messages into an Elasticsearch index. If the index does not exist then it is created with a dynamic mapping. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - opensearch: - urls: [] # No default (required) - index: "" # No default (required) - action: "" # No default (required) - id: ${!counter()}-${!timestamp_unix()} # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - opensearch: - urls: [] # No default (required) - index: "" # No default (required) - action: "" # No default (required) - id: ${!counter()}-${!timestamp_unix()} # No default (required) - pipeline: "" - routing: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - max_in_flight: 64 - basic_auth: - enabled: false - username: "" - password: "" - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - aws: - enabled: false - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -Both the `id` and `index` fields can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). When sending batched messages these interpolations are performed per message part. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Examples - - - - - -When [updating documents](https://opensearch.org/docs/latest/api-reference/document-apis/update-document/) the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors. - -```yaml -output: - processors: - - mapping: | - meta id = this.id - root.doc = this - opensearch: - urls: [ TODO ] - index: foo - id: ${! @id } - action: update -``` - - - - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - http://localhost:9200 -``` - -### `index` - -The index to place messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `action` - -The action to take on the document. This field must resolve to one of the following action types: `index`, `update` or `delete`. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `id` - -The ID for indexed messages. Interpolation should be used in order to create a unique ID for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -id: ${!counter()}-${!timestamp_unix()} -``` - -### `pipeline` - -An optional pipeline id to preprocess incoming documents. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `routing` - -The routing key to use for the document. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `aws` - -Enables and customises connectivity to Amazon Elastic Service. - - -Type: `object` - -### `aws.enabled` - -Whether to connect to Amazon Elastic Service. - - -Type: `bool` -Default: `false` - -### `aws.region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `aws.endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `aws.credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `aws.credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `aws.credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `aws.credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `aws.credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `aws.credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `aws.credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `aws.credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/outputs/pulsar.md b/website/docs/components/outputs/pulsar.md deleted file mode 100644 index edb4a9ac5d..0000000000 --- a/website/docs/components/outputs/pulsar.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: pulsar -slug: pulsar -type: output -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Write messages to an Apache Pulsar server. - -Introduced in version 3.43.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - pulsar: - url: pulsar://localhost:6650 # No default (required) - topic: "" # No default (required) - tls: - root_cas_file: "" - key: "" - ordering_key: "" - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - pulsar: - url: pulsar://localhost:6650 # No default (required) - topic: "" # No default (required) - tls: - root_cas_file: "" - key: "" - ordering_key: "" - max_in_flight: 64 - auth: - oauth2: - enabled: false - audience: "" - issuer_url: "" - private_key_file: "" - token: - enabled: false - token: "" -``` - - - - -## Fields - -### `url` - -A URL to connect to. - - -Type: `string` - -```yml -# Examples - -url: pulsar://localhost:6650 - -url: pulsar://pulsar.us-west.example.com:6650 - -url: pulsar+ssl://pulsar.us-west.example.com:6651 -``` - -### `topic` - -The topic to publish to. - - -Type: `string` - -### `tls` - -Specify the path to a custom CA certificate to trust broker TLS service. - - -Type: `object` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `key` - -The key to publish messages with. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `ordering_key` - -The ordering key to publish messages with. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `auth` - -Optional configuration of Pulsar authentication methods. - - -Type: `object` -Requires version 3.60.0 or newer - -### `auth.oauth2` - -Parameters for Pulsar OAuth2 authentication. - - -Type: `object` - -### `auth.oauth2.enabled` - -Whether OAuth2 is enabled. - - -Type: `bool` -Default: `false` - -### `auth.oauth2.audience` - -OAuth2 audience. - - -Type: `string` -Default: `""` - -### `auth.oauth2.issuer_url` - -OAuth2 issuer URL. - - -Type: `string` -Default: `""` - -### `auth.oauth2.private_key_file` - -The path to a file containing a private key. - - -Type: `string` -Default: `""` - -### `auth.token` - -Parameters for Pulsar Token authentication. - - -Type: `object` - -### `auth.token.enabled` - -Whether Token Auth is enabled. - - -Type: `bool` -Default: `false` - -### `auth.token.token` - -Actual base64 encoded token. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/outputs/pusher.md b/website/docs/components/outputs/pusher.md deleted file mode 100644 index e52eeb86c2..0000000000 --- a/website/docs/components/outputs/pusher.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: pusher -slug: pusher -type: output -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Output for publishing messages to Pusher API (https://pusher.com) - -Introduced in version 4.3.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - pusher: - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - channel: my_channel # No default (required) - event: "" # No default (required) - appId: "" # No default (required) - key: "" # No default (required) - secret: "" # No default (required) - cluster: "" # No default (required) - secure: true - max_in_flight: 1 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - pusher: - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - channel: my_channel # No default (required) - event: "" # No default (required) - appId: "" # No default (required) - key: "" # No default (required) - secret: "" # No default (required) - cluster: "" # No default (required) - secure: true - max_in_flight: 1 -``` - - - - -## Fields - -### `batching` - -maximum batch size is 10 (limit of the pusher library) - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `channel` - -Pusher channel to publish to. Interpolation functions can also be used -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -channel: my_channel - -channel: ${!json("id")} -``` - -### `event` - -Event to publish to - - -Type: `string` - -### `appId` - -Pusher app id - - -Type: `string` - -### `key` - -Pusher key - - -Type: `string` - -### `secret` - -Pusher secret - - -Type: `string` - -### `cluster` - -Pusher cluster - - -Type: `string` - -### `secure` - -Enable SSL encryption - - -Type: `bool` -Default: `true` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `1` - - diff --git a/website/docs/components/outputs/redis_hash.md b/website/docs/components/outputs/redis_hash.md deleted file mode 100644 index f1c535a029..0000000000 --- a/website/docs/components/outputs/redis_hash.md +++ /dev/null @@ -1,343 +0,0 @@ ---- -title: redis_hash -slug: redis_hash -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sets Redis hash objects using the HMSET command. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - redis_hash: - url: redis://:6397 # No default (required) - key: ${! @.kafka_key )} # No default (required) - walk_metadata: false - walk_json_object: false - fields: {} - max_in_flight: 64 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - redis_hash: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - key: ${! @.kafka_key )} # No default (required) - walk_metadata: false - walk_json_object: false - fields: {} - max_in_flight: 64 -``` - - - - -The field `key` supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries), allowing you to create a unique key for each message. - -The field `fields` allows you to specify an explicit map of field names to interpolated values, also evaluated per message of a batch: - -```yaml -output: - redis_hash: - url: tcp://localhost:6379 - key: ${!json("id")} - fields: - topic: ${!meta("kafka_topic")} - partition: ${!meta("kafka_partition")} - content: ${!json("document.text")} -``` - -If the field `walk_metadata` is set to `true` then Benthos will walk all metadata fields of messages and add them to the list of hash fields to set. - -If the field `walk_json_object` is set to `true` then Benthos will walk each message as a JSON object, extracting keys and the string representation of their value and adds them to the list of hash fields to set. - -The order of hash field extraction is as follows: - -1. Metadata (if enabled) -2. JSON object (if enabled) -3. Explicit fields - -Where latter stages will overwrite matching field names of a former stage. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `key` - -The key for each message, function interpolations should be used to create a unique key per message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -key: ${! @.kafka_key )} - -key: ${! this.doc.id } - -key: ${! count("msgs") } -``` - -### `walk_metadata` - -Whether all metadata fields of messages should be walked and added to the list of hash fields to set. - - -Type: `bool` -Default: `false` - -### `walk_json_object` - -Whether to walk each message as a JSON object and add each key/value pair to the list of hash fields to set. - - -Type: `bool` -Default: `false` - -### `fields` - -A map of key/value pairs to set as hash fields. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/redis_list.md b/website/docs/components/outputs/redis_list.md deleted file mode 100644 index 93e21ed71a..0000000000 --- a/website/docs/components/outputs/redis_list.md +++ /dev/null @@ -1,409 +0,0 @@ ---- -title: redis_list -slug: redis_list -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Pushes messages onto the end of a Redis list (which is created if it doesn't already exist) using the RPUSH command. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - redis_list: - url: redis://:6397 # No default (required) - key: some_list # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - redis_list: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - key: some_list # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - command: rpush -``` - - - - -The field `key` supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries), allowing you to create a unique key for each message. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `key` - -The key for each message, function interpolations can be optionally used to create a unique key per message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -key: some_list - -key: ${! @.kafka_key )} - -key: ${! this.doc.id } - -key: ${! count("msgs") } -``` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `command` - -The command used to push elements to the Redis list - - -Type: `string` -Default: `"rpush"` -Requires version 4.22.0 or newer -Options: `rpush`, `lpush`. - - diff --git a/website/docs/components/outputs/redis_pubsub.md b/website/docs/components/outputs/redis_pubsub.md deleted file mode 100644 index 1fc14650fb..0000000000 --- a/website/docs/components/outputs/redis_pubsub.md +++ /dev/null @@ -1,386 +0,0 @@ ---- -title: redis_pubsub -slug: redis_pubsub -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Publishes messages through the Redis PubSub model. It is not possible to guarantee that messages have been received. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - redis_pubsub: - url: redis://:6397 # No default (required) - channel: "" # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - redis_pubsub: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - channel: "" # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -This output will interpolate functions within the channel field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `channel` - -The channel to publish messages to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/redis_streams.md b/website/docs/components/outputs/redis_streams.md deleted file mode 100644 index 781f491a93..0000000000 --- a/website/docs/components/outputs/redis_streams.md +++ /dev/null @@ -1,427 +0,0 @@ ---- -title: redis_streams -slug: redis_streams -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Pushes messages to a Redis (v5.0+) Stream (which is created if it doesn't already exist) using the XADD command. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - redis_streams: - url: redis://:6397 # No default (required) - stream: "" # No default (required) - body_key: body - max_length: 0 - max_in_flight: 64 - metadata: - exclude_prefixes: [] - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - redis_streams: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - stream: "" # No default (required) - body_key: body - max_length: 0 - max_in_flight: 64 - metadata: - exclude_prefixes: [] - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -It's possible to specify a maximum length of the target stream by setting it to a value greater than 0, in which case this cap is applied only when Redis is able to remove a whole macro node, for efficiency. - -Redis stream entries are key/value pairs, as such it is necessary to specify the key to be set to the body of the message. All metadata fields of the message will also be set as key/value pairs, if there is a key collision between a metadata item and the body then the body takes precedence. - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `stream` - -The stream to add messages to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `body_key` - -A key to set the raw body of the message to. - - -Type: `string` -Default: `"body"` - -### `max_length` - -When greater than zero enforces a rough cap on the length of the target stream. - - -Type: `int` -Default: `0` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - -### `metadata` - -Specify criteria for which metadata values are included in the message body. - - -Type: `object` - -### `metadata.exclude_prefixes` - -Provide a list of explicit metadata key prefixes to be excluded when adding metadata to sent messages. - - -Type: `array` -Default: `[]` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/reject.md b/website/docs/components/outputs/reject.md deleted file mode 100644 index 11bb55dd6b..0000000000 --- a/website/docs/components/outputs/reject.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: reject -slug: reject -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Rejects all messages, treating them as though the output destination failed to publish them. - -```yml -# Config fields, showing default values -output: - label: "" - reject: "" -``` - -The routing of messages after this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch. - -If you're still scratching your head as to when this output could be useful check out [the examples below](#examples). - -## Examples - - - - - - -This input is particularly useful for routing messages that have failed during processing, where instead of routing them to some sort of dead letter queue we wish to push the error upstream. We can do this with a switch broker: - -```yaml -output: - switch: - retry_until_success: false - cases: - - check: '!errored()' - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/the_foos - - - output: - reject: "processing failed due to: ${! error() }" -``` - - - - - diff --git a/website/docs/components/outputs/reject_errored.md b/website/docs/components/outputs/reject_errored.md deleted file mode 100644 index e7cc13dc0f..0000000000 --- a/website/docs/components/outputs/reject_errored.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: reject_errored -slug: reject_errored -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Rejects messages that have failed their processing steps, resulting in nack behaviour at the input level, otherwise sends them to a child output. - -```yml -# Config fields, showing default values -output: - label: "" - reject_errored: null # No default (required) -``` - -The routing of messages rejected by this output depends on the type of input it came from. For inputs that support propagating nacks upstream such as AMQP or NATS the message will be nacked. However, for inputs that are sequential such as files or Kafka the messages will simply be reprocessed from scratch. - -## Examples - - - - - - -The most straight forward use case for this output type is to nack messages that have failed their processing steps. In this example our mapping might fail, in which case the messages that failed are rejected and will be nacked by our input: - -```yaml -input: - nats_jetstream: - urls: [ nats://127.0.0.1:4222 ] - subject: foos.pending - -pipeline: - processors: - - mutation: 'root.age = this.fuzzy.age.int64()' - -output: - reject_errored: - nats_jetstream: - urls: [ nats://127.0.0.1:4222 ] - subject: foos.processed -``` - - - - - -Another use case for this output is to send failed messages straight into a dead-letter queue. We use it within a [fallback output](/docs/components/outputs/fallback) that allows us to specify where these failed messages should go to next. - -```yaml -pipeline: - processors: - - mutation: 'root.age = this.fuzzy.age.int64()' - -output: - fallback: - - reject_errored: - http_client: - url: http://foo:4195/post/might/become/unreachable - retries: 3 - retry_period: 1s - - http_client: - url: http://bar:4196/somewhere/else - retries: 3 - retry_period: 1s -``` - - - - - diff --git a/website/docs/components/outputs/resource.md b/website/docs/components/outputs/resource.md deleted file mode 100644 index 93b3228178..0000000000 --- a/website/docs/components/outputs/resource.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: resource -slug: resource -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Resource is an output type that channels messages to a resource output, identified by its name. - -```yml -# Config fields, showing default values -output: - resource: "" -``` - -Resources allow you to tidy up deeply nested configs. For example, the config: - -```yaml -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ TODO ] - topic: foo - - gcp_pubsub: - project: bar - topic: baz -``` - -Could also be expressed as: - -```yaml -output: - broker: - pattern: fan_out - outputs: - - resource: foo - - resource: bar - -output_resources: - - label: foo - kafka: - addresses: [ TODO ] - topic: foo - - - label: bar - gcp_pubsub: - project: bar - topic: baz - ``` - -You can find out more about resources [in this document.](/docs/configuration/resources) - - diff --git a/website/docs/components/outputs/retry.md b/website/docs/components/outputs/retry.md deleted file mode 100644 index 06f9cc4a13..0000000000 --- a/website/docs/components/outputs/retry.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: retry -slug: retry -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Attempts to write messages to a child output and if the write fails for any reason the message is retried either until success or, if the retries or max elapsed time fields are non-zero, either is reached. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - retry: - output: null # No default (required) -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - retry: - max_retries: 0 - backoff: - initial_interval: 500ms - max_interval: 3s - max_elapsed_time: 0s - output: null # No default (required) -``` - - - - -All messages in Benthos are always retried on an output error, but this would usually involve propagating the error back to the source of the message, whereby it would be reprocessed before reaching the output layer once again. - -This output type is useful whenever we wish to avoid reprocessing a message on the event of a failed send. We might, for example, have a dedupe processor that we want to avoid reapplying to the same message more than once in the pipeline. - -Rather than retrying the same output you may wish to retry the send using a different output target (a dead letter queue). In which case you should instead use the [`fallback`](/docs/components/outputs/fallback) output type. - -## Fields - -### `max_retries` - -The maximum number of retries before giving up on the request. If set to zero there is no discrete limit. - - -Type: `int` -Default: `0` - -### `backoff` - -Control time intervals between retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"500ms"` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts. - - -Type: `string` -Default: `"3s"` - -### `backoff.max_elapsed_time` - -The maximum period to wait before retry attempts are abandoned. If zero then no limit is used. - - -Type: `string` -Default: `"0s"` - -### `output` - -A child output. - - -Type: `output` - - diff --git a/website/docs/components/outputs/sftp.md b/website/docs/components/outputs/sftp.md deleted file mode 100644 index a14bac479f..0000000000 --- a/website/docs/components/outputs/sftp.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: sftp -slug: sftp -type: output -status: beta -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Writes files to an SFTP server. - -Introduced in version 3.39.0. - -```yml -# Config fields, showing default values -output: - label: "" - sftp: - address: "" # No default (required) - path: "" # No default (required) - codec: all-bytes - credentials: - username: "" - password: "" - private_key_file: "" - private_key_pass: "" - max_in_flight: 64 -``` - -In order to have a different path for each object you should use function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -## Fields - -### `address` - -The address of the server to connect to. - - -Type: `string` - -### `path` - -The file to save the messages to on the server. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `codec` - -The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. - - -Type: `string` -Default: `"all-bytes"` - -| Option | Summary | -|---|---| -| `all-bytes` | Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. | -| `append` | Append each message to the output stream without any delimiter or special encoding. | -| `delim:x` | Append each message to the output stream followed by a custom delimiter. | -| `lines` | Append each message to the output stream followed by a line break. | - - -```yml -# Examples - -codec: lines - -codec: "delim:\t" - -codec: delim:foobar -``` - -### `credentials` - -The credentials to use to log into the target server. - - -Type: `object` - -### `credentials.username` - -The username to connect to the SFTP server. - - -Type: `string` -Default: `""` - -### `credentials.password` - -The password for the username to connect to the SFTP server. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.private_key_file` - -The private key for the username to connect to the SFTP server. - - -Type: `string` -Default: `""` - -### `credentials.private_key_pass` - -Optional passphrase for private key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `max_in_flight` - -The maximum number of messages to have in flight at a given time. Increase this to improve throughput. - - -Type: `int` -Default: `64` - - diff --git a/website/docs/components/outputs/snowflake_put.md b/website/docs/components/outputs/snowflake_put.md deleted file mode 100644 index 438069a1c2..0000000000 --- a/website/docs/components/outputs/snowflake_put.md +++ /dev/null @@ -1,748 +0,0 @@ ---- -title: snowflake_put -slug: snowflake_put -type: output -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Sends messages to Snowflake stages and, optionally, calls Snowpipe to load this data into one or more tables. - -Introduced in version 4.0.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - snowflake_put: - account: "" # No default (required) - region: us-west-2 # No default (optional) - cloud: aws # No default (optional) - user: "" # No default (required) - password: "" # No default (optional) - private_key_file: "" # No default (optional) - private_key_pass: "" # No default (optional) - role: "" # No default (required) - database: "" # No default (required) - warehouse: "" # No default (required) - schema: "" # No default (required) - stage: "" # No default (required) - path: "" - file_name: "" - file_extension: "" - compression: AUTO - request_id: "" - snowpipe: "" # No default (optional) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - max_in_flight: 1 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - snowflake_put: - account: "" # No default (required) - region: us-west-2 # No default (optional) - cloud: aws # No default (optional) - user: "" # No default (required) - password: "" # No default (optional) - private_key_file: "" # No default (optional) - private_key_pass: "" # No default (optional) - role: "" # No default (required) - database: "" # No default (required) - warehouse: "" # No default (required) - schema: "" # No default (required) - stage: "" # No default (required) - path: "" - file_name: "" - file_extension: "" - upload_parallel_threads: 4 - compression: AUTO - request_id: "" - snowpipe: "" # No default (optional) - client_session_keep_alive: false - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) - max_in_flight: 1 -``` - - - - -In order to use a different stage and / or Snowpipe for each message, you can use function interpolations as described -[here](/docs/configuration/interpolation#bloblang-queries). When using batching, messages are grouped by the calculated -stage and Snowpipe and are streamed to individual files in their corresponding stage and, optionally, a Snowpipe -`insertFiles` REST API call will be made for each individual file. - -### Credentials - -Two authentication mechanisms are supported: -- User/password -- Key Pair Authentication - -#### User/password - -This is a basic authentication mechanism which allows you to PUT data into a stage. However, it is not compatible with -Snowpipe. - -#### Key Pair Authentication - -This authentication mechanism allows Snowpipe functionality, but it does require configuring an SSH Private Key -beforehand. Please consult the [documentation](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication) -for details on how to set it up and assign the Public Key to your user. - -Note that the Snowflake documentation [used to suggest](https://twitter.com/felipehoffa/status/1560811785606684672) -using this command: - -```shell -openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -``` - -to generate an encrypted SSH private key. However, in this case, it uses an encryption algorithm called -`pbeWithMD5AndDES-CBC`, which is part of the PKCS#5 v1.5 and is considered insecure. Due to this, Benthos does not -support it and, if you wish to use password-protected keys directly, you must use PKCS#5 v2.0 to encrypt them by using -the following command (as the current Snowflake docs suggest): - -```shell -openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 des3 -inform PEM -out rsa_key.p8 -``` - -If you have an existing key encrypted with PKCS#5 v1.5, you can re-encrypt it with PKCS#5 v2.0 using this command: - -```shell -openssl pkcs8 -in rsa_key_original.p8 -topk8 -v2 des3 -out rsa_key.p8 -``` - -Please consult [this](https://linux.die.net/man/1/pkcs8) pkcs8 command documentation for details on PKCS#5 algorithms. - -### Batching - -It's common to want to upload messages to Snowflake as batched archives. The easiest way to do this is to batch your -messages at the output level and join the batch of messages with an -[`archive`](/docs/components/processors/archive) and/or [`compress`](/docs/components/processors/compress) -processor. - -For the optimal batch size, please consult the Snowflake [documentation](https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html). - -### Snowpipe - -Given a table called `BENTHOS_TBL` with one column of type `variant`: - -```sql -CREATE OR REPLACE TABLE BENTHOS_DB.PUBLIC.BENTHOS_TBL(RECORD variant) -``` - -and the following `BENTHOS_PIPE` Snowpipe: - -```sql -CREATE OR REPLACE PIPE BENTHOS_DB.PUBLIC.BENTHOS_PIPE AUTO_INGEST = FALSE AS COPY INTO BENTHOS_DB.PUBLIC.BENTHOS_TBL FROM (SELECT * FROM @%BENTHOS_TBL) FILE_FORMAT = (TYPE = JSON COMPRESSION = AUTO) -``` - -you can configure Benthos to use the implicit table stage `@%BENTHOS_TBL` as the `stage` and -`BENTHOS_PIPE` as the `snowpipe`. In this case, you must set `compression` to `AUTO` and, if -using message batching, you'll need to configure an [`archive`](/docs/components/processors/archive) processor -with the `concatenate` format. Since the `compression` is set to `AUTO`, the -[gosnowflake](https://github.com/snowflakedb/gosnowflake) client library will compress the messages automatically so you -don't need to add a [`compress`](/docs/components/processors/compress) processor for message batches. - -If you add `STRIP_OUTER_ARRAY = TRUE` in your Snowpipe `FILE_FORMAT` -definition, then you must use `json_array` instead of `concatenate` as the archive processor format. - -Note: Only Snowpipes with `FILE_FORMAT` `TYPE` `JSON` are currently supported. - -### Snowpipe Troubleshooting - -Snowpipe [provides](https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html) the `insertReport` -and `loadHistoryScan` REST API endpoints which can be used to get information about recent Snowpipe calls. In -order to query them, you'll first need to generate a valid JWT token for your Snowflake account. There are two methods -for doing so: -- Using the `snowsql` [utility](https://docs.snowflake.com/en/user-guide/snowsql.html): - -```shell -snowsql --private-key-path rsa_key.p8 --generate-jwt -a -u -``` - -- Using the Python `sql-api-generate-jwt` [utility](https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python): - -```shell -python3 sql-api-generate-jwt.py --private_key_file_path=rsa_key.p8 --account= --user= -``` - -Once you successfully generate a JWT token and store it into the `JWT_TOKEN` environment variable, then you can, -for example, query the `insertReport` endpoint using `curl`: - -```shell -curl -H "Authorization: Bearer ${JWT_TOKEN}" "https://.snowflakecomputing.com/v1/data/pipes/../insertReport" -``` - -If you need to pass in a valid `requestId` to any of these Snowpipe REST API endpoints, you can set a -[uuid_v4()](https://www.benthos.dev/docs/guides/bloblang/functions#uuid_v4) string in a metadata field called -`request_id`, log it via the [`log`](https://www.benthos.dev/docs/components/processors/log) processor and -then configure `request_id: ${ @request_id }` ). Alternatively, you can enable debug logging as described -[here](/docs/components/logger/about) and Benthos will print the Request IDs that it sends to Snowpipe. - -### General Troubleshooting - -The underlying [`gosnowflake` driver](https://github.com/snowflakedb/gosnowflake) requires write access to -the default directory to use for temporary files. Please consult the [`os.TempDir`](https://pkg.go.dev/os#TempDir) -docs for details on how to change this directory via environment variables. - -A silent failure can occur due to [this issue](https://github.com/snowflakedb/gosnowflake/issues/701), where the -underlying [`gosnowflake` driver](https://github.com/snowflakedb/gosnowflake) doesn't return an error and doesn't -log a failure if it can't figure out the current username. One way to trigger this behaviour is by running Benthos in a -Docker container with a non-existent user ID (such as `--user 1000:1000`). - - -## Performance - -This output benefits from sending multiple messages in flight in parallel for improved performance. You can tune the max number of in flight messages (or message batches) with the field `max_in_flight`. - -This output benefits from sending messages as a batch for improved performance. Batches can be formed at both the input and output level. You can find out more [in this doc](/docs/configuration/batching). - -## Examples - - - - - -Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the [Kafka Connector scheme](https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table) and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition. - -```yaml -input: - kafka: - addresses: - - localhost:9092 - topics: - - foo - consumer_group: benthos - batching: - count: 10 - period: 3s - processors: - - mapping: | - meta kafka_start_offset = meta("kafka_offset").from(0) - meta kafka_end_offset = meta("kafka_offset").from(-1) - meta batch_timestamp = if batch_index() == 0 { now() } - - mapping: | - meta batch_timestamp = if batch_index() != 0 { meta("batch_timestamp").from(0) } - -output: - snowflake_put: - account: benthos - user: test@benthos.dev - private_key_file: path_to_ssh_key.pem - role: ACCOUNTADMIN - database: BENTHOS_DB - warehouse: COMPUTE_WH - schema: PUBLIC - stage: "@%BENTHOS_TBL" - path: benthos/BENTHOS_TBL/${! @kafka_partition } - file_name: ${! @kafka_start_offset }_${! @kafka_end_offset }_${! meta("batch_timestamp") } - upload_parallel_threads: 4 - compression: NONE - snowpipe: BENTHOS_PIPE -``` - - - - -Upload concatenated messages into a `.json` file to a table stage without calling Snowpipe. - -```yaml -output: - snowflake_put: - account: benthos - user: test@benthos.dev - private_key_file: path_to_ssh_key.pem - role: ACCOUNTADMIN - database: BENTHOS_DB - warehouse: COMPUTE_WH - schema: PUBLIC - stage: "@%BENTHOS_TBL" - path: benthos - upload_parallel_threads: 4 - compression: NONE - batching: - count: 10 - period: 3s - processors: - - archive: - format: concatenate -``` - - - - -Upload concatenated messages into a `.parquet` file to a table stage without calling Snowpipe. - -```yaml -output: - snowflake_put: - account: benthos - user: test@benthos.dev - private_key_file: path_to_ssh_key.pem - role: ACCOUNTADMIN - database: BENTHOS_DB - warehouse: COMPUTE_WH - schema: PUBLIC - stage: "@%BENTHOS_TBL" - path: benthos - file_extension: parquet - upload_parallel_threads: 4 - compression: NONE - batching: - count: 10 - period: 3s - processors: - - parquet_encode: - schema: - - name: ID - type: INT64 - - name: CONTENT - type: BYTE_ARRAY - default_compression: snappy -``` - - - - -Upload concatenated messages compressed automatically into a `.gz` archive file to a table stage without calling Snowpipe. - -```yaml -output: - snowflake_put: - account: benthos - user: test@benthos.dev - private_key_file: path_to_ssh_key.pem - role: ACCOUNTADMIN - database: BENTHOS_DB - warehouse: COMPUTE_WH - schema: PUBLIC - stage: "@%BENTHOS_TBL" - path: benthos - upload_parallel_threads: 4 - compression: AUTO - batching: - count: 10 - period: 3s - processors: - - archive: - format: concatenate -``` - - - - -Upload concatenated messages compressed into a `.deflate` archive file to a table stage and call Snowpipe to load them into a table. - -```yaml -output: - snowflake_put: - account: benthos - user: test@benthos.dev - private_key_file: path_to_ssh_key.pem - role: ACCOUNTADMIN - database: BENTHOS_DB - warehouse: COMPUTE_WH - schema: PUBLIC - stage: "@%BENTHOS_TBL" - path: benthos - upload_parallel_threads: 4 - compression: DEFLATE - snowpipe: BENTHOS_PIPE - batching: - count: 10 - period: 3s - processors: - - archive: - format: concatenate - - mapping: | - root = content().compress("zlib") -``` - - - - -Upload concatenated messages compressed into a `.raw_deflate` archive file to a table stage and call Snowpipe to load them into a table. - -```yaml -output: - snowflake_put: - account: benthos - user: test@benthos.dev - private_key_file: path_to_ssh_key.pem - role: ACCOUNTADMIN - database: BENTHOS_DB - warehouse: COMPUTE_WH - schema: PUBLIC - stage: "@%BENTHOS_TBL" - path: benthos - upload_parallel_threads: 4 - compression: RAW_DEFLATE - snowpipe: BENTHOS_PIPE - batching: - count: 10 - period: 3s - processors: - - archive: - format: concatenate - - mapping: | - root = content().compress("flate") -``` - - - - -## Fields - -### `account` - -Account name, which is the same as the Account Identifier -as described [here](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used). -However, when using an [Account Locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier), -the Account Identifier is formatted as `..` and this field needs to be -populated using the `` part. - - -Type: `string` - -### `region` - -Optional region field which needs to be populated when using -an [Account Locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier) -and it must be set to the `` part of the Account Identifier -(`..`). - - -Type: `string` - -```yml -# Examples - -region: us-west-2 -``` - -### `cloud` - -Optional cloud platform field which needs to be populated -when using an [Account Locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier) -and it must be set to the `` part of the Account Identifier -(`..`). - - -Type: `string` - -```yml -# Examples - -cloud: aws - -cloud: gcp - -cloud: azure -``` - -### `user` - -Username. - - -Type: `string` - -### `password` - -An optional password. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `private_key_file` - -The path to a file containing the private SSH key. - - -Type: `string` - -### `private_key_pass` - -An optional private SSH key passphrase. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `role` - -Role. - - -Type: `string` - -### `database` - -Database. - - -Type: `string` - -### `warehouse` - -Warehouse. - - -Type: `string` - -### `schema` - -Schema. - - -Type: `string` - -### `stage` - -Stage name. Use either one of the -[supported](https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html) stage types. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `path` - -Stage path. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -### `file_name` - -Stage file name. Will be equal to the Request ID if not set or empty. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` -Requires version v4.12.0 or newer - -### `file_extension` - -Stage file extension. Will be derived from the configured `compression` if not set or empty. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` -Requires version v4.12.0 or newer - -```yml -# Examples - -file_extension: csv - -file_extension: parquet -``` - -### `upload_parallel_threads` - -Specifies the number of threads to use for uploading files. - - -Type: `int` -Default: `4` - -### `compression` - -Compression type. - - -Type: `string` -Default: `"AUTO"` - -| Option | Summary | -|---|---| -| `AUTO` | Compression (gzip) is applied automatically by the output and messages must contain plain-text JSON. Default `file_extension`: `gz`. | -| `DEFLATE` | Messages must be pre-compressed using the zlib algorithm (with zlib header, RFC1950). Default `file_extension`: `deflate`. | -| `GZIP` | Messages must be pre-compressed using the gzip algorithm. Default `file_extension`: `gz`. | -| `NONE` | No compression is applied and messages must contain plain-text JSON. Default `file_extension`: `json`. | -| `RAW_DEFLATE` | Messages must be pre-compressed using the flate algorithm (without header, RFC1951). Default `file_extension`: `raw_deflate`. | -| `ZSTD` | Messages must be pre-compressed using the Zstandard algorithm. Default `file_extension`: `zst`. | - - -### `request_id` - -Request ID. Will be assigned a random UUID (v4) string if not set or empty. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` -Requires version v4.12.0 or newer - -### `snowpipe` - -An optional Snowpipe name. Use the `` part from `..`. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `client_session_keep_alive` - -Enable Snowflake keepalive mechanism to prevent the client session from expiring after 4 hours (error 390114). - - -Type: `bool` -Default: `false` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `1` - - diff --git a/website/docs/components/outputs/socket.md b/website/docs/components/outputs/socket.md deleted file mode 100644 index bacf6a7d3f..0000000000 --- a/website/docs/components/outputs/socket.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: socket -slug: socket -type: output -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Connects to a (tcp/udp/unix) server and sends a continuous stream of data, dividing messages according to the specified codec. - -```yml -# Config fields, showing default values -output: - label: "" - socket: - network: "" # No default (required) - address: /tmp/benthos.sock # No default (required) - codec: lines -``` - -## Fields - -### `network` - -A network type to connect as. - - -Type: `string` -Options: `unix`, `tcp`, `udp`. - -### `address` - -The address to connect to. - - -Type: `string` - -```yml -# Examples - -address: /tmp/benthos.sock - -address: 127.0.0.1:6000 -``` - -### `codec` - -The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. - - -Type: `string` -Default: `"lines"` - -| Option | Summary | -|---|---| -| `all-bytes` | Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. | -| `append` | Append each message to the output stream without any delimiter or special encoding. | -| `lines` | Append each message to the output stream followed by a line break. | -| `delim:x` | Append each message to the output stream followed by a custom delimiter. | - - -```yml -# Examples - -codec: lines - -codec: "delim:\t" - -codec: delim:foobar -``` - - diff --git a/website/docs/components/outputs/splunk_hec.md b/website/docs/components/outputs/splunk_hec.md deleted file mode 100644 index d1666a8256..0000000000 --- a/website/docs/components/outputs/splunk_hec.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: splunk_hec -slug: splunk_hec -type: output -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Writes messages to a Splunk HTTP Endpoint Collector. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - splunk_hec: - url: "" # No default (required) - token: "" # No default (required) - gzip: false - event_host: "" - event_source: "" - event_sourcetype: "" - event_index: "" - batching_count: 100 - batching_period: 30s - batching_byte_size: 1000000 -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - splunk_hec: - url: "" # No default (required) - token: "" # No default (required) - gzip: false - event_host: "" - event_source: "" - event_sourcetype: "" - event_index: "" - batching_count: 100 - batching_period: 30s - batching_byte_size: 1000000 - rate_limit: "" - max_in_flight: 64 - skip_cert_verify: false -``` - - - - -This output POSTs messages to a Splunk HTTP Endpoint Collector (HEC) using token based authentication. The format of the message must be a [valid event JSON](https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/FormateventsforHTTPEventCollector). Raw is not supported. - - -## Fields - -### `url` - -Full HTTP Endpoint Collector (HEC) URL, ie. https://foobar.splunkcloud.com/services/collector/event - - -Type: `string` - -### `token` - -A bot token used for authentication. - - -Type: `string` - -### `gzip` - -Enable gzip compression - - -Type: `bool` -Default: `false` - -### `event_host` - -Set the host value to assign to the event data. Overrides existing host field if present. - - -Type: `string` -Default: `""` - -### `event_source` - -Set the source value to assign to the event data. Overrides existing source field if present. - - -Type: `string` -Default: `""` - -### `event_sourcetype` - -Set the sourcetype value to assign to the event data. Overrides existing sourcetype field if present. - - -Type: `string` -Default: `""` - -### `event_index` - -Set the index value to assign to the event data. Overrides existing index field if present. - - -Type: `string` -Default: `""` - -### `batching_count` - -A number of messages at which the batch should be flushed. If 0 disables count based batching. - - -Type: `int` -Default: `100` - -### `batching_period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `"30s"` - -### `batching_byte_size` - -An amount of bytes at which the batch should be flushed. If 0 disables size based batching. Splunk Cloud recommends limiting content length of HEC payload to 1 MB. - - -Type: `int` -Default: `1000000` - -### `rate_limit` - -An optional rate limit resource to restrict API requests with. - - -Type: `string` -Default: `""` - -### `max_in_flight` - -The maximum number of parallel message batches to have in flight at any given time. - - -Type: `int` -Default: `64` - -### `skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/outputs/sql.md b/website/docs/components/outputs/sql.md deleted file mode 100644 index 319e8a2eef..0000000000 --- a/website/docs/components/outputs/sql.md +++ /dev/null @@ -1,239 +0,0 @@ ---- -title: sql -slug: sql -type: output -status: deprecated -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::warning DEPRECATED -This component is deprecated and will be removed in the next major version release. Please consider moving onto [alternative components](#alternatives). -::: -Executes an arbitrary SQL query for each message. - -Introduced in version 3.65.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - sql: - driver: "" # No default (required) - data_source_name: "" # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - sql: - driver: "" # No default (required) - data_source_name: "" # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -## Alternatives - -For basic inserts use the [`sql_insert`](/docs/components/outputs/sql) output. For more complex queries use the [`sql_raw`](/docs/components/outputs/sql_raw) output. - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `data_source_name` - -Data source name. - - -Type: `string` - -### `query` - -The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: - -| Driver | Placeholder Style | -|---|---| -| `clickhouse` | Dollar sign | -| `mysql` | Question mark | -| `postgres` | Dollar sign | -| `mssql` | Question mark | -| `sqlite` | Question mark | -| `oracle` | Colon | -| `snowflake` | Question mark | -| `trino` | Question mark | -| `gocosmos` | Colon | - - -Type: `string` - -```yml -# Examples - -query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); -``` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `max_in_flight` - -The maximum number of inserts to run in parallel. - - -Type: `int` -Default: `64` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/sql_insert.md b/website/docs/components/outputs/sql_insert.md deleted file mode 100644 index c6c49b6a62..0000000000 --- a/website/docs/components/outputs/sql_insert.md +++ /dev/null @@ -1,411 +0,0 @@ ---- -title: sql_insert -slug: sql_insert -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Inserts a row into an SQL database for each message. - -Introduced in version 3.59.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - sql_insert: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - sql_insert: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) - prefix: "" # No default (optional) - suffix: ON CONFLICT (name) DO NOTHING # No default (optional) - max_in_flight: 64 - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -## Examples - - - - - - -Here we insert rows into a database by populating the columns id, name and topic with values extracted from messages and metadata: - -```yaml -output: - sql_insert: - driver: mysql - dsn: foouser:foopassword@tcp(localhost:3306)/foodb - table: footable - columns: [ id, name, topic ] - args_mapping: | - root = [ - this.user.id, - this.user.name, - meta("kafka_topic"), - ] -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `table` - -The table to insert to. - - -Type: `string` - -```yml -# Examples - -table: foo -``` - -### `columns` - -A list of columns to insert. - - -Type: `array` - -```yml -# Examples - -columns: - - foo - - bar - - baz -``` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of columns specified. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `prefix` - -An optional prefix to prepend to the insert query (before INSERT). - - -Type: `string` - -### `suffix` - -An optional suffix to append to the insert query. - - -Type: `string` - -```yml -# Examples - -suffix: ON CONFLICT (name) DO NOTHING -``` - -### `max_in_flight` - -The maximum number of inserts to run in parallel. - - -Type: `int` -Default: `64` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/sql_raw.md b/website/docs/components/outputs/sql_raw.md deleted file mode 100644 index fcaab6fc00..0000000000 --- a/website/docs/components/outputs/sql_raw.md +++ /dev/null @@ -1,391 +0,0 @@ ---- -title: sql_raw -slug: sql_raw -type: output -status: stable -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes an arbitrary SQL query for each message. - -Introduced in version 3.65.0. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - sql_raw: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - max_in_flight: 64 - batching: - count: 0 - byte_size: 0 - period: "" - check: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - sql_raw: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - unsafe_dynamic_query: false - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - max_in_flight: 64 - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) - batching: - count: 0 - byte_size: 0 - period: "" - check: "" - processors: [] # No default (optional) -``` - - - - -## Examples - - - - - - -Here we insert rows into a database by populating the columns id, name and topic with values extracted from messages and metadata: - -```yaml -output: - sql_raw: - driver: mysql - dsn: foouser:foopassword@tcp(localhost:3306)/foodb - query: "INSERT INTO footable (id, name, topic) VALUES (?, ?, ?);" - args_mapping: | - root = [ - this.user.id, - this.user.name, - meta("kafka_topic"), - ] -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `query` - -The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: - -| Driver | Placeholder Style | -|---|---| -| `clickhouse` | Dollar sign | -| `mysql` | Question mark | -| `postgres` | Dollar sign | -| `mssql` | Question mark | -| `sqlite` | Question mark | -| `oracle` | Colon | -| `snowflake` | Question mark | -| `trino` | Question mark | -| `gocosmos` | Colon | - - -Type: `string` - -```yml -# Examples - -query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); -``` - -### `unsafe_dynamic_query` - -Whether to enable [interpolation functions](/docs/configuration/interpolation/#bloblang-queries) in the query. Great care should be made to ensure your queries are defended against injection attacks. - - -Type: `bool` -Default: `false` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `max_in_flight` - -The maximum number of inserts to run in parallel. - - -Type: `int` -Default: `64` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - -### `batching` - -Allows you to configure a [batching policy](/docs/configuration/batching). - - -Type: `object` - -```yml -# Examples - -batching: - byte_size: 5000 - count: 0 - period: 1s - -batching: - count: 10 - period: 1s - -batching: - check: this.contains("END BATCH") - count: 0 - period: 1m -``` - -### `batching.count` - -A number of messages at which the batch should be flushed. If `0` disables count based batching. - - -Type: `int` -Default: `0` - -### `batching.byte_size` - -An amount of bytes at which the batch should be flushed. If `0` disables size based batching. - - -Type: `int` -Default: `0` - -### `batching.period` - -A period in which an incomplete batch should be flushed regardless of its size. - - -Type: `string` -Default: `""` - -```yml -# Examples - -period: 1s - -period: 1m - -period: 500ms -``` - -### `batching.check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should end a batch. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "end_of_transaction" -``` - -### `batching.processors` - -A list of [processors](/docs/components/processors/about) to apply to a batch as it is flushed. This allows you to aggregate and archive the batch however you see fit. Please note that all resulting messages are flushed as a single batch, therefore splitting the batch into smaller batches using these processors is a no-op. - - -Type: `array` - -```yml -# Examples - -processors: - - archive: - format: concatenate - -processors: - - archive: - format: lines - -processors: - - archive: - format: json_array -``` - - diff --git a/website/docs/components/outputs/stdout.md b/website/docs/components/outputs/stdout.md deleted file mode 100644 index deedd8c01c..0000000000 --- a/website/docs/components/outputs/stdout.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: stdout -slug: stdout -type: output -status: stable -categories: ["Local"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Prints messages to stdout as a continuous stream of data. - -```yml -# Config fields, showing default values -output: - label: "" - stdout: - codec: lines -``` - -## Fields - -### `codec` - -The way in which the bytes of messages should be written out into the output data stream. It's possible to write lines using a custom delimiter with the `delim:x` codec, where x is the character sequence custom delimiter. - - -Type: `string` -Default: `"lines"` -Requires version 3.46.0 or newer - -| Option | Summary | -|---|---| -| `all-bytes` | Only applicable to file based outputs. Writes each message to a file in full, if the file already exists the old content is deleted. | -| `append` | Append each message to the output stream without any delimiter or special encoding. | -| `lines` | Append each message to the output stream followed by a line break. | -| `delim:x` | Append each message to the output stream followed by a custom delimiter. | - - -```yml -# Examples - -codec: lines - -codec: "delim:\t" - -codec: delim:foobar -``` - - diff --git a/website/docs/components/outputs/subprocess.md b/website/docs/components/outputs/subprocess.md deleted file mode 100644 index c2dd52c79e..0000000000 --- a/website/docs/components/outputs/subprocess.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: subprocess -slug: subprocess -type: output -status: beta -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Executes a command, runs it as a subprocess, and writes messages to it over stdin. - -```yml -# Config fields, showing default values -output: - label: "" - subprocess: - name: "" # No default (required) - args: [] - codec: lines -``` - -Messages are written according to a specified codec. The process is expected to terminate gracefully when stdin is closed. - -If the subprocess exits unexpectedly then Benthos will log anything printed to stderr and will log the exit code, and will attempt to execute the command again until success. - -The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. - -## Fields - -### `name` - -The command to execute as a subprocess. - - -Type: `string` - -### `args` - -A list of arguments to provide the command. - - -Type: `array` -Default: `[]` - -### `codec` - -The way in which messages should be written to the subprocess. - - -Type: `string` -Default: `"lines"` -Options: `lines`. - - diff --git a/website/docs/components/outputs/switch.md b/website/docs/components/outputs/switch.md deleted file mode 100644 index 86ad4cbee9..0000000000 --- a/website/docs/components/outputs/switch.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -title: switch -slug: switch -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -The switch output type allows you to route messages to different outputs based on their contents. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - switch: - retry_until_success: false - cases: [] # No default (required) -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - switch: - retry_until_success: false - strict_mode: false - cases: [] # No default (required) -``` - - - - -Messages that do not pass the check of a single output case are effectively dropped. In order to prevent this outcome set the field [`strict_mode`](#strict_mode) to `true`, in which case messages that do not pass at least one case are considered failed and will be nacked and/or reprocessed depending on your input. - -## Examples - - - - - - -The most common use for a switch output is to multiplex messages across a range of output destinations. The following config checks the contents of the field `type` of messages and sends `foo` type messages to an `amqp_1` output, `bar` type messages to a `gcp_pubsub` output, and everything else to a `redis_streams` output. - -Outputs can have their own processors associated with them, and in this example the `redis_streams` output has a processor that enforces the presence of a type field before sending it. - -```yaml -output: - switch: - cases: - - check: this.type == "foo" - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/the_foos - - - check: this.type == "bar" - output: - gcp_pubsub: - project: dealing_with_mike - topic: mikes_bars - - - output: - redis_streams: - url: tcp://localhost:6379 - stream: everything_else - processors: - - mapping: | - root = this - root.type = this.type | "unknown" -``` - - - - - -The `continue` field allows messages that have passed a case to be tested against the next one also. This can be useful when combining non-mutually-exclusive case checks. - -In the following example a message that passes both the check of the first case as well as the second will be routed to both. - -```yaml -output: - switch: - cases: - - check: 'this.user.interests.contains("walks").catch(false)' - output: - amqp_1: - urls: [ amqps://guest:guest@localhost:5672/ ] - target_address: queue:/people_what_think_good - continue: true - - - check: 'this.user.dislikes.contains("videogames").catch(false)' - output: - gcp_pubsub: - project: people - topic: that_i_dont_want_to_hang_with -``` - - - - -## Fields - -### `retry_until_success` - -If a selected output fails to send a message this field determines whether it is reattempted indefinitely. If set to false the error is instead propagated back to the input level. - -If a message can be routed to >1 outputs it is usually best to set this to true in order to avoid duplicate messages being routed to an output. - - -Type: `bool` -Default: `false` - -### `strict_mode` - -This field determines whether an error should be reported if no condition is met. If set to true, an error is propagated back to the input level. The default behavior is false, which will drop the message. - - -Type: `bool` -Default: `false` - -### `cases` - -A list of switch cases, outlining outputs that can be routed to. - - -Type: `array` - -```yml -# Examples - -cases: - - check: this.urls.contains("http://benthos.dev") - continue: true - output: - cache: - key: ${!json("id")} - target: foo - - output: - s3: - bucket: bar - path: ${!json("id")} -``` - -### `cases[].check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether a message should be routed to the case output. If left empty the case always passes. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "foo" - -check: this.contents.urls.contains("https://benthos.dev/") -``` - -### `cases[].output` - -An [output](/docs/components/outputs/about/) for messages that pass the check to be routed to. - - -Type: `output` - -### `cases[].continue` - -Indicates whether, if this case passes for a message, the next case should also be tested. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/outputs/sync_response.md b/website/docs/components/outputs/sync_response.md deleted file mode 100644 index b9b44ad2f4..0000000000 --- a/website/docs/components/outputs/sync_response.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: sync_response -slug: sync_response -type: output -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Returns the final message payload back to the input origin of the message, where it is dealt with according to that specific input type. - -```yml -# Config fields, showing default values -output: - label: "" - sync_response: {} -``` - -For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this output even when combining input types that might not have support for sync responses. An example of an input able to utilise this is the `http_server`. - -It is safe to combine this output with others using broker types. For example, with the `http_server` input we could send the payload to a Kafka topic and also send a modified payload back with: - -```yaml -input: - http_server: - path: /post -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ TODO:9092 ] - topic: foo_topic - - sync_response: {} - processors: - - mapping: 'root = content().uppercase()' -``` - -Using the above example and posting the message 'hello world' to the endpoint `/post` Benthos would send it unchanged to the topic `foo_topic` and also respond with 'HELLO WORLD'. - -For more information please read [Synchronous Responses](/docs/guides/sync_responses). - - diff --git a/website/docs/components/outputs/websocket.md b/website/docs/components/outputs/websocket.md deleted file mode 100644 index e1f942c978..0000000000 --- a/website/docs/components/outputs/websocket.md +++ /dev/null @@ -1,356 +0,0 @@ ---- -title: websocket -slug: websocket -type: output -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sends messages to an HTTP server via a websocket connection. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - websocket: - url: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - websocket: - url: "" # No default (required) - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} -``` - - - - -## Fields - -### `url` - -The URL to connect to. - - -Type: `string` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - - diff --git a/website/docs/components/outputs/zmq4.md b/website/docs/components/outputs/zmq4.md deleted file mode 100644 index fe2669b72c..0000000000 --- a/website/docs/components/outputs/zmq4.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: zmq4 -type: output -status: stable -categories: ["Network"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Writes messages to a ZeroMQ socket. - - - - - - -```yml -# Common config fields, showing default values -output: - label: "" - zmq4: - urls: [] - bind: true - socket_type: "" -``` - - - - -```yml -# All config fields, showing default values -output: - label: "" - zmq4: - urls: [] - bind: true - socket_type: "" - high_water_mark: 0 - poll_timeout: 5s -``` - - - - -By default Benthos does not build with components that require linking to external libraries. If you wish to build Benthos locally with this component then set the build tag `x_benthos_extra`: - -```shell -# With go -go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest - -# Using make -make TAGS=x_benthos_extra -``` - -There is a specific docker tag postfix `-cgo` for C builds containing this component. - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - tcp://localhost:5556 -``` - -### `bind` - -Whether to bind to the specified URLs (otherwise they are connected to). - - -Type: `bool` -Default: `true` - -### `socket_type` - -The socket type to connect as. - - -Type: `string` -Options: `PUSH`, `PUB`. - -### `high_water_mark` - -The message high water mark to use. - - -Type: `int` -Default: `0` - -### `poll_timeout` - -The poll timeout to use. - - -Type: `string` -Default: `"5s"` - - diff --git a/website/docs/components/processors/about.md b/website/docs/components/processors/about.md deleted file mode 100644 index 4cc3dab6bd..0000000000 --- a/website/docs/components/processors/about.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Processors -sidebar_label: About ---- - -Benthos processors are functions applied to messages passing through a pipeline. The function signature allows a processor to mutate or drop messages depending on the content of the message. There are many types on offer but the most powerful are the [`mapping`][processor.mapping] and [`mutation`][processor.mutation] processors. - -Processors are set via config, and depending on where in the config they are placed they will be run either immediately after a specific input (set in the input section), on all messages (set in the pipeline section) or before a specific output (set in the output section). Most processors apply to all messages and can be placed in the pipeline section: - -```yaml -pipeline: - threads: 1 - processors: - - label: my_cool_mapping - mapping: | - root.message = this - root.meta.link_count = this.links.length() -``` - -The `threads` field in the pipeline section determines how many parallel processing threads are created. You can read more about parallel processing in the [pipeline guide][pipelines]. - -## Labels - -Processors have an optional field `label` that can uniquely identify them in observability data such as metrics and logs. This can be useful when running configs with multiple nested processors, otherwise their metrics labels will be generated based on their composition. For more information check out the [metrics documentation][metrics.about]. - -## Error Handling - -Some processors have conditions whereby they might fail. Rather than throw these messages into the abyss Benthos still attempts to send these messages onwards, and has mechanisms for filtering, recovering or dead-letter queuing messages that have failed which can be read about [here][error_handling]. - -### Error Logs - -Errors that occur during processing can be roughly separated into two groups; those that are unexpected intermittent errors such as connectivity problems, and those that are logical errors such as bad input data or unmatched schemas. - -All processing errors result in the messages being flagged as failed, [error metrics][metrics.about] increasing for the given errored processor, and debug level logs being emitted that describe the error. Only errors that are known to be intermittent are also logged at the error level. - -The reason for this behaviour is to prevent noisy logging in cases where logical errors are expected and will likely be [handled in config][error_handling]. However, this can also sometimes make it easy to miss logical errors in your configs when they lack error handling. If you suspect you are experiencing processing errors and do not wish to add error handling yet then a quick and easy way to expose those errors is to enable debug level logs with the cli flag `--log.level=debug` or by setting the level in config: - -```yaml -logger: - level: DEBUG -``` - -## Using Processors as Outputs - -It might be the case that a processor that results in a side effect, such as the [`sql_insert`][processor.sql_insert] or [`redis`][processor.redis] processors, is the only side effect of a pipeline, and therefore could be considered the output. - -In such cases it's possible to place these processors within a [`reject` output][output.reject] so that they behave the same as regular outputs, where success results in dropping the message with an acknowledgement and failure results in a nack (or retry): - -```yaml -output: - reject: 'failed to send data: ${! error() }' - processors: - - try: - - redis: - url: tcp://localhost:6379 - command: sadd - args_mapping: 'root = [ this.key, this.value ]' - - mapping: root = deleted() -``` - -The way this works is that if your processor with the side effect (`redis` in this case) succeeds then the final `mapping` processor deletes the message which results in an acknowledgement. If the processor fails then the `try` block exits early without executing the `mapping` processor and instead the message is routed to the `reject` output, which nacks the message with an error message containing the error obtained from the `redis` processor. - -import ComponentsByCategory from '@theme/ComponentsByCategory'; - -## Categories - - - -## Batching and Multiple Part Messages - -All Benthos processors support multiple part messages, which are synonymous with batches. This enables some cool [windowed processing][windowed_processing] capabilities. - -Many processors are able to perform their behaviours on specific parts of a message batch, or on all parts, and have a field `parts` for specifying an array of part indexes they should apply to. If the list of target parts is empty these processors will be applied to all message parts. - -Part indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. E.g. if part = -1 then the selected part will be the last part of the message, if part = -2 then the part before the last element will be selected, and so on. - -Some processors such as [`dedupe`][processor.dedupe] act across an entire batch, when instead we might like to perform them on individual messages of a batch. In this case the [`for_each`][processor.for_each] processor can be used. - -You can read more about batching [in this document][batching]. - -[error_handling]: /docs/configuration/error_handling -[batching]: /docs/configuration/batching -[windowed_processing]: /docs/configuration/windowed_processing -[pipelines]: /docs/configuration/processing_pipelines -[output.reject]: /docs/components/outputs/reject -[processor.sql_insert]: /docs/components/processors/sql_insert -[processor.redis]: /docs/components/processors/redis -[processor.mapping]: /docs/components/processors/mapping -[processor.mutation]: /docs/components/processors/mutation -[processor.split]: /docs/components/processors/split -[processor.dedupe]: /docs/components/processors/dedupe -[processor.for_each]: /docs/components/processors/for_each -[metrics.about]: /docs/components/metrics/about diff --git a/website/docs/components/processors/archive.md b/website/docs/components/processors/archive.md deleted file mode 100644 index d4397b3a7e..0000000000 --- a/website/docs/components/processors/archive.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: archive -slug: archive -type: processor -status: stable -categories: ["Parsing","Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Archives all the messages of a batch into a single message according to the selected archive format. - -```yml -# Config fields, showing default values -label: "" -archive: - format: "" # No default (required) - path: "" -``` - -Some archive formats (such as tar, zip) treat each archive item (message part) as a file with a path. Since message parts only contain raw data a unique path must be generated for each part. This can be done by using function interpolations on the 'path' field as described [here](/docs/configuration/interpolation#bloblang-queries). For types that aren't file based (such as binary) the file field is ignored. - -The resulting archived message adopts the metadata of the _first_ message part of the batch. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching). - -## Fields - -### `format` - -The archiving format to apply. - - -Type: `string` - -| Option | Summary | -|---|---| -| `binary` | Archive messages to a [binary blob format](https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96). | -| `concatenate` | Join the raw contents of each message into a single binary message. | -| `json_array` | Attempt to parse each message as a JSON document and append the result to an array, which becomes the contents of the resulting message. | -| `lines` | Join the raw contents of each message and insert a line break between each one. | -| `tar` | Archive messages to a unix standard tape archive. | -| `zip` | Archive messages to a zip file. | - - -### `path` - -The path to set for each message in the archive (when applicable). -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -```yml -# Examples - -path: ${!count("files")}-${!timestamp_unix_nano()}.txt - -path: ${!meta("kafka_key")}-${!json("id")}.json -``` - -## Examples - - - - - - -If we had JSON messages in a batch each of the form: - -```json -{"doc":{"id":"foo","body":"hello world 1"}} -``` - -And we wished to tar archive them, setting their filenames to their respective unique IDs (with the extension `.json`), our config might look like -this: - -```yaml -pipeline: - processors: - - archive: - format: tar - path: ${!json("doc.id")}.json -``` - - - - - diff --git a/website/docs/components/processors/avro.md b/website/docs/components/processors/avro.md deleted file mode 100644 index 00af359dbb..0000000000 --- a/website/docs/components/processors/avro.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: avro -slug: avro -type: processor -status: beta -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Performs Avro based operations on messages based on a schema. - -```yml -# Config fields, showing default values -label: "" -avro: - operator: "" # No default (required) - encoding: textual - schema: "" - schema_path: "" -``` - -WARNING: If you are consuming or generating messages using a schema registry service then it is likely this processor will fail as those services require messages to be prefixed with the identifier of the schema version being used. Instead, try the [`schema_registry_encode`](/docs/components/processors/schema_registry_encode) and [`schema_registry_decode`](/docs/components/processors/schema_registry_decode) processors. - -## Operators - -### `to_json` - -Converts Avro documents into a JSON structure. This makes it easier to -manipulate the contents of the document within Benthos. The encoding field -specifies how the source documents are encoded. - -### `from_json` - -Attempts to convert JSON documents into Avro documents according to the -specified encoding. - -## Fields - -### `operator` - -The [operator](#operators) to execute - - -Type: `string` -Options: `to_json`, `from_json`. - -### `encoding` - -An Avro encoding format to use for conversions to and from a schema. - - -Type: `string` -Default: `"textual"` -Options: `textual`, `binary`, `single`. - -### `schema` - -A full Avro schema to use. - - -Type: `string` -Default: `""` - -### `schema_path` - -The path of a schema document to apply. Use either this or the `schema` field. - - -Type: `string` -Default: `""` - -```yml -# Examples - -schema_path: file://path/to/spec.avsc - -schema_path: http://localhost:8081/path/to/spec/versions/1 -``` - - diff --git a/website/docs/components/processors/awk.md b/website/docs/components/processors/awk.md deleted file mode 100644 index d85a0f07a2..0000000000 --- a/website/docs/components/processors/awk.md +++ /dev/null @@ -1,401 +0,0 @@ ---- -title: awk -slug: awk -type: processor -status: stable -categories: ["Mapping"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes an AWK program on messages. This processor is very powerful as it offers a range of [custom functions](#awk-functions) for querying and mutating message contents and metadata. - -```yml -# Config fields, showing default values -label: "" -awk: - codec: "" # No default (required) - program: "" # No default (required) -``` - -Works by feeding message contents as the program input based on a chosen [codec](#codecs) and replaces the contents of each message with the result. If the result is empty (nothing is printed by the program) then the original message contents remain unchanged. - -Comes with a wide range of [custom functions](#awk-functions) for accessing message metadata, json fields, printing logs, etc. These functions can be overridden by functions within the program. - -Check out the [examples section](#examples) in order to see how this processor can be used. - -This processor uses [GoAWK][goawk], in order to understand the differences in how the program works you can [read more about it here][goawk.differences]. - -## Fields - -### `codec` - -A [codec](#codecs) defines how messages should be inserted into the AWK program as variables. The codec does not change which [custom Benthos functions](#awk-functions) are available. The `text` codec is the closest to a typical AWK use case. - - -Type: `string` -Options: `none`, `text`, `json`. - -### `program` - -An AWK program to execute - - -Type: `string` - -## Examples - - - - - - -Because AWK is a full programming language it's much easier to map documents and perform arithmetic with it than with other Benthos processors. For example, if we were expecting documents of the form: - -```json -{"doc":{"val1":5,"val2":10},"id":"1","type":"add"} -{"doc":{"val1":5,"val2":10},"id":"2","type":"multiply"} -``` - -And we wished to perform the arithmetic specified in the `type` field, -on the values `val1` and `val2` and, finally, map the result into the -document, giving us the following resulting documents: - -```json -{"doc":{"result":15,"val1":5,"val2":10},"id":"1","type":"add"} -{"doc":{"result":50,"val1":5,"val2":10},"id":"2","type":"multiply"} -``` - -We can do that with the following: - -```yaml -pipeline: - processors: - - awk: - codec: none - program: | - function map_add_vals() { - json_set_int("doc.result", json_get("doc.val1") + json_get("doc.val2")); - } - function map_multiply_vals() { - json_set_int("doc.result", json_get("doc.val1") * json_get("doc.val2")); - } - function map_unknown(type) { - json_set("error","unknown document type"); - print_log("Document type not recognised: " type, "ERROR"); - } - { - type = json_get("type"); - if (type == "add") - map_add_vals(); - else if (type == "multiply") - map_multiply_vals(); - else - map_unknown(type); - } -``` - - - - - -It's possible to iterate JSON arrays by appending an index value to the path, this can be used to do things like removing duplicates from arrays. For example, given the following input document: - -```json -{"path":{"to":{"foos":["one","two","three","two","four"]}}} -``` - -We could create a new array `foos_unique` from `foos` giving us the result: - -```json -{"path":{"to":{"foos":["one","two","three","two","four"],"foos_unique":["one","two","three","four"]}}} -``` - -With the following config: - -```yaml -pipeline: - processors: - - awk: - codec: none - program: | - { - array_path = "path.to.foos" - array_len = json_length(array_path) - - for (i = 0; i < array_len; i++) { - ele = json_get(array_path "." i) - if ( ! ( ele in seen ) ) { - json_append(array_path "_unique", ele) - seen[ele] = 1 - } - } - } -``` - - - - -## Codecs - -The chosen codec determines how the contents of the message are fed into the -program. Codecs only impact the input string and variables initialised for your -program, they do not change the range of custom functions available. - -### `none` - -An empty string is fed into the program. Functions can still be used in order to -extract and mutate metadata and message contents. - -This is useful for when your program only uses functions and doesn't need the -full text of the message to be parsed by the program, as it is significantly -faster. - -### `text` - -The full contents of the message are fed into the program as a string, allowing -you to reference tokenised segments of the message with variables ($0, $1, etc). -Custom functions can still be used with this codec. - -This is the default codec as it behaves most similar to typical usage of the awk -command line tool. - -### `json` - -An empty string is fed into the program, and variables are automatically -initialised before execution of your program by walking the flattened JSON -structure. Each value is converted into a variable by taking its full path, -e.g. the object: - -``` json -{ - "foo": { - "bar": { - "value": 10 - }, - "created_at": "2018-12-18T11:57:32" - } -} -``` - -Would result in the following variable declarations: - -``` -foo_bar_value = 10 -foo_created_at = "2018-12-18T11:57:32" -``` - -Custom functions can also still be used with this codec. - -## AWK Functions - -### `json_get` - -Signature: `json_get(path)` - -Attempts to find a JSON value in the input message payload by a -[dot separated path](/docs/configuration/field_paths) and returns it as a string. - -### `json_set` - -Signature: `json_set(path, value)` - -Attempts to set a JSON value in the input message payload identified by a -[dot separated path](/docs/configuration/field_paths), the value argument will be interpreted -as a string. - -In order to set non-string values use one of the following typed varieties: - -- `json_set_int(path, value)` -- `json_set_float(path, value)` -- `json_set_bool(path, value)` - -### `json_append` - -Signature: `json_append(path, value)` - -Attempts to append a value to an array identified by a -[dot separated path](/docs/configuration/field_paths). If the target does not -exist it will be created. If the target exists but is not already an array then -it will be converted into one, with its original contents set to the first -element of the array. - -The value argument will be interpreted as a string. In order to append -non-string values use one of the following typed varieties: - -- `json_append_int(path, value)` -- `json_append_float(path, value)` -- `json_append_bool(path, value)` - -### `json_delete` - -Signature: `json_delete(path)` - -Attempts to delete a JSON field from the input message payload identified by a -[dot separated path](/docs/configuration/field_paths). - -### `json_length` - -Signature: `json_length(path)` - -Returns the size of the string or array value of JSON field from the input -message payload identified by a [dot separated path](/docs/configuration/field_paths). - -If the target field does not exist, or is not a string or array type, then zero -is returned. In order to explicitly check the type of a field use `json_type`. - -### `json_type` - -Signature: `json_type(path)` - -Returns the type of a JSON field from the input message payload identified by a -[dot separated path](/docs/configuration/field_paths). - -Possible values are: "string", "int", "float", "bool", "undefined", "null", -"array", "object". - -### `create_json_object` - -Signature: `create_json_object(key1, val1, key2, val2, ...)` - -Generates a valid JSON object of key value pair arguments. The arguments are -variadic, meaning any number of pairs can be listed. The value will always -resolve to a string regardless of the value type. E.g. the following call: - -`create_json_object("a", "1", "b", 2, "c", "3")` - -Would result in this string: - -`{"a":"1","b":"2","c":"3"}` - -### `create_json_array` - -Signature: `create_json_array(val1, val2, ...)` - -Generates a valid JSON array of value arguments. The arguments are variadic, -meaning any number of values can be listed. The value will always resolve to a -string regardless of the value type. E.g. the following call: - -`create_json_array("1", 2, "3")` - -Would result in this string: - -`["1","2","3"]` - -### `metadata_set` - -Signature: `metadata_set(key, value)` - -Set a metadata key for the message to a value. The value will always resolve to -a string regardless of the value type. - -### `metadata_get` - -Signature: `metadata_get(key) string` - -Get the value of a metadata key from the message. - -### `timestamp_unix` - -Signature: `timestamp_unix() int` - -Returns the current unix timestamp (the number of seconds since 01-01-1970). - -### `timestamp_unix` - -Signature: `timestamp_unix(date) int` - -Attempts to parse a date string by detecting its format and returns the -equivalent unix timestamp (the number of seconds since 01-01-1970). - -### `timestamp_unix` - -Signature: `timestamp_unix(date, format) int` - -Attempts to parse a date string according to a format and returns the equivalent -unix timestamp (the number of seconds since 01-01-1970). - -The format is defined by showing how the reference time, defined to be -`Mon Jan 2 15:04:05 -0700 MST 2006` would be displayed if it were the value. - -### `timestamp_unix_nano` - -Signature: `timestamp_unix_nano() int` - -Returns the current unix timestamp in nanoseconds (the number of nanoseconds -since 01-01-1970). - -### `timestamp_unix_nano` - -Signature: `timestamp_unix_nano(date) int` - -Attempts to parse a date string by detecting its format and returns the -equivalent unix timestamp in nanoseconds (the number of nanoseconds since -01-01-1970). - -### `timestamp_unix_nano` - -Signature: `timestamp_unix_nano(date, format) int` - -Attempts to parse a date string according to a format and returns the equivalent -unix timestamp in nanoseconds (the number of nanoseconds since 01-01-1970). - -The format is defined by showing how the reference time, defined to be -`Mon Jan 2 15:04:05 -0700 MST 2006` would be displayed if it were the value. - -### `timestamp_format` - -Signature: `timestamp_format(unix, format) string` - -Formats a unix timestamp. The format is defined by showing how the reference -time, defined to be `Mon Jan 2 15:04:05 -0700 MST 2006` would be displayed if it -were the value. - -The format is optional, and if omitted RFC3339 (`2006-01-02T15:04:05Z07:00`) -will be used. - -### `timestamp_format_nano` - -Signature: `timestamp_format_nano(unixNano, format) string` - -Formats a unix timestamp in nanoseconds. The format is defined by showing how -the reference time, defined to be `Mon Jan 2 15:04:05 -0700 MST 2006` would be -displayed if it were the value. - -The format is optional, and if omitted RFC3339 (`2006-01-02T15:04:05Z07:00`) -will be used. - -### `print_log` - -Signature: `print_log(message, level)` - -Prints a Benthos log message at a particular log level. The log level is -optional, and if omitted the level `INFO` will be used. - -### `base64_encode` - -Signature: `base64_encode(data)` - -Encodes the input data to a base64 string. - -### `base64_decode` - -Signature: `base64_decode(data)` - -Attempts to base64-decode the input data and returns the decoded string if -successful. It will emit an error otherwise. - -[goawk]: https://github.com/benhoyt/goawk -[goawk.differences]: https://github.com/benhoyt/goawk#differences-from-awk - - diff --git a/website/docs/components/processors/aws_dynamodb_partiql.md b/website/docs/components/processors/aws_dynamodb_partiql.md deleted file mode 100644 index 3ad98fd0bc..0000000000 --- a/website/docs/components/processors/aws_dynamodb_partiql.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: aws_dynamodb_partiql -slug: aws_dynamodb_partiql -type: processor -status: experimental -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a PartiQL expression against a DynamoDB table for each message. - -Introduced in version 3.48.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -aws_dynamodb_partiql: - query: "" # No default (required) - args_mapping: "" -``` - - - - -```yml -# All config fields, showing default values -label: "" -aws_dynamodb_partiql: - query: "" # No default (required) - unsafe_dynamic_query: false - args_mapping: "" - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" -``` - - - - -Both writes or reads are supported, when the query is a read the contents of the message will be replaced with the result. This processor is more efficient when messages are pre-batched as the whole batch will be executed in a single call. - -## Examples - - - - - -The following example inserts rows into the table footable with the columns foo, bar and baz populated with values extracted from messages: - -```yaml -pipeline: - processors: - - aws_dynamodb_partiql: - query: "INSERT INTO footable VALUE {'foo':'?','bar':'?','baz':'?'}" - args_mapping: | - root = [ - { "S": this.foo }, - { "S": meta("kafka_topic") }, - { "S": this.document.content }, - ] -``` - - - - -## Fields - -### `query` - -A PartiQL query to execute for each message. - - -Type: `string` - -### `unsafe_dynamic_query` - -Whether to enable dynamic queries that support interpolation functions. - - -Type: `bool` -Default: `false` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) that, for each message, creates a list of arguments to use with the query. - - -Type: `string` -Default: `""` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/processors/aws_lambda.md b/website/docs/components/processors/aws_lambda.md deleted file mode 100644 index 27e8cda7a7..0000000000 --- a/website/docs/components/processors/aws_lambda.md +++ /dev/null @@ -1,251 +0,0 @@ ---- -title: aws_lambda -slug: aws_lambda -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Invokes an AWS lambda for each message. The contents of the message is the payload of the request, and the result of the invocation will become the new contents of the message. - -Introduced in version 3.36.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -aws_lambda: - parallel: false - function: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -aws_lambda: - parallel: false - function: "" # No default (required) - rate_limit: "" - region: "" - endpoint: "" - credentials: - profile: "" - id: "" - secret: "" - token: "" - from_ec2_role: false - role: "" - role_external_id: "" - timeout: 5s - retries: 3 -``` - - - - -The `rate_limit` field can be used to specify a rate limit [resource](/docs/components/rate_limits/about) to cap the rate of requests across parallel components service wide. - -In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the [`branch` processor](/docs/components/processors/branch). - -### Error Handling - -When Benthos is unable to connect to the AWS endpoint or is otherwise unable to invoke the target lambda function it will retry the request according to the configured number of retries. Once these attempts have been exhausted the failed message will continue through the pipeline with it's contents unchanged, but flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). - -However, if the invocation of the function is successful but the function itself throws an error, then the message will have it's contents updated with a JSON payload describing the reason for the failure, and a metadata field `lambda_function_error` will be added to the message allowing you to detect and handle function errors with a [`branch`](/docs/components/processors/branch): - -```yaml -pipeline: - processors: - - branch: - processors: - - aws_lambda: - function: foo - result_map: | - root = if meta().exists("lambda_function_error") { - throw("Invocation failed due to %v: %v".format(this.errorType, this.errorMessage)) - } else { - this - } -output: - switch: - retry_until_success: false - cases: - - check: errored() - output: - reject: ${! error() } - - output: - resource: somewhere_else -``` - -### Credentials - -By default Benthos will use a shared credentials file when connecting to AWS services. It's also possible to set them explicitly at the component level, allowing you to transfer data across accounts. You can find out more [in this document](/docs/guides/cloud/aws). - -## Examples - - - - - - -This example uses a [`branch` processor](/docs/components/processors/branch/) to map a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged. - -```yaml -pipeline: - processors: - - branch: - request_map: '{"id":this.doc.id,"username":this.user.name}' - processors: - - aws_lambda: - function: trigger_user_update -``` - - - - -## Fields - -### `parallel` - -Whether messages of a batch should be dispatched in parallel. - - -Type: `bool` -Default: `false` - -### `function` - -The function to invoke. - - -Type: `string` - -### `rate_limit` - -An optional [`rate_limit`](/docs/components/rate_limits/about) to throttle invocations by. - - -Type: `string` -Default: `""` - -### `region` - -The AWS region to target. - - -Type: `string` -Default: `""` - -### `endpoint` - -Allows you to specify a custom endpoint for the AWS API. - - -Type: `string` -Default: `""` - -### `credentials` - -Optional manual configuration of AWS credentials to use. More information can be found [in this document](/docs/guides/cloud/aws). - - -Type: `object` - -### `credentials.profile` - -A profile from `~/.aws/credentials` to use. - - -Type: `string` -Default: `""` - -### `credentials.id` - -The ID of credentials to use. - - -Type: `string` -Default: `""` - -### `credentials.secret` - -The secret for the credentials being used. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `credentials.token` - -The token for the credentials being used, required when using short term credentials. - - -Type: `string` -Default: `""` - -### `credentials.from_ec2_role` - -Use the credentials of a host EC2 machine configured to assume [an IAM role associated with the instance](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). - - -Type: `bool` -Default: `false` -Requires version 4.2.0 or newer - -### `credentials.role` - -A role ARN to assume. - - -Type: `string` -Default: `""` - -### `credentials.role_external_id` - -An external ID to provide when assuming a role. - - -Type: `string` -Default: `""` - -### `timeout` - -The maximum period of time to wait before abandoning an invocation. - - -Type: `string` -Default: `"5s"` - -### `retries` - -The maximum number of retry attempts for each message. - - -Type: `int` -Default: `3` - - diff --git a/website/docs/components/processors/azure_cosmosdb.md b/website/docs/components/processors/azure_cosmosdb.md deleted file mode 100644 index 4b14e83abf..0000000000 --- a/website/docs/components/processors/azure_cosmosdb.md +++ /dev/null @@ -1,386 +0,0 @@ ---- -title: azure_cosmosdb -slug: azure_cosmosdb -type: processor -status: experimental -categories: ["Azure"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Creates or updates messages as JSON documents in [Azure CosmosDB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction). - -Introduced in version v4.25.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -azure_cosmosdb: - endpoint: https://localhost:8081 # No default (optional) - account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) - connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) - database: testdb # No default (required) - container: testcontainer # No default (required) - partition_keys_map: root = "blobfish" # No default (required) - operation: Create - item_id: ${! json("id") } # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -label: "" -azure_cosmosdb: - endpoint: https://localhost:8081 # No default (optional) - account_key: '!!!SECRET_SCRUBBED!!!' # No default (optional) - connection_string: '!!!SECRET_SCRUBBED!!!' # No default (optional) - database: testdb # No default (required) - container: testcontainer # No default (required) - partition_keys_map: root = "blobfish" # No default (required) - operation: Create - patch_operations: [] # No default (optional) - patch_condition: from c where not is_defined(c.blobfish) # No default (optional) - auto_id: true - item_id: ${! json("id") } # No default (optional) - enable_content_response_on_write: true -``` - - - - -When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. More details can be found [here](https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents). - -The `partition_keys` field must resolve to the same value(s) across the entire message batch. - - -## Credentials - -You can use one of the following authentication mechanisms: - -- Set the `endpoint` field and the `account_key` field -- Set only the `endpoint` field to use [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) -- Set the `connection_string` field - - -## Metadata - -This component adds the following metadata fields to each message: -``` -- activity_id -- request_charge -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - - -## Batching - -CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (details [here](https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits)). - - -## Examples - - - - - -Query documents from a container and patch them. - -```yaml -input: - azure_cosmosdb: - endpoint: http://localhost:8080 - account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== - database: blobbase - container: blobfish - partition_keys_map: root = "AbyssalPlain" - query: SELECT * FROM blobfish - - processors: - - mapping: | - root = "" - meta habitat = json("habitat") - meta id = this.id - - azure_cosmosdb: - endpoint: http://localhost:8080 - account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== - database: testdb - container: blobfish - partition_keys_map: root = json("habitat") - item_id: ${! meta("id") } - operation: Patch - patch_operations: - # Add a new /diet field - - operation: Add - path: /diet - value_map: root = json("diet") - # Remove the first location from the /locations array field - - operation: Remove - path: /locations/0 - # Add new location at the end of the /locations array field - - operation: Add - path: /locations/- - value_map: root = "Challenger Deep" - # Return the updated document - enable_content_response_on_write: true -``` - - - - -## Fields - -### `endpoint` - -CosmosDB endpoint. - - -Type: `string` - -```yml -# Examples - -endpoint: https://localhost:8081 -``` - -### `account_key` - -Account key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -```yml -# Examples - -account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== -``` - -### `connection_string` - -Connection string. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -```yml -# Examples - -connection_string: AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; -``` - -### `database` - -Database. - - -Type: `string` - -```yml -# Examples - -database: testdb -``` - -### `container` - -Container. - - -Type: `string` - -```yml -# Examples - -container: testcontainer -``` - -### `partition_keys_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a single partition key value or an array of partition key values of type string, integer or boolean. Currently, hierarchical partition keys are not supported so only one value may be provided. - - -Type: `string` - -```yml -# Examples - -partition_keys_map: root = "blobfish" - -partition_keys_map: root = 41 - -partition_keys_map: root = true - -partition_keys_map: root = null - -partition_keys_map: root = json("blobfish").depth -``` - -### `operation` - -Operation. - - -Type: `string` -Default: `"Create"` - -| Option | Summary | -|---|---| -| `Create` | Create operation. | -| `Delete` | Delete operation. | -| `Patch` | Patch operation. | -| `Read` | Read operation. | -| `Replace` | Replace operation. | -| `Upsert` | Upsert operation. | - - -### `patch_operations` - -Patch operations to be performed when `operation: Patch` . - - -Type: `array` - -### `patch_operations[].operation` - -Operation. - - -Type: `string` -Default: `"Add"` - -| Option | Summary | -|---|---| -| `Add` | Add patch operation. | -| `Increment` | Increment patch operation. | -| `Remove` | Remove patch operation. | -| `Replace` | Replace patch operation. | -| `Set` | Set patch operation. | - - -### `patch_operations[].path` - -Path. - - -Type: `string` - -```yml -# Examples - -path: /foo/bar/baz -``` - -### `patch_operations[].value_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to a value of any type that is supported by CosmosDB. - - -Type: `string` - -```yml -# Examples - -value_map: root = "blobfish" - -value_map: root = 41 - -value_map: root = true - -value_map: root = json("blobfish").depth - -value_map: root = [1, 2, 3] -``` - -### `patch_condition` - -Patch operation condition. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -patch_condition: from c where not is_defined(c.blobfish) -``` - -### `auto_id` - -Automatically set the item `id` field to a random UUID v4. If the `id` field is already set, then it will not be overwritten. Setting this to `false` can improve performance, since the messages will not have to be parsed. - - -Type: `bool` -Default: `true` - -### `item_id` - -ID of item to replace or delete. Only used by the Replace and Delete operations -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -item_id: ${! json("id") } -``` - -### `enable_content_response_on_write` - -Enable content response on write operations. To save some bandwidth, set this to false if you don't need to receive the updated message(s) from the server, in which case the processor will not modify the content of the messages which are fed into it. Applies to every operation except Read. - - -Type: `bool` -Default: `true` - - -## CosmosDB Emulator - -If you wish to run the CosmosDB emulator that is referenced in the documentation [here](https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator), the following Docker command should do the trick: - -```shell -> docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator -``` - -Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. - -Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run [mitmproxy](https://mitmproxy.org/) like so: - -```shell -> mitmproxy -k --mode "reverse:https://localhost:8081" -``` - -Then you can access the CosmosDB UI via `http://localhost:8080/_explorer/index.html` and use `http://localhost:8080` as the CosmosDB endpoint. - - diff --git a/website/docs/components/processors/bloblang.md b/website/docs/components/processors/bloblang.md deleted file mode 100644 index 11d9ee3163..0000000000 --- a/website/docs/components/processors/bloblang.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: bloblang -slug: bloblang -type: processor -status: stable -categories: ["Mapping","Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a [Bloblang](/docs/guides/bloblang/about) mapping on messages. - -```yml -# Config fields, showing default values -label: "" -bloblang: "" -``` - -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information [check out the docs](/docs/guides/bloblang/about). - -If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `from ""`, where the path must be absolute, or relative from the location that Benthos is executed from. - -## Component Rename - -This processor was recently renamed to the [`mapping` processor](/docs/components/processors/mapping) in order to make the purpose of the processor more prominent. It is still valid to use the existing `bloblang` name but eventually it will be deprecated and replaced by the new name in example configs. - -## Examples - - - - - - -Given JSON documents containing an array of fans: - -```json -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"grace","obsession":0.21}, - {"name":"ali","obsession":0.89}, - {"name":"vic","obsession":0.43} - ] -} -``` - -We can reduce the fans to only those with an obsession score above 0.5, giving us: - -```json -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"ali","obsession":0.89} - ] -} -``` - -With the following config: - -```yaml -pipeline: - processors: - - bloblang: | - root = this - root.fans = this.fans.filter(fan -> fan.obsession > 0.5) -``` - - - - - -When receiving JSON documents of the form: - -```json -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -``` - -We could collapse the location names from the state of Washington into a field `Cities`: - -```json -{"Cities": "Bellevue, Olympia, Seattle"} -``` - -With the following config: - -```yaml -pipeline: - processors: - - bloblang: | - root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -``` - - - - -## Error Handling - -Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use -[standard processor error handling patterns](/docs/configuration/error_handling). - -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behaviour, which you can read about [in this section](/docs/guides/bloblang/about#error-handling). - diff --git a/website/docs/components/processors/bounds_check.md b/website/docs/components/processors/bounds_check.md deleted file mode 100644 index aeb8a334ad..0000000000 --- a/website/docs/components/processors/bounds_check.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: bounds_check -slug: bounds_check -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Removes messages (and batches) that do not fit within certain size boundaries. - - - - - - -```yml -# Common config fields, showing default values -label: "" -bounds_check: - max_part_size: 1073741824 - min_part_size: 1 -``` - - - - -```yml -# All config fields, showing default values -label: "" -bounds_check: - max_part_size: 1073741824 - min_part_size: 1 - max_parts: 100 - min_parts: 1 -``` - - - - -## Fields - -### `max_part_size` - -The maximum size of a message to allow (in bytes) - - -Type: `int` -Default: `1073741824` - -### `min_part_size` - -The minimum size of a message to allow (in bytes) - - -Type: `int` -Default: `1` - -### `max_parts` - -The maximum size of message batches to allow (in message count) - - -Type: `int` -Default: `100` - -### `min_parts` - -The minimum size of message batches to allow (in message count) - - -Type: `int` -Default: `1` - - diff --git a/website/docs/components/processors/branch.md b/website/docs/components/processors/branch.md deleted file mode 100644 index a3480c272b..0000000000 --- a/website/docs/components/processors/branch.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: branch -slug: branch -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -The `branch` processor allows you to create a new request message via a [Bloblang mapping](/docs/guides/bloblang/about), execute a list of processors on the request messages, and, finally, map the result back into the source message using another mapping. - -```yml -# Config fields, showing default values -label: "" -branch: - request_map: "" - processors: [] # No default (required) - result_map: "" -``` - -This is useful for preserving the original message contents when using processors that would otherwise replace the entire contents. - -### Metadata - -Metadata fields that are added to messages during branch processing will not be automatically copied into the resulting message. In order to do this you should explicitly declare in your `result_map` either a wholesale copy with `meta = metadata()`, or selective copies with `meta foo = metadata("bar")` and so on. It is also possible to reference the metadata of the origin message in the `result_map` using the [`@` operator](/docs/guides/bloblang/about#metadata). - -### Error Handling - -If the `request_map` fails the child processors will not be executed. If the child processors themselves result in an (uncaught) error then the `result_map` will not be executed. If the `result_map` fails the message will remain unchanged. Under any of these conditions standard [error handling methods](/docs/configuration/error_handling) can be used in order to filter, DLQ or recover the failed messages. - -### Conditional Branching - -If the root of your request map is set to `deleted()` then the branch processors are skipped for the given message, this allows you to conditionally branch messages. - -## Fields - -### `request_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata). - - -Type: `string` -Default: `""` - -```yml -# Examples - -request_map: |- - root = { - "id": this.doc.id, - "content": this.doc.body.text - } - -request_map: |- - root = if this.type == "foo" { - this.foo.request - } else { - deleted() - } -``` - -### `processors` - -A list of processors to apply to mapped requests. When processing message batches the resulting batch must match the size and ordering of the input batch, therefore filtering, grouping should not be performed within these processors. - - -Type: `array` - -### `result_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata). - - -Type: `string` -Default: `""` - -```yml -# Examples - -result_map: |- - meta foo_code = metadata("code") - root.foo_result = this - -result_map: |- - meta = metadata() - root.bar.body = this.body - root.bar.id = this.user.id - -result_map: root.raw_result = content().string() - -result_map: |- - root.enrichments.foo = if metadata("request_failed") != null { - throw(metadata("request_failed")) - } else { - this - } - -result_map: |- - # Retain only the updated metadata fields which were present in the origin message - meta = metadata().filter(v -> @.get(v.key) != null) -``` - -## Examples - - - - - - -This example strips the request message into an empty body, grabs an HTTP payload, and places the result back into the original message at the path `image.pull_count`: - -```yaml -pipeline: - processors: - - branch: - request_map: 'root = ""' - processors: - - http: - url: https://hub.docker.com/v2/repositories/jeffail/benthos - verb: GET - headers: - Content-Type: application/json - result_map: root.image.pull_count = this.pull_count - -# Example input: {"id":"foo","some":"pre-existing data"} -# Example output: {"id":"foo","some":"pre-existing data","image":{"pull_count":1234}} -``` - - - - - -When the result of your branch processors is unstructured and you wish to simply set a resulting field to the raw output use the content function to obtain the raw bytes of the resulting message and then coerce it into your value type of choice: - -```yaml -pipeline: - processors: - - branch: - request_map: 'root = this.document.id' - processors: - - cache: - resource: descriptions_cache - key: ${! content() } - operator: get - result_map: root.document.description = content().string() - -# Example input: {"document":{"id":"foo","content":"hello world"}} -# Example output: {"document":{"id":"foo","content":"hello world","description":"this is a cool doc"}} -``` - - - - - -This example maps a new payload for triggering a lambda function with an ID and username from the original message, and the result of the lambda is discarded, meaning the original message is unchanged. - -```yaml -pipeline: - processors: - - branch: - request_map: '{"id":this.doc.id,"username":this.user.name}' - processors: - - aws_lambda: - function: trigger_user_update - -# Example input: {"doc":{"id":"foo","body":"hello world"},"user":{"name":"fooey"}} -# Output matches the input, which is unchanged -``` - - - - - -This example caches a document by a message ID only when the type of the document is a foo: - -```yaml -pipeline: - processors: - - branch: - request_map: | - meta id = this.id - root = if this.type == "foo" { - this.document - } else { - deleted() - } - processors: - - cache: - resource: TODO - operator: set - key: ${! @id } - value: ${! content() } -``` - - - - - diff --git a/website/docs/components/processors/cache.md b/website/docs/components/processors/cache.md deleted file mode 100644 index d410e99f68..0000000000 --- a/website/docs/components/processors/cache.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: cache -slug: cache -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Performs operations against a [cache resource](/docs/components/caches/about) for each message, allowing you to store or retrieve data within message payloads. - - - - - - -```yml -# Common config fields, showing default values -label: "" -cache: - resource: "" # No default (required) - operator: "" # No default (required) - key: "" # No default (required) - value: "" # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -label: "" -cache: - resource: "" # No default (required) - operator: "" # No default (required) - key: "" # No default (required) - value: "" # No default (optional) - ttl: 60s # No default (optional) -``` - - - - -For use cases where you wish to cache the result of processors consider using the [`cached` processor](/docs/components/processors/cached) instead. - -This processor will interpolate functions within the `key` and `value` fields individually for each message. This allows you to specify dynamic keys and values based on the contents of the message payloads and metadata. You can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries). - -## Examples - - - - - - -Deduplication can be done using the add operator with a key extracted from the message payload, since it fails when a key already exists we can remove the duplicates using a [`mapping` processor](/docs/components/processors/mapping): - -```yaml -pipeline: - processors: - - cache: - resource: foocache - operator: add - key: '${! json("message.id") }' - value: "storeme" - - mapping: root = if errored() { deleted() } - -cache_resources: - - label: foocache - redis: - url: tcp://TODO:6379 -``` - - - - - -Sometimes it's necessary to deduplicate a batch of messages (AKA a window) by a single identifying value. This can be done by introducing a [`branch` processor](/docs/components/processors/branch), which executes the cache only once on behalf of the batch, in this case with a value make from a field extracted from the first and last messages of the batch: - -```yaml -pipeline: - processors: - # Try and add one message to a cache that identifies the whole batch - - branch: - request_map: | - root = if batch_index() == 0 { - json("id").from(0) + json("meta.tail_id").from(-1) - } else { deleted() } - processors: - - cache: - resource: foocache - operator: add - key: ${! content() } - value: t - # Delete all messages if we failed - - mapping: | - root = if errored().from(0) { - deleted() - } -``` - - - - - -It's possible to enrich payloads with content previously stored in a cache by using the [`branch`](/docs/components/processors/branch) processor: - -```yaml -pipeline: - processors: - - branch: - processors: - - cache: - resource: foocache - operator: get - key: '${! json("message.document_id") }' - result_map: 'root.message.document = this' - - # NOTE: If the data stored in the cache is not valid JSON then use - # something like this instead: - # result_map: 'root.message.document = content().string()' - -cache_resources: - - label: foocache - memcached: - addresses: [ "TODO:11211" ] -``` - - - - -## Fields - -### `resource` - -The [`cache` resource](/docs/components/caches/about) to target with this processor. - - -Type: `string` - -### `operator` - -The [operation](#operators) to perform with the cache. - - -Type: `string` -Options: `set`, `add`, `get`, `delete`. - -### `key` - -A key to use with the cache. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `value` - -A value to use with the cache (when applicable). -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `ttl` - -The TTL of each individual item as a duration string. After this period an item will be eligible for removal during the next compaction. Not all caches support per-key TTLs, those that do will have a configuration field `default_ttl`, and those that do not will fall back to their generally configured TTL setting. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 3.33.0 or newer - -```yml -# Examples - -ttl: 60s - -ttl: 5m - -ttl: 36h -``` - -## Operators - -### `set` - -Set a key in the cache to a value. If the key already exists the contents are -overridden. - -### `add` - -Set a key in the cache to a value. If the key already exists the action fails -with a 'key already exists' error, which can be detected with -[processor error handling](/docs/configuration/error_handling). - -### `get` - -Retrieve the contents of a cached key and replace the original message payload -with the result. If the key does not exist the action fails with an error, which -can be detected with [processor error handling](/docs/configuration/error_handling). - -### `delete` - -Delete a key and its contents from the cache. If the key does not exist the -action is a no-op and will not fail with an error. - diff --git a/website/docs/components/processors/cached.md b/website/docs/components/processors/cached.md deleted file mode 100644 index 78731c8ff1..0000000000 --- a/website/docs/components/processors/cached.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: cached -slug: cached -type: processor -status: experimental -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Cache the result of applying one or more processors to messages identified by a key. If the key already exists within the cache the contents of the message will be replaced with the cached result instead of applying the processors. This component is therefore useful in situations where an expensive set of processors need only be executed periodically. - -Introduced in version 4.3.0. - -```yml -# Config fields, showing default values -label: "" -cached: - cache: "" # No default (required) - skip_on: errored() # No default (optional) - key: my_foo_result # No default (required) - ttl: "" # No default (optional) - processors: [] # No default (required) -``` - -The format of the data when stored within the cache is a custom and versioned schema chosen to balance performance and storage space. It is therefore not possible to point this processor to a cache that is pre-populated with data that this processor has not created itself. - -## Examples - - - - - -In the following example we want to we enrich messages consumed from Kafka with data specific to the origin topic partition, we do this by placing an `http` processor within a `branch`, where the HTTP URL contains interpolation functions with the topic and partition in the path. - -However, it would be inefficient to make this HTTP request for every single message as the result is consistent for all data of a given topic partition. We can solve this by placing our enrichment call within a `cached` processor where the key contains the topic and partition, resulting in messages that originate from the same topic/partition combination using the cached result of the prior. - -```yaml -pipeline: - processors: - - branch: - processors: - - cached: - key: '${! meta("kafka_topic") }-${! meta("kafka_partition") }' - cache: foo_cache - processors: - - mapping: 'root = ""' - - http: - url: http://example.com/enrichment/${! meta("kafka_topic") }/${! meta("kafka_partition") } - verb: GET - result_map: 'root.enrichment = this' - -cache_resources: - - label: foo_cache - memory: - # Disable compaction so that cached items never expire - compaction_interval: "" -``` - - - - -In the following example we enrich all messages with the same data obtained from a static URL with an `http` processor within a `branch`. However, we expect the data from this URL to change roughly every 10 minutes, so we configure a `cached` processor with a static key (since this request is consistent for all messages) and a TTL of `10m`. - -```yaml -pipeline: - processors: - - branch: - request_map: 'root = ""' - processors: - - cached: - key: static_foo - cache: foo_cache - ttl: 10m - processors: - - http: - url: http://example.com/get/foo.json - verb: GET - result_map: 'root.foo = this' - -cache_resources: - - label: foo_cache - memory: {} -``` - - - - -## Fields - -### `cache` - -The cache resource to read and write processor results from. - - -Type: `string` - -### `skip_on` - -A condition that can be used to skip caching the results from the processors. - - -Type: `string` - -```yml -# Examples - -skip_on: errored() -``` - -### `key` - -A key to be resolved for each message, if the key already exists in the cache then the cached result is used, otherwise the processors are applied and the result is cached under this key. The key could be static and therefore apply generally to all messages or it could be an interpolated expression that is potentially unique for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -key: my_foo_result - -key: ${! this.document.id } - -key: ${! meta("kafka_key") } - -key: ${! meta("kafka_topic") } -``` - -### `ttl` - -An optional expiry period to set for each cache entry. Some caches only have a general TTL and will therefore ignore this setting. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `processors` - -The list of processors whose result will be cached. - - -Type: `array` - - diff --git a/website/docs/components/processors/catch.md b/website/docs/components/processors/catch.md deleted file mode 100644 index d1c6642e11..0000000000 --- a/website/docs/components/processors/catch.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: catch -slug: catch -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Applies a list of child processors _only_ when a previous processing step has failed. - -```yml -# Config fields, showing default values -label: "" -catch: [] -``` - -Behaves similarly to the [`for_each`](/docs/components/processors/for_each) processor, where a list of child processors are applied to individual messages of a batch. However, processors are only applied to messages that failed a processing step prior to the catch. - -For example, with the following config: - -```yaml -pipeline: - processors: - - resource: foo - - catch: - - resource: bar - - resource: baz -``` - -If the processor `foo` fails for a particular message, that message will be fed into the processors `bar` and `baz`. Messages that do not fail for the processor `foo` will skip these processors. - -When messages leave the catch block their fail flags are cleared. This processor is useful for when it's possible to recover failed messages, or when special actions (such as logging/metrics) are required before dropping them. - -More information about error handling can be found [here](/docs/configuration/error_handling). - - diff --git a/website/docs/components/processors/command.md b/website/docs/components/processors/command.md deleted file mode 100644 index d08ba14d99..0000000000 --- a/website/docs/components/processors/command.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: command -slug: command -type: processor -status: experimental -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a command for each message. - -Introduced in version 4.21.0. - -```yml -# Config fields, showing default values -label: "" -command: - name: bash # No default (required) - args_mapping: '[ "-c", this.script_path ]' # No default (optional) -``` - -The specified command is executed for each message processed, with the raw bytes of the message being fed into the stdin of the command process, and the resulting message having its contents replaced with the stdout of it. - -## Performance - -Since this processor executes a new process for each message performance will likely be an issue for high throughput streams. If this is the case then consider using the [`subprocess` processor](/docs/components/processors/subprocess) instead as it keeps the underlying process alive long term and uses codecs to insert and extract inputs and outputs to it via stdin/stdout. - -## Error Handling - -If a non-zero error code is returned by the command then an error containing the entirety of stderr (or a generic message if nothing is written) is set on the message. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about these patterns [here](/docs/configuration/error_handling). - -If the command is successful but stderr is written to then a metadata field `command_stderr` is populated with its contents. - - -## Fields - -### `name` - -The name of the command to execute. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -name: bash - -name: go - -name: ${! @command } -``` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) that, when specified, should resolve into an array of arguments to pass to the command. Command arguments are expressed this way in order to support dynamic behaviour. - - -Type: `string` - -```yml -# Examples - -args_mapping: '[ "-c", this.script_path ]' -``` - -## Examples - - - - - -This example uses a [`generate` input](/docs/components/inputs/generate) to trigger a command on a cron schedule: - -```yaml -input: - generate: - interval: '0,30 */2 * * * *' - mapping: 'root = ""' # Empty string as we do not need to pipe anything to stdin - processors: - - command: - name: df - args_mapping: '[ "-h" ]' -``` - - - - -This example config takes structured messages of the form `{"command":"echo","args":["foo"]}` and uses their contents to execute the contained command and arguments dynamically, replacing its contents with the command result printed to stdout: - -```yaml -pipeline: - processors: - - command: - name: ${! this.command } - args_mapping: 'this.args' -``` - - - - - diff --git a/website/docs/components/processors/compress.md b/website/docs/components/processors/compress.md deleted file mode 100644 index ce18316051..0000000000 --- a/website/docs/components/processors/compress.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: compress -slug: compress -type: processor -status: stable -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Compresses messages according to the selected algorithm. Supported compression algorithms are: [flate gzip lz4 pgzip snappy zlib] - -```yml -# Config fields, showing default values -label: "" -compress: - algorithm: "" # No default (required) - level: -1 -``` - -The 'level' field might not apply to all algorithms. - -## Fields - -### `algorithm` - -The compression algorithm to use. - - -Type: `string` -Options: `flate`, `gzip`, `lz4`, `pgzip`, `snappy`, `zlib`. - -### `level` - -The level of compression to use. May not be applicable to all algorithms. - - -Type: `int` -Default: `-1` - - diff --git a/website/docs/components/processors/couchbase.md b/website/docs/components/processors/couchbase.md deleted file mode 100644 index 37de8db477..0000000000 --- a/website/docs/components/processors/couchbase.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: couchbase -slug: couchbase -type: processor -status: experimental -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Performs operations against Couchbase for each message, allowing you to store or retrieve data within message payloads. - -Introduced in version 4.11.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -couchbase: - url: couchbase://localhost:11210 # No default (required) - username: "" # No default (optional) - password: "" # No default (optional) - bucket: "" # No default (required) - id: ${! json("id") } # No default (required) - content: "" # No default (optional) - operation: get -``` - - - - -```yml -# All config fields, showing default values -label: "" -couchbase: - url: couchbase://localhost:11210 # No default (required) - username: "" # No default (optional) - password: "" # No default (optional) - bucket: "" # No default (required) - collection: _default - transcoder: legacy - timeout: 15s - id: ${! json("id") } # No default (required) - content: "" # No default (optional) - operation: get -``` - - - - -When inserting, replacing or upserting documents, each must have the `content` property set. - -## Fields - -### `url` - -Couchbase connection string. - - -Type: `string` - -```yml -# Examples - -url: couchbase://localhost:11210 -``` - -### `username` - -Username to connect to the cluster. - - -Type: `string` - -### `password` - -Password to connect to the cluster. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `bucket` - -Couchbase bucket. - - -Type: `string` - -### `collection` - -Bucket collection. - - -Type: `string` -Default: `"_default"` - -### `transcoder` - -Couchbase transcoder to use. - - -Type: `string` -Default: `"legacy"` - -| Option | Summary | -|---|---| -| `json` | JSONTranscoder implements the default transcoding behavior and applies JSON transcoding to all values. This will apply the following behavior to the value: binary ([]byte) -> error. default -> JSON value, JSON Flags. | -| `legacy` | LegacyTranscoder implements the behaviour for a backward-compatible transcoder. This transcoder implements behaviour matching that of gocb v1.This will apply the following behavior to the value: binary ([]byte) -> binary bytes, Binary expectedFlags. string -> string bytes, String expectedFlags. default -> JSON value, JSON expectedFlags. | -| `raw` | RawBinaryTranscoder implements passthrough behavior of raw binary data. This transcoder does not apply any serialization. This will apply the following behavior to the value: binary ([]byte) -> binary bytes, binary expectedFlags. default -> error. | -| `rawjson` | RawJSONTranscoder implements passthrough behavior of JSON data. This transcoder does not apply any serialization. It will forward data across the network without incurring unnecessary parsing costs. This will apply the following behavior to the value: binary ([]byte) -> JSON bytes, JSON expectedFlags. string -> JSON bytes, JSON expectedFlags. default -> error. | -| `rawstring` | RawStringTranscoder implements passthrough behavior of raw string data. This transcoder does not apply any serialization. This will apply the following behavior to the value: string -> string bytes, string expectedFlags. default -> error. | - - -### `timeout` - -Operation timeout. - - -Type: `string` -Default: `"15s"` - -### `id` - -Document id. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -id: ${! json("id") } -``` - -### `content` - -Document content. - - -Type: `string` - -### `operation` - -Couchbase operation to perform. - - -Type: `string` -Default: `"get"` - -| Option | Summary | -|---|---| -| `get` | fetch a document. | -| `insert` | insert a new document. | -| `remove` | delete a document. | -| `replace` | replace the contents of a document. | -| `upsert` | creates a new document if it does not exist, if it does exist then it updates it. | - - - diff --git a/website/docs/components/processors/decompress.md b/website/docs/components/processors/decompress.md deleted file mode 100644 index 153989c0d8..0000000000 --- a/website/docs/components/processors/decompress.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: decompress -slug: decompress -type: processor -status: stable -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Decompresses messages according to the selected algorithm. Supported decompression algorithms are: [bzip2 flate gzip lz4 pgzip snappy zlib] - -```yml -# Config fields, showing default values -label: "" -decompress: - algorithm: "" # No default (required) -``` - -## Fields - -### `algorithm` - -The decompression algorithm to use. - - -Type: `string` -Options: `bzip2`, `flate`, `gzip`, `lz4`, `pgzip`, `snappy`, `zlib`. - - diff --git a/website/docs/components/processors/dedupe.md b/website/docs/components/processors/dedupe.md deleted file mode 100644 index c48e6e0d06..0000000000 --- a/website/docs/components/processors/dedupe.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: dedupe -slug: dedupe -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Deduplicates messages by storing a key value in a cache using the `add` operator. If the key already exists within the cache it is dropped. - -```yml -# Config fields, showing default values -label: "" -dedupe: - cache: "" # No default (required) - key: ${! meta("kafka_key") } # No default (required) - drop_on_err: true -``` - -Caches must be configured as resources, for more information check out the [cache documentation here](/docs/components/caches/about). - -When using this processor with an output target that might fail you should always wrap the output within an indefinite [`retry`](/docs/components/outputs/retry) block. This ensures that during outages your messages aren't reprocessed after failures, which would result in messages being dropped. - -## Batch Deduplication - -This processor enacts on individual messages only, in order to perform a deduplication on behalf of a batch (or window) of messages instead use the [`cache` processor](/docs/components/processors/cache#examples). - -## Delivery Guarantees - -Performing deduplication on a stream using a distributed cache voids any at-least-once guarantees that it previously had. This is because the cache will preserve message signatures even if the message fails to leave the Benthos pipeline, which would cause message loss in the event of an outage at the output sink followed by a restart of the Benthos instance (or a server crash, etc). - -This problem can be mitigated by using an in-memory cache and distributing messages to horizontally scaled Benthos pipelines partitioned by the deduplication key. However, in situations where at-least-once delivery guarantees are important it is worth avoiding deduplication in favour of implement idempotent behaviour at the edge of your stream pipelines. - -## Fields - -### `cache` - -The [`cache` resource](/docs/components/caches/about) to target with this processor. - - -Type: `string` - -### `key` - -An interpolated string yielding the key to deduplicate by for each message. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -key: ${! meta("kafka_key") } - -key: ${! content().hash("xxhash64") } -``` - -### `drop_on_err` - -Whether messages should be dropped when the cache returns a general error such as a network issue. - - -Type: `bool` -Default: `true` - -## Examples - - - - - -The following configuration demonstrates a pipeline that deduplicates messages based on the Kafka key. - -```yaml -pipeline: - processors: - - dedupe: - cache: keycache - key: ${! meta("kafka_key") } - -cache_resources: - - label: keycache - memory: - default_ttl: 60s -``` - - - - - diff --git a/website/docs/components/processors/for_each.md b/website/docs/components/processors/for_each.md deleted file mode 100644 index 0a4a7ed31a..0000000000 --- a/website/docs/components/processors/for_each.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: for_each -slug: for_each -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message. - -```yml -# Config fields, showing default values -label: "" -for_each: [] -``` - -This is useful for forcing batch wide processors such as [`dedupe`](/docs/components/processors/dedupe) or interpolations such as the `value` field of the `metadata` processor to execute on individual message parts of a batch instead. - -Please note that most processors already process per message of a batch, and this processor is not needed in those cases. - - diff --git a/website/docs/components/processors/gcp_bigquery_select.md b/website/docs/components/processors/gcp_bigquery_select.md deleted file mode 100644 index 490a7fb30a..0000000000 --- a/website/docs/components/processors/gcp_bigquery_select.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: gcp_bigquery_select -slug: gcp_bigquery_select -type: processor -status: experimental -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a `SELECT` query against BigQuery and replaces messages with the rows returned. - -Introduced in version 3.64.0. - -```yml -# Config fields, showing default values -label: "" -gcp_bigquery_select: - project: "" # No default (required) - table: bigquery-public-data.samples.shakespeare # No default (required) - columns: [] # No default (required) - where: type = ? and created_at > ? # No default (optional) - job_labels: {} - args_mapping: root = [ "article", now().ts_format("2006-01-02") ] # No default (optional) - prefix: "" # No default (optional) - suffix: "" # No default (optional) -``` - -## Examples - - - - - - -Given a stream of English terms, enrich the messages with the word count from Shakespeare's public works: - -```yaml -pipeline: - processors: - - branch: - processors: - - gcp_bigquery_select: - project: test-project - table: bigquery-public-data.samples.shakespeare - columns: - - word - - sum(word_count) as total_count - where: word = ? - suffix: | - GROUP BY word - ORDER BY total_count DESC - LIMIT 10 - args_mapping: root = [ this.term ] - result_map: | - root.count = this.get("0.total_count") -``` - - - - -## Fields - -### `project` - -GCP project where the query job will execute. - - -Type: `string` - -### `table` - -Fully-qualified BigQuery table name to query. - - -Type: `string` - -```yml -# Examples - -table: bigquery-public-data.samples.shakespeare -``` - -### `columns` - -A list of columns to query. - - -Type: `array` - -### `where` - -An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks (`?`). - - -Type: `string` - -```yml -# Examples - -where: type = ? and created_at > ? - -where: user_id = ? -``` - -### `job_labels` - -A list of labels to add to the query job. - - -Type: `object` -Default: `{}` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ "article", now().ts_format("2006-01-02") ] -``` - -### `prefix` - -An optional prefix to prepend to the select query (before SELECT). - - -Type: `string` - -### `suffix` - -An optional suffix to append to the select query. - - -Type: `string` - - diff --git a/website/docs/components/processors/grok.md b/website/docs/components/processors/grok.md deleted file mode 100644 index 920cc66e4b..0000000000 --- a/website/docs/components/processors/grok.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: grok -slug: grok -type: processor -status: stable -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Parses messages into a structured format by attempting to apply a list of Grok expressions, the first expression to result in at least one value replaces the original message with a JSON object containing the values. - - - - - - -```yml -# Common config fields, showing default values -label: "" -grok: - expressions: [] # No default (required) - pattern_definitions: {} - pattern_paths: [] -``` - - - - -```yml -# All config fields, showing default values -label: "" -grok: - expressions: [] # No default (required) - pattern_definitions: {} - pattern_paths: [] - named_captures_only: true - use_default_patterns: true - remove_empty_values: true -``` - - - - -Type hints within patterns are respected, therefore with the pattern `%{WORD:first},%{INT:second:int}` and a payload of `foo,1` the resulting payload would be `{"first":"foo","second":1}`. - -### Performance - -This processor currently uses the [Go RE2](https://golang.org/s/re2syntax) regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information see [https://swtch.com/~rsc/regexp/regexp1.html](https://swtch.com/~rsc/regexp/regexp1.html). - -## Examples - - - - - - -Grok can be used to parse unstructured logs such as VPC flow logs that look like this: - -```text -2 123456789010 eni-1235b8ca123456789 172.31.16.139 172.31.16.21 20641 22 6 20 4249 1418530010 1418530070 ACCEPT OK -``` - -Into structured objects that look like this: - -```json -{"accountid":"123456789010","action":"ACCEPT","bytes":4249,"dstaddr":"172.31.16.21","dstport":22,"end":1418530070,"interfaceid":"eni-1235b8ca123456789","logstatus":"OK","packets":20,"protocol":6,"srcaddr":"172.31.16.139","srcport":20641,"start":1418530010,"version":2} -``` - -With the following config: - -```yaml -pipeline: - processors: - - grok: - expressions: - - '%{VPCFLOWLOG}' - pattern_definitions: - VPCFLOWLOG: '%{NUMBER:version:int} %{NUMBER:accountid} %{NOTSPACE:interfaceid} %{NOTSPACE:srcaddr} %{NOTSPACE:dstaddr} %{NOTSPACE:srcport:int} %{NOTSPACE:dstport:int} %{NOTSPACE:protocol:int} %{NOTSPACE:packets:int} %{NOTSPACE:bytes:int} %{NUMBER:start:int} %{NUMBER:end:int} %{NOTSPACE:action} %{NOTSPACE:logstatus}' -``` - - - - -## Fields - -### `expressions` - -One or more Grok expressions to attempt against incoming messages. The first expression to match at least one value will be used to form a result. - - -Type: `array` - -### `pattern_definitions` - -A map of pattern definitions that can be referenced within `patterns`. - - -Type: `object` -Default: `{}` - -### `pattern_paths` - -A list of paths to load Grok patterns from. This field supports wildcards, including super globs (double star). - - -Type: `array` -Default: `[]` - -### `named_captures_only` - -Whether to only capture values from named patterns. - - -Type: `bool` -Default: `true` - -### `use_default_patterns` - -Whether to use a [default set of patterns](#default-patterns). - - -Type: `bool` -Default: `true` - -### `remove_empty_values` - -Whether to remove values that are empty from the resulting structure. - - -Type: `bool` -Default: `true` - -## Default Patterns - -A summary of the default patterns on offer can be [found here](https://github.com/Jeffail/grok/blob/master/patterns.go#L5). - diff --git a/website/docs/components/processors/group_by.md b/website/docs/components/processors/group_by.md deleted file mode 100644 index c2a168edde..0000000000 --- a/website/docs/components/processors/group_by.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: group_by -slug: group_by -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Splits a [batch of messages](/docs/configuration/batching) into N batches, where each resulting batch contains a group of messages determined by a [Bloblang query](/docs/guides/bloblang/about). - -```yml -# Config fields, showing default values -label: "" -group_by: [] # No default (required) -``` - -Once the groups are established a list of processors are applied to their respective grouped batch, which can be used to label the batch as per their grouping. Messages that do not pass the check of any specified group are placed in their own group. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching). - -## Fields - -### `[].check` - -A [Bloblang query](/docs/guides/bloblang/about) that should return a boolean value indicating whether a message belongs to a given group. - - -Type: `string` - -```yml -# Examples - -check: this.type == "foo" - -check: this.contents.urls.contains("https://benthos.dev/") - -check: "true" -``` - -### `[].processors` - -A list of [processors](/docs/components/processors/about) to execute on the newly formed group. - - -Type: `array` -Default: `[]` - -## Examples - - - - - -Imagine we have a batch of messages that we wish to split into a group of foos and everything else, which should be sent to different output destinations based on those groupings. We also need to send the foos as a tar gzip archive. For this purpose we can use the `group_by` processor with a [`switch`](/docs/components/outputs/switch) output: - -```yaml -pipeline: - processors: - - group_by: - - check: content().contains("this is a foo") - processors: - - archive: - format: tar - - compress: - algorithm: gzip - - mapping: 'meta grouping = "foo"' - -output: - switch: - cases: - - check: meta("grouping") == "foo" - output: - gcp_pubsub: - project: foo_prod - topic: only_the_foos - - output: - gcp_pubsub: - project: somewhere_else - topic: no_foos_here -``` - - - - - diff --git a/website/docs/components/processors/group_by_value.md b/website/docs/components/processors/group_by_value.md deleted file mode 100644 index 8eae8e2e43..0000000000 --- a/website/docs/components/processors/group_by_value.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: group_by_value -slug: group_by_value -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Splits a batch of messages into N batches, where each resulting batch contains a group of messages determined by a [function interpolated string](/docs/configuration/interpolation#bloblang-queries) evaluated per message. - -```yml -# Config fields, showing default values -label: "" -group_by_value: - value: ${! meta("kafka_key") } # No default (required) -``` - -This allows you to group messages using arbitrary fields within their content or metadata, process them individually, and send them to unique locations as per their group. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching). - -## Fields - -### `value` - -The interpolated string to group based on. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -value: ${! meta("kafka_key") } - -value: ${! json("foo.bar") }-${! meta("baz") } -``` - -## Examples - -If we were consuming Kafka messages and needed to group them by their key, archive the groups, and send them to S3 with the key as part of the path we could achieve that with the following: - -```yaml -pipeline: - processors: - - group_by_value: - value: ${! meta("kafka_key") } - - archive: - format: tar - - compress: - algorithm: gzip -output: - aws_s3: - bucket: TODO - path: docs/${! meta("kafka_key") }/${! count("files") }-${! timestamp_unix_nano() }.tar.gz -``` - diff --git a/website/docs/components/processors/http.md b/website/docs/components/processors/http.md deleted file mode 100644 index 82c9f1424d..0000000000 --- a/website/docs/components/processors/http.md +++ /dev/null @@ -1,735 +0,0 @@ ---- -title: http -slug: http -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Performs an HTTP request using a message batch as the request body, and replaces the original message parts with the body of the response. - - - - - - -```yml -# Common config fields, showing default values -label: "" -http: - url: "" # No default (required) - verb: POST - headers: {} - rate_limit: "" # No default (optional) - timeout: 5s - parallel: false -``` - - - - -```yml -# All config fields, showing default values -label: "" -http: - url: "" # No default (required) - verb: POST - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - dump_request_log_level: "" - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - oauth2: - enabled: false - client_key: "" - client_secret: "" - token_url: "" - scopes: [] - endpoint_params: {} - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - extract_headers: - include_prefixes: [] - include_patterns: [] - rate_limit: "" # No default (optional) - timeout: 5s - retry_period: 1s - max_retry_backoff: 300s - retries: 3 - backoff_on: - - 429 - drop_on: [] - successful_on: [] - proxy_url: "" # No default (optional) - batch_as_multipart: false - parallel: false -``` - - - - -The `rate_limit` field can be used to specify a rate limit [resource](/docs/components/rate_limits/about) to cap the rate of requests across all parallel components service wide. - -The URL and header values of this type can be dynamically set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries). - -In order to map or encode the payload to a specific request body, and map the response back into the original payload instead of replacing it entirely, you can use the [`branch` processor](/docs/components/processors/branch). - -## Response Codes - -Benthos considers any response code between 200 and 299 inclusive to indicate a successful response, you can add more success status codes with the field `successful_on`. - -When a request returns a response code within the `backoff_on` field it will be retried after increasing intervals. - -When a request returns a response code within the `drop_on` field it will not be reattempted and is immediately considered a failed request. - -## Adding Metadata - -If the request returns an error response code this processor sets a metadata field `http_status_code` on the resulting message. - -Use the field `extract_headers` to specify rules for which other headers should be copied into the resulting message from the response. - -## Error Handling - -When all retry attempts for a message are exhausted the processor cancels the attempt. These failed messages will continue through the pipeline unchanged, but can be dropped or placed in a dead letter queue according to your config, you can read about these patterns [here](/docs/configuration/error_handling). - -## Examples - - - - - -This example uses a [`branch` processor](/docs/components/processors/branch/) to strip the request message into an empty body, grab an HTTP payload, and place the result back into the original message at the path `repo.status`: - -```yaml -pipeline: - processors: - - branch: - request_map: 'root = ""' - processors: - - http: - url: https://hub.docker.com/v2/repositories/jeffail/benthos - verb: GET - headers: - Content-Type: application/json - result_map: 'root.repo.status = this' -``` - - - - -## Fields - -### `url` - -The URL to connect to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `verb` - -A verb to connect with - - -Type: `string` -Default: `"POST"` - -```yml -# Examples - -verb: POST - -verb: GET - -verb: DELETE -``` - -### `headers` - -A map of headers to add to the request. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -headers: - Content-Type: application/octet-stream - traceparent: ${! tracing_span().traceparent } -``` - -### `metadata` - -Specify optional matching rules to determine which metadata keys should be added to the HTTP request as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `dump_request_log_level` - -EXPERIMENTAL: Optionally set a level at which the request and response payload of each request made will be logged. - - -Type: `string` -Default: `""` -Requires version 4.12.0 or newer -Options: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, ``. - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth2` - -Allows you to specify open authentication via OAuth version 2 using the client credentials token flow. - - -Type: `object` - -### `oauth2.enabled` - -Whether to use OAuth version 2 in requests. - - -Type: `bool` -Default: `false` - -### `oauth2.client_key` - -A value used to identify the client to the token provider. - - -Type: `string` -Default: `""` - -### `oauth2.client_secret` - -A secret used to establish ownership of the client key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth2.token_url` - -The URL of the token provider. - - -Type: `string` -Default: `""` - -### `oauth2.scopes` - -A list of optional requested permissions. - - -Type: `array` -Default: `[]` -Requires version 3.45.0 or newer - -### `oauth2.endpoint_params` - -A list of optional endpoint parameters, values should be arrays of strings. - - -Type: `object` -Default: `{}` -Requires version 4.21.0 or newer - -```yml -# Examples - -endpoint_params: - bar: - - woof - foo: - - meow - - quack -``` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `extract_headers` - -Specify which response headers should be added to resulting messages as metadata. Header keys are lowercased before matching, so ensure that your patterns target lowercased versions of the header keys that you expect. - - -Type: `object` - -### `extract_headers.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `extract_headers.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `rate_limit` - -An optional [rate limit](/docs/components/rate_limits/about) to throttle requests by. - - -Type: `string` - -### `timeout` - -A static timeout to apply to requests. - - -Type: `string` -Default: `"5s"` - -### `retry_period` - -The base period to wait between failed requests. - - -Type: `string` -Default: `"1s"` - -### `max_retry_backoff` - -The maximum period to wait between failed requests. - - -Type: `string` -Default: `"300s"` - -### `retries` - -The maximum number of retry attempts to make. - - -Type: `int` -Default: `3` - -### `backoff_on` - -A list of status codes whereby the request should be considered to have failed and retries should be attempted, but the period between them should be increased gradually. - - -Type: `array` -Default: `[429]` - -### `drop_on` - -A list of status codes whereby the request should be considered to have failed but retries should not be attempted. This is useful for preventing wasted retries for requests that will never succeed. Note that with these status codes the _request_ is dropped, but _message_ that caused the request will not be dropped. - - -Type: `array` -Default: `[]` - -### `successful_on` - -A list of status codes whereby the attempt should be considered successful, this is useful for dropping requests that return non-2XX codes indicating that the message has been dealt with, such as a 303 See Other or a 409 Conflict. All 2XX codes are considered successful unless they are present within `backoff_on` or `drop_on`, regardless of this field. - - -Type: `array` -Default: `[]` - -### `proxy_url` - -An optional HTTP proxy URL. - - -Type: `string` - -### `batch_as_multipart` - -Send message batches as a single request using [RFC1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). - - -Type: `bool` -Default: `false` - -### `parallel` - -When processing batched messages, whether to send messages of the batch in parallel, otherwise they are sent serially. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/processors/insert_part.md b/website/docs/components/processors/insert_part.md deleted file mode 100644 index 3b94aa9254..0000000000 --- a/website/docs/components/processors/insert_part.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: insert_part -slug: insert_part -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Insert a new message into a batch at an index. If the specified index is greater than the length of the existing batch it will be appended to the end. - -```yml -# Config fields, showing default values -label: "" -insert_part: - index: -1 - content: "" -``` - -The index can be negative, and if so the message will be inserted from the end counting backwards starting from -1. E.g. if index = -1 then the new message will become the last of the batch, if index = -2 then the new message will be inserted before the last message, and so on. If the negative index is greater than the length of the existing batch it will be inserted at the beginning. - -The new message will have metadata copied from the first pre-existing message of the batch. - -This processor will interpolate functions within the 'content' field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries). - -## Fields - -### `index` - -The index within the batch to insert the message at. - - -Type: `int` -Default: `-1` - -### `content` - -The content of the message being inserted. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/processors/javascript.md b/website/docs/components/processors/javascript.md deleted file mode 100644 index eea2638a83..0000000000 --- a/website/docs/components/processors/javascript.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -title: javascript -slug: javascript -type: processor -status: experimental -categories: ["Mapping"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a provided JavaScript code block or file for each message. - -Introduced in version 4.14.0. - -```yml -# Config fields, showing default values -label: "" -javascript: - code: "" # No default (optional) - file: "" # No default (optional) - global_folders: [] -``` - -The [execution engine](https://github.com/dop251/goja) behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. - -Imports via `require` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be [found here](https://github.com/dop251/goja#known-incompatibilities-and-caveats). - -This processor is implemented using the [github.com/dop251/goja](https://github.com/dop251/goja) library. - -## Fields - -### `code` - -An inline JavaScript program to run. One of `code` or `file` must be defined. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `file` - -A file containing a JavaScript program to run. One of `code` or `file` must be defined. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -### `global_folders` - -List of folders that will be used to load modules from if the requested JS module is not found elsewhere. - - -Type: `array` -Default: `[]` - -## Examples - - - - - -In this example we define a simple function that performs a basic mutation against messages, treating their contents as raw strings. - -```yaml -pipeline: - processors: - - javascript: - code: 'benthos.v0_msg_set_string(benthos.v0_msg_as_string() + "hello world");' -``` - - - - -In this example we define a function that performs basic mutations against a structured message. Note that we encapsulate the logic within an anonymous function that is called for each invocation, this is required in order to avoid duplicate variable declarations in the global state. - -```yaml -pipeline: - processors: - - javascript: - code: | - (() => { - let thing = benthos.v0_msg_as_structured(); - thing.num_keys = Object.keys(thing).length; - delete thing["b"]; - benthos.v0_msg_set_structured(thing); - })(); -``` - - - - -## Runtime - -In order to optimise code execution JS runtimes are created on demand (in order to support parallel execution) and are reused across invocations. Therefore, it is important to understand that global state created by your programs will outlive individual invocations. In order for your programs to avoid failing after the first invocation ensure that you do not define variables at the global scope. - -Although technically possible, it is recommended that you do not rely on the global state for maintaining state across invocations as the pooling nature of the runtimes will prevent deterministic behaviour. We aim to support deterministic strategies for mutating global state in the future. - -## Functions - -### `benthos.v0_fetch` - -Executes an HTTP request synchronously and returns the result as an object of the form `{"status":200,"body":"foo"}`. - -#### Parameters - -**`url`** <string> The URL to fetch -**`headers`** <object(string,string)> An object of string/string key/value pairs to add the request as headers. -**`method`** <string> The method of the request. -**`body`** <(optional) string> A body to send. - -#### Examples - -```javascript -let result = benthos.v0_fetch("http://example.com", {}, "GET", "") -benthos.v0_msg_set_structured(result); -``` - -### `benthos.v0_msg_as_string` - -Obtain the raw contents of the processed message as a string. - -#### Examples - -```javascript -let contents = benthos.v0_msg_as_string(); -``` - -### `benthos.v0_msg_as_structured` - -Obtain the root of the processed message as a structured value. If the message is not valid JSON or has not already been expanded into a structured form this function will throw an error. - -#### Examples - -```javascript -let foo = benthos.v0_msg_as_structured().foo; -``` - -### `benthos.v0_msg_exists_meta` - -Check that a metadata key exists. - -#### Parameters - -**`name`** <string> The metadata key to search for. - -#### Examples - -```javascript -if (benthos.v0_msg_exists_meta("kafka_key")) {} -``` - -### `benthos.v0_msg_get_meta` - -Get the value of a metadata key from the processed message. - -#### Parameters - -**`name`** <string> The metadata key to search for. - -#### Examples - -```javascript -let key = benthos.v0_msg_get_meta("kafka_key"); -``` - -### `benthos.v0_msg_set_meta` - -Set a metadata key on the processed message to a value. - -#### Parameters - -**`name`** <string> The metadata key to set. -**`value`** <anything> The value to set it to. - -#### Examples - -```javascript -benthos.v0_msg_set_meta("thing", "hello world"); -``` - -### `benthos.v0_msg_set_string` - -Set the contents of the processed message to a given string. - -#### Parameters - -**`value`** <string> The value to set it to. - -#### Examples - -```javascript -benthos.v0_msg_set_string("hello world"); -``` - -### `benthos.v0_msg_set_structured` - -Set the root of the processed message to a given value of any type. - -#### Parameters - -**`value`** <anything> The value to set it to. - -#### Examples - -```javascript -benthos.v0_msg_set_structured({ - "foo": "a thing", - "bar": "something else", - "baz": 1234 -}); -``` - - - diff --git a/website/docs/components/processors/jmespath.md b/website/docs/components/processors/jmespath.md deleted file mode 100644 index 280d3f92b9..0000000000 --- a/website/docs/components/processors/jmespath.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: jmespath -slug: jmespath -type: processor -status: stable -categories: ["Mapping"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a [JMESPath query](http://jmespath.org/) on JSON documents and replaces the message with the resulting document. - -```yml -# Config fields, showing default values -label: "" -jmespath: - query: "" # No default (required) -``` - -:::note Try out Bloblang -For better performance and improved capabilities try out native Benthos mapping with the [`mapping` processor](/docs/components/processors/mapping). -::: - - -## Fields - -### `query` - -The JMESPath query to apply to messages. - - -Type: `string` - -## Examples - - - - - - -When receiving JSON documents of the form: - -```json -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -``` - -We could collapse the location names from the state of Washington into a field `Cities`: - -```json -{"Cities": "Bellevue, Olympia, Seattle"} -``` - -With the following config: - -```yaml -pipeline: - processors: - - jmespath: - query: "locations[?state == 'WA'].name | sort(@) | {Cities: join(', ', @)}" -``` - - - - - diff --git a/website/docs/components/processors/jq.md b/website/docs/components/processors/jq.md deleted file mode 100644 index 55c4d6e2d9..0000000000 --- a/website/docs/components/processors/jq.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: jq -slug: jq -type: processor -status: stable -categories: ["Mapping"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Transforms and filters messages using jq queries. - - - - - - -```yml -# Common config fields, showing default values -label: "" -jq: - query: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -jq: - query: "" # No default (required) - raw: false - output_raw: false -``` - - - - -:::note Try out Bloblang -For better performance and improved capabilities try out native Benthos mapping with the [`mapping` processor](/docs/components/processors/mapping). -::: - -The provided query is executed on each message, targeting either the contents as a structured JSON value or as a raw string using the field `raw`, and the message is replaced with the query result. - -Message metadata is also accessible within the query from the variable `$metadata`. - -This processor uses the [gojq library][gojq], and therefore does not require jq to be installed as a dependency. However, this also means there are some differences in how these queries are executed versus the jq cli which you can [read about here][gojq-difference]. - -If the query does not emit any value then the message is filtered, if the query returns multiple values then the resulting message will be an array containing all values. - -The full query syntax is described in [jq's documentation][jq-docs]. - -## Error Handling - -Queries can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). - -## Fields - -### `query` - -The jq query to filter and transform messages with. - - -Type: `string` - -### `raw` - -Whether to process the input as a raw string instead of as JSON. - - -Type: `bool` -Default: `false` - -### `output_raw` - -Whether to output raw text (unquoted) instead of JSON strings when the emitted values are string types. - - -Type: `bool` -Default: `false` - -## Examples - - - - - - -When receiving JSON documents of the form: - -```json -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -``` - -We could collapse the location names from the state of Washington into a field `Cities`: - -```json -{"Cities": "Bellevue, Olympia, Seattle"} -``` - -With the following config: - -```yaml -pipeline: - processors: - - jq: - query: '{Cities: .locations | map(select(.state == "WA").name) | sort | join(", ") }' -``` - - - - -[gojq]: https://github.com/itchyny/gojq -[gojq-difference]: https://github.com/itchyny/gojq#difference-to-jq -[jq-docs]: https://stedolan.github.io/jq/manual/ - diff --git a/website/docs/components/processors/json_schema.md b/website/docs/components/processors/json_schema.md deleted file mode 100644 index 4432520140..0000000000 --- a/website/docs/components/processors/json_schema.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: json_schema -slug: json_schema -type: processor -status: stable -categories: ["Mapping"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Checks messages against a provided JSONSchema definition but does not change the payload under any circumstances. If a message does not match the schema it can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -```yml -# Config fields, showing default values -label: "" -json_schema: - schema: "" # No default (optional) - schema_path: "" # No default (optional) -``` - -Please refer to the [JSON Schema website](https://json-schema.org/) for information and tutorials regarding the syntax of the schema. - -## Fields - -### `schema` - -A schema to apply. Use either this or the `schema_path` field. - - -Type: `string` - -### `schema_path` - -The path of a schema document to apply. Use either this or the `schema` field. - - -Type: `string` - -## Examples - -With the following JSONSchema document: - -```json -{ - "$id": "https://example.com/person.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -} -``` - -And the following Benthos configuration: - -```yaml -pipeline: - processors: - - json_schema: - schema_path: "file://path_to_schema.json" - - catch: - - log: - level: ERROR - message: "Schema validation failed due to: ${!error()}" - - mapping: 'root = deleted()' # Drop messages that fail -``` - -If a payload being processed looked like: - -```json -{"firstName":"John","lastName":"Doe","age":-21} -``` - -Then a log message would appear explaining the fault and the payload would be -dropped. - diff --git a/website/docs/components/processors/log.md b/website/docs/components/processors/log.md deleted file mode 100644 index 70341a10b9..0000000000 --- a/website/docs/components/processors/log.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: log -slug: log -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Prints a log event for each message. Messages always remain unchanged. The log message can be set using function interpolations described [here](/docs/configuration/interpolation#bloblang-queries) which allows you to log the contents and metadata of messages. - -```yml -# Config fields, showing default values -label: "" -log: - level: INFO - fields_mapping: |- # No default (optional) - root.reason = "cus I wana" - root.id = this.id - root.age = this.user.age.number() - root.kafka_topic = meta("kafka_topic") - message: "" -``` - -The `level` field determines the log level of the printed events and can be any of the following values: TRACE, DEBUG, INFO, WARN, ERROR. - -### Structured Fields - -It's also possible add custom fields to logs when the format is set to a structured form such as `json` or `logfmt` with the config field [`fields_mapping`](#fields_mapping): - -```yaml -pipeline: - processors: - - log: - level: DEBUG - message: hello world - fields_mapping: | - root.reason = "cus I wana" - root.id = this.id - root.age = this.user.age - root.kafka_topic = meta("kafka_topic") -``` - - -## Fields - -### `level` - -The log level to use. - - -Type: `string` -Default: `"INFO"` -Options: `FATAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`, `ALL`. - -### `fields_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) that can be used to specify extra fields to add to the log. If log fields are also added with the `fields` field then those values will override matching keys from this mapping. - - -Type: `string` - -```yml -# Examples - -fields_mapping: |- - root.reason = "cus I wana" - root.id = this.id - root.age = this.user.age.number() - root.kafka_topic = meta("kafka_topic") -``` - -### `message` - -The message to print. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - - diff --git a/website/docs/components/processors/mapping.md b/website/docs/components/processors/mapping.md deleted file mode 100644 index b5829376c4..0000000000 --- a/website/docs/components/processors/mapping.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: mapping -slug: mapping -type: processor -status: stable -categories: ["Mapping","Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a [Bloblang](/docs/guides/bloblang/about) mapping on messages, creating a new document that replaces (or filters) the original message. - -Introduced in version 4.5.0. - -```yml -# Config fields, showing default values -label: "" -mapping: "" # No default (required) -``` - -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information [check out the docs](/docs/guides/bloblang/about). - -If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `from ""`, where the path must be absolute, or relative from the location that Benthos is executed from. - -Note: This processor is equivalent to the [bloblang](/docs/components/processors/bloblang#component-rename) one. The latter will be deprecated in a future release. - -## Input Document Immutability - -Mapping operates by creating an entirely new object during assignments, this has the advantage of treating the original referenced document as immutable and therefore queryable at any stage of your mapping. For example, with the following mapping: - -```coffee -root.id = this.id -root.invitees = this.invitees.filter(i -> i.mood >= 0.5) -root.rejected = this.invitees.filter(i -> i.mood < 0.5) -``` - -Notice that we mutate the value of `invitees` in the resulting document by filtering out objects with a lower mood. However, even after doing so we're still able to reference the unchanged original contents of this value from the input document in order to populate a second field. Within this mapping we also have the flexibility to reference the mutable mapped document by using the keyword `root` (i.e. `root.invitees`) on the right-hand side instead. - -Mapping documents is advantageous in situations where the result is a document with a dramatically different shape to the input document, since we are effectively rebuilding the document in its entirety and might as well keep a reference to the unchanged input document throughout. However, in situations where we are only performing minor alterations to the input document, the rest of which is unchanged, it might be more efficient to use the [`mutation` processor](/docs/components/processors/mutation) instead. - -## Error Handling - -Bloblang mappings can fail, in which case the message remains unchanged, errors are logged, and the message is flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). - -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behaviour, which you can read about [in this section](/docs/guides/bloblang/about#error-handling). - - -## Examples - - - - - - -Given JSON documents containing an array of fans: - -```json -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"grace","obsession":0.21}, - {"name":"ali","obsession":0.89}, - {"name":"vic","obsession":0.43} - ] -} -``` - -We can reduce the documents down to just the ID and only those fans with an obsession score above 0.5, giving us: - -```json -{ - "id":"foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"ali","obsession":0.89} - ] -} -``` - -With the following config: - -```yaml -pipeline: - processors: - - mapping: | - root.id = this.id - root.fans = this.fans.filter(fan -> fan.obsession > 0.5) -``` - - - - - -When receiving JSON documents of the form: - -```json -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -``` - -We could collapse the location names from the state of Washington into a field `Cities`: - -```json -{"Cities": "Bellevue, Olympia, Seattle"} -``` - -With the following config: - -```yaml -pipeline: - processors: - - mapping: | - root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -``` - - - - - diff --git a/website/docs/components/processors/metric.md b/website/docs/components/processors/metric.md deleted file mode 100644 index a6f5216f43..0000000000 --- a/website/docs/components/processors/metric.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: metric -slug: metric -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Emit custom metrics by extracting values from messages. - -```yml -# Config fields, showing default values -label: "" -metric: - type: "" # No default (required) - name: "" # No default (required) - labels: {} # No default (optional) - value: "" -``` - -This processor works by evaluating an [interpolated field `value`](/docs/configuration/interpolation#bloblang-queries) for each message and updating a emitted metric according to the [type](#types). - -Custom metrics such as these are emitted along with Benthos internal metrics, where you can customize where metrics are sent, which metric names are emitted and rename them as/when appropriate. For more information check out the [metrics docs here](/docs/components/metrics/about). - -## Fields - -### `type` - -The metric [type](#types) to create. - - -Type: `string` -Options: `counter`, `counter_by`, `gauge`, `timing`. - -### `name` - -The name of the metric to create, this must be unique across all Benthos components otherwise it will overwrite those other metrics. - - -Type: `string` - -### `labels` - -A map of label names and values that can be used to enrich metrics. Labels are not supported by some metric destinations, in which case the metrics series are combined. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` - -```yml -# Examples - -labels: - topic: ${! meta("kafka_topic") } - type: ${! json("doc.type") } -``` - -### `value` - -For some metric types specifies a value to set, increment. Certain metrics exporters such as Prometheus support floating point values, but those that do not will cast a floating point value into an integer. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Default: `""` - -## Examples - - - - - -In this example we emit a counter metric called `Foos`, which increments for every message processed, and we label the metric with some metadata about where the message came from and a field from the document that states what type it is. We also configure our metrics to emit to CloudWatch, and explicitly only allow our custom metric and some internal Benthos metrics to emit. - -```yaml -pipeline: - processors: - - metric: - name: Foos - type: counter - labels: - topic: ${! meta("kafka_topic") } - partition: ${! meta("kafka_partition") } - type: ${! json("document.type").or("unknown") } - -metrics: - mapping: | - root = if ![ - "Foos", - "input_received", - "output_sent" - ].contains(this) { deleted() } - aws_cloudwatch: - namespace: ProdConsumer -``` - - - - -In this example we emit a gauge metric called `FooSize`, which is given a value extracted from JSON messages at the path `foo.size`. We then also configure our Prometheus metric exporter to only emit this custom metric and nothing else. We also label the metric with some metadata. - -```yaml -pipeline: - processors: - - metric: - name: FooSize - type: gauge - labels: - topic: ${! meta("kafka_topic") } - value: ${! json("foo.size") } - -metrics: - mapping: 'if this != "FooSize" { deleted() }' - prometheus: {} -``` - - - - -## Types - -### `counter` - -Increments a counter by exactly 1, the contents of `value` are ignored -by this type. - -### `counter_by` - -If the contents of `value` can be parsed as a positive integer value -then the counter is incremented by this value. - -For example, the following configuration will increment the value of the -`count.custom.field` metric by the contents of `field.some.value`: - -```yaml -pipeline: - processors: - - metric: - type: counter_by - name: CountCustomField - value: ${!json("field.some.value")} -``` - -### `gauge` - -If the contents of `value` can be parsed as a positive integer value -then the gauge is set to this value. - -For example, the following configuration will set the value of the -`gauge.custom.field` metric to the contents of `field.some.value`: - -```yaml -pipeline: - processors: - - metric: - type: gauge - name: GaugeCustomField - value: ${!json("field.some.value")} -``` - -### `timing` - -Equivalent to `gauge` where instead the metric is a timing. It is recommended that timing values are recorded in nanoseconds in order to be consistent with standard Benthos timing metrics, as in some cases these values are automatically converted into other units such as when exporting timings as histograms with Prometheus metrics. - diff --git a/website/docs/components/processors/mongodb.md b/website/docs/components/processors/mongodb.md deleted file mode 100644 index 0d151a6137..0000000000 --- a/website/docs/components/processors/mongodb.md +++ /dev/null @@ -1,240 +0,0 @@ ---- -title: mongodb -slug: mongodb -type: processor -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Performs operations against MongoDB for each message, allowing you to store or retrieve data within message payloads. - -Introduced in version 3.43.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - operation: insert-one - write_concern: - w: "" - j: false - w_timeout: "" - document_map: "" - filter_map: "" - hint_map: "" - upsert: false -``` - - - - -```yml -# All config fields, showing default values -label: "" -mongodb: - url: mongodb://localhost:27017 # No default (required) - database: "" # No default (required) - username: "" - password: "" - collection: "" # No default (required) - operation: insert-one - write_concern: - w: "" - j: false - w_timeout: "" - document_map: "" - filter_map: "" - hint_map: "" - upsert: false - json_marshal_mode: canonical -``` - - - - -## Fields - -### `url` - -The URL of the target MongoDB server. - - -Type: `string` - -```yml -# Examples - -url: mongodb://localhost:27017 -``` - -### `database` - -The name of the target MongoDB database. - - -Type: `string` - -### `username` - -The username to connect to the database. - - -Type: `string` -Default: `""` - -### `password` - -The password to connect to the database. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `collection` - -The name of the target collection. - - -Type: `string` - -### `operation` - -The mongodb operation to perform. - - -Type: `string` -Default: `"insert-one"` -Options: `insert-one`, `delete-one`, `delete-many`, `replace-one`, `update-one`, `find-one`. - -### `write_concern` - -The write concern settings for the mongo connection. - - -Type: `object` - -### `write_concern.w` - -W requests acknowledgement that write operations propagate to the specified number of mongodb instances. - - -Type: `string` -Default: `""` - -### `write_concern.j` - -J requests acknowledgement from MongoDB that write operations are written to the journal. - - -Type: `bool` -Default: `false` - -### `write_concern.w_timeout` - -The write concern timeout. - - -Type: `string` -Default: `""` - -### `document_map` - -A bloblang map representing a document to store within MongoDB, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). The document map is required for the operations insert-one, replace-one and update-one. - - -Type: `string` -Default: `""` - -```yml -# Examples - -document_map: |- - root.a = this.foo - root.b = this.bar -``` - -### `filter_map` - -A bloblang map representing a filter for a MongoDB command, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. - - -Type: `string` -Default: `""` - -```yml -# Examples - -filter_map: |- - root.a = this.foo - root.b = this.bar -``` - -### `hint_map` - -A bloblang map representing the hint for the MongoDB command, expressed as [extended JSON in canonical form](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. - - -Type: `string` -Default: `""` - -```yml -# Examples - -hint_map: |- - root.a = this.foo - root.b = this.bar -``` - -### `upsert` - -The upsert setting is optional and only applies for update-one and replace-one operations. If the filter specified in filter_map matches, the document is updated or replaced accordingly, otherwise it is created. - - -Type: `bool` -Default: `false` -Requires version 3.60.0 or newer - -### `json_marshal_mode` - -The json_marshal_mode setting is optional and controls the format of the output message. - - -Type: `string` -Default: `"canonical"` -Requires version 3.60.0 or newer - -| Option | Summary | -|---|---| -| `canonical` | A string format that emphasizes type preservation at the expense of readability and interoperability. That is, conversion from canonical to BSON will generally preserve type information except in certain specific cases. | -| `relaxed` | A string format that emphasizes readability and interoperability at the expense of type preservation. That is, conversion from relaxed format to BSON can lose type information. | - - - diff --git a/website/docs/components/processors/msgpack.md b/website/docs/components/processors/msgpack.md deleted file mode 100644 index 34a6902f68..0000000000 --- a/website/docs/components/processors/msgpack.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: msgpack -slug: msgpack -type: processor -status: beta -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Converts messages to or from the [MessagePack](https://msgpack.org/) format. - -Introduced in version 3.59.0. - -```yml -# Config fields, showing default values -label: "" -msgpack: - operator: "" # No default (required) -``` - -## Fields - -### `operator` - -The operation to perform on messages. - - -Type: `string` - -| Option | Summary | -|---|---| -| `from_json` | Convert JSON messages to MessagePack format | -| `to_json` | Convert MessagePack messages to JSON format | - - - diff --git a/website/docs/components/processors/mutation.md b/website/docs/components/processors/mutation.md deleted file mode 100644 index 0103901616..0000000000 --- a/website/docs/components/processors/mutation.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: mutation -slug: mutation -type: processor -status: stable -categories: ["Mapping","Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a [Bloblang](/docs/guides/bloblang/about) mapping and directly transforms the contents of messages, mutating (or deleting) them. - -Introduced in version 4.5.0. - -```yml -# Config fields, showing default values -label: "" -mutation: "" # No default (required) -``` - -Bloblang is a powerful language that enables a wide range of mapping, transformation and filtering tasks. For more information [check out the docs](/docs/guides/bloblang/about). - -If your mapping is large and you'd prefer for it to live in a separate file then you can execute a mapping directly from a file with the expression `from ""`, where the path must be absolute, or relative from the location that Benthos is executed from. - -## Input Document Mutability - -A mutation is a mapping that transforms input documents directly, this has the advantage of reducing the need to copy the data fed into the mapping. However, this also means that the referenced document is mutable and therefore changes throughout the mapping. For example, with the following Bloblang: - -```coffee -root.rejected = this.invitees.filter(i -> i.mood < 0.5) -root.invitees = this.invitees.filter(i -> i.mood >= 0.5) -``` - -Notice that we create a field `rejected` by copying the array field `invitees` and filtering out objects with a high mood. We then overwrite the field `invitees` by filtering out objects with a low mood, resulting in two array fields that are each a subset of the original. If we were to reverse the ordering of these assignments like so: - -```coffee -root.invitees = this.invitees.filter(i -> i.mood >= 0.5) -root.rejected = this.invitees.filter(i -> i.mood < 0.5) -``` - -Then the new field `rejected` would be empty as we have already mutated `invitees` to exclude the objects that it would be populated by. We can solve this problem either by carefully ordering our assignments or by capturing the original array using a variable (`let invitees = this.invitees`). - -Mutations are advantageous over a standard mapping in situations where the result is a document with mostly the same shape as the input document, since we can avoid unnecessarily copying data from the referenced input document. However, in situations where we are creating an entirely new document shape it can be more convenient to use the traditional [`mapping` processor](/docs/components/processors/mapping) instead. - -## Error Handling - -Bloblang mappings can fail, in which case the error is logged and the message is flagged as having failed, allowing you to use [standard processor error handling patterns](/docs/configuration/error_handling). - -However, Bloblang itself also provides powerful ways of ensuring your mappings do not fail by specifying desired fallback behaviour, which you can read about [in this section](/docs/guides/bloblang/about#error-handling). - - -## Examples - - - - - - -Given JSON documents containing an array of fans: - -```json -{ - "id":"foo", - "description":"a show about foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"grace","obsession":0.21}, - {"name":"ali","obsession":0.89}, - {"name":"vic","obsession":0.43} - ] -} -``` - -We can reduce the documents down to just the ID and only those fans with an obsession score above 0.5, giving us: - -```json -{ - "id":"foo", - "fans":[ - {"name":"bev","obsession":0.57}, - {"name":"ali","obsession":0.89} - ] -} -``` - -With the following config: - -```yaml -pipeline: - processors: - - mutation: | - root.description = deleted() - root.fans = this.fans.filter(fan -> fan.obsession > 0.5) -``` - - - - - -When receiving JSON documents of the form: - -```json -{ - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] -} -``` - -We could collapse the location names from the state of Washington into a field `Cities`: - -```json -{"Cities": "Bellevue, Olympia, Seattle"} -``` - -With the following config: - -```yaml -pipeline: - processors: - - mutation: | - root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -``` - - - - - diff --git a/website/docs/components/processors/nats_kv.md b/website/docs/components/processors/nats_kv.md deleted file mode 100644 index 01f5963ed6..0000000000 --- a/website/docs/components/processors/nats_kv.md +++ /dev/null @@ -1,433 +0,0 @@ ---- -title: nats_kv -slug: nats_kv -type: processor -status: beta -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Perform operations on a NATS key-value bucket. - -Introduced in version 4.12.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - operation: "" # No default (required) - key: foo # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -nats_kv: - urls: [] # No default (required) - bucket: my_kv_bucket # No default (required) - operation: "" # No default (required) - key: foo # No default (required) - revision: "42" # No default (optional) - timeout: 5s - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) -``` - - - - -### KV Operations - -The NATS KV processor supports a multitude of KV operations via the [operation](#operation) field. Along with `get`, `put`, and `delete`, this processor supports atomic operations like `update` and `create`, as well as utility operations like `purge`, `history`, and `keys`. - -### Metadata - -This processor adds the following metadata fields to each message, depending on the chosen `operation`: - -#### get, get_revision -``` text -- nats_kv_key -- nats_kv_bucket -- nats_kv_revision -- nats_kv_delta -- nats_kv_operation -- nats_kv_created -``` - -#### create, update, delete, purge -``` text -- nats_kv_key -- nats_kv_bucket -- nats_kv_revision -- nats_kv_operation -``` - -#### keys -``` text -- nats_kv_bucket -``` - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `bucket` - -The name of the KV bucket. - - -Type: `string` - -```yml -# Examples - -bucket: my_kv_bucket -``` - -### `operation` - -The operation to perform on the KV bucket. - - -Type: `string` - -| Option | Summary | -|---|---| -| `create` | Adds the key/value pair if it does not exist. Returns an error if it already exists. | -| `delete` | Deletes the key/value pair, but keeps historical values. | -| `get` | Returns the latest value for `key`. | -| `get_revision` | Returns the value of `key` for the specified `revision`. | -| `history` | Returns historical values of `key` as an array of objects containing the following fields: `key`, `value`, `bucket`, `revision`, `delta`, `operation`, `created`. | -| `keys` | Returns the keys in the `bucket` which match the `keys_filter` as an array of strings. | -| `purge` | Deletes the key/value pair and all historical values. | -| `put` | Places a new value for the key into the store. | -| `update` | Updates the value for `key` only if the `revision` matches the latest revision. | - - -### `key` - -The key for each message. Supports [wildcards](https://docs.nats.io/nats-concepts/subjects#wildcards) for the `history` and `keys` operations. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -key: foo - -key: foo.bar.baz - -key: foo.* - -key: foo.> - -key: foo.${! json("meta.type") } -``` - -### `revision` - -The revision of the key to operate on. Used for `get_revision` and `update` operations. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -revision: "42" - -revision: ${! @nats_kv_revision } -``` - -### `timeout` - -The maximum period to wait on an operation before aborting and returning an error. - - -Type: `string` -Default: `"5s"` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - - diff --git a/website/docs/components/processors/nats_request_reply.md b/website/docs/components/processors/nats_request_reply.md deleted file mode 100644 index bf8ee7cba6..0000000000 --- a/website/docs/components/processors/nats_request_reply.md +++ /dev/null @@ -1,447 +0,0 @@ ---- -title: nats_request_reply -slug: nats_request_reply -type: processor -status: experimental -categories: ["Services"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Sends a message to a NATS subject and expects a reply, from a NATS subscriber acting as a responder, back. - -Introduced in version 4.27.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -nats_request_reply: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - timeout: 3s -``` - - - - -```yml -# All config fields, showing default values -label: "" -nats_request_reply: - urls: [] # No default (required) - subject: foo.bar.baz # No default (required) - inbox_prefix: _INBOX_joe # No default (optional) - headers: {} - metadata: - include_prefixes: [] - include_patterns: [] - timeout: 3s - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - auth: - nkey_file: ./seed.nk # No default (optional) - user_credentials_file: ./user.creds # No default (optional) - user_jwt: "" # No default (optional) - user_nkey_seed: "" # No default (optional) -``` - - - - -### Metadata - -This input adds the following metadata fields to each message: - -```text -- nats_subject -- nats_sequence_stream -- nats_sequence_consumer -- nats_num_delivered -- nats_num_pending -- nats_domain -- nats_timestamp_unix_nano -``` - -You can access these metadata fields using [function interpolation](/docs/configuration/interpolation#bloblang-queries). - -### Connection Name - -When monitoring and managing a production NATS system, it is often useful to -know which connection a message was send/received from. This can be achieved by -setting the connection name option when creating a NATS connection. - -Benthos will automatically set the connection name based off the label of the given -NATS component, so that monitoring tools between NATS and benthos can stay in sync. -### Authentication - -There are several components within Benthos which utilise NATS services. You will find that each of these components -support optional advanced authentication parameters for [NKeys](https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth) -and [User Credentials](https://docs.nats.io/developing-with-nats/security/creds). - -An in depth tutorial can be found [here](https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt). - -#### NKey file - -The NATS server can use these NKeys in several ways for authentication. The simplest is for the server to be configured -with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey -configured in the `nkey_file` field. - -More details [here](https://docs.nats.io/developing-with-nats/security/nkey). - -#### User Credentials - -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an [user JWT](https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens) -and a corresponding [NKey secret](https://docs.nats.io/developing-with-nats/security/nkey) when connecting to a server -which is configured to use this authentication scheme. - -The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the [nsc tool](https://docs.nats.io/nats-tools/nsc). - -Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain -the plain text NKey Seed. - -More details [here](https://docs.nats.io/developing-with-nats/security/creds). - -## Fields - -### `urls` - -A list of URLs to connect to. If an item of the list contains commas it will be expanded into multiple URLs. - - -Type: `array` - -```yml -# Examples - -urls: - - nats://127.0.0.1:4222 - -urls: - - nats://username:password@127.0.0.1:4222 -``` - -### `subject` - -A subject to write to. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -subject: foo.bar.baz - -subject: ${! meta("kafka_topic") } - -subject: foo.${! json("meta.type") } -``` - -### `inbox_prefix` - -Set an explicit inbox prefix for the response subject - - -Type: `string` - -```yml -# Examples - -inbox_prefix: _INBOX_joe -``` - -### `headers` - -Explicit message headers to add to messages. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` -Default: `{}` - -```yml -# Examples - -headers: - Content-Type: application/json - Timestamp: ${!meta("Timestamp")} -``` - -### `metadata` - -Determine which (if any) metadata values should be added to messages as headers. - - -Type: `object` - -### `metadata.include_prefixes` - -Provide a list of explicit metadata key prefixes to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_prefixes: - - foo_ - - bar_ - -include_prefixes: - - kafka_ - -include_prefixes: - - content- -``` - -### `metadata.include_patterns` - -Provide a list of explicit metadata key regular expression (re2) patterns to match against. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -include_patterns: - - .* - -include_patterns: - - _timestamp_unix$ -``` - -### `timeout` - -A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as 300ms, -1.5h or 2h45m. Valid time units are ns, us (or µs), ms, s, m, h. - - -Type: `string` -Default: `"3s"` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `auth` - -Optional configuration of NATS authentication parameters. - - -Type: `object` - -### `auth.nkey_file` - -An optional file containing a NKey seed. - - -Type: `string` - -```yml -# Examples - -nkey_file: ./seed.nk -``` - -### `auth.user_credentials_file` - -An optional file containing user credentials which consist of an user JWT and corresponding NKey seed. - - -Type: `string` - -```yml -# Examples - -user_credentials_file: ./user.creds -``` - -### `auth.user_jwt` - -An optional plain text user JWT (given along with the corresponding user NKey Seed). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - -### `auth.user_nkey_seed` - -An optional plain text user NKey Seed (given along with the corresponding user JWT). -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` - - diff --git a/website/docs/components/processors/noop.md b/website/docs/components/processors/noop.md deleted file mode 100644 index c6d0a558c6..0000000000 --- a/website/docs/components/processors/noop.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: noop -slug: noop -type: processor -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Noop is a processor that does nothing, the message passes through unchanged. Why? Sometimes doing nothing is the braver option. - -```yml -# Config fields, showing default values -label: "" -noop: {} -``` - - diff --git a/website/docs/components/processors/parallel.md b/website/docs/components/processors/parallel.md deleted file mode 100644 index ac35ef7e5a..0000000000 --- a/website/docs/components/processors/parallel.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: parallel -slug: parallel -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A processor that applies a list of child processors to messages of a batch as though they were each a batch of one message (similar to the [`for_each`](/docs/components/processors/for_each) processor), but where each message is processed in parallel. - -```yml -# Config fields, showing default values -label: "" -parallel: - cap: 0 - processors: [] # No default (required) -``` - -The field `cap`, if greater than zero, caps the maximum number of parallel processing threads. - -The functionality of this processor depends on being applied across messages that are batched. You can find out more about batching [in this doc](/docs/configuration/batching). - -## Fields - -### `cap` - -The maximum number of messages to have processing at a given time. - - -Type: `int` -Default: `0` - -### `processors` - -A list of child processors to apply. - - -Type: `array` - - diff --git a/website/docs/components/processors/parquet.md b/website/docs/components/processors/parquet.md deleted file mode 100644 index 6ea5b859d2..0000000000 --- a/website/docs/components/processors/parquet.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: parquet -slug: parquet -type: processor -status: deprecated -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::warning DEPRECATED -This component is deprecated and will be removed in the next major version release. Please consider moving onto [alternative components](#alternatives). -::: -Converts batches of documents to or from [Parquet files](https://parquet.apache.org/docs/). - -Introduced in version 3.62.0. - -```yml -# Config fields, showing default values -label: "" -parquet: - operator: "" # No default (required) - compression: snappy - schema_file: schemas/foo.json # No default (optional) - schema: |- # No default (optional) - { - "Tag": "name=root, repetitiontype=REQUIRED", - "Fields": [ - {"Tag":"name=name,inname=NameIn,type=BYTE_ARRAY,convertedtype=UTF8, repetitiontype=REQUIRED"}, - {"Tag":"name=age,inname=Age,type=INT32,repetitiontype=REQUIRED"} - ] - } -``` - -### Alternatives - -This processor is now deprecated, it's recommended that you use the new [`parquet_decode`](/docs/components/processors/parquet_decode) and [`parquet_encode`](/docs/components/processors/parquet_encode) processors as they provide a number of advantages, the most important of which is better error messages for when schemas are mismatched or files could not be consumed. - -### Troubleshooting - -This processor is experimental and the error messages that it provides are often vague and unhelpful. An error message of the form `interface {} is nil, not ` implies that a field of the given type was expected but not found in the processed message when writing parquet files. - -Unfortunately the name of the field will sometimes be missing from the error, in which case it's worth double checking the schema you provided to make sure that there are no typos in the field names, and if that doesn't reveal the issue it can help to mark fields as OPTIONAL in the schema and gradually change them back to REQUIRED until the error returns. - -### Defining the Schema - -The schema must be specified as a JSON string, containing an object that describes the fields expected at the root of each document. Each field can itself have more fields defined, allowing for nested structures: - -```json -{ - "Tag": "name=root, repetitiontype=REQUIRED", - "Fields": [ - {"Tag": "name=name, inname=NameIn, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"}, - {"Tag": "name=age, inname=Age, type=INT32, repetitiontype=REQUIRED"}, - {"Tag": "name=id, inname=Id, type=INT64, repetitiontype=REQUIRED"}, - {"Tag": "name=weight, inname=Weight, type=FLOAT, repetitiontype=REQUIRED"}, - { - "Tag": "name=favPokemon, inname=FavPokemon, type=LIST, repetitiontype=OPTIONAL", - "Fields": [ - {"Tag": "name=name, inname=PokeName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"}, - {"Tag": "name=coolness, inname=Coolness, type=FLOAT, repetitiontype=REQUIRED"} - ] - } - ] -} -``` - -A schema can be derived from a source file using https://github.com/xitongsys/parquet-go/tree/master/tool/parquet-tools: - -```sh -./parquet-tools -cmd schema -file foo.parquet -``` - -## Fields - -### `operator` - -Determines whether the processor converts messages into a parquet file or expands parquet files into messages. Converting into JSON allows subsequent processors and mappings to convert the data into any other format. - - -Type: `string` - -| Option | Summary | -|---|---| -| `from_json` | Compress a batch of JSON documents into a file. | -| `to_json` | Expand a file into one or more JSON messages. | - - -### `compression` - -The type of compression to use when writing parquet files, this field is ignored when consuming parquet files. - - -Type: `string` -Default: `"snappy"` -Options: `uncompressed`, `snappy`, `gzip`, `lz4`, `zstd`. - -### `schema_file` - -A file path containing a schema used to describe the parquet files being generated or consumed, the format of the schema is a JSON document detailing the tag and fields of documents. The schema can be found at: https://pkg.go.dev/github.com/xitongsys/parquet-go#readme-json. Either a `schema_file` or `schema` field must be specified when creating Parquet files via the `from_json` operator. - - -Type: `string` - -```yml -# Examples - -schema_file: schemas/foo.json -``` - -### `schema` - -A schema used to describe the parquet files being generated or consumed, the format of the schema is a JSON document detailing the tag and fields of documents. The schema can be found at: https://pkg.go.dev/github.com/xitongsys/parquet-go#readme-json. Either a `schema_file` or `schema` field must be specified when creating Parquet files via the `from_json` operator. - - -Type: `string` - -```yml -# Examples - -schema: |- - { - "Tag": "name=root, repetitiontype=REQUIRED", - "Fields": [ - {"Tag":"name=name,inname=NameIn,type=BYTE_ARRAY,convertedtype=UTF8, repetitiontype=REQUIRED"}, - {"Tag":"name=age,inname=Age,type=INT32,repetitiontype=REQUIRED"} - ] - } -``` - - diff --git a/website/docs/components/processors/parquet_decode.md b/website/docs/components/processors/parquet_decode.md deleted file mode 100644 index d99be1ee50..0000000000 --- a/website/docs/components/processors/parquet_decode.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: parquet_decode -slug: parquet_decode -type: processor -status: experimental -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Decodes [Parquet files](https://parquet.apache.org/docs/) into a batch of structured messages. - -Introduced in version 4.4.0. - -```yml -# Config fields, showing default values -label: "" -parquet_decode: {} -``` - -This processor uses [https://github.com/parquet-go/parquet-go](https://github.com/parquet-go/parquet-go), which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. - -## Examples - - - - - -In this example we consume files from AWS S3 as they're written by listening onto an SQS queue for upload events. We make sure to use the `to_the_end` scanner which means files are read into memory in full, which then allows us to use a `parquet_decode` processor to expand each file into a batch of messages. Finally, we write the data out to local files as newline delimited JSON. - -```yaml -input: - aws_s3: - bucket: TODO - prefix: foos/ - scanner: - to_the_end: {} - sqs: - url: TODO - processors: - - parquet_decode: {} - -output: - file: - codec: lines - path: './foos/${! meta("s3_key") }.jsonl' -``` - - - - - diff --git a/website/docs/components/processors/parquet_encode.md b/website/docs/components/processors/parquet_encode.md deleted file mode 100644 index 7253396486..0000000000 --- a/website/docs/components/processors/parquet_encode.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: parquet_encode -slug: parquet_encode -type: processor -status: experimental -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Encodes [Parquet files](https://parquet.apache.org/docs/) from a batch of structured messages. - -Introduced in version 4.4.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -parquet_encode: - schema: [] # No default (required) - default_compression: uncompressed -``` - - - - -```yml -# All config fields, showing default values -label: "" -parquet_encode: - schema: [] # No default (required) - default_compression: uncompressed - default_encoding: DELTA_LENGTH_BYTE_ARRAY -``` - - - - -This processor uses [https://github.com/parquet-go/parquet-go](https://github.com/parquet-go/parquet-go), which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. - - -## Examples - - - - - -In this example we use the batching mechanism of an `aws_s3` output to collect a batch of messages in memory, which then converts it to a parquet file and uploads it. - -```yaml -output: - aws_s3: - bucket: TODO - path: 'stuff/${! timestamp_unix() }-${! uuid_v4() }.parquet' - batching: - count: 1000 - period: 10s - processors: - - parquet_encode: - schema: - - name: id - type: INT64 - - name: weight - type: DOUBLE - - name: content - type: BYTE_ARRAY - default_compression: zstd -``` - - - - -## Fields - -### `schema` - -Parquet schema. - - -Type: `array` - -### `schema[].name` - -The name of the column. - - -Type: `string` - -### `schema[].type` - -The type of the column, only applicable for leaf columns with no child fields. Some logical types can be specified here such as UTF8. - - -Type: `string` -Options: `BOOLEAN`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `BYTE_ARRAY`, `UTF8`. - -### `schema[].repeated` - -Whether the field is repeated. - - -Type: `bool` -Default: `false` - -### `schema[].optional` - -Whether the field is optional. - - -Type: `bool` -Default: `false` - -### `schema[].fields` - -A list of child fields. - - -Type: `array` - -```yml -# Examples - -fields: - - name: foo - type: INT64 - - name: bar - type: BYTE_ARRAY -``` - -### `default_compression` - -The default compression type to use for fields. - - -Type: `string` -Default: `"uncompressed"` -Options: `uncompressed`, `snappy`, `gzip`, `brotli`, `zstd`, `lz4raw`. - -### `default_encoding` - -The default encoding type to use for fields. A custom default encoding is only necessary when consuming data with libraries that do not support `DELTA_LENGTH_BYTE_ARRAY` and is therefore best left unset where possible. - - -Type: `string` -Default: `"DELTA_LENGTH_BYTE_ARRAY"` -Requires version 4.11.0 or newer -Options: `DELTA_LENGTH_BYTE_ARRAY`, `PLAIN`. - - diff --git a/website/docs/components/processors/parse_log.md b/website/docs/components/processors/parse_log.md deleted file mode 100644 index 739da5f59d..0000000000 --- a/website/docs/components/processors/parse_log.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: parse_log -slug: parse_log -type: processor -status: stable -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Parses common log [formats](#formats) into [structured data](#codecs). This is easier and often much faster than [`grok`](/docs/components/processors/grok). - - - - - - -```yml -# Common config fields, showing default values -label: "" -parse_log: - format: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -parse_log: - format: "" # No default (required) - best_effort: true - allow_rfc3339: true - default_year: current - default_timezone: UTC -``` - - - - -## Fields - -### `format` - -A common log [format](#formats) to parse. - - -Type: `string` -Options: `syslog_rfc5424`, `syslog_rfc3164`. - -### `best_effort` - -Still returns partially parsed messages even if an error occurs. - - -Type: `bool` -Default: `true` - -### `allow_rfc3339` - -Also accept timestamps in rfc3339 format while parsing. Applicable to format `syslog_rfc3164`. - - -Type: `bool` -Default: `true` - -### `default_year` - -Sets the strategy used to set the year for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. When set to `current` the current year will be set, when set to an integer that value will be used. Leave this field empty to not set a default year at all. - - -Type: `string` -Default: `"current"` - -### `default_timezone` - -Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) format. - - -Type: `string` -Default: `"UTC"` - -## Codecs - -Currently the only supported structured data codec is `json`. - -## Formats - -### `syslog_rfc5424` - -Attempts to parse a log following the [Syslog rfc5424](https://tools.ietf.org/html/rfc5424) spec. The resulting structured document may contain any of the following fields: - -- `message` (string) -- `timestamp` (string, RFC3339) -- `facility` (int) -- `severity` (int) -- `priority` (int) -- `version` (int) -- `hostname` (string) -- `procid` (string) -- `appname` (string) -- `msgid` (string) -- `structureddata` (object) - -### `syslog_rfc3164` - -Attempts to parse a log following the [Syslog rfc3164](https://tools.ietf.org/html/rfc3164) spec. The resulting structured document may contain any of the following fields: - -- `message` (string) -- `timestamp` (string, RFC3339) -- `facility` (int) -- `severity` (int) -- `priority` (int) -- `hostname` (string) -- `procid` (string) -- `appname` (string) -- `msgid` (string) - - diff --git a/website/docs/components/processors/processors.md b/website/docs/components/processors/processors.md deleted file mode 100644 index b00319fe26..0000000000 --- a/website/docs/components/processors/processors.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: processors -slug: processors -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A processor grouping several sub-processors. - -```yml -# Config fields, showing default values -label: "" -processors: [] -``` - -This processor is useful in situations where you want to collect several processors under a single resource identifier, whether it is for making your configuration easier to read and navigate, or for improving the testability of your configuration. The behaviour of child processors will match exactly the behaviour they would have under any other processors block. - -## Examples - - - - - -Imagine we have a collection of processors who cover a specific functionality. We could use this processor to group them together and make it easier to read and mock during testing by giving the whole block a label: - -```yaml -pipeline: - processors: - - label: my_super_feature - processors: - - log: - message: "Let's do something cool" - - archive: - format: json_array - - mapping: root.items = this -``` - - - - - diff --git a/website/docs/components/processors/protobuf.md b/website/docs/components/processors/protobuf.md deleted file mode 100644 index 309d886f72..0000000000 --- a/website/docs/components/processors/protobuf.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: protobuf -slug: protobuf -type: processor -status: stable -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - -Performs conversions to or from a protobuf message. This processor uses reflection, meaning conversions can be made directly from the target .proto files. - - -```yml -# Config fields, showing default values -label: "" -protobuf: - operator: "" # No default (required) - message: "" # No default (required) - discard_unknown: false - use_proto_names: false - import_paths: [] -``` - -The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: [https://developers.google.com/protocol-buffers/docs/proto3#json](https://developers.google.com/protocol-buffers/docs/proto3#json) - -Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at [https://github.com/benthosdev/benthos-plugin-example](https://github.com/benthosdev/benthos-plugin-example) - -## Operators - -### `to_json` - -Converts protobuf messages into a generic JSON structure. This makes it easier to manipulate the contents of the document within Benthos. - -### `from_json` - -Attempts to create a target protobuf message from a generic JSON structure. - - -## Examples - - - - - - -If we have the following protobuf definition within a directory called `testing/schema`: - -```protobuf -syntax = "proto3"; -package testing; - -import "google/protobuf/timestamp.proto"; - -message Person { - string first_name = 1; - string last_name = 2; - string full_name = 3; - int32 age = 4; - int32 id = 5; // Unique ID number for this person. - string email = 6; - - google.protobuf.Timestamp last_updated = 7; -} -``` - -And a stream of JSON documents of the form: - -```json -{ - "firstName": "caleb", - "lastName": "quaye", - "email": "caleb@myspace.com" -} -``` - -We can convert the documents into protobuf messages with the following config: - -```yaml -pipeline: - processors: - - protobuf: - operator: from_json - message: testing.Person - import_paths: [ testing/schema ] -``` - - - - - -If we have the following protobuf definition within a directory called `testing/schema`: - -```protobuf -syntax = "proto3"; -package testing; - -import "google/protobuf/timestamp.proto"; - -message Person { - string first_name = 1; - string last_name = 2; - string full_name = 3; - int32 age = 4; - int32 id = 5; // Unique ID number for this person. - string email = 6; - - google.protobuf.Timestamp last_updated = 7; -} -``` - -And a stream of protobuf messages of the type `Person`, we could convert them into JSON documents of the format: - -```json -{ - "firstName": "caleb", - "lastName": "quaye", - "email": "caleb@myspace.com" -} -``` - -With the following config: - -```yaml -pipeline: - processors: - - protobuf: - operator: to_json - message: testing.Person - import_paths: [ testing/schema ] -``` - - - - -## Fields - -### `operator` - -The [operator](#operators) to execute - - -Type: `string` -Options: `to_json`, `from_json`. - -### `message` - -The fully qualified name of the protobuf message to convert to/from. - - -Type: `string` - -### `discard_unknown` - -If `true`, the `from_json` operator discards fields that are unknown to the schema. - - -Type: `bool` -Default: `false` - -### `use_proto_names` - -If `true`, the `to_json` operator deserializes fields exactly as named in schema file. - - -Type: `bool` -Default: `false` - -### `import_paths` - -A list of directories containing .proto files, including all definitions required for parsing the target message. If left empty the current directory is used. Each directory listed will be walked with all found .proto files imported. - - -Type: `array` -Default: `[]` - - diff --git a/website/docs/components/processors/rate_limit.md b/website/docs/components/processors/rate_limit.md deleted file mode 100644 index f040fab2d5..0000000000 --- a/website/docs/components/processors/rate_limit.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: rate_limit -slug: rate_limit -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Throttles the throughput of a pipeline according to a specified [`rate_limit`](/docs/components/rate_limits/about) resource. Rate limits are shared across components and therefore apply globally to all processing pipelines. - -```yml -# Config fields, showing default values -label: "" -rate_limit: - resource: "" # No default (required) -``` - -## Fields - -### `resource` - -The target [`rate_limit` resource](/docs/components/rate_limits/about). - - -Type: `string` - - diff --git a/website/docs/components/processors/redis.md b/website/docs/components/processors/redis.md deleted file mode 100644 index 8144260781..0000000000 --- a/website/docs/components/processors/redis.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -title: redis -slug: redis -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Performs actions against Redis that aren't possible using a [`cache`](/docs/components/processors/cache) processor. Actions are -performed for each message and the message contents are replaced with the result. In order to merge the result into the original message compose this processor within a [`branch` processor](/docs/components/processors/branch). - - - - - - -```yml -# Common config fields, showing default values -label: "" -redis: - url: redis://:6397 # No default (required) - command: scard # No default (optional) - args_mapping: root = [ this.key ] # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -label: "" -redis: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - command: scard # No default (optional) - args_mapping: root = [ this.key ] # No default (optional) - retries: 3 - retry_period: 500ms -``` - - - - -## Examples - - - - - -If given payloads containing a metadata field `set_key` it's possible to query and store the cardinality of the set for each message using a [`branch` processor](/docs/components/processors/branch) in order to augment rather than replace the message contents: - -```yaml -pipeline: - processors: - - branch: - processors: - - redis: - url: TODO - command: scard - args_mapping: 'root = [ meta("set_key") ]' - result_map: 'root.cardinality = this' -``` - - - - -If we have JSON data containing number of friends visited during covid 19: - -```json -{"name":"ash","month":"feb","year":2019,"friends_visited":10} -{"name":"ash","month":"apr","year":2019,"friends_visited":-2} -{"name":"bob","month":"feb","year":2019,"friends_visited":3} -{"name":"bob","month":"apr","year":2019,"friends_visited":1} -``` - -We can add a field that contains the running total number of friends visited: - -```json -{"name":"ash","month":"feb","year":2019,"friends_visited":10,"total":10} -{"name":"ash","month":"apr","year":2019,"friends_visited":-2,"total":8} -{"name":"bob","month":"feb","year":2019,"friends_visited":3,"total":3} -{"name":"bob","month":"apr","year":2019,"friends_visited":1,"total":4} -``` - -Using the `incrby` command: - -```yaml -pipeline: - processors: - - branch: - processors: - - redis: - url: TODO - command: incrby - args_mapping: 'root = [ this.name, this.friends_visited ]' - result_map: 'root.total = this' -``` - - - - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `command` - -The command to execute. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` -Requires version 4.3.0 or newer - -```yml -# Examples - -command: scard - -command: incrby - -command: ${! meta("command") } -``` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis command. - - -Type: `string` -Requires version 4.3.0 or newer - -```yml -# Examples - -args_mapping: root = [ this.key ] - -args_mapping: root = [ meta("kafka_key"), this.count ] -``` - -### `retries` - -The maximum number of retries before abandoning a request. - - -Type: `int` -Default: `3` - -### `retry_period` - -The time to wait before consecutive retry attempts. - - -Type: `string` -Default: `"500ms"` - - diff --git a/website/docs/components/processors/redis_script.md b/website/docs/components/processors/redis_script.md deleted file mode 100644 index 3bb5665f53..0000000000 --- a/website/docs/components/processors/redis_script.md +++ /dev/null @@ -1,357 +0,0 @@ ---- -title: redis_script -slug: redis_script -type: processor -status: beta -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Performs actions against Redis using [LUA scripts](https://redis.io/docs/manual/programmability/eval-intro/). - -Introduced in version 4.11.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -redis_script: - url: redis://:6397 # No default (required) - script: return redis.call('set', KEYS[1], ARGV[1]) # No default (required) - args_mapping: root = [ this.key ] # No default (required) - keys_mapping: root = [ this.key ] # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -redis_script: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - script: return redis.call('set', KEYS[1], ARGV[1]) # No default (required) - args_mapping: root = [ this.key ] # No default (required) - keys_mapping: root = [ this.key ] # No default (required) - retries: 3 - retry_period: 500ms -``` - - - - -Actions are performed for each message and the message contents are replaced with the result. - -In order to merge the result into the original message compose this processor within a [`branch` processor](/docs/components/processors/branch). - -## Examples - - - - - -The following example will use a script execution to get next element from a sorted set and set its score with timestamp unix nano value. - -```yaml -pipeline: - processors: - - redis_script: - url: TODO - script: | - local value = redis.call("ZRANGE", KEYS[1], '0', '0') - - if next(elements) == nil then - return '' - end - - redis.call("ZADD", "XX", KEYS[1], ARGV[1], value) - - return value - keys_mapping: 'root = [ meta("key") ]' - args_mapping: 'root = [ timestamp_unix_nano() ]' -``` - - - - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `script` - -A script to use for the target operator. It has precedence over the 'command' field. - - -Type: `string` - -```yml -# Examples - -script: return redis.call('set', KEYS[1], ARGV[1]) -``` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of arguments required for the specified Redis script. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.key ] - -args_mapping: root = [ meta("kafka_key"), "hardcoded_value" ] -``` - -### `keys_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of keys matching in size to the number of arguments required for the specified Redis script. - - -Type: `string` - -```yml -# Examples - -keys_mapping: root = [ this.key ] - -keys_mapping: root = [ meta("kafka_key"), this.count ] -``` - -### `retries` - -The maximum number of retries before abandoning a request. - - -Type: `int` -Default: `3` - -### `retry_period` - -The time to wait before consecutive retry attempts. - - -Type: `string` -Default: `"500ms"` - - diff --git a/website/docs/components/processors/resource.md b/website/docs/components/processors/resource.md deleted file mode 100644 index c181d3962b..0000000000 --- a/website/docs/components/processors/resource.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: resource -slug: resource -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Resource is a processor type that runs a processor resource identified by its label. - -```yml -# Config fields, showing default values -resource: "" -``` - -This processor allows you to reference the same configured processor resource in multiple places, and can also tidy up large nested configs. For example, the config: - -```yaml -pipeline: - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() -``` - -Is equivalent to: - -```yaml -pipeline: - processors: - - resource: foo_proc - -processor_resources: - - label: foo_proc - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() -``` - -You can find out more about resources [in this document.](/docs/configuration/resources) - - diff --git a/website/docs/components/processors/retry.md b/website/docs/components/processors/retry.md deleted file mode 100644 index b1386fefb8..0000000000 --- a/website/docs/components/processors/retry.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: retry -slug: retry -type: processor -status: beta -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Attempts to execute a series of child processors until success. - -Introduced in version 4.27.0. - -```yml -# Config fields, showing default values -label: "" -retry: - backoff: - initial_interval: 500ms - max_interval: 10s - max_elapsed_time: 1m - processors: [] # No default (required) - parallel: false -``` - -Executes child processors and if a resulting message is errored then, after a specified backoff period, the same original message will be attempted again through those same processors. If the child processors result in more than one message then the retry mechanism will kick in if _any_ of the resulting messages are errored. - -It is important to note that any mutations performed on the message during these child processors will be discarded for the next retry, and therefore it is safe to assume that each execution of the child processors will always be performed on the data as it was when it first reached the retry processor. - -By default the retry backoff has a specified [`max_elapsed_time`](#backoffmax_elapsed_time), if this time period is reached during retries and an error still occurs these errored messages will proceed through to the next processor after the retry (or your outputs). Normal [error handling patterns](/docs/configuration/error_handling) can be used on these messages. - -In order to avoid permanent loops any error associated with messages as they first enter a retry processor will be cleared. - -:::caution Batching -If you wish to wrap a batch-aware series of processors then take a look at the [batching section](#batching) below. -::: - - -## Examples - - - - - - -Here we have a config where I generate animal noises and send them to Taz via HTTP. Taz has a tendency to stop his servers whenever I dispatch my animals upon him, and therefore these HTTP requests sometimes fail. However, I have the retry processor and with this super power I can specify a back off policy and it will ensure that for each animal noise the HTTP processor is attempted until either it succeeds or my Benthos instance is stopped. - -I even go as far as to zero-out the maximum elapsed time field, which means that for each animal noise I will wait indefinitely, because I really really want Taz to receive every single animal noise that he is entitled to. - -```yaml -input: - generate: - interval: 1s - mapping: 'root.noise = [ "woof", "meow", "moo", "quack" ].index(random_int(min: 0, max: 3))' - -pipeline: - processors: - - retry: - backoff: - initial_interval: 100ms - max_interval: 5s - max_elapsed_time: 0s - processors: - - http: - url: 'http://example.com/try/not/to/dox/taz' - verb: POST - -output: - # Drop everything because it's junk data, I don't want it lol - drop: {} -``` - - - - -## Fields - -### `backoff` - -Determine time intervals and cut offs for retry attempts. - - -Type: `object` - -### `backoff.initial_interval` - -The initial period to wait between retry attempts. - - -Type: `string` -Default: `"500ms"` - -```yml -# Examples - -initial_interval: 50ms - -initial_interval: 1s -``` - -### `backoff.max_interval` - -The maximum period to wait between retry attempts - - -Type: `string` -Default: `"10s"` - -```yml -# Examples - -max_interval: 5s - -max_interval: 1m -``` - -### `backoff.max_elapsed_time` - -The maximum overall period of time to spend on retry attempts before the request is aborted. Setting this value to a zeroed duration (such as `0s`) will result in unbounded retries. - - -Type: `string` -Default: `"1m"` - -```yml -# Examples - -max_elapsed_time: 1m - -max_elapsed_time: 1h -``` - -### `processors` - -A list of [processors](/docs/components/processors/about/) to execute on each message. - - -Type: `array` - -### `parallel` - -When processing batches of messages these batches are ignored and the processors apply to each message sequentially. However, when this field is set to `true` each message will be processed in parallel. Caution should be made to ensure that batch sizes do not surpass a point where this would cause resource (CPU, memory, API limits) contention. - - -Type: `bool` -Default: `false` - -## Batching - -When messages are batched the child processors of a retry are executed for each individual message in isolation, performed serially by default but in parallel when the field [`parallel`](#parallel) is set to `true`. This is an intentional limitation of the retry processor and is done in order to ensure that errors are correctly associated with a given input message. Otherwise, the archiving, expansion, grouping, filtering and so on of the child processors could obfuscate this relationship. - -If the target behaviour of your retried processors is "batch aware", in that you wish to perform some processing across the entire batch of messages and repeat it in the event of errors, you can use an [`archive` processor](/docs/components/processors/archive) to collapse the batch into an individual message. Then, within these child processors either perform your batch aware processing on the archive, or use an [`unarchive` processor](/docs/components/processors/unarchive) in order to expand the single message back out into a batch. - -For example, if the retry processor were being used to wrap an HTTP request where the payload data is a batch archived into a JSON array it should look something like this: - -```yaml -pipeline: - processors: - - archive: - format: json_array - - retry: - processors: - - http: - url: example.com/nope - verb: POST - - unarchive: - format: json_array -``` - - diff --git a/website/docs/components/processors/schema_registry_decode.md b/website/docs/components/processors/schema_registry_decode.md deleted file mode 100644 index 52a9e55d25..0000000000 --- a/website/docs/components/processors/schema_registry_decode.md +++ /dev/null @@ -1,383 +0,0 @@ ---- -title: schema_registry_decode -slug: schema_registry_decode -type: processor -status: beta -categories: ["Parsing","Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Automatically decodes and validates messages with schemas from a Confluent Schema Registry service. - - - - - - -```yml -# Common config fields, showing default values -label: "" -schema_registry_decode: - url: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -schema_registry_decode: - avro_raw_json: false - url: "" # No default (required) - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} - tls: - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] -``` - - - - -Decodes messages automatically from a schema stored within a [Confluent Schema Registry service](https://docs.confluent.io/platform/current/schema-registry/index.html) by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. - -### Avro JSON Format - -This processor creates documents formatted as [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding) when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - -- if its type is `null`, then it is encoded as a JSON `null`; -- otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. - -For example, the union schema `["null","string","Foo"]`, where `Foo` is a record name, would encode: - -- `null` as `null`; -- the string `"a"` as `{"string": "a"}`; and -- a `Foo` instance as `{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. - -However, it is possible to instead create documents in [standard/raw JSON format](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) by setting the field [`avro_raw_json`](#avro_raw_json) to `true`. -### Protobuf Format - -This processor decodes protobuf messages to JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json - - -## Fields - -### `avro_raw_json` - -Whether Avro messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding). If `true` the schema returned from the subject should be decoded as [standard json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) instead of as [avro json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec). There is a [comment in goavro](https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249), the [underlining library used for avro serialization](https://github.com/linkedin/goavro), that explains in more detail the difference between the standard json and avro json. - - -Type: `bool` -Default: `false` - -### `url` - -The base URL of the schema registry service. - - -Type: `string` - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` -Requires version 4.7.0 or newer - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` -Requires version 4.7.0 or newer - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` -Requires version 4.7.0 or newer - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - - diff --git a/website/docs/components/processors/schema_registry_encode.md b/website/docs/components/processors/schema_registry_encode.md deleted file mode 100644 index 238ee47bab..0000000000 --- a/website/docs/components/processors/schema_registry_encode.md +++ /dev/null @@ -1,435 +0,0 @@ ---- -title: schema_registry_encode -slug: schema_registry_encode -type: processor -status: beta -categories: ["Parsing","Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Automatically encodes and validates messages with schemas from a Confluent Schema Registry service. - -Introduced in version 3.58.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -schema_registry_encode: - url: "" # No default (required) - subject: foo # No default (required) - refresh_period: 10m -``` - - - - -```yml -# All config fields, showing default values -label: "" -schema_registry_encode: - url: "" # No default (required) - subject: foo # No default (required) - refresh_period: 10m - avro_raw_json: false - oauth: - enabled: false - consumer_key: "" - consumer_secret: "" - access_token: "" - access_token_secret: "" - basic_auth: - enabled: false - username: "" - password: "" - jwt: - enabled: false - private_key_file: "" - signing_method: "" - claims: {} - headers: {} - tls: - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] -``` - - - - -Encodes messages automatically from schemas obtains from a [Confluent Schema Registry service](https://docs.confluent.io/platform/current/schema-registry/index.html) by polling the service for the latest schema version for target subjects. - -If a message fails to encode under the schema then it will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. - -### Avro JSON Format - -By default this processor expects documents formatted as [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding) when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - -- if its type is `null`, then it is encoded as a JSON `null`; -- otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. - -For example, the union schema `["null","string","Foo"]`, where `Foo` is a record name, would encode: - -- `null` as `null`; -- the string `"a"` as `{"string": "a"}`; and -- a `Foo` instance as `{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. - -However, it is possible to instead consume documents in [standard/raw JSON format](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) by setting the field [`avro_raw_json`](#avro_raw_json) to `true`. - -#### Known Issues - -Important! There is an outstanding issue in the [avro serializing library](https://github.com/linkedin/goavro) that benthos uses which means it [doesn't encode logical types correctly](https://github.com/linkedin/goavro/issues/252). It's still possible to encode logical types that are in-line with the spec if `avro_raw_json` is set to true, though now of course non-logical types will not be in-line with the spec. - -### Protobuf Format - -This processor encodes protobuf messages either from any format parsed within Benthos (encoded as JSON by default), or from raw JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json - -#### Multiple Message Support - -When a target subject presents a protobuf schema that contains multiple messages it becomes ambiguous which message definition a given input data should be encoded against. In such scenarios Benthos will attempt to encode the data against each of them and select the first to successfully match against the data, this process currently *ignores all nested message definitions*. In order to speed up this exhaustive search the last known successful message will be attempted first for each subsequent input. - -We will be considering alternative approaches in future so please [get in touch](/community) with thoughts and feedback. - - -## Fields - -### `url` - -The base URL of the schema registry service. - - -Type: `string` - -### `subject` - -The schema subject to derive schemas from. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -subject: foo - -subject: ${! meta("kafka_topic") } -``` - -### `refresh_period` - -The period after which a schema is refreshed for each subject, this is done by polling the schema registry service. - - -Type: `string` -Default: `"10m"` - -```yml -# Examples - -refresh_period: 60s - -refresh_period: 1h -``` - -### `avro_raw_json` - -Whether messages encoded in Avro format should be parsed as normal JSON ("json that meets the expectations of regular internet json") rather than [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding). If `true` the schema returned from the subject should be parsed as [standard json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) instead of as [avro json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec). There is a [comment in goavro](https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249), the [underlining library used for avro serialization](https://github.com/linkedin/goavro), that explains in more detail the difference between standard json and avro json. - - -Type: `bool` -Default: `false` -Requires version 3.59.0 or newer - -### `oauth` - -Allows you to specify open authentication via OAuth version 1. - - -Type: `object` -Requires version 4.7.0 or newer - -### `oauth.enabled` - -Whether to use OAuth version 1 in requests. - - -Type: `bool` -Default: `false` - -### `oauth.consumer_key` - -A value used to identify the client to the service provider. - - -Type: `string` -Default: `""` - -### `oauth.consumer_secret` - -A secret used to establish ownership of the consumer key. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `oauth.access_token` - -A value used to gain access to the protected resources on behalf of the user. - - -Type: `string` -Default: `""` - -### `oauth.access_token_secret` - -A secret provided in order to establish ownership of a given access token. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `basic_auth` - -Allows you to specify basic authentication. - - -Type: `object` -Requires version 4.7.0 or newer - -### `basic_auth.enabled` - -Whether to use basic authentication in requests. - - -Type: `bool` -Default: `false` - -### `basic_auth.username` - -A username to authenticate as. - - -Type: `string` -Default: `""` - -### `basic_auth.password` - -A password to authenticate with. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `jwt` - -BETA: Allows you to specify JWT authentication. - - -Type: `object` -Requires version 4.7.0 or newer - -### `jwt.enabled` - -Whether to use JWT authentication in requests. - - -Type: `bool` -Default: `false` - -### `jwt.private_key_file` - -A file with the PEM encoded via PKCS1 or PKCS8 as private key. - - -Type: `string` -Default: `""` - -### `jwt.signing_method` - -A method used to sign the token such as RS256, RS384, RS512 or EdDSA. - - -Type: `string` -Default: `""` - -### `jwt.claims` - -A value used to identify the claims that issued the JWT. - - -Type: `object` -Default: `{}` - -### `jwt.headers` - -Add optional key/value headers to the JWT. - - -Type: `object` -Default: `{}` - -### `tls` - -Custom TLS settings can be used to override system defaults. - - -Type: `object` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - - diff --git a/website/docs/components/processors/select_parts.md b/website/docs/components/processors/select_parts.md deleted file mode 100644 index 8640f263f8..0000000000 --- a/website/docs/components/processors/select_parts.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: select_parts -slug: select_parts -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Cherry pick a set of messages from a batch by their index. Indexes larger than the number of messages are simply ignored. - -```yml -# Config fields, showing default values -label: "" -select_parts: - parts: [] -``` - -The selected parts are added to the new message batch in the same order as the selection array. E.g. with 'parts' set to [ 2, 0, 1 ] and the message parts [ '0', '1', '2', '3' ], the output will be [ '2', '0', '1' ]. - -If none of the selected parts exist in the input batch (resulting in an empty output message) the batch is dropped entirely. - -Message indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. E.g. if index = -1 then the selected part will be the last part of the message, if index = -2 then the part before the last element with be selected, and so on. - -This processor is only applicable to [batched messages](/docs/configuration/batching). - -## Fields - -### `parts` - -An array of message indexes of a batch. Indexes can be negative, and if so the part will be selected from the end counting backwards starting from -1. - - -Type: `array` -Default: `[]` - - diff --git a/website/docs/components/processors/sentry_capture.md b/website/docs/components/processors/sentry_capture.md deleted file mode 100644 index 17e71094aa..0000000000 --- a/website/docs/components/processors/sentry_capture.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: sentry_capture -slug: sentry_capture -type: processor -status: experimental ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Captures log events from messages and submits them to [Sentry](https://sentry.io/). - -Introduced in version 4.16.0. - -```yml -# Config fields, showing default values -label: "" -sentry_capture: - dsn: "" - message: webhook event received # No default (required) - context: 'root = {"order": {"product_id": "P93174", "quantity": 5}}' # No default (optional) - tags: {} # No default (optional) - environment: "" - release: "" - level: INFO - transport_mode: async - flush_timeout: 5s - sampling_rate: 1 -``` - -## Fields - -### `dsn` - -The DSN address to send sentry events to. If left empty, then SENTRY_DSN is used. - - -Type: `string` -Default: `""` - -### `message` - -A message to set on the sentry event -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - -```yml -# Examples - -message: webhook event received - -message: 'failed to find product in database: ${! error() }' -``` - -### `context` - -A mapping that must evaluate to an object-of-objects or `deleted()`. If this mapping produces a value, then it is set on a sentry event as additional context. - - -Type: `string` - -```yml -# Examples - -context: 'root = {"order": {"product_id": "P93174", "quantity": 5}}' - -context: root = deleted() -``` - -### `tags` - -Sets key/value string tags on an event. Unlike context, these are indexed and searchable on Sentry but have length limitations. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `object` - -### `environment` - -The environment to be sent with events. If left empty, then SENTRY_ENVIRONMENT is used. - - -Type: `string` -Default: `""` - -### `release` - -The version of the code deployed to an environment. If left empty, then the Sentry client will attempt to detect the release from the environment. - - -Type: `string` -Default: `""` - -### `level` - -Sets the level on sentry events similar to logging levels. - - -Type: `string` -Default: `"INFO"` -Options: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`. - -### `transport_mode` - -Determines how events are sent. A sync transport will block when sending each event until a response is received from the Sentry server. The recommended async transport will enqueue events in a buffer and send them in the background. - - -Type: `string` -Default: `"async"` -Options: `async`, `sync`. - -### `flush_timeout` - -The duration to wait when closing the processor to flush any remaining enqueued events. - - -Type: `string` -Default: `"5s"` - -### `sampling_rate` - -The rate at which events are sent to the server. A value of 0 disables capturing sentry events entirely. A value of 1 results in sending all events to Sentry. Any value in between results sending some percentage of events. - - -Type: `float` -Default: `1` - - diff --git a/website/docs/components/processors/sleep.md b/website/docs/components/processors/sleep.md deleted file mode 100644 index 1f0ba2d805..0000000000 --- a/website/docs/components/processors/sleep.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: sleep -slug: sleep -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Sleep for a period of time specified as a duration string for each message. This processor will interpolate functions within the `duration` field, you can find a list of functions [here](/docs/configuration/interpolation#bloblang-queries). - -```yml -# Config fields, showing default values -label: "" -sleep: - duration: "" # No default (required) -``` - -## Fields - -### `duration` - -The duration of time to sleep for each execution. -This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries). - - -Type: `string` - - diff --git a/website/docs/components/processors/split.md b/website/docs/components/processors/split.md deleted file mode 100644 index e03f83eacc..0000000000 --- a/website/docs/components/processors/split.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: split -slug: split -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Breaks message batches (synonymous with multiple part messages) into smaller batches. The size of the resulting batches are determined either by a discrete size or, if the field `byte_size` is non-zero, then by total size in bytes (which ever limit is reached first). - -```yml -# Config fields, showing default values -label: "" -split: - size: 1 - byte_size: 0 -``` - -This processor is for breaking batches down into smaller ones. In order to break a single message out into multiple messages use the [`unarchive` processor](/docs/components/processors/unarchive). - -If there is a remainder of messages after splitting a batch the remainder is also sent as a single batch. For example, if your target size was 10, and the processor received a batch of 95 message parts, the result would be 9 batches of 10 messages followed by a batch of 5 messages. - -## Fields - -### `size` - -The target number of messages. - - -Type: `int` -Default: `1` - -### `byte_size` - -An optional target of total message bytes. - - -Type: `int` -Default: `0` - - diff --git a/website/docs/components/processors/sql.md b/website/docs/components/processors/sql.md deleted file mode 100644 index 90700dca6d..0000000000 --- a/website/docs/components/processors/sql.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: sql -slug: sql -type: processor -status: deprecated -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::warning DEPRECATED -This component is deprecated and will be removed in the next major version release. Please consider moving onto [alternative components](#alternatives). -::: -Runs an arbitrary SQL query against a database and (optionally) returns the result as an array of objects, one for each row returned. - -Introduced in version 3.65.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -sql: - driver: "" # No default (required) - data_source_name: "" # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - result_codec: none -``` - - - - -```yml -# All config fields, showing default values -label: "" -sql: - driver: "" # No default (required) - data_source_name: "" # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - unsafe_dynamic_query: false - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - result_codec: none -``` - - - - -If the query fails to execute then the message will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -## Alternatives - -For basic inserts or select queries use either the [`sql_insert`](/docs/components/processors/sql_insert) or the [`sql_select`](/docs/components/processors/sql_select) processor. For more complex queries use the [`sql_raw`](/docs/components/processors/sql_raw) processor. - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `data_source_name` - -Data source name. - - -Type: `string` - -### `query` - -The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: - -| Driver | Placeholder Style | -|---|---| -| `clickhouse` | Dollar sign | -| `mysql` | Question mark | -| `postgres` | Dollar sign | -| `mssql` | Question mark | -| `sqlite` | Question mark | -| `oracle` | Colon | -| `snowflake` | Question mark | -| `trino` | Question mark | -| `gocosmos` | Colon | - - -Type: `string` - -```yml -# Examples - -query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); -``` - -### `unsafe_dynamic_query` - -Whether to enable [interpolation functions](/docs/configuration/interpolation/#bloblang-queries) in the query. Great care should be made to ensure your queries are defended against injection attacks. - - -Type: `bool` -Default: `false` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `result_codec` - -Result codec. - - -Type: `string` -Default: `"none"` - - diff --git a/website/docs/components/processors/sql_insert.md b/website/docs/components/processors/sql_insert.md deleted file mode 100644 index 704f15d747..0000000000 --- a/website/docs/components/processors/sql_insert.md +++ /dev/null @@ -1,295 +0,0 @@ ---- -title: sql_insert -slug: sql_insert -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Inserts rows into an SQL database for each message, and leaves the message unchanged. - -Introduced in version 3.59.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -sql_insert: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -sql_insert: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (required) - prefix: "" # No default (optional) - suffix: ON CONFLICT (name) DO NOTHING # No default (optional) - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) -``` - - - - -If the insert fails to execute then the message will still remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -## Examples - - - - - - -Here we insert rows into a database by populating the columns id, name and topic with values extracted from messages and metadata: - -```yaml -pipeline: - processors: - - sql_insert: - driver: mysql - dsn: foouser:foopassword@tcp(localhost:3306)/foodb - table: footable - columns: [ id, name, topic ] - args_mapping: | - root = [ - this.user.id, - this.user.name, - meta("kafka_topic"), - ] -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `table` - -The table to insert to. - - -Type: `string` - -```yml -# Examples - -table: foo -``` - -### `columns` - -A list of columns to insert. - - -Type: `array` - -```yml -# Examples - -columns: - - foo - - bar - - baz -``` - -### `args_mapping` - -A [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of columns specified. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `prefix` - -An optional prefix to prepend to the insert query (before INSERT). - - -Type: `string` - -### `suffix` - -An optional suffix to append to the insert query. - - -Type: `string` - -```yml -# Examples - -suffix: ON CONFLICT (name) DO NOTHING -``` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - - diff --git a/website/docs/components/processors/sql_raw.md b/website/docs/components/processors/sql_raw.md deleted file mode 100644 index 3b06c0270f..0000000000 --- a/website/docs/components/processors/sql_raw.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -title: sql_raw -slug: sql_raw -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Runs an arbitrary SQL query against a database and (optionally) returns the result as an array of objects, one for each row returned. - -Introduced in version 3.65.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -sql_raw: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - exec_only: false -``` - - - - -```yml -# All config fields, showing default values -label: "" -sql_raw: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); # No default (required) - unsafe_dynamic_query: false - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - exec_only: false - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) -``` - - - - -If the query fails to execute then the message will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -## Examples - - - - - -The following example inserts rows into the table footable with the columns foo, bar and baz populated with values extracted from messages. - -```yaml -pipeline: - processors: - - sql_raw: - driver: mysql - dsn: foouser:foopassword@tcp(localhost:3306)/foodb - query: "INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?);" - args_mapping: '[ document.foo, document.bar, meta("kafka_topic") ]' - exec_only: true -``` - - - - -Here we query a database for columns of footable that share a `user_id` with the message field `user.id`. A [`branch` processor](/docs/components/processors/branch) is used in order to insert the resulting array into the original message at the path `foo_rows`. - -```yaml -pipeline: - processors: - - branch: - processors: - - sql_raw: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - query: "SELECT * FROM footable WHERE user_id = $1;" - args_mapping: '[ this.user.id ]' - result_map: 'root.foo_rows = this' -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `query` - -The query to execute. The style of placeholder to use depends on the driver, some drivers require question marks (`?`) whereas others expect incrementing dollar signs (`$1`, `$2`, and so on) or colons (`:1`, `:2` and so on). The style to use is outlined in this table: - -| Driver | Placeholder Style | -|---|---| -| `clickhouse` | Dollar sign | -| `mysql` | Question mark | -| `postgres` | Dollar sign | -| `mssql` | Question mark | -| `sqlite` | Question mark | -| `oracle` | Colon | -| `snowflake` | Question mark | -| `trino` | Question mark | -| `gocosmos` | Colon | - - -Type: `string` - -```yml -# Examples - -query: INSERT INTO footable (foo, bar, baz) VALUES (?, ?, ?); - -query: SELECT * FROM footable WHERE user_id = $1; -``` - -### `unsafe_dynamic_query` - -Whether to enable [interpolation functions](/docs/configuration/interpolation/#bloblang-queries) in the query. Great care should be made to ensure your queries are defended against injection attacks. - - -Type: `bool` -Default: `false` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `query`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `exec_only` - -Whether the query result should be discarded. When set to `true` the message contents will remain unchanged, which is useful in cases where you are executing inserts, updates, etc. - - -Type: `bool` -Default: `false` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - - diff --git a/website/docs/components/processors/sql_select.md b/website/docs/components/processors/sql_select.md deleted file mode 100644 index 06f546014a..0000000000 --- a/website/docs/components/processors/sql_select.md +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: sql_select -slug: sql_select -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Runs an SQL select query against a database and returns the result as an array of objects, one for each row returned, containing a key for each column queried and its value. - -Introduced in version 3.59.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -sql_select: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - where: meow = ? and woof = ? # No default (optional) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -label: "" -sql_select: - driver: "" # No default (required) - dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 # No default (required) - table: foo # No default (required) - columns: [] # No default (required) - where: meow = ? and woof = ? # No default (optional) - args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] # No default (optional) - prefix: "" # No default (optional) - suffix: "" # No default (optional) - init_files: [] # No default (optional) - init_statement: | # No default (optional) - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; - conn_max_idle_time: "" # No default (optional) - conn_max_life_time: "" # No default (optional) - conn_max_idle: 2 - conn_max_open: 0 # No default (optional) -``` - - - - -If the query fails to execute then the message will remain unchanged and the error can be caught using error handling methods outlined [here](/docs/configuration/error_handling). - -## Examples - - - - - - -Here we query a database for columns of footable that share a `user_id` -with the message `user.id`. A [`branch` processor](/docs/components/processors/branch) -is used in order to insert the resulting array into the original message at the -path `foo_rows`: - -```yaml -pipeline: - processors: - - branch: - processors: - - sql_select: - driver: postgres - dsn: postgres://foouser:foopass@localhost:5432/testdb?sslmode=disable - table: footable - columns: [ '*' ] - where: user_id = ? - args_mapping: '[ this.user.id ]' - result_map: 'root.foo_rows = this' -``` - - - - -## Fields - -### `driver` - -A database [driver](#drivers) to use. - - -Type: `string` -Options: `mysql`, `postgres`, `clickhouse`, `mssql`, `sqlite`, `oracle`, `snowflake`, `trino`, `gocosmos`. - -### `dsn` - -A Data Source Name to identify the target database. - -#### Drivers - -The following is a list of supported drivers, their placeholder style, and their respective DSN formats: - -| Driver | Data Source Name Format | -|---|---| -| `clickhouse` | [`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`](https://github.com/ClickHouse/clickhouse-go#dsn) | -| `mysql` | `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` | -| `postgres` | `postgres://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` | -| `mssql` | `sqlserver://[user[:password]@][netloc][:port][?database=dbname¶m1=value1&...]` | -| `sqlite` | `file:/path/to/filename.db[?param&=value1&...]` | -| `oracle` | `oracle://[username[:password]@][netloc][:port]/service_name?server=server2&server=server3` | -| `snowflake` | `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` | -| `trino` | [`http[s]://user[:pass]@host[:port][?parameters]`](https://github.com/trinodb/trino-go-client#dsn-data-source-name) | -| `gocosmos` | [`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`](https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage) | - -Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. - -The `snowflake` driver supports multiple DSN formats. Please consult [the docs](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) for more details. For [key pair authentication](https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication), the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. - -The [`gocosmos`](https://pkg.go.dev/github.com/microsoft/gocosmos) driver is still experimental, but it has support for [hierarchical partition keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) as well as [cross-partition queries](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query). Please refer to the [SQL notes](https://github.com/microsoft/gocosmos/blob/main/SQL.md) for details. - - -Type: `string` - -```yml -# Examples - -dsn: clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 - -dsn: foouser:foopassword@tcp(localhost:3306)/foodb - -dsn: postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable - -dsn: oracle://foouser:foopass@localhost:1521/service_name -``` - -### `table` - -The table to query. - - -Type: `string` - -```yml -# Examples - -table: foo -``` - -### `columns` - -A list of columns to query. - - -Type: `array` - -```yml -# Examples - -columns: - - '*' - -columns: - - foo - - bar - - baz -``` - -### `where` - -An optional where clause to add. Placeholder arguments are populated with the `args_mapping` field. Placeholders should always be question marks, and will automatically be converted to dollar syntax when the postgres or clickhouse drivers are used. - - -Type: `string` - -```yml -# Examples - -where: meow = ? and woof = ? - -where: user_id = ? -``` - -### `args_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) which should evaluate to an array of values matching in size to the number of placeholder arguments in the field `where`. - - -Type: `string` - -```yml -# Examples - -args_mapping: root = [ this.cat.meow, this.doc.woofs[0] ] - -args_mapping: root = [ meta("user.id") ] -``` - -### `prefix` - -An optional prefix to prepend to the query (before SELECT). - - -Type: `string` - -### `suffix` - -An optional suffix to append to the select query. - - -Type: `string` - -### `init_files` - -An optional list of file paths containing SQL statements to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Glob patterns are supported, including super globs (double star). - -Care should be taken to ensure that the statements are idempotent, and therefore would not cause issues when run multiple times after service restarts. If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If a statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `array` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_files: - - ./init/*.sql - -init_files: - - ./foo.sql - - ./bar.sql -``` - -### `init_statement` - -An optional SQL statement to execute immediately upon the first connection to the target database. This is a useful way to initialise tables before processing data. Care should be taken to ensure that the statement is idempotent, and therefore would not cause issues when run multiple times after service restarts. - -If both `init_statement` and `init_files` are specified the `init_statement` is executed _after_ the `init_files`. - -If the statement fails for any reason a warning log will be emitted but the operation of this component will not be stopped. - - -Type: `string` -Requires version 4.10.0 or newer - -```yml -# Examples - -init_statement: |2 - CREATE TABLE IF NOT EXISTS some_table ( - foo varchar(50) not null, - bar integer, - baz varchar(50), - primary key (foo) - ) WITHOUT ROWID; -``` - -### `conn_max_idle_time` - -An optional maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections idle time. - - -Type: `string` - -### `conn_max_life_time` - -An optional maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If `value <= 0`, connections are not closed due to a connections age. - - -Type: `string` - -### `conn_max_idle` - -An optional maximum number of connections in the idle connection pool. If conn_max_open is greater than 0 but less than the new conn_max_idle, then the new conn_max_idle will be reduced to match the conn_max_open limit. If `value <= 0`, no idle connections are retained. The default max idle connections is currently 2. This may change in a future release. - - -Type: `int` -Default: `2` - -### `conn_max_open` - -An optional maximum number of open connections to the database. If conn_max_idle is greater than 0 and the new conn_max_open is less than conn_max_idle, then conn_max_idle will be reduced to match the new conn_max_open limit. If `value <= 0`, then there is no limit on the number of open connections. The default is 0 (unlimited). - - -Type: `int` - - diff --git a/website/docs/components/processors/subprocess.md b/website/docs/components/processors/subprocess.md deleted file mode 100644 index 244a6c4bd3..0000000000 --- a/website/docs/components/processors/subprocess.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: subprocess -slug: subprocess -type: processor -status: stable -categories: ["Integration"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a command as a subprocess and, for each message, will pipe its contents to the stdin stream of the process followed by a newline. - - - - - - -```yml -# Common config fields, showing default values -label: "" -subprocess: - name: cat # No default (required) - args: [] -``` - - - - -```yml -# All config fields, showing default values -label: "" -subprocess: - name: cat # No default (required) - args: [] - max_buffer: 65536 - codec_send: lines - codec_recv: lines -``` - - - - -:::info -This processor keeps the subprocess alive and requires very specific behaviour from the command executed. If you wish to simply execute a command for each message take a look at the [`command` processor](/docs/components/processors/command) instead. -::: - -The subprocess must then either return a line over stdout or stderr. If a response is returned over stdout then its contents will replace the message. If a response is instead returned from stderr it will be logged and the message will continue unchanged and will be [marked as failed](/docs/configuration/error_handling). - -Rather than separating data by a newline it's possible to specify alternative [`codec_send`](#codec_send) and [`codec_recv`](#codec_recv) values, which allow binary messages to be encoded for logical separation. - -The execution environment of the subprocess is the same as the Benthos instance, including environment variables and the current working directory. - -The field `max_buffer` defines the maximum response size able to be read from the subprocess. This value should be set significantly above the real expected maximum response size. - -## Subprocess requirements - -It is required that subprocesses flush their stdout and stderr pipes for each line. Benthos will attempt to keep the process alive for as long as the pipeline is running. If the process exits early it will be restarted. - -## Messages containing line breaks - -If a message contains line breaks each line of the message is piped to the subprocess and flushed, and a response is expected from the subprocess before another line is fed in. - -## Fields - -### `name` - -The command to execute as a subprocess. - - -Type: `string` - -```yml -# Examples - -name: cat - -name: sed - -name: awk -``` - -### `args` - -A list of arguments to provide the command. - - -Type: `array` -Default: `[]` - -### `max_buffer` - -The maximum expected response size. - - -Type: `int` -Default: `65536` - -### `codec_send` - -Determines how messages written to the subprocess are encoded, which allows them to be logically separated. - - -Type: `string` -Default: `"lines"` -Requires version 3.37.0 or newer -Options: `lines`, `length_prefixed_uint32_be`, `netstring`. - -### `codec_recv` - -Determines how messages read from the subprocess are decoded, which allows them to be logically separated. - - -Type: `string` -Default: `"lines"` -Requires version 3.37.0 or newer -Options: `lines`, `length_prefixed_uint32_be`, `netstring`. - - diff --git a/website/docs/components/processors/switch.md b/website/docs/components/processors/switch.md deleted file mode 100644 index ba24333867..0000000000 --- a/website/docs/components/processors/switch.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: switch -slug: switch -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Conditionally processes messages based on their contents. - -```yml -# Config fields, showing default values -label: "" -switch: [] # No default (required) -``` - -For each switch case a [Bloblang query](/docs/guides/bloblang/about) is checked and, if the result is true (or the check is empty) the child processors are executed on the message. - -## Fields - -### `[].check` - -A [Bloblang query](/docs/guides/bloblang/about) that should return a boolean value indicating whether a message should have the processors of this case executed on it. If left empty the case always passes. If the check mapping throws an error the message will be flagged [as having failed](/docs/configuration/error_handling) and will not be tested against any other cases. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: this.type == "foo" - -check: this.contents.urls.contains("https://benthos.dev/") -``` - -### `[].processors` - -A list of [processors](/docs/components/processors/about/) to execute on a message. - - -Type: `array` -Default: `[]` - -### `[].fallthrough` - -Indicates whether, if this case passes for a message, the next case should also be executed. - - -Type: `bool` -Default: `false` - -## Examples - - - - - - -We have a system where we're counting a metric for all messages that pass through our system. However, occasionally we get messages from George where he's rambling about dumb stuff we don't care about. - -For Georges messages we want to instead emit a metric that gauges how angry he is about being ignored and then we drop it. - -```yaml -pipeline: - processors: - - switch: - - check: this.user.name.first != "George" - processors: - - metric: - type: counter - name: MessagesWeCareAbout - - - processors: - - metric: - type: gauge - name: GeorgesAnger - value: ${! json("user.anger") } - - mapping: root = deleted() -``` - - - - -## Batching - -When a switch processor executes on a [batch of messages](/docs/configuration/batching) they are checked individually and can be matched independently against cases. During processing the messages matched against a case are processed as a batch, although the ordering of messages during case processing cannot be guaranteed to match the order as received. - -At the end of switch processing the resulting batch will follow the same ordering as the batch was received. If any child processors have split or otherwise grouped messages this grouping will be lost as the result of a switch is always a single batch. In order to perform conditional grouping and/or splitting use the [`group_by` processor](/docs/components/processors/group_by). - diff --git a/website/docs/components/processors/sync_response.md b/website/docs/components/processors/sync_response.md deleted file mode 100644 index 5060fc4525..0000000000 --- a/website/docs/components/processors/sync_response.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: sync_response -slug: sync_response -type: processor -status: stable -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Adds the payload in its current state as a synchronous response to the input source, where it is dealt with according to that specific input type. - -```yml -# Config fields, showing default values -label: "" -sync_response: {} -``` - -For most inputs this mechanism is ignored entirely, in which case the sync response is dropped without penalty. It is therefore safe to use this processor even when combining input types that might not have support for sync responses. An example of an input able to utilise this is the `http_server`. - -For more information please read [Synchronous Responses](/docs/guides/sync_responses). - - diff --git a/website/docs/components/processors/try.md b/website/docs/components/processors/try.md deleted file mode 100644 index 8309b2ece9..0000000000 --- a/website/docs/components/processors/try.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: try -slug: try -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a list of child processors on messages only if no prior processors have failed (or the errors have been cleared). - -```yml -# Config fields, showing default values -label: "" -try: [] -``` - -This processor behaves similarly to the [`for_each`](/docs/components/processors/for_each) processor, where a list of child processors are applied to individual messages of a batch. However, if a message has failed any prior processor (before or during the try block) then that message will skip all following processors. - -For example, with the following config: - -```yaml -pipeline: - processors: - - resource: foo - - try: - - resource: bar - - resource: baz - - resource: buz -``` - -If the processor `bar` fails for a particular message, that message will skip the processors `baz` and `buz`. Similarly, if `bar` succeeds but `baz` does not then `buz` will be skipped. If the processor `foo` fails for a message then none of `bar`, `baz` or `buz` are executed on that message. - -This processor is useful for when child processors depend on the successful output of previous processors. This processor can be followed with a [catch](/docs/components/processors/catch) processor for defining child processors to be applied only to failed messages. - -More information about error handing can be found [here](/docs/configuration/error_handling). - -### Nesting within a catch block - -In some cases it might be useful to nest a try block within a catch block, since the [`catch` processor](/docs/components/processors/catch) only clears errors _after_ executing its child processors this means a nested try processor will not execute unless the errors are explicitly cleared beforehand. - -This can be done by inserting an empty catch block before the try block like as follows: - -```yaml -pipeline: - processors: - - resource: foo - - catch: - - log: - level: ERROR - message: "Foo failed due to: ${! error() }" - - catch: [] # Clear prior error - - try: - - resource: bar - - resource: baz -``` - - diff --git a/website/docs/components/processors/unarchive.md b/website/docs/components/processors/unarchive.md deleted file mode 100644 index 4a9751f943..0000000000 --- a/website/docs/components/processors/unarchive.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: unarchive -slug: unarchive -type: processor -status: stable -categories: ["Parsing","Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Unarchives messages according to the selected archive format into multiple messages within a [batch](/docs/configuration/batching). - -```yml -# Config fields, showing default values -label: "" -unarchive: - format: "" # No default (required) -``` - -When a message is unarchived the new messages replace the original message in the batch. Messages that are selected but fail to unarchive (invalid format) will remain unchanged in the message batch but will be flagged as having failed, allowing you to [error handle them](/docs/configuration/error_handling). - -## Metadata - -The metadata found on the messages handled by this processor will be copied into the resulting messages. For the unarchive formats that contain file information (tar, zip), a metadata field is also added to each message called `archive_filename` with the extracted filename. - - -## Fields - -### `format` - -The unarchiving format to apply. - - -Type: `string` - -| Option | Summary | -|---|---| -| `binary` | Extract messages from a [binary blob format](https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96). | -| `csv` | Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message. | -| `csv:x` | Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message using a custom delimiter. The custom delimiter must be a single character, e.g. the format "csv:\t" would consume a tab delimited file. | -| `json_array` | Attempt to parse a message as a JSON array, and extract each element into its own message. | -| `json_documents` | Attempt to parse a message as a stream of concatenated JSON documents. Each parsed document is expanded into a new message. | -| `json_map` | Attempt to parse the message as a JSON map and for each element of the map expands its contents into a new message. A metadata field is added to each message called `archive_key` with the relevant key from the top-level map. | -| `lines` | Extract the lines of a message each into their own message. | -| `tar` | Extract messages from a unix standard tape archive. | -| `zip` | Extract messages from a zip file. | - - - diff --git a/website/docs/components/processors/wasm.md b/website/docs/components/processors/wasm.md deleted file mode 100644 index 29349b32d8..0000000000 --- a/website/docs/components/processors/wasm.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: wasm -slug: wasm -type: processor -status: experimental -categories: ["Utility"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Executes a function exported by a WASM module for each message. - -Introduced in version 4.11.0. - -```yml -# Config fields, showing default values -label: "" -wasm: - module_path: "" # No default (required) - function: process -``` - -This processor uses [Wazero](https://github.com/tetratelabs/wazero) to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. - -This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in [the codebase](https://github.com/benthosdev/benthos/tree/main/public/wasm/README.md). - -These examples, as well as the processor itself, is a work in progress. - -### Parallelism - -It's not currently possible to execute a single WASM runtime across parallel threads with this processor. Therefore, in order to support parallel processing this processor implements pooling of module runtimes. Ideally your WASM module shouldn't depend on any global state, but if it does then you need to ensure the processor [is only run on a single thread](/docs/configuration/processing_pipelines). - - -## Fields - -### `module_path` - -The path of the target WASM module to execute. - - -Type: `string` - -### `function` - -The name of the function exported by the target WASM module to run for each message. - - -Type: `string` -Default: `"process"` - - diff --git a/website/docs/components/processors/while.md b/website/docs/components/processors/while.md deleted file mode 100644 index b28b87d7b6..0000000000 --- a/website/docs/components/processors/while.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: while -slug: while -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A processor that checks a [Bloblang query](/docs/guides/bloblang/about/) against each batch of messages and executes child processors on them for as long as the query resolves to true. - - - - - - -```yml -# Common config fields, showing default values -label: "" -while: - at_least_once: false - check: "" - processors: [] # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -while: - at_least_once: false - max_loops: 0 - check: "" - processors: [] # No default (required) -``` - - - - -The field `at_least_once`, if true, ensures that the child processors are always executed at least one time (like a do .. while loop.) - -The field `max_loops`, if greater than zero, caps the number of loops for a message batch to this value. - -If following a loop execution the number of messages in a batch is reduced to zero the loop is exited regardless of the condition result. If following a loop execution there are more than 1 message batches the query is checked against the first batch only. - -The conditions of this processor are applied across entire message batches. You can find out more about batching [in this doc](/docs/configuration/batching). - -## Fields - -### `at_least_once` - -Whether to always run the child processors at least one time. - - -Type: `bool` -Default: `false` - -### `max_loops` - -An optional maximum number of loops to execute. Helps protect against accidentally creating infinite loops. - - -Type: `int` -Default: `0` - -### `check` - -A [Bloblang query](/docs/guides/bloblang/about/) that should return a boolean value indicating whether the while loop should execute again. - - -Type: `string` -Default: `""` - -```yml -# Examples - -check: errored() - -check: this.urls.unprocessed.length() > 0 -``` - -### `processors` - -A list of child processors to execute on each loop. - - -Type: `array` - - diff --git a/website/docs/components/processors/workflow.md b/website/docs/components/processors/workflow.md deleted file mode 100644 index 58e5115408..0000000000 --- a/website/docs/components/processors/workflow.md +++ /dev/null @@ -1,390 +0,0 @@ ---- -title: workflow -slug: workflow -type: processor -status: stable -categories: ["Composition"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Executes a topology of [`branch` processors][processors.branch], performing them in parallel where possible. - - - - - - -```yml -# Common config fields, showing default values -label: "" -workflow: - meta_path: meta.workflow - order: [] - branches: {} -``` - - - - -```yml -# All config fields, showing default values -label: "" -workflow: - meta_path: meta.workflow - order: [] - branch_resources: [] - branches: {} -``` - - - - -## Why Use a Workflow - -### Performance - -Most of the time the best way to compose processors is also the simplest, just configure them in series. This is because processors are often CPU bound, low-latency, and you can gain vertical scaling by increasing the number of processor pipeline threads, allowing Benthos to process [multiple messages in parallel][configuration.pipelines]. - -However, some processors such as [`http`][processors.http], [`aws_lambda`][processors.aws_lambda] or [`cache`][processors.cache] interact with external services and therefore spend most of their time waiting for a response. These processors tend to be high-latency and low CPU activity, which causes messages to process slowly. - -When a processing pipeline contains multiple network processors that aren't dependent on each other we can benefit from performing these processors in parallel for each individual message, reducing the overall message processing latency. - -### Simplifying Processor Topology - -A workflow is often expressed as a [DAG][dag_wiki] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. - -For example, if we had processing stages A, B, C and D, where stage A could result in either stage B or C being next, always followed by D, it might look something like this: - -```text - /--> B --\ -A --| |--> D - \--> C --/ -``` - -This flow would be easy to express in a standard Benthos config, we could simply use a [`switch` processor][processors.switch] to route to either B or C depending on a condition on the result of A. However, this method of flow control quickly becomes unfeasible as the DAG gets more complicated, imagine expressing this flow using switch processors: - -```text - /--> B -------------|--> D - / / -A --| /--> E --| - \--> C --| \ - \----------|--> F -``` - -And imagine doing so knowing that the diagram is subject to change over time. Yikes! Instead, with a workflow we can either trust it to automatically resolve the DAG or express it manually as simply as `order: [ [ A ], [ B, C ], [ E ], [ D, F ] ]`, and the conditional logic for determining if a stage is executed is defined as part of the branch itself. - -## Examples - - - - - - -When the field `order` is omitted a best attempt is made to determine a dependency tree between branches based on their request and result mappings. In the following example the branches foo and bar will be executed first in parallel, and afterwards the branch baz will be executed. - -```yaml -pipeline: - processors: - - workflow: - meta_path: meta.workflow - branches: - foo: - request_map: 'root = ""' - processors: - - http: - url: TODO - result_map: 'root.foo = this' - - bar: - request_map: 'root = this.body' - processors: - - aws_lambda: - function: TODO - result_map: 'root.bar = this' - - baz: - request_map: | - root.fooid = this.foo.id - root.barstuff = this.bar.content - processors: - - cache: - resource: TODO - operator: set - key: ${! json("fooid") } - value: ${! json("barstuff") } -``` - - - - - -Branches of a workflow are skipped when the `request_map` assigns `deleted()` to the root. In this example the branch A is executed when the document type is "foo", and branch B otherwise. Branch C is executed afterwards and is skipped unless either A or B successfully provided a result at `tmp.result`. - -```yaml -pipeline: - processors: - - workflow: - branches: - A: - request_map: | - root = if this.document.type != "foo" { - deleted() - } - processors: - - http: - url: TODO - result_map: 'root.tmp.result = this' - - B: - request_map: | - root = if this.document.type == "foo" { - deleted() - } - processors: - - aws_lambda: - function: TODO - result_map: 'root.tmp.result = this' - - C: - request_map: | - root = if this.tmp.result != null { - deleted() - } - processors: - - http: - url: TODO_SOMEWHERE_ELSE - result_map: 'root.tmp.result = this' -``` - - - - - -The `order` field can be used in order to refer to [branch processor resources](#resources), this can sometimes make your pipeline configuration cleaner, as well as allowing you to reuse branch configurations in order places. It's also possible to mix and match branches configured within the workflow and configured as resources. - -```yaml -pipeline: - processors: - - workflow: - order: [ [ foo, bar ], [ baz ] ] - branches: - bar: - request_map: 'root = this.body' - processors: - - aws_lambda: - function: TODO - result_map: 'root.bar = this' - -processor_resources: - - label: foo - branch: - request_map: 'root = ""' - processors: - - http: - url: TODO - result_map: 'root.foo = this' - - - label: baz - branch: - request_map: | - root.fooid = this.foo.id - root.barstuff = this.bar.content - processors: - - cache: - resource: TODO - operator: set - key: ${! json("fooid") } - value: ${! json("barstuff") } -``` - - - - -## Fields - -### `meta_path` - -A [dot path](/docs/configuration/field_paths) indicating where to store and reference [structured metadata](#structured-metadata) about the workflow execution. - - -Type: `string` -Default: `"meta.workflow"` - -### `order` - -An explicit declaration of branch ordered tiers, which describes the order in which parallel tiers of branches should be executed. Branches should be identified by the name as they are configured in the field `branches`. It's also possible to specify branch processors configured [as a resource](#resources). - - -Type: `two-dimensional array` -Default: `[]` - -```yml -# Examples - -order: - - - foo - - bar - - - baz - -order: - - - foo - - - bar - - - baz -``` - -### `branch_resources` - -An optional list of [`branch` processor](/docs/components/processors/branch) names that are configured as [resources](#resources). These resources will be included in the workflow with any branches configured inline within the [`branches`](#branches) field. The order and parallelism in which branches are executed is automatically resolved based on the mappings of each branch. When using resources with an explicit order it is not necessary to list resources in this field. - - -Type: `array` -Default: `[]` -Requires version 3.38.0 or newer - -### `branches` - -An object of named [`branch` processors](/docs/components/processors/branch) that make up the workflow. The order and parallelism in which branches are executed can either be made explicit with the field `order`, or if omitted an attempt is made to automatically resolve an ordering based on the mappings of each branch. - - -Type: `object` -Default: `{}` - -### `branches..request_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) that describes how to create a request payload suitable for the child processors of this branch. If left empty then the branch will begin with an exact copy of the origin message (including metadata). - - -Type: `string` -Default: `""` - -```yml -# Examples - -request_map: |- - root = { - "id": this.doc.id, - "content": this.doc.body.text - } - -request_map: |- - root = if this.type == "foo" { - this.foo.request - } else { - deleted() - } -``` - -### `branches..processors` - -A list of processors to apply to mapped requests. When processing message batches the resulting batch must match the size and ordering of the input batch, therefore filtering, grouping should not be performed within these processors. - - -Type: `array` - -### `branches..result_map` - -A [Bloblang mapping](/docs/guides/bloblang/about) that describes how the resulting messages from branched processing should be mapped back into the original payload. If left empty the origin message will remain unchanged (including metadata). - - -Type: `string` -Default: `""` - -```yml -# Examples - -result_map: |- - meta foo_code = metadata("code") - root.foo_result = this - -result_map: |- - meta = metadata() - root.bar.body = this.body - root.bar.id = this.user.id - -result_map: root.raw_result = content().string() - -result_map: |- - root.enrichments.foo = if metadata("request_failed") != null { - throw(metadata("request_failed")) - } else { - this - } - -result_map: |- - # Retain only the updated metadata fields which were present in the origin message - meta = metadata().filter(v -> @.get(v.key) != null) -``` - -## Structured Metadata - -When the field `meta_path` is non-empty the workflow processor creates an object describing which workflows were successful, skipped or failed for each message and stores the object within the message at the end. - -The object is of the following form: - -```json -{ - "succeeded": [ "foo" ], - "skipped": [ "bar" ], - "failed": { - "baz": "the error message from the branch" - } -} -``` - -If a message already has a meta object at the given path when it is processed then the object is used in order to determine which branches have already been performed on the message (or skipped) and can therefore be skipped on this run. - -This is a useful pattern when replaying messages that have failed some branches previously. For example, given the above example object the branches foo and bar would automatically be skipped, and baz would be reattempted. - -The previous meta object will also be preserved in the field `.previous` when the new meta object is written, preserving a full record of all workflow executions. - -If a field `.apply` exists in the meta object for a message and is an array then it will be used as an explicit list of stages to apply, all other stages will be skipped. - -## Resources - -It's common to configure processors (and other components) [as resources][configuration.resources] in order to keep the pipeline configuration cleaner. With the workflow processor you can include branch processors configured as resources within your workflow either by specifying them by name in the field `order`, if Benthos doesn't find a branch within the workflow configuration of that name it'll refer to the resources. - -Alternatively, if you do not wish to have an explicit ordering, you can add resource names to the field `branch_resources` and they will be included in the workflow with automatic DAG resolution along with any branches configured in the `branches` field. - -### Resource Error Conditions - -There are two error conditions that could potentially occur when resources included in your workflow are mutated, and if you are planning to mutate resources in your workflow it is important that you understand them. - -The first error case is that a resource in the workflow is removed and not replaced, when this happens the workflow will still be executed but the individual branch will fail. This should only happen if you explicitly delete a branch resource, as any mutation operation will create the new resource before removing the old one. - -The second error case is when automatic DAG resolution is being used and a resource in the workflow is changed in a way that breaks the DAG (circular dependencies, etc). When this happens it is impossible to execute the workflow and therefore the processor will fail, which is possible to capture and handle using [standard error handling patterns][configuration.error-handling]. - -## Error Handling - -The recommended approach to handle failures within a workflow is to query against the [structured metadata](#structured-metadata) it provides, as it provides granular information about exactly which branches failed and which ones succeeded and therefore aren't necessary to perform again. - -For example, if our meta object is stored at the path `meta.workflow` and we wanted to check whether a message has failed for any branch we can do that using a [Bloblang query][guides.bloblang] like `this.meta.workflow.failed.length() | 0 > 0`, or to check whether a specific branch failed we can use `this.exists("meta.workflow.failed.foo")`. - -However, if structured metadata is disabled by setting the field `meta_path` to empty then the workflow processor instead adds a general error flag to messages when any executed branch fails. In this case it's possible to handle failures using [standard error handling patterns][configuration.error-handling]. - -[dag_wiki]: https://en.wikipedia.org/wiki/Directed_acyclic_graph -[processors.switch]: /docs/components/processors/switch -[processors.http]: /docs/components/processors/http -[processors.aws_lambda]: /docs/components/processors/aws_lambda -[processors.cache]: /docs/components/processors/cache -[processors.branch]: /docs/components/processors/branch -[guides.bloblang]: /docs/guides/bloblang/about -[configuration.pipelines]: /docs/configuration/processing_pipelines -[configuration.error-handling]: /docs/configuration/error_handling -[configuration.resources]: /docs/configuration/resources - - diff --git a/website/docs/components/processors/xml.md b/website/docs/components/processors/xml.md deleted file mode 100644 index 0583ba68cc..0000000000 --- a/website/docs/components/processors/xml.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: xml -slug: xml -type: processor -status: beta -categories: ["Parsing"] ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution BETA -This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found. -::: -Parses messages as an XML document, performs a mutation on the data, and then overwrites the previous contents with the new value. - -```yml -# Config fields, showing default values -label: "" -xml: - operator: "" - cast: false -``` - -## Operators - -### `to_json` - -Converts an XML document into a JSON structure, where elements appear as keys of an object according to the following rules: - -- If an element contains attributes they are parsed by prefixing a hyphen, `-`, to the attribute label. -- If the element is a simple element and has attributes, the element value is given the key `#text`. -- XML comments, directives, and process instructions are ignored. -- When elements are repeated the resulting JSON value is an array. - -For example, given the following XML: - -```xml - - This is a title - This is a description - foo1 - foo2 - foo3 - -``` - -The resulting JSON structure would look like this: - -```json -{ - "root":{ - "title":"This is a title", - "description":{ - "#text":"This is a description", - "-tone":"boring" - }, - "elements":[ - {"#text":"foo1","-id":"1"}, - {"#text":"foo2","-id":"2"}, - "foo3" - ] - } -} -``` - -With cast set to true, the resulting JSON structure would look like this: - -```json -{ - "root":{ - "title":"This is a title", - "description":{ - "#text":"This is a description", - "-tone":"boring" - }, - "elements":[ - {"#text":"foo1","-id":1}, - {"#text":"foo2","-id":2}, - "foo3" - ] - } -} -``` - -## Fields - -### `operator` - -An XML [operation](#operators) to apply to messages. - - -Type: `string` -Default: `""` -Options: `to_json`. - -### `cast` - -Whether to try to cast values that are numbers and booleans to the right type. Default: all values are strings. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/rate_limits/about.md b/website/docs/components/rate_limits/about.md deleted file mode 100644 index 21fb9c23f6..0000000000 --- a/website/docs/components/rate_limits/about.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Rate Limits -sidebar_label: About ---- - -A rate limit is a strategy for limiting the usage of a shared resource across parallel components in a Benthos instance, or potentially across multiple instances. They are configured as a resource: - -```yaml -rate_limit_resources: - - label: foobar - local: - count: 500 - interval: 1s -``` - -And most components that hit external services have a field `rate_limit` for specifying a rate limit resource to use, identified by the `label` field. For example, if we wanted to use our `foobar` rate limit with an [`http_client`][input.http_client] input it would look like this: - -```yaml -input: - http_client: - url: TODO - verb: GET - rate_limit: foobar -``` - -By using a rate limit in this way we can guarantee that our input will only poll our HTTP source at the rate of 500 requests per second. - -Some components don't have a `rate_limit` field but we might still wish to throttle them by a rate limit, in which case we can use the [`rate_limit` processor][processor.rate_limit] that applies back pressure to a processing pipeline when the limit is reached. For example, if we wished to limit the consumption of lines of a [`csv` file input][input.csv] to a specified rate limit we can do that with the following: - -```yaml -input: - csv: - paths: - - ./foo.csv - processors: - - rate_limit: - resource: foobar -``` - -You can find out more about resources [in this document.][config.resources] - -import ComponentSelect from '@theme/ComponentSelect'; - - - -[processor.rate_limit]: /docs/components/processors/rate_limit -[input.csv]: /docs/components/inputs/csv -[input.http_client]: /docs/components/inputs/http_client -[config.resources]: /docs/configuration/resources diff --git a/website/docs/components/rate_limits/local.md b/website/docs/components/rate_limits/local.md deleted file mode 100644 index 69ed9aa5af..0000000000 --- a/website/docs/components/rate_limits/local.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: local -slug: local -type: rate_limit -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -The local rate limit is a simple X every Y type rate limit that can be shared across any number of components within the pipeline but does not support distributed rate limits across multiple running instances of Benthos. - -```yml -# Config fields, showing default values -label: "" -local: - count: 1000 - interval: 1s -``` - -## Fields - -### `count` - -The maximum number of requests to allow for a given period of time. - - -Type: `int` -Default: `1000` - -### `interval` - -The time window to limit requests by. - - -Type: `string` -Default: `"1s"` - - diff --git a/website/docs/components/rate_limits/redis.md b/website/docs/components/rate_limits/redis.md deleted file mode 100644 index 36c611b5ef..0000000000 --- a/website/docs/components/rate_limits/redis.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -title: redis -slug: redis -type: rate_limit -status: experimental ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -A rate limit implementation using Redis. It works by using a simple token bucket algorithm to limit the number of requests to a given count within a given time period. The rate limit is shared across all instances of Benthos that use the same Redis instance, which must all have a consistent count and interval. - -Introduced in version 4.12.0. - - - - - - -```yml -# Common config fields, showing default values -label: "" -redis: - url: redis://:6397 # No default (required) - count: 1000 - interval: 1s - key: "" # No default (required) -``` - - - - -```yml -# All config fields, showing default values -label: "" -redis: - url: redis://:6397 # No default (required) - kind: simple - master: "" - tls: - enabled: false - skip_cert_verify: false - enable_renegotiation: false - root_cas: "" - root_cas_file: "" - client_certs: [] - count: 1000 - interval: 1s - key: "" # No default (required) -``` - - - - -## Fields - -### `url` - -The URL of the target Redis server. Database is optional and is supplied as the URL path. - - -Type: `string` - -```yml -# Examples - -url: redis://:6397 - -url: redis://localhost:6379 - -url: redis://foousername:foopassword@redisplace:6379 - -url: redis://:foopassword@redisplace:6379 - -url: redis://localhost:6379/1 - -url: redis://localhost:6379/1,redis://localhost:6380/1 -``` - -### `kind` - -Specifies a simple, cluster-aware, or failover-aware redis client. - - -Type: `string` -Default: `"simple"` -Options: `simple`, `cluster`, `failover`. - -### `master` - -Name of the redis master when `kind` is `failover` - - -Type: `string` -Default: `""` - -```yml -# Examples - -master: mymaster -``` - -### `tls` - -Custom TLS settings can be used to override system defaults. - -**Troubleshooting** - -Some cloud hosted instances of Redis (such as Azure Cache) might need some hand holding in order to establish stable connections. Unfortunately, it is often the case that TLS issues will manifest as generic error messages such as "i/o timeout". If you're using TLS and are seeing connectivity problems consider setting `enable_renegotiation` to `true`, and ensuring that the server supports at least TLS version 1.2. - - -Type: `object` - -### `tls.enabled` - -Whether custom TLS settings are enabled. - - -Type: `bool` -Default: `false` - -### `tls.skip_cert_verify` - -Whether to skip server side certificate verification. - - -Type: `bool` -Default: `false` - -### `tls.enable_renegotiation` - -Whether to allow the remote server to repeatedly request renegotiation. Enable this option if you're seeing the error message `local error: tls: no renegotiation`. - - -Type: `bool` -Default: `false` -Requires version 3.45.0 or newer - -### `tls.root_cas` - -An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas: |- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- -``` - -### `tls.root_cas_file` - -An optional path of a root certificate authority file to use. This is a file, often with a .pem extension, containing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. - - -Type: `string` -Default: `""` - -```yml -# Examples - -root_cas_file: ./root_cas.pem -``` - -### `tls.client_certs` - -A list of client certificates to use. For each certificate either the fields `cert` and `key`, or `cert_file` and `key_file` should be specified, but not both. - - -Type: `array` -Default: `[]` - -```yml -# Examples - -client_certs: - - cert: foo - key: bar - -client_certs: - - cert_file: ./example.pem - key_file: ./example.key -``` - -### `tls.client_certs[].cert` - -A plain text certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key` - -A plain text certificate key to use. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -### `tls.client_certs[].cert_file` - -The path of a certificate to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].key_file` - -The path of a certificate key to use. - - -Type: `string` -Default: `""` - -### `tls.client_certs[].password` - -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. -:::warning Secret -This field contains sensitive information that usually shouldn't be added to a config directly, read our [secrets page for more info](/docs/configuration/secrets). -::: - - -Type: `string` -Default: `""` - -```yml -# Examples - -password: foo - -password: ${KEY_PASSWORD} -``` - -### `count` - -The maximum number of messages to allow for a given period of time. - - -Type: `int` -Default: `1000` - -### `interval` - -The time window to limit requests by. - - -Type: `string` -Default: `"1s"` - -### `key` - -The key to use for the rate limit. - - -Type: `string` - - diff --git a/website/docs/components/scanners/about.md b/website/docs/components/scanners/about.md deleted file mode 100644 index 0622e287d4..0000000000 --- a/website/docs/components/scanners/about.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Scanners -sidebar_label: About ---- - -For most Benthos [inputs][input.about] the data consumed comes pre-partitioned into discrete messages which can be comfortably held and processed in memory. However, some inputs such as the [`file` input][input.file] often need to consume data that is large enough that it cannot be processed entirely within memory, and others such as the [`socket` input][input.socket] don't have a concept of consuming the data "entirely". - -For such inputs it's necessary to define a mechanism by which the stream of source bytes can be chopped into smaller logical messages, processed and outputted as a continuous process whilst the stream is being read, as this dramatically reduces the memory usage of Benthos as a whole and results in a more fluid flow of data. - -The way in which we define this chopping mechanism is through scanners, configured as a field on each input that requires one. For example, if we wished to consume files line-by-line, which each individual line being processed as a discrete message, we could use the [`lines` scanner][scanner.lines] with our [`file` input][input.file]: - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - - - - -```yaml -input: - file: - paths: [ "./*.txt" ] - scanner: - lines: {} -``` - - - - -```yaml -# Instead of newlines, use a custom delimiter: -input: - file: - paths: [ "./*.txt" ] - scanner: - lines: - custom_delimiter: "---END---" - max_buffer_size: 100_000_000 # 100MB line buffer -``` - - - - -A scanner is a plugin similar to any other core Benthos component (inputs, processors, outputs, etc), which means it's possible to define your own scanners that can be utilised by inputs that need them. - -import ComponentSelect from '@theme/ComponentSelect'; - - - -[input.about]: /docs/components/inputs/about -[input.file]: /docs/components/inputs/file -[input.socket]: /docs/components/inputs/socket -[scanner.lines]: /docs/components/scanners/lines diff --git a/website/docs/components/scanners/avro.md b/website/docs/components/scanners/avro.md deleted file mode 100644 index e6b68723e4..0000000000 --- a/website/docs/components/scanners/avro.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: avro -slug: avro -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consume a stream of Avro OCF datum. - - - - - - -```yml -# Common config fields, showing default values -avro: {} -``` - - - - -```yml -# All config fields, showing default values -avro: - raw_json: false -``` - - - - -### Avro JSON Format - -This scanner yields documents formatted as [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding) when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - -- if its type is `null`, then it is encoded as a JSON `null`; -- otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. - -For example, the union schema `["null","string","Foo"]`, where `Foo` is a record name, would encode: - -- `null` as `null`; -- the string `"a"` as `{"string": "a"}`; and -- a `Foo` instance as `{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. - -However, it is possible to instead create documents in [standard/raw JSON format](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) by setting the field [`avro_raw_json`](#avro_raw_json) to `true`. - - -## Fields - -### `raw_json` - -Whether messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than [Avro JSON](https://avro.apache.org/docs/current/specification/_print/#json-encoding). If `true` the schema returned from the subject should be decoded as [standard json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull) instead of as [avro json](https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec). There is a [comment in goavro](https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249), the [underlining library used for avro serialization](https://github.com/linkedin/goavro), that explains in more detail the difference between the standard json and avro json. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/scanners/chunker.md b/website/docs/components/scanners/chunker.md deleted file mode 100644 index 242b6cc97e..0000000000 --- a/website/docs/components/scanners/chunker.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: chunker -slug: chunker -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Split an input stream into chunks of a given number of bytes. - -```yml -# Config fields, showing default values -chunker: - size: 0 # No default (required) -``` - -## Fields - -### `size` - -The size of each chunk in bytes. - - -Type: `int` - - diff --git a/website/docs/components/scanners/csv.md b/website/docs/components/scanners/csv.md deleted file mode 100644 index bc5709f5ea..0000000000 --- a/website/docs/components/scanners/csv.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: csv -slug: csv -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consume comma-separated values row by row, including support for custom delimiters. - -```yml -# Config fields, showing default values -csv: - custom_delimiter: "" # No default (optional) - parse_header_row: true - lazy_quotes: false - continue_on_error: false -``` - -### Metadata - -This scanner adds the following metadata to each message: - -- `csv_row` The index of each row, beginning at 0. - - - -## Fields - -### `custom_delimiter` - -Use a provided custom delimiter instead of the default comma. - - -Type: `string` - -### `parse_header_row` - -Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, each message will consist of an array of values from the corresponding CSV row. - - -Type: `bool` -Default: `true` - -### `lazy_quotes` - -If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field. - - -Type: `bool` -Default: `false` - -### `continue_on_error` - -If a row fails to parse due to any error emit an empty message marked with the error and then continue consuming subsequent rows when possible. This can sometimes be useful in situations where input data contains individual rows which are malformed. However, when a row encounters a parsing error it is impossible to guarantee that following rows are valid, as this indicates that the input data is unreliable and could potentially emit misaligned rows. - - -Type: `bool` -Default: `false` - - diff --git a/website/docs/components/scanners/decompress.md b/website/docs/components/scanners/decompress.md deleted file mode 100644 index 9b8c788e51..0000000000 --- a/website/docs/components/scanners/decompress.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: decompress -slug: decompress -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Decompress the stream of bytes according to an algorithm, before feeding it into a child scanner. - -```yml -# Config fields, showing default values -decompress: - algorithm: "" # No default (required) - into: - to_the_end: {} -``` - -## Fields - -### `algorithm` - -One of `gzip`, `pgzip`, `zlib`, `bzip2`, `flate`, `snappy`, `lz4`, `zstd`. - - -Type: `string` - -### `into` - -The child scanner to feed the decompressed stream into. - - -Type: `scanner` -Default: `{"to_the_end":{}}` - - diff --git a/website/docs/components/scanners/json_documents.md b/website/docs/components/scanners/json_documents.md deleted file mode 100644 index 0c27d844b3..0000000000 --- a/website/docs/components/scanners/json_documents.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: json_documents -slug: json_documents -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consumes a stream of one or more JSON documents. - -Introduced in version 4.27.0. - -```yml -# Config fields, showing default values -json_documents: {} -``` - - diff --git a/website/docs/components/scanners/lines.md b/website/docs/components/scanners/lines.md deleted file mode 100644 index d49ae1ebf5..0000000000 --- a/website/docs/components/scanners/lines.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: lines -slug: lines -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Split an input stream into a message per line of data. - -```yml -# Config fields, showing default values -lines: - custom_delimiter: "" # No default (optional) - max_buffer_size: 65536 -``` - -## Fields - -### `custom_delimiter` - -Use a provided custom delimiter for detecting the end of a line rather than a single line break. - - -Type: `string` - -### `max_buffer_size` - -Set the maximum buffer size for storing line data, this limits the maximum size that a line can be without causing an error. - - -Type: `int` -Default: `65536` - - diff --git a/website/docs/components/scanners/re_match.md b/website/docs/components/scanners/re_match.md deleted file mode 100644 index 92f04ad56e..0000000000 --- a/website/docs/components/scanners/re_match.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: re_match -slug: re_match -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Split an input stream into segments matching against a regular expression. - -```yml -# Config fields, showing default values -re_match: - pattern: (?m)^\d\d:\d\d:\d\d # No default (required) - max_buffer_size: 65536 -``` - -## Fields - -### `pattern` - -The pattern to match against. - - -Type: `string` - -```yml -# Examples - -pattern: (?m)^\d\d:\d\d:\d\d -``` - -### `max_buffer_size` - -Set the maximum buffer size for storing line data, this limits the maximum size that a message can be without causing an error. - - -Type: `int` -Default: `65536` - - diff --git a/website/docs/components/scanners/skip_bom.md b/website/docs/components/scanners/skip_bom.md deleted file mode 100644 index c235f76e1b..0000000000 --- a/website/docs/components/scanners/skip_bom.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: skip_bom -slug: skip_bom -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Skip one or more byte order marks for each opened child scanner. - -```yml -# Config fields, showing default values -skip_bom: - into: - to_the_end: {} -``` - -## Fields - -### `into` - -The child scanner to feed the resulting stream into. - - -Type: `scanner` -Default: `{"to_the_end":{}}` - - diff --git a/website/docs/components/scanners/switch.md b/website/docs/components/scanners/switch.md deleted file mode 100644 index 1c88f949c5..0000000000 --- a/website/docs/components/scanners/switch.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: switch -slug: switch -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Select a child scanner dynamically for source data based on factors such as the filename. - -```yml -# Config fields, showing default values -switch: [] # No default (required) -``` - -This scanner outlines a list of potential child scanner candidates to be chosen, and for each source of data the first candidate to pass will be selected. A candidate without any conditions acts as a catch-all and will pass for every source, it is recommended to always have a catch-all scanner at the end of your list. If a given source of data does not pass a candidate an error is returned and the data is rejected. - -## Fields - -### `[].re_match_name` - -A regular expression to test against the name of each source of data fed into the scanner (filename or equivalent). If this pattern matches the child scanner is selected. - - -Type: `string` - -### `[].scanner` - -The scanner to activate if this candidate passes. - - -Type: `scanner` - -## Examples - - - - - -In this example a file input chooses a scanner based on the extension of each file - -```yaml -input: - file: - paths: [ ./data/* ] - scanner: - switch: - - re_match_name: '\.avro$' - scanner: { avro: {} } - - - re_match_name: '\.csv$' - scanner: { csv: {} } - - - re_match_name: '\.csv.gz$' - scanner: - decompress: - algorithm: gzip - into: - csv: {} - - - re_match_name: '\.tar$' - scanner: { tar: {} } - - - re_match_name: '\.tar.gz$' - scanner: - decompress: - algorithm: gzip - into: - tar: {} - - - scanner: { to_the_end: {} } -``` - - - - - diff --git a/website/docs/components/scanners/tar.md b/website/docs/components/scanners/tar.md deleted file mode 100644 index 2bcebb8c6f..0000000000 --- a/website/docs/components/scanners/tar.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: tar -slug: tar -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Consume a tar archive file by file. - -```yml -# Config fields, showing default values -tar: {} -``` - -### Metadata - -This scanner adds the following metadata to each message: - -- `tar_name` - - - - diff --git a/website/docs/components/scanners/to_the_end.md b/website/docs/components/scanners/to_the_end.md deleted file mode 100644 index eddb33e7e5..0000000000 --- a/website/docs/components/scanners/to_the_end.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: to_the_end -slug: to_the_end -type: scanner -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Read the input stream all the way until the end and deliver it as a single message. - -```yml -# Config fields, showing default values -to_the_end: {} -``` - -:::caution -Some sources of data may not have a logical end, therefore caution should be made to exclusively use this scanner when the end of an input stream is clearly defined (and well within memory). -::: - - - diff --git a/website/docs/components/tracers/about.md b/website/docs/components/tracers/about.md deleted file mode 100644 index 3ddaa2e888..0000000000 --- a/website/docs/components/tracers/about.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Tracers -sidebar_label: About ---- - -A tracer type represents a destination for Benthos to send tracing events to such as [Jaeger][jaeger]. - -When a tracer is configured all messages will be allocated a root span during ingestion that represents their journey through a Benthos pipeline. Many Benthos processors create spans, and so tracing is a great way to analyse the pathways of individual messages as they progress through a Benthos instance. - -Some inputs, such as `http_server` and `http_client`, are capable of extracting a root span from the source of the message (HTTP headers). This is -a work in progress and should eventually expand so that all inputs have a way of doing so. - -Other inputs, such as `kafka` can be configured to extract a root span by using the `extract_tracing_map` field. - -A tracer config section looks like this: - -```yaml -tracer: - jaeger: - agent_address: localhost:6831 - sampler_type: const - sampler_param: 1 -``` - -WARNING: Although the configuration spec of this component is stable the format of spans, tags and logs created by Benthos is subject to change as it is tuned for improvement. - -import ComponentSelect from '@theme/ComponentSelect'; - - - - -[jaeger]: https://www.jaegertracing.io/ diff --git a/website/docs/components/tracers/gcp_cloudtrace.md b/website/docs/components/tracers/gcp_cloudtrace.md deleted file mode 100644 index 1b1e3f7fce..0000000000 --- a/website/docs/components/tracers/gcp_cloudtrace.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: gcp_cloudtrace -slug: gcp_cloudtrace -type: tracer -status: experimental ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Send tracing events to a [Google Cloud Trace](https://cloud.google.com/trace). - -Introduced in version 4.2.0. - - - - - - -```yml -# Common config fields, showing default values -tracer: - gcp_cloudtrace: - project: "" # No default (required) - sampling_ratio: 1 - flush_interval: "" # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -tracer: - gcp_cloudtrace: - project: "" # No default (required) - sampling_ratio: 1 - tags: {} - flush_interval: "" # No default (optional) -``` - - - - -## Fields - -### `project` - -The google project with Cloud Trace API enabled. If this is omitted then the Google Cloud SDK will attempt auto-detect it from the environment. - - -Type: `string` - -### `sampling_ratio` - -Sets the ratio of traces to sample. Tuning the sampling ratio is recommended for high-volume production workloads. - - -Type: `float` -Default: `1` - -```yml -# Examples - -sampling_ratio: 1 -``` - -### `tags` - -A map of tags to add to tracing spans. - - -Type: `object` -Default: `{}` - -### `flush_interval` - -The period of time between each flush of tracing spans. - - -Type: `string` - - diff --git a/website/docs/components/tracers/jaeger.md b/website/docs/components/tracers/jaeger.md deleted file mode 100644 index f0a8a0bcd6..0000000000 --- a/website/docs/components/tracers/jaeger.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: jaeger -slug: jaeger -type: tracer -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Send tracing events to a [Jaeger](https://www.jaegertracing.io/) agent or collector. - - - - - - -```yml -# Common config fields, showing default values -tracer: - jaeger: - agent_address: "" - collector_url: "" - sampler_type: const - flush_interval: "" # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -tracer: - jaeger: - agent_address: "" - collector_url: "" - sampler_type: const - sampler_param: 1 - tags: {} - flush_interval: "" # No default (optional) -``` - - - - -## Fields - -### `agent_address` - -The address of a Jaeger agent to send tracing events to. - - -Type: `string` -Default: `""` - -```yml -# Examples - -agent_address: jaeger-agent:6831 -``` - -### `collector_url` - -The URL of a Jaeger collector to send tracing events to. If set, this will override `agent_address`. - - -Type: `string` -Default: `""` -Requires version 3.38.0 or newer - -```yml -# Examples - -collector_url: https://jaeger-collector:14268/api/traces -``` - -### `sampler_type` - -The sampler type to use. - - -Type: `string` -Default: `"const"` - -| Option | Summary | -|---|---| -| `const` | Sample a percentage of traces. 1 or more means all traces are sampled, 0 means no traces are sampled and anything in between means a percentage of traces are sampled. Tuning the sampling rate is recommended for high-volume production workloads. | - - -### `sampler_param` - -A parameter to use for sampling. This field is unused for some sampling types. - - -Type: `float` -Default: `1` - -### `tags` - -A map of tags to add to tracing spans. - - -Type: `object` -Default: `{}` - -### `flush_interval` - -The period of time between each flush of tracing spans. - - -Type: `string` - - diff --git a/website/docs/components/tracers/none.md b/website/docs/components/tracers/none.md deleted file mode 100644 index 0662804160..0000000000 --- a/website/docs/components/tracers/none.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: none -slug: none -type: tracer -status: stable ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Do not send tracing events anywhere. - -```yml -# Config fields, showing default values -tracer: - none: {} -``` - - diff --git a/website/docs/components/tracers/open_telemetry_collector.md b/website/docs/components/tracers/open_telemetry_collector.md deleted file mode 100644 index 2b2e849622..0000000000 --- a/website/docs/components/tracers/open_telemetry_collector.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: open_telemetry_collector -slug: open_telemetry_collector -type: tracer -status: experimental ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution EXPERIMENTAL -This component is experimental and therefore subject to change or removal outside of major version releases. -::: -Send tracing events to an [Open Telemetry collector](https://opentelemetry.io/docs/collector/). - - - - - - -```yml -# Common config fields, showing default values -tracer: - open_telemetry_collector: - http: [] # No default (required) - grpc: [] # No default (required) - sampling: - enabled: false - ratio: 0.85 # No default (optional) -``` - - - - -```yml -# All config fields, showing default values -tracer: - open_telemetry_collector: - http: [] # No default (required) - grpc: [] # No default (required) - tags: {} - sampling: - enabled: false - ratio: 0.85 # No default (optional) -``` - - - - -## Fields - -### `http` - -A list of http collectors. - - -Type: `array` - -### `http[].address` - -The endpoint of a collector to send tracing events to. - - -Type: `string` - -```yml -# Examples - -address: localhost:4318 -``` - -### `http[].secure` - -Connect to the collector over HTTPS - - -Type: `bool` -Default: `false` - -### `grpc` - -A list of grpc collectors. - - -Type: `array` - -### `grpc[].address` - -The endpoint of a collector to send tracing events to. - - -Type: `string` - -```yml -# Examples - -address: localhost:4317 -``` - -### `grpc[].secure` - -Connect to the collector with client transport security - - -Type: `bool` -Default: `false` - -### `tags` - -A map of tags to add to all tracing spans. - - -Type: `object` -Default: `{}` - -### `sampling` - -Settings for trace sampling. Sampling is recommended for high-volume production workloads. - - -Type: `object` -Requires version 4.25.0 or newer - -### `sampling.enabled` - -Whether to enable sampling. - - -Type: `bool` -Default: `false` - -### `sampling.ratio` - -Sets the ratio of traces to sample. - - -Type: `float` - -```yml -# Examples - -ratio: 0.85 - -ratio: 0.5 -``` - - diff --git a/website/docs/configuration/about.md b/website/docs/configuration/about.md deleted file mode 100644 index 98c00bba5b..0000000000 --- a/website/docs/configuration/about.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: Configuration -sidebar_label: About -description: Learn about Benthos configuration ---- - -Benthos pipelines are configured in a YAML file that consists of a number of root sections, arranged like so: - -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - - - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - -pipeline: - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - -output: - aws_s3: - bucket: TODO - path: '${! meta("kafka_topic") }/${! json("message.id") }.json' -``` - - - - -```yaml -http: - address: 0.0.0.0:4195 - debug_endpoints: false - -input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - -buffer: - none: {} - -pipeline: - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - -output: - aws_s3: - bucket: TODO - path: '${! meta("kafka_topic") }/${! json("message.id") }.json' - -input_resources: [] -cache_resources: [] -processor_resources: [] -rate_limit_resources: [] -output_resources: [] - -logger: - level: INFO - static_fields: - '@service': benthos - -metrics: - prometheus: {} - -tracer: - none: {} - -shutdown_timeout: 20s -shutdown_delay: "" -``` - - - - - -Most sections represent a component type, which you can read about in more detail in [this document][components]. - -These types are hierarchical. For example, an `input` can have a list of child `processor` types attached to it, which in turn can have their own `processor` children. - -This is powerful but can potentially lead to large and cumbersome configuration files. This document outlines tooling provided by Benthos to help with writing and managing these more complex configuration files. - -### Testing - -For guidance on how to write and run unit tests for your configuration files read [this guide][config.testing]. - -## Customising Your Configuration - -Sometimes it's useful to write a configuration where certain fields can be defined during deployment. For this purpose Benthos supports [environment variable interpolation][config-interp], allowing you to set fields in your config with environment variables like so: - -```yaml -input: - kafka: - addresses: - - ${KAFKA_BROKER:localhost:9092} - topics: - - ${KAFKA_TOPIC:default-topic} -``` - -This is very useful for sharing configuration files across different deployment environments. - -## Reusing Configuration Snippets - -Sometimes it's necessary to use a rather large component multiple times. Instead of copy/pasting the configuration or using YAML anchors you can define your component [as a resource][config.resources]. - -In the following example we want to make an HTTP request with our payloads. Occasionally the payload might get rejected due to garbage within its contents, and so we catch these rejected requests, attempt to "cleanse" the contents and try to make the same HTTP request again. Since the HTTP request component is quite large (and likely to change over time) we make sure to avoid duplicating it by defining it as a resource `get_foo`: - -```yaml -pipeline: - processors: - - resource: get_foo - - catch: - - mapping: | - root = this - root.content = this.content.strip_html() - - resource: get_foo - -processor_resources: - - label: get_foo - http: - url: http://example.com/foo - verb: POST - headers: - SomeThing: "set-to-this" - SomeThingElse: "set-to-something-else" -``` - -### Feature Toggles - -Resources can be imported separately to your config file with the cli flag `-r` or `-resources`, which is a useful way to switch out resources with common names based on your chosen environment. For example, with a main configuration file `config.yaml`: - -```yaml -pipeline: - processors: - - resource: get_foo -``` - -And then two resource files, one stored at the path `./staging/request.yaml`: - -```yaml -processor_resources: - - label: get_foo - http: - url: http://example.com/foo - verb: POST - headers: - SomeThing: "set-to-this" - SomeThingElse: "set-to-something-else" -``` - -And another stored at the path `./production/request.yaml`: - -```yaml -processor_resources: - - label: get_foo - http: - url: http://example.com/bar - verb: PUT - headers: - Desires: "are-empty" -``` - -We can select our chosen resource by changing which file we import, either running: - -```sh -benthos -r ./staging/request.yaml -c ./config.yaml -``` - -Or: - -```sh -benthos -r ./production/request.yaml -c ./config.yaml -``` - -These flags also support wildcards, which allows you to import an entire directory of resource files like `benthos -r "./staging/*.yaml" -c ./config.yaml`. You can find out more about configuration resources in the [resources document][config.resources]. - -### Templating - -Resources can only be instantiated with a single configuration, which means they aren't suitable for cases where the configuration is required in multiple places but with slightly different parameters, ugh! - -But hey, why don't you chill out? Benthos has a (currently experimental) alternative feature called templates, with which it's possible to define a custom configuration schema and a template for building a configuration from that schema. You can read more about templates [in this guide][config.templating]. - -## Reloading - -It's possible to have a running instance of Benthos reload configurations, including resource files imported with `-r`/`--resources`, automatically when the files are updated without needing to manually restart the service. This is done by specifying the `-w`/`--watcher` flag when running Benthos in normal mode or in streams mode: - -```sh -# Normal mode -benthos -w -r ./production/request.yaml -c ./config.yaml -``` - -```sh -# Streams mode -benthos -w -r ./production/request.yaml streams ./stream_configs/*.yaml -``` - -If a file update results in configuration parsing or linting errors then the change is ignored (with logs informing you of the problem) and the previous configuration will continue to be run (until the issues are fixed). - -## Enabling Discovery - -The discoverability of configuration fields is a common headache with any configuration driven application. The classic solution is to provide curated documentation that is often hosted on a dedicated site. - -However, a user often only needs to get their hands on a short, runnable example config file for their use case. They just need to see the format and field names as the fields themselves are usually self explanatory. Forcing such a user to navigate a website, scrolling through paragraphs of text, seems inefficient when all they actually needed to see was something like: - -```yaml -input: - amqp_0_9: - urls: [ amqp://guest:guest@localhost:5672/ ] - consumer_tag: benthos-consumer - queue: benthos-queue - prefetch_count: 10 - prefetch_size: 0 -output: - stdout: {} -``` - -In order to make this process easier Benthos is able to generate usable configuration examples for any types, and you can do this from the binary using the `create` subcommand. - -If, for example, we wanted to generate a config with a websocket input, a Kafka output and a [`mapping` processor][processors.mapping] in the middle, we could do it with the following command: - -```text -benthos create websocket/mapping/kafka -``` - -> If you need a gentle reminder as to which components Benthos offers you can see those as well with `benthos list`. - -All of these generated configuration examples also include other useful config sections such as `metrics`, `logging`, etc with sensible defaults. - -For more information read the output from `benthos create --help`. - -## Help With Debugging - -Once you have a config written you now move onto the next headache of proving that it works, and understanding why it doesn't. Benthos, like most good config driven services, performs validation on configs and tries to provide sensible error messages. - -However, with validation it can be hard to capture all problems, and the user usually understands their intentions better than the service. In order to help expose and diagnose config errors Benthos provides two mechanisms, linting and echoing. - -### Linting - -If you attempt to run a config that has linting errors Benthos will print the errors and halt execution. If, however, you want to test your configs before deployment you can do so with the `lint` subcommand: - -For example, imagine we have a config `foo.yaml`, where we intend to read from AMQP, but there is a typo in our config struct: - -```text -input: - amqp_0_9: - yourl: amqp://guest:guest@rabbitmqserver:5672/ -``` - -We can catch this error before attempting to run the config: - -```sh -$ benthos lint ./foo.yaml -./foo.yaml: line 3: field yourl not recognised -``` - -For more information read the output from `benthos lint --help`. - -### Echoing - -Echoing is where Benthos can print back your configuration _after_ it has been parsed. It is done with the `echo` subcommand, which is able to show you a normalised version of your config, allowing you to see how it was interpreted: - -```sh -benthos -c ./your-config.yaml echo -``` - -You can check the output of the above command to see if certain sections are missing or fields are incorrect, which allows you to pinpoint typos in the config. - -## Shutting down - -Under normal operating conditions, the Benthos process will shut down when there are no more messages produced by inputs and the final message has been processed. The shutdown procedure can also be initiated by sending the process a interrupt (`SIGINT`) or termination (`SIGTERM`) signal. There are two top-level configuration options that control the shutdown behaviour: `shutdown_timeout` and `shutdown_delay`. - -### Shutdown delay - -The `shutdown_delay` option can be used to delay the start of the shutdown procedure. This is useful for pipelines that need a short grace period to have their metrics and traces scraped. While the shutdown delay is in effect, the HTTP metrics endpoint continues to be available for scraping and any active tracers are free to flush remaining traces. - -The shutdown delay can be interrupted by sending the Benthos process a second OS interrupt or termination signal. - -### Shutdown timeout - -The `shutdown_timeout` option sets a hard deadline for Benthos process to gracefully terminate. If this duration is exceeded then the process is forcefully terminated and any messages that were in-flight will be dropped. - -This option takes effect after the `shutdown_delay` duration has passed if that is enabled. - -[processors]: /docs/components/processors/about -[processors.mapping]: /docs/components/processors/mapping -[config-interp]: /docs/configuration/interpolation -[config.testing]: /docs/configuration/unit_testing -[config.templating]: /docs/configuration/templating -[config.resources]: /docs/configuration/resources -[json-references]: https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 -[components]: /docs/components/about diff --git a/website/docs/configuration/batching.md b/website/docs/configuration/batching.md deleted file mode 100644 index 58b910d6f9..0000000000 --- a/website/docs/configuration/batching.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Message Batching ---- - -Benthos is able to join sources and sinks with sometimes conflicting batching behaviours without sacrificing its strong delivery guarantees. It's also able to perform powerful [processing functions][windowing] across batches of messages such as grouping, archiving and reduction. Therefore, batching within Benthos is a mechanism that serves multiple purposes: - -1. [Performance (throughput)](#performance) -2. [Grouped message processing](#grouped-message-processing) -3. [Compatibility (mixing multi and single part message protocols)](#compatibility) - -## Performance - -For most users the only benefit of batching messages is improving throughput over your output protocol. For some protocols this can happen in the background and requires no configuration from you. However, if an output has a `batching` configuration block this means it benefits from batching and requires you to specify how you'd like your batches to be formed by configuring a [batching policy](#batch-policy): - -```yaml -output: - kafka: - addresses: [ todo:9092 ] - topic: benthos_stream - - # Either send batches when they reach 10 messages or when 100ms has passed - # since the last batch. - batching: - count: 10 - period: 100ms -``` - -However, a small number of inputs such as [`kafka`][input_kafka] must be consumed sequentially (in this case by partition) and therefore benefit from specifying your batch policy at the input level instead: - -```yaml -input: - kafka: - addresses: [ todo:9092 ] - topics: [ benthos_input_stream ] - batching: - count: 10 - period: 100ms - -output: - kafka: - addresses: [ todo:9092 ] - topic: benthos_stream -``` - -Inputs that behave this way are documented as such and have a `batching` configuration block. - -Sometimes you may prefer to create your batches before processing in order to benefit from [batch wide processing](#grouped-message-processing), in which case if your input doesn't already support [a batch policy](#batch-policy) you can instead use a [`broker`][input_broker], which also allows you to combine inputs with a single batch policy: - -```yaml -input: - broker: - inputs: - - resource: foo - - resource: bar - batching: - count: 50 - period: 500ms -``` - -This also works the same with [output brokers][output_broker]. - -## Grouped Message Processing - -And some processors such as [`while`][processor.while] are executed once across a whole batch, you can avoid this behaviour with the [`for_each` processor][proc_for_each]: - -```yaml -pipeline: - processors: - - for_each: - - while: - at_least_once: true - max_loops: 0 - check: errored() - processors: - - catch: [] # Wipe any previous error - - resource: foo # Attempt this processor until success -``` - -There's a vast number of processors that specialise in operations across batches such as [grouping][proc_group_by] and [archiving][proc_archive]. For example, the following processors group a batch of messages according to a metadata field and compresses them into separate `.tar.gz` archives: - -```yaml -pipeline: - processors: - - group_by_value: - value: ${! meta("kafka_partition") } - - archive: - format: tar - - compress: - algorithm: gzip - -output: - aws_s3: - bucket: TODO - path: docs/${! meta("kafka_partition") }/${! count("files") }-${! timestamp_unix_nano() }.tar.gz -``` - -For more examples of batched (or windowed) processing check out [this document][windowing]. - -## Compatibility - -Benthos is able to read and write over protocols that support multiple part messages, and all payloads travelling through Benthos are represented as a multiple part message. Therefore, all components within Benthos are able to work with multiple parts in a message as standard. - -When messages reach an output that _doesn't_ support multiple parts the message is broken down into an individual message per part, and then one of two behaviours happen depending on the output. If the output supports batch sending messages then the collection of messages are sent as a single batch. Otherwise, Benthos falls back to sending the messages sequentially in multiple, individual requests. - -This behaviour means that not only can multiple part message protocols be easily matched with single part protocols, but also the concept of multiple part messages and message batches are interchangeable within Benthos. - -### Shrinking Batches - -A message batch (or multiple part message) can be broken down into smaller batches using the [`split`][split] processor: - -```yaml -input: - # Consume messages that arrive in three parts. - resource: foo - processors: - # Drop the third part - - select_parts: - parts: [ 0, 1 ] - # Then break our message parts into individual messages - - split: - size: 1 -``` - -This is also useful when your input source creates batches that are too large for your output protocol: - -```yaml -input: - aws_s3: - bucket: todo - -pipeline: - processors: - - decompress: - algorithm: gzip - - unarchive: - format: tar - # Limit batch sizes to 5MB - - split: - byte_size: 5_000_000 -``` - -## Batch Policy - -When an input or output component has a config field `batching` that means it supports a batch policy. This is a mechanism that allows you to configure exactly how your batching should work on messages before they are routed to the input or output it's associated with. Batches are considered complete and will be flushed downstream when either of the following conditions are met: - - -- The `byte_size` field is non-zero and the total size of the batch in bytes matches or exceeds it (disregarding metadata.) -- The `count` field is non-zero and the total number of messages in the batch matches or exceeds it. -- A message added to the batch causes the [`check`][bloblang] to return to `true`. -- The `period` field is non-empty and the time since the last batch exceeds its value. - -This allows you to combine conditions: - -```yaml -output: - kafka: - addresses: [ todo:9092 ] - topic: benthos_stream - - # Either send batches when they reach 10 messages or when 100ms has passed - # since the last batch. - batching: - count: 10 - period: 100ms -``` - -:::caution -A batch policy has the capability to _create_ batches, but not to break them down. -::: - -If your configured pipeline is processing messages that are batched _before_ they reach the batch policy then they may circumvent the conditions you've specified here, resulting in sizes you aren't expecting. - -If you are affected by this limitation then consider breaking the batches down with a [`split` processor][split] before they reach the batch policy. - -### Post-Batch Processing - -A batch policy also has a field `processors` which allows you to define an optional list of [processors][processors] to apply to each batch before it is flushed. This is a good place to aggregate or archive the batch into a compatible format for an output: - -```yaml -output: - http_client: - url: http://localhost:4195/post - batching: - count: 10 - processors: - - archive: - format: lines -``` - -The above config will batch up messages and then merge them into a line delimited format before sending it over HTTP. This is an easier format to parse than the default which would have been [rfc1342](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). - -During shutdown any remaining messages waiting for a batch to complete will be flushed down the pipeline. - -[processors]: /docs/components/processors/about -[processor.while]: /docs/components/processors/while -[split]: /docs/components/processors/split -[archive]: /docs/components/processors/archive -[unarchive]: /docs/components/processors/unarchive -[proc_for_each]: /docs/components/processors/for_each -[proc_group_by]: /docs/components/processors/group_by -[proc_archive]: /docs/components/processors/archive -[input_broker]: /docs/components/inputs/broker -[output_broker]: /docs/components/outputs/broker -[input_kafka]: /docs/components/inputs/kafka -[function_interpolation]: /docs/configuration/interpolation#bloblang-queries -[bloblang]: /docs/guides/bloblang/about -[windowing]: /docs/configuration/windowed_processing \ No newline at end of file diff --git a/website/docs/configuration/dynamic_inputs_and_outputs.md b/website/docs/configuration/dynamic_inputs_and_outputs.md deleted file mode 100644 index a38837d654..0000000000 --- a/website/docs/configuration/dynamic_inputs_and_outputs.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Dynamic Inputs and Outputs ---- - -It is possible to have sets of inputs and outputs in Benthos that can be added, -updated and removed during runtime with the [dynamic fan in][dynamic_inputs] and -[dynamic fan out][dynamic_outputs] types. - -Dynamic inputs and outputs are each identified by unique string labels, which -are specified when adding them either in configuration or via the HTTP API. The -labels are useful when querying which types are active. - -## API - -The API for dynamic types (both inputs and outputs) is a collection of HTTP REST -endpoints: - -### `/inputs` - -Returns a JSON object that maps input labels to an object containing details -about the input, including uptime and configuration. If the input has terminated -naturally the uptime will be set to `stopped`. - -``` json -{ - "": { - "uptime": "", - "config": - }, - ... -} -``` - -### `/inputs/{input_label}` - -GET returns the configuration of the input idenfified by `input_label`. - -POST sets the input `input_label` to the body of the request parsed as a JSON -configuration. If the input label already exists the previous input is first -stopped and removed. - -DELETE stops and removes the input identified by `input_label`. - -### `/outputs` - -Returns a JSON object that maps output labels to an object containing details -about the output, including uptime and configuration. If the output has -terminated naturally the uptime will be set to `stopped`. - -``` json -{ - "": { - "uptime": "", - "config": - }, - ... -} -``` - -### `/outputs/{output_label}` - -GET returns the configuration of the output idenfified by `output_label`. - -POST sets the output `output_label` to the body of the request parsed as a JSON -configuration. If the output label already exists the previous output is first -stopped and removed. - -DELETE stops and removes the output identified by `output_label`. - -A custom prefix can be set for these endpoints in configuration. - -## Applications - -Dynamic types are useful when a platforms data streams might need to change -regularly and automatically. It is also useful for triggering batches of -platform data, e.g. a cron job can be created to send hourly curl requests that -adds a dynamic input to read a file of sample data: - -``` sh -curl http://localhost:4195/inputs/read_sample -d @- << EOF -{ - "file": { - "path": "/tmp/line_delim_sample_data.txt" - } -} -EOF -``` - -Some inputs have a finite lifetime, e.g. `s3` without an SQS queue configured -will close once the whole bucket has been read. When a dynamic types lifetime -ends the `uptime` field of an input listing will be set to `stopped`. You can -use this to write tools that trigger new inputs (to move onto the next bucket, -for example). - -[dynamic_inputs]: /docs/components/inputs/dynamic -[dynamic_outputs]: /docs/components/outputs/dynamic diff --git a/website/docs/configuration/error_handling.md b/website/docs/configuration/error_handling.md deleted file mode 100644 index 86e13052d1..0000000000 --- a/website/docs/configuration/error_handling.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: Error Handling ---- - -It's always possible for things to go wrong, be a good captain and plan ahead. - -
- -Benthos supports a range of [processors][processors] such as `http` and `aws_lambda` that have the potential to fail if their retry attempts are exhausted. When this happens the data is not dropped but instead continues through the pipeline mostly unchanged, but a metadata flag is added allowing you to handle the errors in a way that suits your needs. - -This document outlines common patterns for dealing with errors, such as dropping them, recovering them with more processing, routing them to a dead-letter queue, or any combination thereof. - -## Abandon on Failure - -It's possible to define a list of processors which should be skipped for messages that failed a previous stage using the [`try` processor][processor.try]: - -```yaml -pipeline: - processors: - - try: - - resource: foo - - resource: bar # Skipped if foo failed - - resource: baz # Skipped if foo or bar failed -``` - -## Recover Failed Messages - -Failed messages can be fed into their own processor steps with a [`catch` processor][processor.catch]: - -```yaml -pipeline: - processors: - - resource: foo # Processor that might fail - - catch: - - resource: bar # Recover here -``` - -Once messages finish the catch block they will have their failure flags removed and are treated like regular messages. If this behaviour is not desired then it is possible to simulate a catch block with a [`switch` processor][processor.switch]: - -```yaml -pipeline: - processors: - - resource: foo # Processor that might fail - - switch: - - check: errored() - processors: - - resource: bar # Recover here -``` - -## Logging Errors - -When an error occurs there will occasionally be useful information stored within the error flag that can be exposed with the interpolation function [`error`][configuration.interpolation]. This allows you to expose the information with processors. - -For example, when catching failed processors you can [`log`][processor.log] the messages: - -```yaml -pipeline: - processors: - - resource: foo # Processor that might fail - - catch: - - log: - message: "Processing failed due to: ${!error()}" -``` - -Or perhaps augment the message payload with the error message: - -```yaml -pipeline: - processors: - - resource: foo # Processor that might fail - - catch: - - mapping: | - root = this - root.meta.error = error() -``` - -## Attempt Until Success - -It's possible to reattempt a processor for a particular message until it is successful with a [`retry`][processor.retry] processor: - -```yaml -pipeline: - processors: - - retry: - backoff: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s - processors: - # Attempt this processor until success, or the maximum elapsed time is reached. - - resource: foo -``` - -## Drop Failed Messages - -In order to filter out any failed messages from your pipeline you can use a [`mapping` processor][processor.mapping]: - -```yaml -pipeline: - processors: - - mapping: root = if errored() { deleted() } -``` - -This will remove any failed messages from a batch. Furthermore, dropping a message will propagate an acknowledgement (also known as "ack") upstream to the pipeline's input. - -## Reject Messages - -Some inputs such as NATS, GCP Pub/Sub and AMQP support nacking (rejecting) messages. We can perform a nack (or rejection) on data that has failed to process rather than delivering it to our output with a [`reject_errored` output][output.reject_errored]: - -```yaml -output: - reject_errored: - resource: foo # Only non-errored messages go here -``` - -## Route to a Dead-Letter Queue - -And by placing the above within a [`fallback` output][output.fallback] we can instead route the failed messages to a different output: - -```yaml -output: - fallback: - - reject_errored: - resource: foo # Only non-errored messages go here - - - resource: bar # Only errored messages, or those that failed to be delivered to foo, go here -``` - -And, finally, in cases where we wish to route data differently depending on the error message itself we can use a [`switch` output][output.switch]: - -```yaml -output: - switch: - cases: - # Capture specifically cat related errors - - check: errored() && error().contains("meow") - output: - resource: foo - - # Capture all other errors - - check: errored() - output: - resource: bar - - # Finally, route messages that haven't errored - - output: - resource: baz -``` - -[processors]: /docs/components/processors/about -[processor.mapping]: /docs/components/processors/mapping -[processor.switch]: /docs/components/processors/switch -[processor.retry]: /docs/components/processors/retry -[processor.for_each]: /docs/components/processors/for_each -[processor.catch]: /docs/components/processors/catch -[processor.try]: /docs/components/processors/try -[processor.log]: /docs/components/processors/log -[output.switch]: /docs/components/outputs/switch -[output.fallback]: /docs/components/outputs/fallback -[output.reject_errored]: /docs/components/outputs/reject_errored -[configuration.interpolation]: /docs/configuration/interpolation#bloblang-queries diff --git a/website/docs/configuration/field_paths.md b/website/docs/configuration/field_paths.md deleted file mode 100644 index 6a51a7d7ac..0000000000 --- a/website/docs/configuration/field_paths.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Field Paths ---- - -Many components within Benthos allow you to target certain fields using a JSON dot path. The syntax of a path within Benthos is similar to [JSON Pointers][json-pointers], except with dot separators instead of slashes (and no leading dot.) When a path is used to set a value any path segment that does not yet exist in the structure is created as an object. - -For example, if we had the following JSON structure: - -```json -{ - "foo": { - "bar": 21 - } -} -``` - -The query path `foo.bar` would return `21`. - -The characters `~` (%x7E) and `.` (%x2E) have special meaning in Benthos paths. Therefore `~` needs to be encoded as `~0` and `.` needs to be encoded as `~1` when these characters appear within a key. - -For example, if we had the following JSON structure: - -```json -{ - "foo.foo": { - "bar~bo": { - "": { - "baz": 22 - } - } - } -} -``` - -The query path `foo~1foo.bar~0bo..baz` would return `22`. - -## Arrays - -When Benthos encounters an array whilst traversing a JSON structure it requires the next path segment to be either an integer of an existing index, or, depending on whether the path is used to query or set the target value, the character `*` or `-` respectively. - -For example, if we had the following JSON structure: - -```json -{ - "foo": [ - 0, 1, { "bar": 23 } - ] -} -``` - -The query path `foo.2.bar` would return `23`. - -### Querying - -When a query reaches an array the character `*` indicates that the query should return the value of the remaining path from each element of the array (within an array.) - -### Setting - -When an array is reached the character `-` indicates that a new element should be appended to the end of the existing elements, if this character is not the final segment of the path then an object is created. - -[json-pointers]: https://tools.ietf.org/html/rfc6901 \ No newline at end of file diff --git a/website/docs/configuration/interpolation.md b/website/docs/configuration/interpolation.md deleted file mode 100644 index 5ff9e06675..0000000000 --- a/website/docs/configuration/interpolation.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Interpolation ---- - -Benthos allows you to dynamically set config fields with environment variables anywhere within a config file using the syntax `${}` (or `${:}` in order to specify a default value). This is useful for setting environment specific fields such as addresses: - -```yaml -input: - kafka: - addresses: [ "${BROKERS}" ] - consumer_group: benthos_bridge_consumer - topics: [ "haha_business" ] -``` - -```sh -BROKERS="foo:9092,bar:9092" benthos -c ./config.yaml -``` - -If a literal string is required that matches this pattern (`${foo}`) you can escape it with double brackets. For example, the string `${{foo}}` is read as the literal `${foo}`. - -### Undefined Variables - -When an environment variable interpolation is found within a config, does not have a default value specified, and the environment variable is not defined a linting error will be reported. In order to avoid this it is possible to specify environment variable interpolations with an explicit empty default value by adding the colon without a following value, i.e. `${FOO:}` would be equivalent to `${FOO}` and would not trigger a linting error should `FOO` not be defined. - -## Bloblang Queries - -Some Benthos fields also support [Bloblang][bloblang] function interpolations, which are much more powerful expressions that allow you to query the contents of messages and perform arithmetic. The syntax of a function interpolation is `${!}`, where the contents are a bloblang query (the right-hand-side of a bloblang map) including a range of [functions][bloblang_functions]. For example, with the following config: - -```yaml -output: - kafka: - addresses: [ "TODO:6379" ] - topic: 'meow-${! json("topic") }' -``` - -A message with the contents `{"topic":"foo","message":"hello world"}` would be routed to the Kafka topic `meow-foo`. - -If a literal string is required that matches this pattern (`${!foo}`) then, similar to environment variables, you can escape it with double brackets. For example, the string `${{!foo}}` would be read as the literal `${!foo}`. - -Bloblang supports arithmetic, boolean operators, coalesce and mapping expressions. For more in-depth details about the language [check out the docs][bloblang]. - -## Examples - -### Reference Metadata - -A common usecase for interpolated functions is dynamic routing at the output level using metadata: - -```yaml -output: - kafka: - addresses: [ TODO ] - topic: ${! meta("output_topic") } - key: ${! meta("key") } -``` - -### Coalesce and Mapping - -Bloblang supports coalesce and mapping, which makes it easy to extract values from slightly varying data structures: - -```yaml -pipeline: - processors: - - cache: - resource: foocache - operator: set - key: '${! json().message.(foo | bar).id }' - value: '${! content() }' -``` - -Here's a map of inputs to resulting values: - -``` -{"foo":{"a":{"baz":"from_a"},"c":{"baz":"from_c"}}} -> from_a -{"foo":{"b":{"baz":"from_b"},"c":{"baz":"from_c"}}} -> from_b -{"foo":{"b":null,"c":{"baz":"from_c"}}} -> from_c -``` - -### Delayed Processing - -We have a stream of JSON documents each with a unix timestamp field `doc.received_at` which is set when our platform receives it. We wish to only process messages an hour _after_ they were received. We can achieve this by running the `sleep` processor using an interpolation function to calculate the seconds needed to wait for: - -```yaml -pipeline: - processors: - - sleep: - duration: '${! 3600 - ( timestamp_unix() - json("doc.created_at").number() ) }s' -``` - -If the calculated result is less than or equal to zero the processor does not sleep at all. If the value of `doc.created_at` is a string then our method `.number()` will attempt to parse it into a number. - -[error_handling]: /docs/configuration/error_handling -[field_paths]: /docs/configuration/field_paths -[meta_proc]: /docs/components/processors/metadata -[bloblang]: /docs/guides/bloblang/about -[bloblang_functions]: /docs/guides/bloblang/about#functions \ No newline at end of file diff --git a/website/docs/configuration/metadata.md b/website/docs/configuration/metadata.md deleted file mode 100644 index a56bf5909c..0000000000 --- a/website/docs/configuration/metadata.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Metadata ---- - -In Benthos each message has raw contents and metadata, which is a map of key/value pairs representing an arbitrary amount of complementary data. - -When an input protocol supports attributes or metadata they will automatically be added to your messages, refer to the respective input documentation for a list of metadata keys. When an output supports attributes or metadata any metadata key/value pairs in a message will be sent (subject to service limits). - -## Editing Metadata - -Benthos allows you to add and remove metadata using the [`mapping` processor][processors.mapping]. For example, you can do something like this in your pipeline: - -```yaml -pipeline: - processors: - - mapping: | - # Remove all existing metadata from messages - meta = deleted() - - # Add a new metadata field `time` from the contents of a JSON - # field `event.timestamp` - meta time = event.timestamp -``` - -You can also use [Bloblang][guides.bloblang] to delete individual metadata keys with: - -```coffee -meta foo = deleted() -``` - -Or do more interesting things like remove all metadata keys with a certain prefix: - -```coffee -meta = @.filter(kv -> !kv.key.has_prefix("kafka_")) -``` - -## Using Metadata - -Metadata values can be referenced in any field that supports [interpolation functions][interpolation]. For example, you can route messages to Kafka topics using interpolation of metadata keys: - -```yaml -output: - kafka: - addresses: [ TODO ] - topic: ${! meta("target_topic") } -``` - -Benthos also allows you to conditionally process messages based on their metadata with the [`switch` processor][processors.switch]: - -```yaml -pipeline: - processors: - - switch: - - check: '@doc_type == "nested"' - processors: - - sql_insert: - driver: mysql - dsn: foouser:foopassword@tcp(localhost:3306)/foodb - table: footable - columns: [ foo, bar, baz ] - args_mapping: | - root = [ - this.document.foo, - this.document.bar, - @kafka_topic, - ] -``` - -## Restricting Metadata - -Outputs that support metadata, headers or some other variant of enriched fields on messages will attempt to send all metadata key/value pairs by default. However, sometimes it's useful to refer to metadata fields at the output level even though we do not wish to send them with our data. In this case it's possible to restrict the metadata keys that are sent with the field `metadata.exclude_prefixes` within the respective output config. - -For example, if we were sending messages to kafka using a metadata key `target_topic` to determine the topic but we wished to prevent that metadata key from being sent as a header we could use the following configuration: - -```yaml -output: - kafka: - addresses: [ TODO ] - topic: ${! meta("target_topic") } - metadata: - exclude_prefixes: - - target_topic -``` - -And when the list of metadata keys that we do _not_ want to send is large it can be helpful to use a [Bloblang mapping][guides.bloblang] in order to give all of these "private" keys a common prefix: - -```yaml -pipeline: - processors: - # Has an explicit list of public metadata keys, and everything else is given - # an underscore prefix. - - mapping: | - let allowed_meta = [ - "foo", - "bar", - "baz", - ] - meta = @.map_each_key(key -> if !$allowed_meta.contains(key) { - "_" + key - }) - -output: - kafka: - addresses: [ TODO ] - topic: ${! meta("_target_topic") } - metadata: - exclude_prefixes: [ "_" ] -``` - -[interpolation]: /docs/configuration/interpolation -[processors.switch]: /docs/components/processors/switch -[processors.mapping]: /docs/components/processors/mapping -[guides.bloblang]: /docs/guides/bloblang/about diff --git a/website/docs/configuration/processing_pipelines.md b/website/docs/configuration/processing_pipelines.md deleted file mode 100644 index 2537fe7146..0000000000 --- a/website/docs/configuration/processing_pipelines.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Processing Pipelines ---- - -Within a Benthos configuration, in between `input` and `output`, is a `pipeline` section. This section describes an array of [processors][processors] that are to be applied to *all* messages, and are not bound to any particular input or output. - -If you have processors that are heavy on CPU and aren't specific to a certain input or output they are best suited for the pipeline section. It is advantageous to use the pipeline section as it allows you to set an explicit number of parallel threads of execution: - -```yaml -input: - resource: foo - -pipeline: - threads: 4 - processors: - - mapping: | - root = this - fans = fans.map_each(match { - this.obsession > 0.5 => this - _ => deleted() - }) - -output: - resource: bar -``` - -If the field `threads` is set to `-1` (the default) it will automatically match the number of logical CPUs available. By default almost all Benthos sources will utilise as many processing threads as have been configured, which makes horizontal scaling easy. - -[processors]: /docs/components/processors/about diff --git a/website/docs/configuration/resources.md b/website/docs/configuration/resources.md deleted file mode 100644 index 00be013e94..0000000000 --- a/website/docs/configuration/resources.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Resources ---- - -Resources are components within Benthos that are declared with a unique label and can be referenced any number of times within a configuration. Only one instance of each named resource is created, but it is safe to use it in multiple places as they can be shared without consequence. - -Some components such as caches and rate limits can _only_ be created as a resource. However, for components where it's optional there are a few reasons why it might be advantageous to do so. - -```yaml -input: - resource: foo - -pipeline: - processors: - - resource: bar - - cache: - operator: set - resource: baz - key: ${! json("id") } - value: ${! content() } - -output: - resource: buz - -input_resources: - - label: foo - file: - paths: [ ./in.txt ] - -processor_resources: - - label: bar - mapping: 'root = content.lowercase()' - -cache_resources: - - label: baz - memory: - default_ttl: 300s - -output_resources: - - label: buz - file: - path: ./out.txt -``` - -## Reusability - -Sometimes it's necessary to use a rather large component multiple times. Instead of copy/pasting the configuration or using YAML anchors you can define your component as a resource. - -In the following example we want to make an HTTP request with our payloads. Occasionally the payload might get rejected due to garbage within its contents, and so we catch these rejected requests, attempt to "cleanse" the contents and try to make the same HTTP request again. Since the HTTP request component is quite large (and likely to change over time) we make sure to avoid duplicating it by defining it as a resource `get_foo`: - -```yaml -pipeline: - processors: - - resource: get_foo - - catch: - - mapping: | - root = this - root.content = this.content.strip_html() - - resource: get_foo - -processor_resources: - - label: get_foo - http: - url: http://example.com/foo - verb: POST - headers: - SomeThing: "set-to-this" - SomeThingElse: "set-to-something-else" -``` - -## Feature Toggling - -### With Environment Variables - -There are two ways of using resources for feature toggling, the first is to define your feature components with unique names and then apply the old switcheroo with environment variables to select the one you wish to execute: - -```yaml -pipeline: - processors: - - resource: ${FEATURE_REQUEST} - -processor_resources: - - label: get_foo - http: - url: http://example.com/foo - verb: POST - headers: - SomeThing: "set-to-this" - SomeThingElse: "set-to-something-else" - - - label: get_bar - http: - url: http://example.com/bar - verb: PUT - headers: - Desires: "are-empty" -``` - -Then when you execute Benthos use the environment variable to choose your resource: `FEATURE_REQUEST=get_foo benthos -c ./your_config.yaml`. - -### With Imports - -However, Benthos allows you to import resources from separate files with the cli flag `-r` or `-resources`, which can be a useful way to switch out resources with common names based on your chosen environment. For example, with a main configuration file `config.yaml`: - -```yaml -pipeline: - processors: - - resource: get_foo -``` - -And then two resource files, one stored at the path `./staging/request.yaml`: - -```yaml -processor_resources: - - label: get_foo - http: - url: http://example.com/foo - verb: POST - headers: - SomeThing: "set-to-this" - SomeThingElse: "set-to-something-else" -``` - -And another stored at the path `./production/request.yaml`: - -```yaml -processor_resources: - - label: get_foo - http: - url: http://example.com/bar - verb: PUT - headers: - Desires: "are-empty" -``` - -We can select our chosen resource by changing which file we import, either running: - -```sh -benthos -r ./staging/request.yaml -c ./config.yaml -``` - -Or: - -```sh -benthos -r ./production/request.yaml -c ./config.yaml -``` - -These flags also support wildcards, which allows you to import an entire directory of resource files like `benthos -r "./staging/*.yaml" -c ./config.yaml`. diff --git a/website/docs/configuration/secrets.md b/website/docs/configuration/secrets.md deleted file mode 100644 index c49d0d7247..0000000000 --- a/website/docs/configuration/secrets.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Secrets ---- - -I sometimes like to fill my mouth with acorns and pretend I am a rodent, free of the burdens of humanity. That was a secret of mine and, similarly to secrets within your software, it's best not to share them publicly lest they become disturbing publications instead. This document outlines how to add secrets to a Benthos config without persisting them, and we won't mention acorns again. - -## Using Environment Variables - -One of the most prolific approaches to providing secrets to a service is via environment variables. Benthos allows you to inject the values of environment variables into a configuration with the interpolation syntax `${FOO}`, within a config it looks like this: - -```yml -thing: - super_secret: "${SECRET}" -``` - -:::info Use quotes -Note that it would be valid to have `super_secret: ${SECRET}` above (without the quotes), but if `SECRET` is unset then the config becomes structurally different. Therefore, it's always best to wrap environment variable interpolations with quotes so that when the variable is unset you still have a valid config (with an empty string). -::: - -More information about this syntax can be found on the [interpolation field page][interpolation]. - -## Using CLI Flags - -As an alternative to environment variables it's possible to set specific fields within a config using the CLI flag `--set` where the syntax is a `=` pair, the path being a [dot-separated path to the field being set][field_paths] and the value being the thing to set it to. If, for example, we had the config: - -```yml -thing: - super_secret: "" -``` - -And we wanted to set the value of `super_secret` to a value stored within something like Hashicorp Vault we could run the config using the `--set` flag with backticks to execute a shell command for the value: - -```sh -benthos -c ./config.yaml \ - --set "thing.super_secret=`vault kv get -mount=secret thing_secret`" -``` - -Using this method we can inject the secret into the config without "leaking" it into an environment variable. - -## Avoiding Leaked Secrets - -There are a few ways in which configs parsed by Benthos can be exported back out of the service. In all of these cases Benthos will attempt to scrub any field values within the config that are known secrets (any field marked as a secret in the docs). - -However, if you're embedding secrets within a config outside of the value of secret fields, maybe as part of a Bloblang mapping, then care should be made to avoid exposing the resulting config. This specifically means you should not enable [debug HTTP endpoints][http.debug] when the port is exposed, and don't use the `benthos echo` subcommand on configs containing secrets unless you're printing to a secure pipe. - -[interpolation]: /docs/configuration/interpolation -[field_paths]: /docs/configuration/field_paths -[http.debug]: /docs/components/http/about#debug-endpoints - diff --git a/website/docs/configuration/templating.md b/website/docs/configuration/templating.md deleted file mode 100644 index 7e8434e0a8..0000000000 --- a/website/docs/configuration/templating.md +++ /dev/null @@ -1,285 +0,0 @@ ---- -title: Templating -description: Learn how Benthos templates work. ---- - - - -:::warning EXPERIMENTAL -Templates are an experimental feature and therefore subject to change outside of major version releases. -::: - -Templates are a way to define new Benthos components (similar to plugins) that are implemented by generating a Benthos config snippet from pre-defined parameter fields. This is useful when a common pattern of Benthos configuration is used but with varying parameters each time. - -A template is defined in a YAML file that can be imported when Benthos runs using the flag `-t`: - -```sh -benthos -t "./templates/*.yaml" -c ./config.yaml -``` - -The template describes the type of the component and configuration fields that can be used to customize it, followed by a [Bloblang mapping][bloblang.about] that translates an object containing those fields into a benthos config structure. This allows you to use logic to generate more complex configurations: - -import Tabs from '@theme/Tabs'; - - - -import TabItem from '@theme/TabItem'; - - - -```yml -name: aws_sqs_list -type: input - -fields: - - name: urls - type: string - kind: list - - name: region - type: string - default: us-east-1 - -mapping: | - root.broker.inputs = this.urls.map_each(url -> { - "aws_sqs": { - "url": url, - "region": this.region, - } - }) -``` - - - - -```yml -input: - aws_sqs_list: - urls: - - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 - - https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 - -pipeline: - processors: - - mapping: | - root.id = uuid_v4() - root.foo = this.inner.foo - root.body = this.outer -``` - - - - -```yaml -input: - broker: - inputs: - - aws_sqs: - url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue1 - region: us-east-1 - - aws_sqs: - url: https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue2 - region: us-east-1 - -pipeline: - processors: - - mapping: | - root.id = uuid_v4() - root.foo = this.inner.foo - root.body = this.outer -``` - - - - - -You can see more examples of templates at [https://github.com/benthosdev/benthos/tree/main/config/template_examples](https://github.com/benthosdev/benthos/tree/main/config/template_examples). - -## Fields - -The schema of a template file is as follows: - -### `name` - -The name of the component this template will create. - - -Type: `string` - -### `type` - -The type of the component this template will create. - - -Type: `string` -Options: `cache`, `input`, `output`, `processor`, `rate_limit`. - -### `status` - -The stability of the template describing the likelihood that the configuration spec of the template, or it's behaviour, will change. - - -Type: `string` -Default: `"stable"` - -| Option | Summary | -|---|---| -| `stable` | This template is stable and will therefore not change in a breaking way outside of major version releases. | -| `beta` | This template is beta and will therefore not change in a breaking way unless a major problem is found. | -| `experimental` | This template is experimental and therefore subject to breaking changes outside of major version releases. | - - -### `categories` - -An optional list of tags, which are used for arbitrarily grouping components in documentation. - - -Type: list of `string` -Default: `[]` - -### `summary` - -A short summary of the component. - - -Type: `string` -Default: `""` - -### `description` - -A longer form description of the component and how to use it. - - -Type: `string` -Default: `""` - -### `fields` - -The configuration fields of the template, fields specified here will be parsed from a Benthos config and will be accessible from the template mapping. - - -Type: list of `object` - -### `fields[].name` - -The name of the field. - - -Type: `string` - -### `fields[].description` - -A description of the field. - - -Type: `string` -Default: `""` - -### `fields[].type` - -The scalar type of the field. - - -Type: `string` - -| Option | Summary | -|---|---| -| `string` | standard string type | -| `int` | standard integer type | -| `float` | standard float type | -| `bool` | a boolean true/false | -| `unknown` | allows for nesting arbitrary configuration inside of a field | - - -### `fields[].kind` - -The kind of the field. - - -Type: `string` -Default: `"scalar"` -Options: `scalar`, `map`, `list`. - -### `fields[].default` - -An optional default value for the field. If a default value is not specified then a configuration without the field is considered incorrect. - - -Type: `unknown` - -### `fields[].advanced` - -Whether this field is considered advanced. - - -Type: `bool` -Default: `false` - -### `mapping` - -A [Bloblang](/docs/guides/bloblang/about) mapping that translates the fields of the template into a valid Benthos configuration for the target component type. - - -Type: `string` - -### `metrics_mapping` - -An optional [Bloblang mapping](/docs/guides/bloblang/about) that allows you to rename or prevent certain metrics paths from being exported. For more information check out the [metrics documentation](/docs/components/metrics/about#metric-mapping). When metric paths are created, renamed and dropped a trace log is written, enabling TRACE level logging is therefore a good way to diagnose path mappings. - -Invocations of this mapping are able to reference a variable $label in order to obtain the value of the label provided to the template config. This allows you to match labels with the root of the config. - - -Type: `string` -Default: `""` - -```yml -# Examples - -metrics_mapping: this.replace("input", "source").replace("output", "sink") - -metrics_mapping: |- - root = if ![ - "input_received", - "input_latency", - "output_sent" - ].contains(this) { deleted() } -``` - -### `tests` - -Optional unit test definitions for the template that verify certain configurations produce valid configs. These tests are executed with the command `benthos template lint`. - - -Type: list of `object` -Default: `[]` - -### `tests[].name` - -A name to identify the test. - - -Type: `string` - -### `tests[].config` - -A configuration to run this test with, the config resulting from applying the template with this config will be linted. - - -Type: `object` - -### `tests[].expected` - -An optional configuration describing the expected result of applying the template, when specified the result will be diffed and any mismatching fields will be reported as a test error. - - -Type: `object` - -[bloblang.about]: /docs/guides/bloblang/about diff --git a/website/docs/configuration/unit_testing.md b/website/docs/configuration/unit_testing.md deleted file mode 100644 index dfffe38dd5..0000000000 --- a/website/docs/configuration/unit_testing.md +++ /dev/null @@ -1,602 +0,0 @@ ---- -title: Unit Testing ---- - - - -The Benthos service offers a command `benthos test` for running unit tests on sections of a configuration file. This makes it easy to protect your config files from regressions over time. - -## Contents - -1. [Writing a Test](#writing-a-test) -2. [Output Conditions](#output-conditions) -3. [Running Tests](#running-tests) -4. [Mocking Processors](#mocking-processors) -5. [Config Field Spec](#fields) - -## Writing a Test - -Let's imagine we have a configuration file `foo.yaml` containing some processors: - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - -pipeline: - processors: - - mapping: '"%vend".format(content().uppercase().string())' - -output: - aws_s3: - bucket: TODO - path: '${! meta("kafka_topic") }/${! json("message.id") }.json' -``` - -One way to write our unit tests for this config is to accompany it with a file of the same name and extension but suffixed with `_benthos_test`, which in this case would be `foo_benthos_test.yaml`. - -```yml -tests: - - name: example test - target_processors: '/pipeline/processors' - environment: {} - input_batch: - - content: 'example content' - metadata: - example_key: example metadata value - output_batches: - - - - content_equals: EXAMPLE CONTENTend - metadata_equals: - example_key: example metadata value -``` - -Under `tests` we have a list of any number of unit tests to execute for the config file. Each test is run in complete isolation, including any resources defined by the config file. Tests should be allocated a unique `name` that identifies the feature being tested. - -The field `target_processors` is either the label of a processor to test, or a [JSON Pointer][json-pointer] that identifies the position of a processor, or list of processors, within the file which should be executed by the test. For example a value of `foo` would target a processor with the label `foo`, and a value of `/input/processors` would target all processors within the input section of the config. - -The field `environment` allows you to define an object of key/value pairs that set environment variables to be evaluated during the parsing of the target config file. These are unique to each test, allowing you to test different environment variable interpolation combinations. - -The field `input_batch` lists one or more messages to be fed into the targeted processors as a batch. Each message of the batch may have its raw content defined as well as metadata key/value pairs. - -For the common case where the messages are in JSON format, you can use `json_content` instead of `content` to specify the message structurally rather than verbatim. - -The field `output_batches` lists any number of batches of messages which are expected to result from the target processors. Each batch lists any number of messages, each one defining [`conditions`](#output-conditions) to describe the expected contents of the message. - -If the number of batches defined does not match the resulting number of batches the test will fail. If the number of messages defined in each batch does not match the number in the resulting batches the test will fail. If any condition of a message fails then the test fails. - -### Inline Tests - -Sometimes it's more convenient to define your tests within the config being tested. This is fine, simply add the `tests` field to the end of the config being tested. - -### Bloblang Tests - -Sometimes when working with large [Bloblang mappings][bloblang] it's preferred to have the full mapping in a separate file to your Benthos configuration. In this case it's possible to write unit tests that target and execute the mapping directly with the field `target_mapping`, which when specified is interpreted as either an absolute path or a path relative to the test definition file that points to a file containing only a Bloblang mapping. - -For example, if we were to have a file `cities.blobl` containing a mapping: - -```coffee -root.Cities = this.locations. - filter(loc -> loc.state == "WA"). - map_each(loc -> loc.name). - sort().join(", ") -``` - -We can accompany it with a test file `cities_test.yaml` containing a regular test definition: - -```yml -tests: - - name: test cities mapping - target_mapping: './cities.blobl' - environment: {} - input_batch: - - content: | - { - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] - } - output_batches: - - - - json_equals: {"Cities": "Bellevue, Olympia, Seattle"} -``` - -And execute this test the same way we execute other Benthos tests (`benthos test ./dir/cities_test.yaml`, `benthos test ./dir/...`, etc). - -### Fragmented Tests - -Sometimes the number of tests you need to define in order to cover a config file is so vast that it's necessary to split them across multiple test definition files. This is possible but Benthos still requires a way to detect the configuration file being targeted by these fragmented test definition files. In order to do this we must prefix our `target_processors` field with the path of the target relative to the definition file. - -The syntax of `target_processors` in this case is a full [JSON Pointer][json-pointer] that should look something like `target.yaml#/pipeline/processors`. For example, if we saved our test definition above in an arbitrary location like `./tests/first.yaml` and wanted to target our original `foo.yaml` config file, we could do that with the following: - -```yml -tests: - - name: example test - target_processors: '../foo.yaml#/pipeline/processors' - environment: {} - input_batch: - - content: 'example content' - metadata: - example_key: example metadata value - output_batches: - - - - content_equals: EXAMPLE CONTENTend - metadata_equals: - example_key: example metadata value -``` - -## Input Definitions - -### `content` - -Sets the raw content of the message. - -### `json_content` - -```yml -json_content: - foo: foo value - bar: [ element1, 10 ] -``` - -Sets the raw content of the message to a JSON document matching the structure of the value. - -### `file_content` - -```yml -file_content: ./foo/bar.txt -``` - -Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. - -### `metadata` - -A map of key/value pairs that sets the metadata values of the message. - -## Output Conditions - -### `bloblang` - -```yml -bloblang: 'this.age > 10 && @foo.length() > 0' -``` - -Executes a [Bloblang expression][bloblang] on a message, if the result is anything other than a boolean equalling `true` the test fails. - -### `content_equals` - -```yml -content_equals: example content -``` - -Checks the full raw contents of a message against a value. - -### `content_matches` - -```yml -content_matches: "^foo [a-z]+ bar$" -``` - -Checks whether the full raw contents of a message matches a regular expression (re2). - -### `metadata_equals` - -```yml -metadata_equals: - example_key: example metadata value -``` - -Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. - -### `file_equals` - -```yml -file_equals: ./foo/bar.txt -``` - -Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. - -### `file_json_equals` - -```yml -file_json_equals: ./foo/bar.json -``` - -Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. - -### `json_equals` - -```yml -json_equals: { "key": "value" } -``` - -Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. - -You can also structure the condition content as YAML and it will be converted to the equivalent JSON document for testing: - -```yml -json_equals: - key: value -``` - -### `json_contains` - -```yml -json_contains: { "key": "value" } -``` - -Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. - -## Running Tests - -Executing tests for a specific config can be done by pointing the subcommand `test` at either the config to be tested or its test definition, e.g. `benthos test ./config.yaml` and `benthos test ./config_benthos_test.yaml` are equivalent. - -The `test` subcommand also supports wildcard patterns e.g. `benthos test ./foo/*.yaml` will execute all tests within matching files. In order to walk a directory tree and execute all tests found you can use the shortcut `./...`, e.g. `benthos test ./...` will execute all tests found in the current directory, any child directories, and so on. - -If you want to allow components to write logs at a provided level to stdout when running the tests, you can use -`benthos test --log `. Please consult the [logger docs][logger] for further details. - -## Mocking Processors - -BETA: This feature is currently in a BETA phase, which means breaking changes could be made if a fundamental issue with the feature is found. - -Sometimes you'll want to write tests for a series of processors, where one or more of them are networked (or otherwise stateful). Rather than creating and managing mocked services you can define mock versions of those processors in the test definition. For example, if we have a config with the following processors: - -```yaml -pipeline: - processors: - - mapping: 'root = "simon says: " + content()' - - label: get_foobar_api - http: - url: http://example.com/foobar - verb: GET - - mapping: 'root = content().uppercase()' -``` - -Rather than create a fake service for the `http` processor to interact with we can define a mock in our test definition that replaces it with a [`mapping` processor][processors.mapping]. Mocks are configured as a map of labels that identify a processor to replace and the config to replace it with: - -```yaml -tests: - - name: mocks the http proc - target_processors: '/pipeline/processors' - mocks: - get_foobar_api: - mapping: 'root = content().string() + " this is some mock content"' - input_batch: - - content: "hello world" - output_batches: - - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" -``` - -With the above test definition the `http` processor will be swapped out for `mapping: 'root = content().string() + " this is some mock content"'`. For the purposes of mocking it is recommended that you use a [`mapping` processor][processors.mapping] that simply mutates the message in a way that you would expect the mocked processor to. - -> Note: It's not currently possible to mock components that are imported as separate resource files (using `--resource`/`-r`). It is recommended that you mock these by maintaining separate definitions for test purposes (`-r "./test/*.yaml"`). - -### More granular mocking - -It is also possible to target specific fields within the test config by [JSON pointers][json-pointer] as an alternative to labels. The following test definition would create the same mock as the previous: - -```yaml -tests: - - name: mocks the http proc - target_processors: '/pipeline/processors' - mocks: - /pipeline/processors/1: - mapping: 'root = content().string() + " this is some mock content"' - input_batch: - - content: "hello world" - output_batches: - - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" -``` - -## Fields - -The schema of a template file is as follows: - -### `tests` - -A list of one or more unit tests to execute. - - -Type: list of `object` - -### `tests[].name` - -The name of the test, this should be unique and give a rough indication of what behaviour is being tested. - - -Type: `string` - -### `tests[].environment` - -An optional map of environment variables to set for the duration of the test. - - -Type: map of `string` - -### `tests[].target_processors` - -A [JSON Pointer][json-pointer] that identifies the specific processors which should be executed by the test. The target can either be a single processor or an array of processors. Alternatively a resource label can be used to identify a processor. - -It is also possible to target processors in a separate file by prefixing the target with a path relative to the test file followed by a # symbol. - - -Type: `string` -Default: `"/pipeline/processors"` - -```yml -# Examples - -target_processors: foo_processor - -target_processors: /pipeline/processors/0 - -target_processors: target.yaml#/pipeline/processors - -target_processors: target.yaml#/pipeline/processors -``` - -### `tests[].target_mapping` - -A file path relative to the test definition path of a Bloblang file to execute as an alternative to testing processors with the `target_processors` field. This allows you to define unit tests for Bloblang mappings directly. - - -Type: `string` -Default: `""` - -### `tests[].mocks` - -An optional map of processors to mock. Keys should contain either a label or a JSON pointer of a processor that should be mocked. Values should contain a processor definition, which will replace the mocked processor. Most of the time you'll want to use a [`mapping` processor][processors.mapping] here, and use it to create a result that emulates the target processor. - - -Type: map of `unknown` - -```yml -# Examples - -mocks: - get_foobar_api: - mapping: root = content().string() + " this is some mock content" - -mocks: - /pipeline/processors/1: - mapping: root = content().string() + " this is some mock content" -``` - -### `tests[].input_batch` - -Define a batch of messages to feed into your test, specify either an `input_batch` or a series of `input_batches`. - - -Type: list of `object` - -### `tests[].input_batch[].content` - -The raw content of the input message. - - -Type: `string` - -### `tests[].input_batch[].json_content` - -Sets the raw content of the message to a JSON document matching the structure of the value. - - -Type: `unknown` - -```yml -# Examples - -json_content: - bar: - - element1 - - 10 - foo: foo value -``` - -### `tests[].input_batch[].file_content` - -Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. - - -Type: `string` - -```yml -# Examples - -file_content: ./foo/bar.txt -``` - -### `tests[].input_batch[].metadata` - -A map of metadata key/values to add to the input message. - - -Type: map of `unknown` - -### `tests[].input_batches` - -Define a series of batches of messages to feed into your test, specify either an `input_batch` or a series of `input_batches`. - - -Type: `object` - -### `tests[].input_batches[][].content` - -The raw content of the input message. - - -Type: `string` - -### `tests[].input_batches[][].json_content` - -Sets the raw content of the message to a JSON document matching the structure of the value. - - -Type: `unknown` - -```yml -# Examples - -json_content: - bar: - - element1 - - 10 - foo: foo value -``` - -### `tests[].input_batches[][].file_content` - -Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. - - -Type: `string` - -```yml -# Examples - -file_content: ./foo/bar.txt -``` - -### `tests[].input_batches[][].metadata` - -A map of metadata key/values to add to the input message. - - -Type: map of `unknown` - -### `tests[].output_batches` - -List of output batches. - - -Type: `object` - -### `tests[].output_batches[][].bloblang` - -Executes a Bloblang mapping on the output message, if the result is anything other than a boolean equalling `true` the test fails. - - -Type: `string` - -```yml -# Examples - -bloblang: this.age > 10 && @foo.length() > 0 -``` - -### `tests[].output_batches[][].content_equals` - -Checks the full raw contents of a message against a value. - - -Type: `string` - -### `tests[].output_batches[][].content_matches` - -Checks whether the full raw contents of a message matches a regular expression (re2). - - -Type: `string` - -```yml -# Examples - -content_matches: ^foo [a-z]+ bar$ -``` - -### `tests[].output_batches[][].metadata_equals` - -Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. - - -Type: map of `unknown` - -```yml -# Examples - -metadata_equals: - example_key: example metadata value -``` - -### `tests[].output_batches[][].file_equals` - -Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. - - -Type: `string` - -```yml -# Examples - -file_equals: ./foo/bar.txt -``` - -### `tests[].output_batches[][].file_json_equals` - -Checks that both the message and the file contents are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. - - -Type: `string` - -```yml -# Examples - -file_json_equals: ./foo/bar.json -``` - -### `tests[].output_batches[][].json_equals` - -Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. - - -Type: `unknown` - -```yml -# Examples - -json_equals: - key: value -``` - -### `tests[].output_batches[][].json_contains` - -Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. - - -Type: `unknown` - -```yml -# Examples - -json_contains: - key: value -``` - -### `tests[].output_batches[][].file_json_contains` - -Checks that both the message and the file contents are valid JSON documents, and that the message is a superset of the condition. Will ignore formatting and ordering differences. The path of the file should be relative to the path of the test file. - - -Type: `string` - -```yml -# Examples - -file_json_contains: ./foo/bar.json -``` - -[json-pointer]: https://tools.ietf.org/html/rfc6901 -[bloblang]: /docs/guides/bloblang/about -[logger]: /docs/components/logger/about -[processors.mapping]: /docs/components/processors/mapping diff --git a/website/docs/configuration/using_cue.md b/website/docs/configuration/using_cue.md deleted file mode 100644 index 8d5d55d044..0000000000 --- a/website/docs/configuration/using_cue.md +++ /dev/null @@ -1,317 +0,0 @@ ---- -title: Using CUE ---- - -:::warning EXPERIMENTAL -CUE support is experimental. It may change for some time to improve CUE's ability to type-check Benthos configurations at the expense of causing new validation errors when moving from one Benthos release to the next. -::: - -[**CUE**](https://cuelang.org/) is a powerful configuration language that makes it easier and safer to build Benthos configurations. It achieves this by validating and type-checking configurations as well as allowing you to build useful utilities that reduce boilerplate. In this guide, we will see how to build a Benthos configuration using CUE, export it to YAML and execute it. - -## Prerequisites - -Before you get started, ensure that you have installed CUE by [following this guide](https://cuelang.org/docs/install/). If this is your first time workin with it, then it's a great idea to step through the [CUE tutorial](https://cuelang.org/docs/tutorials/) and familiarize yourself with the language. - -## Create - -Create a directory for the CUE module that will contain our benthos configuration: - -```shell -mkdir hello-cue -cd hello-cue -cue mod init example.com/hello-cue -touch config.cue -``` - -> CUE modules must start with a hostname. This will typically be the URL of your repository. For example: `cue mod init github.com/benthosdev/hello-cue`. - -The `benthos list` command will generate a CUE package containing the types we'll need to build our configuration. Let's write this package into our project: - -```shell -mkdir benthos -benthos list --format cue > benthos/schema.cue -``` - -At this point, you should now have the following directory structure: - -``` -hello-cue/ - benthos/ - schema.cue - cue.mod/ - pkg/ - usr/ - module.cue - config.cue -``` - -We are now ready to write our Benthos config in CUE. Let's start by editing our `config.cue` to include the following snippet: - -```cue -import "example.com/hello-cue/benthos" - -benthos.#Config & { - input: { - generate: { - mapping: """ - root = { "message": "Hello, CUE!" } - """ - } - } - - - pipeline: { - processors: [ - { - mapping: """ - root = this - root.id = uuid_v4() - """ - } - ] - } - - output: { - stdout: {} - } -} -``` - -Let's see what this will look like as YAML by running `cue export` while in the `hello-cue` directory: - -```shell -cue export --out yaml config.cue -``` - -This should output something like this: - -```yaml -input: - generate: - mapping: 'root = { "message": "Hello, CUE!" }' -pipeline: - processors: - - mapping: |- - root = this - root.id = uuid_v4() -output: - stdout: {} -tests: [] -``` - -We can run this with Benthos to see that it indeed works: - -```shell -benthos -c <(cue export --out yaml config.cue) -``` - -When you are satisfied with the results, terminate the Benthos process and let's move on to look at some of the nice features that we get with CUE. - -## Enhance - -The `config.cue` above looks eerily like JSON. This is because CUE is a superset of JSON and shares its syntax. However, we can shorten our configuration to reduce identation and curly brackets. Let's rewrite `config.cue` to look like this: - -```cue -import "example.com/hello-cue/benthos" - -benthos.#Config & { - input: generate: mapping: """ - root = { "message": "Hello, CUE!" } - """ - - - pipeline: processors: [ - { - mapping: """ - root = this - root.id = uuid_v4() - """ - } - ] - - output: stdout: {} -} -``` - -If you run the same `cue export` command from earlier, you'll notice that the YAML output is the same. - -Next, we'll look at what some error handling patterns might look like with CUE. One typical technique to detect messages with errors is to use the `switch` output to wrap another output with some error detection and handling. Another pattern involves limiting the number of retries on a given output that is misbehaving and rejecting or dropping messages with some useful logging. If we combine all these concepts together we get: - -```yaml -output: - switch: - cases: - - check: errored() - output: - reject: "failed to process message: ${! error() }" - - output: - retry: - max_retries: 5 - output: - gcp_pubsub: - project: "sample-project" - topic: "sample-topic" -``` - -There are quite a few lines of YAML here and we seem to be going sideways as we compose more functionality. We can try and make this more manageable with CUE! - -Let's create a new file in our `hello-cue` directory called `benthos/helpers.cue`: - -```shell -touch benthos/helpers.cue -``` - -In this file, add the following snippet: - -```cue -package benthos - -#Guarded: self = { - // The desired output that will be wrapped with error handling mechanisms - #output: #Output - - // The error text to emit if the output receives any messages which contained - // processing errors - #errorMessage: string - - // The number of retries to attempt on the desired output (default is 3) - #maxRetries: uint | *3 - - // The error message to emit if the retry attempts are exhausted - #retryErrorMessage: string - - // Whether to drop or reject any failed messages - #errorHandling: "drop" | "reject" - - switch: cases: [ - { - check: "errored()" - output: { - if self.#errorHandling == "reject" { reject: self.#errorMessage } - - if self.#errorHandling == "drop" { - drop: {} - processors: [{ log: message: self.#errorMessage }] - } - } - }, - { - output: fallback: [ - { - retry: { - max_retries: self.#maxRetries - output: self.#output - } - }, - { - if self.#errorHandling == "reject" { reject: self.#retryErrorMessage } - - if self.#errorHandling == "drop" { - drop: {} - processors: [{ log: message: self.#retryErrorMessage }] - } - } - ] - } - ] -} -``` - -Now, let's get back to `config.cue` and edit a few bits while leveraging this helper: - -```cue -import "example.com/hello-cue/benthos" - -benthos.#Config & { - input: generate: { - count: 1 - interval: "0" - mapping: """ - root = { "message": "Hello, CUE!" } - """ - } - - output: benthos.#Guarded & { - #errorMessage: "failed to process message: ${! error() }" - - #maxRetries: 3 - #retryErrorMessage: "failed to output message after \(#maxRetries) retries" - - #errorHandling: "drop" - - #output: http_client: { - url: "http://localhost:4195/sad-blob" - retries: 0 - } - } -} -``` - -If you rerun `cue export` now, you'll see that we've wrapped our output with a couple of error handling mechanisms. We also had access to powerful CUE features like conditional fields based on `#errorHandling`, default values and interpolations. - -```yaml -input: - generate: - count: 1 - interval: "0" - mapping: 'root = { "message": "Hello, CUE!" }' -output: - switch: - cases: - - check: errored() - output: - drop: {} - processors: - - log: - message: 'failed to process message: ${! error() }' - - output: - fallback: - - retry: - max_retries: 3 - output: - http_client: - url: http://localhost:4195/sad-blob - retries: 0 - - drop: {} - processors: - - log: - message: failed to output message after 3 retries -tests: [] -``` - -The final directory structure of your hello-cue project should look like this: - -``` -hello-cue/ - benthos/ - schema.cue - helpers.cue - cue.mod/ - pkg/ - usr/ - module.cue - config.cue -``` - -## Included CUE types - -The `benthos.cue` file we emitted earlier contains a number of useful types that we can use when build configuration files and helpers. These include: - -* `benthos.#Config` - -This definition describes the format of a Benthos config file. You'll want to use it at the top of your configuration file to validate its overall structure. - -* `benthos.#Input` -* `benthos.#Output` -* `benthos.#Processor` -* `benthos.#RateLimit` -* `benthos.#Buffer` -* `benthos.#Cache` -* `benthos.#Metric` -* `benthos.#Tracer` - -Each of these definitions is a disjunction that holds all the corresponding components in Benthos. In other words, a CUE field that is specified as `benthos.#Input`, such as `myfield: benthos.#Input`, must resolve to a valid Benthos input. - -## Wrap up - -Being able to define helper packages and definitions like `#Guarded` and reusing them across your Benthos configurations is a really powerful feature of CUE. This will allow you to share consistent good practices without messy boilerplate across projects and teams! diff --git a/website/docs/configuration/windowed_processing.md b/website/docs/configuration/windowed_processing.md deleted file mode 100644 index 7c7e173bd9..0000000000 --- a/website/docs/configuration/windowed_processing.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Windowed Processing -description: Learn how to process periodic windows of messages with Benthos ---- - -A window is a batch of messages made with respect to time, with which we are able to perform processing that can analyse or aggregate the messages of the window. This is useful in stream processing as the dataset is never "complete", and therefore in order to perform analysis against a collection of messages we must do so by creating a continuous feed of windows (collections), where our analysis is made against each window. - -For example, given a stream of messages relating to cars passing through various traffic lights: - -```json -{ - "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", - "created_at": "2021-08-07T09:49:35Z", - "registration_plate": "AB1C DEF", - "passengers": 3 -} -``` - -Windowing allows us to produce a stream of messages representing the total traffic for each light every hour: - -```json -{ - "traffic_light": "cbf2eafc-806e-4067-9211-97be7e42cee3", - "created_at": "2021-08-07T10:00:00Z", - "unique_cars": 15, - "passengers": 43 -} -``` - -## Creating Windows - -The first step in processing windows is producing the windows themselves, this can be done by configuring a window producing buffer after your input: - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - - - -A [`system_window` buffer][buffers.system_window] creates windows by following the system clock of the running machine. Windows will be created and emitted at predictable times, but this also means windows for historic data will not be emitted and therefore prevents backfills of traffic data: - -```yaml -input: - kafka: - addresses: [ TODO ] - topics: [ traffic_data ] - consumer_group: traffic_consumer - checkpoint_limit: 1000 - -buffer: - system_window: - timestamp_mapping: root = this.created_at - size: 1h - allowed_lateness: 3m -``` - -For more information about this buffer refer to [the `system_window` buffer docs][buffers.system_window]. - - - - -## Grouping - -With a window buffer chosen our stream of messages will be emitted periodically as batches of all messages that fit within each window. Since we want to analyse the window separately for each traffic light we need to expand this single batch out into one for each traffic light identifier within the window. For that purpose we have two processor options: [`group_by`][processors.group_by] and [`group_by_value`][processors.group_by_value]. - -In our case we want to group by the value of the field `traffic_light` of each message, which we can do with the following: - -```yaml -pipeline: - processors: - - group_by_value: - value: ${! json("traffic_light") } -``` - -## Aggregating - -Once our window has been grouped the next step is to calculate the aggregated passenger and unique cars counts. For this purpose the Benthos [mapping language Bloblang][bloblang.about] comes in handy as the method [`from_all`][bloblang.methods.from_all] executes the target function against the entire batch and returns an array of the values, allowing us to mutate the result with chained methods such as [`sum`][bloblang.methods.sum]: - -```yaml -pipeline: - processors: - - group_by_value: - value: ${! json("traffic_light") } - - - mapping: | - let is_first_message = batch_index() == 0 - - root.traffic_light = this.traffic_light - root.created_at = @window_end_timestamp - root.total_cars = if $is_first_message { - json("registration_plate").from_all().unique().length() - } - root.passengers = if $is_first_message { - json("passengers").from_all().sum() - } - - # Only keep the first batch message containing the aggregated results. - root = if ! $is_first_message { - deleted() - } -``` - -[Bloblang][bloblang.about] is very powerful, and by using [`from`][bloblang.methods.from] and [`from_all`][bloblang.methods.from_all] it's possible to perform a wide range of batch-wide processing. If you fancy a challenge try updating the above mapping to only count passengers from the first journey of each registration plate in the window (hint: the [`fold` method][bloblang.methods.fold] might come in handy). - -[buffers.system_window]: /docs/components/buffers/system_window -[processors.group_by]: /docs/components/processors/group_by -[processors.group_by_value]: /docs/components/processors/group_by_value -[bloblang.about]: /docs/guides/bloblang/about -[bloblang.methods.from_all]: /docs/guides/bloblang/methods#from_all -[bloblang.methods.sum]: /docs/guides/bloblang/methods#sum -[bloblang.methods.unique]: /docs/guides/bloblang/methods#unique -[bloblang.methods.from]: /docs/guides/bloblang/methods#from -[bloblang.methods.fold]: /docs/guides/bloblang/methods#fold diff --git a/website/docs/guides/bloblang/about.md b/website/docs/guides/bloblang/about.md deleted file mode 100644 index 7b710bc2c9..0000000000 --- a/website/docs/guides/bloblang/about.md +++ /dev/null @@ -1,427 +0,0 @@ ---- -title: Bloblang -sidebar_label: About -description: The Benthos native mapping language ---- - -Bloblang, or blobl for short, is a language designed for mapping data of a wide variety of forms. It's a safe, fast, and powerful way to perform document mapping within Benthos. It also has a [Go API for writing your own functions and methods][plugin-api] as plugins. - -Bloblang is available as a [processor][blobl.proc] and it's also possible to use blobl queries in [function interpolations][blobl.interp]. - -You can also execute Bloblang mappings on the command-line with the `blobl` subcommand: - -```shell -$ cat data.jsonl | benthos blobl 'foo.(bar | baz).buz' -``` - -This document outlines the core features of the Bloblang language, but if you're totally new to Bloblang then it's worth following [the walkthrough first][blobl.walkthrough]. - -## Assignment - -A Bloblang mapping expresses how to create a new document by extracting data from an existing input document. Assignments consist of a dot separated path segments on the left-hand side describing a field to be created within the new document, and a right-hand side query describing what the content of the new field should be. - -The keyword `root` on the left-hand side refers to the root of the new document, the keyword `this` on the right-hand side refers to the current context of the query, which is the read-only input document when querying from the root of a mapping: - -```coffee -root.id = this.thing.id -root.type = "yo" - -# Both `root` and `this` are optional, and will be inferred in their absence. -content = thing.doc.message - -# In: {"thing":{"id":"wat1","doc":{"title":"wut","message":"hello world"}}} -# Out: {"content":"hello world","id":"wat1","type":"yo"} -``` - -Since the document being created starts off empty it is sometimes useful to begin a mapping by copying the entire contents of the input document, which can be expressed by assigning `this` to `root`. - -```coffee -root = this -root.foo = "added value" - -# In: {"id":"wat1","message":"hello world"} -# Out: {"id":"wat1","message":"hello world","foo":"added value"} -``` - -If the new document `root` is never assigned to or otherwise mutated then the original document remains unchanged. - -### Special Characters in Paths - -Quotes can be used to describe sections of a field path that contain whitespace, dots or other special characters: - -```coffee -# Use quotes around a path segment in order to include whitespace or dots within -# the path -root."foo.bar".baz = this."buz bev".fub - -# In: {"buz bev":{"fub":"hello world"}} -# Out: {"foo.bar":{"baz":"hello world"}} -``` - -### Non-structured Data - -Bloblang is able to map data that is unstructured, whether it's a log line or a binary blob, by referencing it with the [`content` function][blobl.functions.content], which returns the raw bytes of the input document: - -```coffee -# Parse a base64 encoded JSON document -root = content().decode("base64").parse_json() - -# In: eyJmb28iOiJiYXIifQ== -# Out: {"foo":"bar"} -``` - -And your newly mapped document can also be unstructured, simply assign a value type to the `root` of your document: - -```coffee -root = this.foo - -# In: {"foo":"hello world"} -# Out: hello world -``` - -And the resulting message payload will be the raw value you've assigned. - -### Deleting - -It's possible to selectively delete fields from an object by assigning the function `deleted()` to the field path: - -```coffee -root = this -root.bar = deleted() - -# In: {"id":"wat1","message":"hello world","bar":"remove me"} -# Out: {"id":"wat1","message":"hello world"} -``` - -### Variables - -Another type of assignment is a `let` statement, which creates a variable that can be referenced elsewhere within a mapping. Variables are discarded at the end of the mapping and are mostly useful for query reuse. Variables are referenced within queries with `$`: - -```coffee -# Set a temporary variable -let foo = "yo" - -root.new_doc.type = $foo -``` - -### Metadata - -Benthos messages contain metadata that is separate from the main payload, in Bloblang you can modify the metadata of the resulting message with the `meta` assignment keyword. Metadata values of the resulting message are referenced within queries with the `@` operator or the [`metadata()` function][blobl.functions.metadata]: - -```coffee -# Reference a metadata value -root.new_doc.bar = @kafka_topic # Or `@.kafka_topic` or `metadata("kafka_topic")` - -# Delete all metadata -meta = deleted() - -# Set metadata values -meta bar = "hello world" -meta baz = { - "something": "structured" -} - -# Get an object of key/values for all metadata -root.meta_obj = @ # Or `metadata()` -``` - -## Coalesce - -The pipe operator (`|`) used within brackets allows you to coalesce multiple candidates for a path segment. The first field that exists and has a non-null value will be selected: - -```coffee -root.new_doc.type = this.thing.(article | comment | this).type - -# In: {"thing":{"article":{"type":"foo"}}} -# Out: {"new_doc":{"type":"foo"}} - -# In: {"thing":{"comment":{"type":"bar"}}} -# Out: {"new_doc":{"type":"bar"}} - -# In: {"thing":{"type":"baz"}} -# Out: {"new_doc":{"type":"baz"}} -``` - -Opening brackets on a field begins a query where the context of `this` changes to value of the path it is opened upon, therefore in the above example `this` within the brackets refers to the contents of `this.thing`. - -## Literals - -Bloblang supports number, boolean, string, null, array and object literals: - -```coffee -root = [ - 7, false, "string", null, { - "first": 11, - "second": {"foo":"bar"}, - "third": """multiple -lines on this -string""" - } -] - -# In: {} -# Out: [7,false,"string",null,{"first":11,"second":{"foo":"bar"},"third":"multiple\nlines on this\nstring"}] -``` - -The values within literal arrays and objects can be dynamic query expressions, as well as the keys of object literals. - -## Comments - -You might've already spotted, comments are started with a hash (`#`) and end with a line break: - -```coffee -root = this.some.value # And now this is a comment -``` - -## Boolean Logic and Arithmetic - -Bloblang supports a range of boolean operators `!`, `>`, `>=`, `==`, `<`, `<=`, `&&`, `||` and mathematical operators `+`, `-`, `*`, `/`, `%`: - -```coffee -root.is_big = this.number > 100 -root.multiplied = this.number * 7 - -# In: {"number":50} -# Out: {"is_big":false,"multiplied":350} - -# In: {"number":150} -# Out: {"is_big":true,"multiplied":1050} -``` - -For more information about these operators and how they work check out [the arithmetic page][blobl.arithmetic]. - -## Conditional Mapping - -Use `if` as either a statement or an expression in order to perform maps conditionally: - -```coffee -root = this - -root.sorted_foo = if this.foo.type() == "array" { this.foo.sort() } - -if this.foo.type() == "string" { - root.upper_foo = this.foo.uppercase() - root.lower_foo = this.foo.lowercase() -} - -# In: {"foo":"FooBar"} -# Out: {"foo":"FooBar","lower_foo":"foobar","upper_foo":"FOOBAR"} - -# In: {"foo":["foo","bar"]} -# Out: {"foo":["foo","bar"],"sorted_foo":["bar","foo"]} -``` - -And add as many `else if` queries as you like, followed by an optional final fallback `else`: - -```coffee -root.sound = if this.type == "cat" { - this.cat.meow -} else if this.type == "dog" { - this.dog.woof.uppercase() -} else { - "sweet sweet silence" -} - -# In: {"type":"cat","cat":{"meow":"meeeeooooow!"}} -# Out: {"sound":"meeeeooooow!"} - -# In: {"type":"dog","dog":{"woof":"guurrrr woof woof!"}} -# Out: {"sound":"GUURRRR WOOF WOOF!"} - -# In: {"type":"caterpillar","caterpillar":{"name":"oleg"}} -# Out: {"sound":"sweet sweet silence"} -``` - -## Pattern Matching - -A `match` expression allows you to perform conditional mappings on a value, each case should be either a boolean expression, a literal value to compare against the target value, or an underscore (`_`) which captures values that have not matched a prior case: - -```coffee -root.new_doc = match this.doc { - this.type == "article" => this.article - this.type == "comment" => this.comment - _ => this -} - -# In: {"doc":{"type":"article","article":{"id":"foo","content":"qux"}}} -# Out: {"new_doc":{"id":"foo","content":"qux"}} - -# In: {"doc":{"type":"comment","comment":{"id":"bar","content":"quz"}}} -# Out: {"new_doc":{"id":"bar","content":"quz"}} - -# In: {"doc":{"type":"neither","content":"some other stuff unchanged"}} -# Out: {"new_doc":{"type":"neither","content":"some other stuff unchanged"}} -``` - -Within a match block the context of `this` changes to the pattern matched expression, therefore `this` within the match expression above refers to `this.doc`. - -Match cases can specify a literal value for simple comparison: - -```coffee -root = this -root.type = match this.type { "doc" => "document", "art" => "article", _ => this } - -# In: {"type":"doc","foo":"bar"} -# Out: {"type":"document","foo":"bar"} -``` - -The match expression can also be left unset which means the context remains unchanged, and the catch-all case can also be omitted: - -```coffee -root.new_doc = match { - this.doc.type == "article" => this.doc.article - this.doc.type == "comment" => this.doc.comment -} - -# In: {"doc":{"type":"neither","content":"some other stuff unchanged"}} -# Out: {"doc":{"type":"neither","content":"some other stuff unchanged"}} -``` - -If no case matches then the mapping is skipped entirely, hence we would end up with the original document in this case. - -## Functions - -Functions can be placed anywhere and allow you to extract information from your environment, generate values, or access data from the underlying message being mapped: - -```coffee -root.doc.id = uuid_v4() -root.doc.received_at = now() -root.doc.host = hostname() -``` - -Functions support both named and nameless style arguments: - -```coffee -root.values_one = range(start: 0, stop: this.max, step: 2) -root.values_two = range(0, this.max, 2) -``` - -You can find a full list of functions and their parameters in [the functions page][blobl.functions]. - -## Methods - -Methods are similar to functions but enact upon a target value, these provide most of the power in Bloblang as they allow you to augment query values and can be added to any expression (including other methods): - -```coffee -root.doc.id = this.thing.id.string().catch(uuid_v4()) -root.doc.reduced_nums = this.thing.nums.map_each(num -> if num < 10 { - deleted() -} else { - num - 10 -}) -root.has_good_taste = ["pikachu","mewtwo","magmar"].contains(this.user.fav_pokemon) -``` - -Methods also support both named and nameless style arguments: - -```coffee -root.foo_one = this.(bar | baz).trim().replace_all(old: "dog", new: "cat") -root.foo_two = this.(bar | baz).trim().replace_all("dog", "cat") -``` - -You can find a full list of methods and their parameters in [the methods page][blobl.methods]. - -## Maps - -Defining named maps allows you to reuse common mappings on values with the [`apply` method][blobl.methods.apply]: - -```coffee -map things { - root.first = this.thing_one - root.second = this.thing_two -} - -root.foo = this.value_one.apply("things") -root.bar = this.value_two.apply("things") - -# In: {"value_one":{"thing_one":"hey","thing_two":"yo"},"value_two":{"thing_one":"sup","thing_two":"waddup"}} -# Out: {"foo":{"first":"hey","second":"yo"},"bar":{"first":"sup","second":"waddup"}} -``` - -Within a map the keyword `root` refers to a newly created document that will replace the target of the map, and `this` refers to the original value of the target. The argument of `apply` is a string, which allows you to dynamically resolve the mapping to apply. - -## Import Maps - -It's possible to import maps defined in a file with an `import` statement: - -```coffee -import "./common_maps.blobl" - -root.foo = this.value_one.apply("things") -root.bar = this.value_two.apply("things") -``` - -Imports from a Bloblang mapping within a Benthos config are relative to the process running the config. Imports from an imported file are relative to the file that is importing it. - -## Filtering - -By assigning the root of a mapped document to the `deleted()` function you can delete a message entirely: - -```coffee -# Filter all messages that have fewer than 10 URLs. -root = if this.doc.urls.length() < 10 { deleted() } -``` - -## Error Handling - -Functions and methods can fail under certain circumstances, such as when they receive types they aren't able to act upon. These failures, when not caught, will cause the entire mapping to fail. However, the [method `catch`][blobl.methods.catch] can be used in order to return a value when a failure occurs instead: - -```coffee -# Map an empty array to `foo` if the field `bar` is not a string. -root.foo = this.bar.split(",").catch([]) -``` - -Since `catch` is a method it can also be attached to bracketed map expressions: - -```coffee -# Map `false` if any of the operations in this boolean query fail. -root.thing = ( this.foo > this.bar && this.baz.contains("wut") ).catch(false) -``` - -And one of the more powerful features of Bloblang is that a single `catch` method at the end of a chain of methods can recover errors from any method in the chain: - -```coffee -# Catch errors caused by: -# - foo not existing -# - foo not being a string -# - an element from split foo not being a valid JSON string -root.things = this.foo.split(",").map_each( ele -> ele.parse_json() ).catch([]) - -# Specifically catch a JSON parse error -root.things = this.foo.split(",").map_each( ele -> ele.parse_json().catch({}) ) -``` - -However, the `catch` method only acts on errors, sometimes it's also useful to set a fall back value when a query returns `null` in which case the [method `or`][blobl.methods.or] can be used the same way: - -```coffee -# Map "default" if either the element index 5 does not exist, or the underlying -# element is `null`. -root.foo = this.bar.index(5).or("default") -``` - -## Unit Testing - -It's possible to execute unit tests for your Bloblang mappings using the standard Benthos unit test capabilities outlined [in this document][configuration.unit_testing]. - -## Trouble Shooting - -1. I'm seeing `unable to reference message as structured (with 'this')` when I try to run mappings with `benthos blobl`. - -That particular error message means the mapping is failing to parse what's being fed in as a JSON document. Make sure that the data you are feeding in is valid JSON, and also that the documents *do not* contain line breaks as `benthos blobl` will parse each line individually. - -Why? That's a good question. Bloblang supports non-JSON formats too, so it can't delimit documents with a streaming JSON parser like tools such as `jq`, so instead it uses line breaks to determine the boundaries of each message. - -[blobl.arithmetic]: /docs/guides/bloblang/arithmetic -[blobl.walkthrough]: /docs/guides/bloblang/walkthrough -[blobl.variables]: #variables -[blobl.proc]: /docs/components/processors/mapping -[blobl.interp]: /docs/configuration/interpolation#bloblang-queries -[blobl.functions]: /docs/guides/bloblang/functions -[blobl.functions.content]: /docs/guides/bloblang/functions#content -[blobl.functions.metadata]: /docs/guides/bloblang/functions#metadata -[blobl.methods]: /docs/guides/bloblang/methods -[blobl.methods.apply]: /docs/guides/bloblang/methods#apply -[blobl.methods.catch]: /docs/guides/bloblang/methods#catch -[blobl.methods.or]: /docs/guides/bloblang/methods#or -[plugin-api]: https://pkg.go.dev/github.com/benthosdev/benthos/v4/public/bloblang -[configuration.unit_testing]: /docs/configuration/unit_testing diff --git a/website/docs/guides/bloblang/advanced.md b/website/docs/guides/bloblang/advanced.md deleted file mode 100644 index 083598ee78..0000000000 --- a/website/docs/guides/bloblang/advanced.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Advanced Bloblang -sidebar_label: Advanced -description: Some advanced Bloblang patterns ---- - -## Map Parameters - -A map definition only has one input parameter, which is the context that it is called upon: - -```coffee -map formatting { - root = "(%v)".format(this) -} - -root.a = this.a.apply("formatting") -root.b = this.b.apply("formatting") - -# In: {"a":"foo","b":"bar"} -# Out: {"a":"(foo)","b":"(bar)"} -``` - -However, we can use object literals in order to provide multiple map parameters. Imagine if we wanted a map that is the exact same as above except the pattern is `[%v]` instead, with the potential for even more patterns in the future. To do that we can pass an object with a field `value` with our target to map and a field `pattern` which allows us to specify the pattern to apply: - -```coffee -map formatting { - root = this.pattern.format(this.value) -} - -root.a = { - "value":this.a, - "pattern":this.pattern, -}.apply("formatting") - -root.b = { - "value":this.b, - "pattern":this.pattern, -}.apply("formatting") - -# In: {"a":"foo","b":"bar","pattern":"[%v]"} -# Out: {"a":"[foo]","b":"[bar]"} -``` - -## Walking the Tree - -Sometimes it's necessary to perform a mapping on all values within an unknown tree structure. You can do that easily with recursive mapping: - -```coffee -map unescape_values { - root = match { - this.type() == "object" => this.map_each(item -> item.value.apply("unescape_values")), - this.type() == "array" => this.map_each(ele -> ele.apply("unescape_values")), - this.type() == "string" => this.unescape_html(), - this.type() == "bytes" => this.unescape_html(), - _ => this, - } -} -root = this.apply("unescape_values") - -# In: {"first":{"nested":"foo & bar"},"second":10,"third":["1 < 2",{"also_nested":"2 > 1"}]} -# Out: {"first":{"nested":"foo & bar"},"second":10,"third":["1 < 2",{"also_nested":"2 > 1"}]} -``` - -## Message Expansion - -Expanding a single message into multiple messages can be done by mapping messages into an array and following it up with an [`unarchive` processor][processors.unarchive]. For example, given documents of this format: - -```json -{ - "id": "foobar", - "items": [ - {"content":"foo"}, - {"content":"bar"}, - {"content":"baz"} - ] -} -``` - -We can pull `items` out to the root with `root = items` with a [`mapping` processor][processors.mapping] and follow it with an [`unarchive` processor][processors.unarchive] to expand each element into its own independent message: - -```yaml -pipeline: - processors: - - mapping: root = this.items - - unarchive: - format: json_array -``` - -However, most of the time we also need to map the elements before expanding them, and often that includes copying fields outside of our target array. We can do that with methods such as `map_each` and `merge`: - -```coffee -root = this.items.map_each(ele -> this.without("items").merge(ele)) - -# In: {"id":"foobar","items":[{"content":"foo"},{"content":"bar"},{"content":"baz"}]} -# Out: [{"content":"foo","id":"foobar"},{"content":"bar","id":"foobar"},{"content":"baz","id":"foobar"}] -``` - -However, the above mapping is slightly inefficient as we would create a copy of our source object for each element with the `this.without("items")` part. A more efficient way to do this would be to capture that query within a variable: - -```coffee -let doc_root = this.without("items") -root = this.items.map_each($doc_root.merge(this)) - -# In: {"id":"foobar","items":[{"content":"foo"},{"content":"bar"},{"content":"baz"}]} -# Out: [{"content":"foo","id":"foobar"},{"content":"bar","id":"foobar"},{"content":"baz","id":"foobar"}] -``` - -Also note that when we set `doc_root` we remove the field `items` from the target document. The full config would now be: - -```yaml -pipeline: - processors: - - mapping: | - let doc_root = this.without("items") - root = this.items.map_each($doc_root.merge(this)) - - unarchive: - format: json_array -``` - -## Creating CSV - -Benthos has a few different ways of outputting a stream of CSV data. However, the best way to do it is by converting the documents into CSV rows with Bloblang as this gives you full control over exactly how the schema is generated, erroneous data is handled, and escaping of column data is performed. - -A common and simple use case is to simply flatten documents and write out the column values in alphabetical order. The first row we generate should also be prefixed with a row containing those column names. Here's a mapping that achieves this by using a `count` function to detect the very first invocation of the mapping in a stream pipeline: - -```coffee -map escape_csv { - root = if this.re_match("[\"\n,]+") { - "\"" + this.replace_all("\"", "\"\"") + "\"" - } else { - this - } -} - -# Extract key/value pairs as an array and sort by the key -let kvs = this.key_values().sort_by(v -> v.key) - -# Create a header prefix for our output only on the first row -let header = if count("rows_in_file") == 1 { - $kvs.map_each(kv -> kv.key.apply("escape_csv")).join(",") + "\n" -} else { "" } - -root = $header + $kvs.map_each(kv -> kv.value.string().apply("escape_csv")).join(",") -``` - -And with this mapping we can write the data to a newly created CSV file using an output with a simple `lines` codec: - -```yaml -output: - file: - path: ./result.csv - codec: lines -``` - -Perhaps the first expansion of this mapping that would be worthwhile is to add an explicit list of column names, or at least confirm that the number of values in a row matches an expected count. - -[processors.mapping]: /docs/components/processors/mapping -[processors.unarchive]: /docs/components/processors/unarchive diff --git a/website/docs/guides/bloblang/arithmetic.md b/website/docs/guides/bloblang/arithmetic.md deleted file mode 100644 index 10055f906f..0000000000 --- a/website/docs/guides/bloblang/arithmetic.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Bloblang Arithmetic -sidebar_label: Arithmetic -description: How arithmetic works within Bloblang ---- - -Bloblang supports a range of comparison operators `!`, `>`, `>=`, `==`, `<`, `<=`, `&&`, `||` and mathematical operators `+`, `-`, `*`, `/`, `%`. How these operators behave is dependent on the type of the values they're used with, and therefore it's worth fully understanding these behaviors if you intend to use them heavily in your mappings. - -## Mathematical - -All mathematical operators (`+`, `-`, `*`, `/`, `%`) are valid against number values, and addition (`+`) is also supported when both the left and right hand side arguments are strings. If a mathematical operator is used with an argument that is non-numeric (with the aforementioned string exception) then a [recoverable mapping error will be thrown][blobl.error_handling]. - -### Number Degradation - -In Bloblang any number resulting from a method, function or arithmetic is either a 64-bit signed integer or a 64-bit floating point value. Numbers from input documents can be any combination of size and be signed or unsigned. - -When a mathematical operation is performed with two or more integer values Bloblang will create an integer result, with the exception of division. However, if any number within a mathematical operation is a floating point then the result will be a floating point value. - -In order to explicitly coerce numbers into integer types you can use the [`.ceil()`, `.floor()`, or `.round()` methods][blobl.methods.number_manipulation]. - -## Comparison - -The not (`!`) operator reverses the boolean value of the expression immediately following it, and is valid to place before any query that yields a boolean value. If the following expression yields a non-boolean value then a [recoverable mapping error will be thrown][blobl.error_handling]. - -If you wish to reverse the boolean result of a complex query then simply place the query within brackets (`!(this.foo > this.bar)`). - -### Equality - -The equality operators (`==` and `!=`) are valid to use against any value type. In order for arguments to be considered equal they must match in both their basic type (`string`, `number`, `null`, `bool`, etc) as well as their value. If you wish to compare mismatched value types then use [coercion methods][blobl.methods.type_coercion]. - -Number arguments are considered equal if their value is the same when represented the same way, which means their underlying representations (integer, float, etc) do not need to match in order for them to be considered equal. - -### Numerical - -Numerical comparisons (`>`, `>=`, `<`, `<=`) are valid to use against number values only. If a non-number value is used as an argument then a [recoverable mapping error will be thrown][blobl.error_handling]. - -### Boolean - -Boolean comparison operators (`||`, `&&`) are valid to use against boolean values only (`true` or `false`). If a non-boolean value is used as an argument then a [recoverable mapping error will be thrown][blobl.error_handling]. - -[blobl.error_handling]: /docs/guides/bloblang/about#error-handling -[blobl.methods.number_manipulation]: /docs/guides/bloblang/methods#number-manipulation -[blobl.methods.type_coercion]: /docs/guides/bloblang/methods#type-coercion diff --git a/website/docs/guides/bloblang/functions.md b/website/docs/guides/bloblang/functions.md deleted file mode 100644 index a3ab8cb79b..0000000000 --- a/website/docs/guides/bloblang/functions.md +++ /dev/null @@ -1,727 +0,0 @@ ---- -title: Bloblang Functions -sidebar_label: Functions -description: A list of Bloblang functions ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Functions can be placed anywhere and allow you to extract information from your environment, generate values, or access data from the underlying message being mapped: - -```coffee -root.doc.id = uuid_v4() -root.doc.received_at = now() -root.doc.host = hostname() -``` - -Functions support both named and nameless style arguments: - -```coffee -root.values_one = range(start: 0, stop: this.max, step: 2) -root.values_two = range(0, this.max, 2) -``` - -## General - -### `counter` - -:::caution EXPERIMENTAL -This function is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Returns a non-negative integer that increments each time it is resolved, yielding the minimum (`1` by default) as the first value. Each instantiation of `counter` has its own independent count. Once the maximum integer (or `max` argument) is reached the counter resets back to the minimum. - -#### Parameters - -**`min`** <query expression, default `1`> The minimum value of the counter, this is the first value that will be yielded. If this parameter is dynamic it will be resolved only once during the lifetime of the mapping. -**`max`** <query expression, default `9223372036854775807`> The maximum value of the counter, once this value is yielded the counter will reset back to the min. If this parameter is dynamic it will be resolved only once during the lifetime of the mapping. -**`set`** <(optional) query expression> An optional mapping that when specified will be executed each time the counter is resolved. When this mapping resolves to a non-negative integer value it will cause the counter to reset to this value and yield it. If this mapping is omitted or doesn't resolve to anything then the counter will increment and yield the value as normal. If this mapping resolves to `null` then the counter is not incremented and the current value is yielded. If this mapping resolves to a deletion then the counter is reset to the `min` value. - -#### Examples - - -```coffee -root.id = counter() - -# In: {} -# Out: {"id":1} - -# In: {} -# Out: {"id":2} -``` - -It's possible to increment a counter multiple times within a single mapping invocation using a map. - -```coffee - -map foos { - root = counter() -} - -root.meow_id = null.apply("foos") -root.woof_id = null.apply("foos") - - -# In: {} -# Out: {"meow_id":1,"woof_id":2} - -# In: {} -# Out: {"meow_id":3,"woof_id":4} -``` - -By specifying an optional `set` parameter it is possible to dynamically reset the counter based on input data. - -```coffee -root.consecutive_doggos = counter(min: 1, set: if !this.sound.lowercase().contains("woof") { 0 }) - -# In: {"sound":"woof woof"} -# Out: {"consecutive_doggos":1} - -# In: {"sound":"woofer wooooo"} -# Out: {"consecutive_doggos":2} - -# In: {"sound":"meow"} -# Out: {"consecutive_doggos":0} - -# In: {"sound":"uuuuh uh uh woof uhhhhhh"} -# Out: {"consecutive_doggos":1} -``` - -The `set` parameter can also be utilised to peek at the counter without mutating it by returning `null`. - -```coffee -root.things = counter(set: if this.id == null { null }) - -# In: {"id":"a"} -# Out: {"things":1} - -# In: {"id":"b"} -# Out: {"things":2} - -# In: {"what":"just checking"} -# Out: {"things":2} - -# In: {"id":"c"} -# Out: {"things":3} -``` - -### `deleted` - -A function that returns a result indicating that the mapping target should be deleted. Deleting, also known as dropping, messages will result in them being acknowledged as successfully processed to inputs in a Benthos pipeline. For more information about error handling patterns read [here][error_handling]. - -#### Examples - - -```coffee -root = this -root.bar = deleted() - -# In: {"bar":"bar_value","baz":"baz_value","foo":"foo value"} -# Out: {"baz":"baz_value","foo":"foo value"} -``` - -Since the result is a value it can be used to do things like remove elements of an array within `map_each`. - -```coffee -root.new_nums = this.nums.map_each(num -> if num < 10 { deleted() } else { num - 10 }) - -# In: {"nums":[3,11,4,17]} -# Out: {"new_nums":[1,7]} -``` - -### `ksuid` - -Generates a new ksuid each time it is invoked and prints a string representation. - -#### Examples - - -```coffee -root.id = ksuid() -``` - -### `nanoid` - -Generates a new nanoid each time it is invoked and prints a string representation. - -#### Parameters - -**`length`** <(optional) integer> An optional length. -**`alphabet`** <(optional) string> An optional custom alphabet to use for generating IDs. When specified the field `length` must also be present. - -#### Examples - - -```coffee -root.id = nanoid() -``` - -It is possible to specify an optional length parameter. - -```coffee -root.id = nanoid(54) -``` - -It is also possible to specify an optional custom alphabet after the length parameter. - -```coffee -root.id = nanoid(54, "abcde") -``` - -### `random_int` - - -Generates a non-negative pseudo-random 64-bit integer. An optional integer argument can be provided in order to seed the random number generator. - -Optional `min` and `max` arguments can be provided in order to only generate numbers within a range. Neither of these parameters can be set via a dynamic expression (i.e. from values taken from mapped data). Instead, for dynamic ranges extract a min and max manually using a modulo operator (`random_int() % a + b`). - -#### Parameters - -**`seed`** <query expression, default `{"Value":0}`> A seed to use, if a query is provided it will only be resolved once during the lifetime of the mapping. -**`min`** <integer, default `0`> The minimum value the random generated number will have. The default value is 0. -**`max`** <integer, default `9223372036854775806`> The maximum value the random generated number will have. The default value is 9223372036854775806 (math.MaxInt64 - 1). - -#### Examples - - -```coffee -root.first = random_int() -root.second = random_int(1) -root.third = random_int(max:20) -root.fourth = random_int(min:10, max:20) -root.fifth = random_int(timestamp_unix_nano(), 5, 20) -root.sixth = random_int(seed:timestamp_unix_nano(), max:20) - -``` - -It is possible to specify a dynamic seed argument, in which case the argument will only be resolved once during the lifetime of the mapping. - -```coffee -root.first = random_int(timestamp_unix_nano()) -``` - -### `range` - -The `range` function creates an array of integers following a range between a start, stop and optional step integer argument. If the step argument is omitted then it defaults to 1. A negative step can be provided as long as stop < start. - -#### Parameters - -**`start`** <integer> The start value. -**`stop`** <integer> The stop value. -**`step`** <integer, default `1`> The step value. - -#### Examples - - -```coffee -root.a = range(0, 10) -root.b = range(start: 0, stop: this.max, step: 2) # Using named params -root.c = range(0, -this.max, -2) - -# In: {"max":10} -# Out: {"a":[0,1,2,3,4,5,6,7,8,9],"b":[0,2,4,6,8],"c":[0,-2,-4,-6,-8]} -``` - -### `snowflake_id` - -Generate a new snowflake ID each time it is invoked and prints a string representation. I.e.: 1559229974454472704 - -#### Parameters - -**`node_id`** <integer, default `1`> It is possible to specify the node_id. - -#### Examples - - -```coffee -root.id = snowflake_id() -``` - -It is possible to specify the node_id. - -```coffee -root.id = snowflake_id(2) -``` - -### `throw` - -Throws an error similar to a regular mapping error. This is useful for abandoning a mapping entirely given certain conditions. - -#### Parameters - -**`why`** <string> A string explanation for why an error was thrown, this will be added to the resulting error message. - -#### Examples - - -```coffee -root.doc.type = match { - this.exists("header.id") => "foo" - this.exists("body.data") => "bar" - _ => throw("unknown type") -} -root.doc.contents = (this.body.content | this.thing.body) - -# In: {"header":{"id":"first"},"thing":{"body":"hello world"}} -# Out: {"doc":{"contents":"hello world","type":"foo"}} - -# In: {"nothing":"matches"} -# Out: Error("failed assignment (line 1): unknown type") -``` - -### `ulid` - -:::caution EXPERIMENTAL -This function is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Generate a random ULID. - -#### Parameters - -**`encoding`** <string, default `"crockford"`> The format to encode a ULID into. Valid options are: crockford, hex -**`random_source`** <string, default `"secure_random"`> The source of randomness to use for generating ULIDs. "secure_random" is recommended for most use cases. "fast_random" can be used if security is not a concern. - -#### Examples - - -Using the defaults of Crockford Base32 encoding and secure random source - -```coffee -root.id = ulid() -``` - -ULIDs can be hex-encoded too. - -```coffee -root.id = ulid("hex") -``` - -They can be generated using a fast, but unsafe, random source for use cases that are not security-sensitive. - -```coffee -root.id = ulid("crockford", "fast_random") -``` - -### `uuid_v4` - -Generates a new RFC-4122 UUID each time it is invoked and prints a string representation. - -#### Examples - - -```coffee -root.id = uuid_v4() -``` - -## Message Info - -### `batch_index` - -Returns the index of the mapped message within a batch. This is useful for applying maps only on certain messages of a batch. - -#### Examples - - -```coffee -root = if batch_index() > 0 { deleted() } -``` - -### `batch_size` - -Returns the size of the message batch. - -#### Examples - - -```coffee -root.foo = batch_size() -``` - -### `content` - -Returns the full raw contents of the mapping target message as a byte array. When mapping to a JSON field the value should be encoded using the method [`encode`][methods.encode], or cast to a string directly using the method [`string`][methods.string], otherwise it will be base64 encoded by default. - -#### Examples - - -```coffee -root.doc = content().string() - -# In: {"foo":"bar"} -# Out: {"doc":"{\"foo\":\"bar\"}"} -``` - -### `error` - -If an error has occurred during the processing of a message this function returns the reported cause of the error as a string, otherwise `null`. For more information about error handling patterns read [here][error_handling]. - -#### Examples - - -```coffee -root.doc.error = error() -``` - -### `errored` - -Returns a boolean value indicating whether an error has occurred during the processing of a message. For more information about error handling patterns read [here][error_handling]. - -#### Examples - - -```coffee -root.doc.status = if errored() { 400 } else { 200 } -``` - -### `json` - -Returns the value of a field within a JSON message located by a [dot path][field_paths] argument. This function always targets the entire source JSON document regardless of the mapping context. - -#### Parameters - -**`path`** <string, default `""`> An optional [dot path][field_paths] identifying a field to obtain. - -#### Examples - - -```coffee -root.mapped = json("foo.bar") - -# In: {"foo":{"bar":"hello world"}} -# Out: {"mapped":"hello world"} -``` - -The path argument is optional and if omitted the entire JSON payload is returned. - -```coffee -root.doc = json() - -# In: {"foo":{"bar":"hello world"}} -# Out: {"doc":{"foo":{"bar":"hello world"}}} -``` - -### `metadata` - -Returns the value of a metadata key from the input message, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map, in order to query metadata mutations made within a mapping use the [`@` operator](/docs/guides/bloblang/about#metadata). This function supports extracting metadata from other messages of a batch with the `from` method. - -#### Parameters - -**`key`** <string, default `""`> An optional key of a metadata value to obtain. - -#### Examples - - -```coffee -root.topic = metadata("kafka_topic") -``` - -The key parameter is optional and if omitted the entire metadata contents are returned as an object. - -```coffee -root.all_metadata = metadata() -``` - -### `tracing_id` - -:::caution EXPERIMENTAL -This function is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Provides the message trace id. The returned value will be zeroed if the message does not contain a span. - -#### Examples - - -```coffee -meta trace_id = tracing_id() -``` - -### `tracing_span` - -:::caution EXPERIMENTAL -This function is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Provides the message tracing span [(created via Open Telemetry APIs)](/docs/components/tracers/about) as an object serialised via text map formatting. The returned value will be `null` if the message does not have a span. - -#### Examples - - -```coffee -root.headers.traceparent = tracing_span().traceparent - -# In: {"some_stuff":"just can't be explained by science"} -# Out: {"headers":{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}} -``` - -## Environment - -### `env` - -Returns the value of an environment variable, or `null` if the environment variable does not exist. - -#### Parameters - -**`name`** <string> The name of an environment variable. -**`no_cache`** <bool, default `false`> Force the variable lookup to occur for each mapping invocation. - -#### Examples - - -```coffee -root.thing.key = env("key").or("default value") -``` - -```coffee -root.thing.key = env(this.thing.key_name) -``` - -When the name parameter is static this function will only resolve once and yield the same result for each invocation as an optimisation, this means that updates to env vars during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the variable lookup to be performed for each execution of the mapping. - -```coffee -root.thing.key = env(name: "key", no_cache: true) -``` - -### `file` - -Reads a file and returns its contents. Relative paths are resolved from the directory of the process executing the mapping. In order to read files relative to the mapping file use the newer [`file_rel` function](#file_rel) - -#### Parameters - -**`path`** <string> The path of the target file. -**`no_cache`** <bool, default `false`> Force the file to be read for each mapping invocation. - -#### Examples - - -```coffee -root.doc = file(env("BENTHOS_TEST_BLOBLANG_FILE")).parse_json() - -# In: {} -# Out: {"doc":{"foo":"bar"}} -``` - -When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimisation, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping. - -```coffee -root.doc = file(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json() - -# In: {} -# Out: {"doc":{"foo":"bar"}} -``` - -### `file_rel` - -Reads a file and returns its contents. Relative paths are resolved from the directory of the mapping. - -#### Parameters - -**`path`** <string> The path of the target file. -**`no_cache`** <bool, default `false`> Force the file to be read for each mapping invocation. - -#### Examples - - -```coffee -root.doc = file_rel(env("BENTHOS_TEST_BLOBLANG_FILE")).parse_json() - -# In: {} -# Out: {"doc":{"foo":"bar"}} -``` - -When the path parameter is static this function will only read the specified file once and yield the same result for each invocation as an optimisation, this means that updates to files during runtime will not be reflected. You can disable this cache with the optional parameter `no_cache`, which when set to `true` will cause the file to be read for each execution of the mapping. - -```coffee -root.doc = file_rel(path: env("BENTHOS_TEST_BLOBLANG_FILE"), no_cache: true).parse_json() - -# In: {} -# Out: {"doc":{"foo":"bar"}} -``` - -### `hostname` - -Returns a string matching the hostname of the machine running Benthos. - -#### Examples - - -```coffee -root.thing.host = hostname() -``` - -### `now` - -Returns the current timestamp as a string in RFC 3339 format with the local timezone. Use the method `ts_format` in order to change the format and timezone. - -#### Examples - - -```coffee -root.received_at = now() -``` - -```coffee -root.received_at = now().ts_format("Mon Jan 2 15:04:05 -0700 MST 2006", "UTC") -``` - -### `timestamp_unix` - -Returns the current unix timestamp in seconds. - -#### Examples - - -```coffee -root.received_at = timestamp_unix() -``` - -### `timestamp_unix_micro` - -Returns the current unix timestamp in microseconds. - -#### Examples - - -```coffee -root.received_at = timestamp_unix_micro() -``` - -### `timestamp_unix_milli` - -Returns the current unix timestamp in milliseconds. - -#### Examples - - -```coffee -root.received_at = timestamp_unix_milli() -``` - -### `timestamp_unix_nano` - -Returns the current unix timestamp in nanoseconds. - -#### Examples - - -```coffee -root.received_at = timestamp_unix_nano() -``` - -## Fake Data Generation - -### `fake` - -:::caution BETA -This function is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Takes in a string that maps to a [faker](https://github.com/go-faker/faker) function and returns the result from that faker function. Returns an error if the given string doesn't match a supported faker function. Supported functions: `latitude`, `longitude`, `unix_time`, `date`, `time_string`, `month_name`, `year_string`, `day_of_week`, `day_of_month`, `timestamp`, `century`, `timezone`, `time_period`, `email`, `mac_address`, `domain_name`, `url`, `username`, `ipv4`, `ipv6`, `password`, `jwt`, `word`, `sentence`, `paragraph`, `cc_type`, `cc_number`, `currency`, `amount_with_currency`, `title_male`, `title_female`, `first_name`, `first_name_male`, `first_name_female`, `last_name`, `name`, `gender`, `chinese_first_name`, `chinese_last_name`, `chinese_name`, `phone_number`, `toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the [faker](https://github.com/go-faker/faker) docs for details on these functions. - -#### Parameters - -**`function`** <string, default `""`> The name of the function to use to generate the value. - -#### Examples - - -Use `time_string` to generate a time in the format `00:00:00`: - -```coffee -root.time = fake("time_string") -``` - -Use `email` to generate a string in email address format: - -```coffee -root.email = fake("email") -``` - -Use `jwt` to generate a JWT token: - -```coffee -root.jwt = fake("jwt") -``` - -Use `uuid_hyphenated` to generate a hypenated UUID: - -```coffee -root.uuid = fake("uuid_hyphenated") -``` - -## Deprecated - -### `count` - -The `count` function is a counter starting at 1 which increments after each time it is called. Count takes an argument which is an identifier for the counter, allowing you to specify multiple unique counters in your configuration. - -#### Parameters - -**`name`** <string> An identifier for the counter. - -#### Examples - - -```coffee -root = this -root.id = count("bloblang_function_example") - -# In: {"message":"foo"} -# Out: {"id":1,"message":"foo"} - -# In: {"message":"bar"} -# Out: {"id":2,"message":"bar"} -``` - -### `meta` - -Returns the value of a metadata key from the input message as a string, or `null` if the key does not exist. Since values are extracted from the read-only input message they do NOT reflect changes made from within the map. In order to query metadata mutations made within a mapping use the [`root_meta` function](#root_meta). This function supports extracting metadata from other messages of a batch with the `from` method. - -#### Parameters - -**`key`** <string, default `""`> An optional key of a metadata value to obtain. - -#### Examples - - -```coffee -root.topic = meta("kafka_topic") -``` - -The key parameter is optional and if omitted the entire metadata contents are returned as an object. - -```coffee -root.all_metadata = meta() -``` - -### `root_meta` - -Returns the value of a metadata key from the new message being created as a string, or `null` if the key does not exist. Changes made to metadata during a mapping will be reflected by this function. - -#### Parameters - -**`key`** <string, default `""`> An optional key of a metadata value to obtain. - -#### Examples - - -```coffee -root.topic = root_meta("kafka_topic") -``` - -The key parameter is optional and if omitted the entire metadata contents are returned as an object. - -```coffee -root.all_metadata = root_meta() -``` - -[error_handling]: /docs/configuration/error_handling -[field_paths]: /docs/configuration/field_paths -[meta_proc]: /docs/components/processors/metadata -[methods.encode]: /docs/guides/bloblang/methods#encode -[methods.string]: /docs/guides/bloblang/methods#string diff --git a/website/docs/guides/bloblang/methods.md b/website/docs/guides/bloblang/methods.md deleted file mode 100644 index 08ad5d5f1e..0000000000 --- a/website/docs/guides/bloblang/methods.md +++ /dev/null @@ -1,3834 +0,0 @@ ---- -title: Bloblang Methods -sidebar_label: Methods -description: A list of Bloblang methods ---- - - - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Methods provide most of the power in Bloblang as they allow you to augment values and can be added to any expression (including other methods): - -```coffee -root.doc.id = this.thing.id.string().catch(uuid_v4()) -root.doc.reduced_nums = this.thing.nums.map_each(num -> if num < 10 { - deleted() -} else { - num - 10 -}) -root.has_good_taste = ["pikachu","mewtwo","magmar"].contains(this.user.fav_pokemon) -``` - -Methods support both named and nameless style arguments: - -```coffee -root.foo_one = this.(bar | baz).trim().replace_all(old: "dog", new: "cat") -root.foo_two = this.(bar | baz).trim().replace_all("dog", "cat") -``` - -## General - -### `apply` - -Apply a declared mapping to a target value. - -#### Parameters - -**`mapping`** <string> The mapping to apply. - -#### Examples - - -```coffee -map thing { - root.inner = this.first -} - -root.foo = this.doc.apply("thing") - -# In: {"doc":{"first":"hello world"}} -# Out: {"foo":{"inner":"hello world"}} -``` - -```coffee -map create_foo { - root.name = "a foo" - root.purpose = "to be a foo" -} - -root = this -root.foo = null.apply("create_foo") - -# In: {"id":"1234"} -# Out: {"foo":{"name":"a foo","purpose":"to be a foo"},"id":"1234"} -``` - -### `catch` - -If the result of a target query fails (due to incorrect types, failed parsing, etc) the argument is returned instead. - -#### Parameters - -**`fallback`** <query expression> A value to yield, or query to execute, if the target query fails. - -#### Examples - - -```coffee -root.doc.id = this.thing.id.string().catch(uuid_v4()) -``` - -The fallback argument can be a mapping, allowing you to capture the error string and yield structured data back. - -```coffee -root.url = this.url.parse_url().catch(err -> {"error":err,"input":this.url}) - -# In: {"url":"invalid %&# url"} -# Out: {"url":{"error":"field `this.url`: parse \"invalid %&\": invalid URL escape \"%&\"","input":"invalid %&# url"}} -``` - -When the input document is not structured attempting to reference structured fields with `this` will result in an error. Therefore, a convenient way to delete non-structured data is with a catch. - -```coffee -root = this.catch(deleted()) - -# In: {"doc":{"foo":"bar"}} -# Out: {"doc":{"foo":"bar"}} - -# In: not structured data -# Out: -``` - -### `exists` - -Checks that a field, identified via a [dot path][field_paths], exists in an object. - -#### Parameters - -**`path`** <string> A [dot path][field_paths] to a field. - -#### Examples - - -```coffee -root.result = this.foo.exists("bar.baz") - -# In: {"foo":{"bar":{"baz":"yep, I exist"}}} -# Out: {"result":true} - -# In: {"foo":{"bar":{}}} -# Out: {"result":false} - -# In: {"foo":{}} -# Out: {"result":false} -``` - -### `from` - -Modifies a target query such that certain functions are executed from the perspective of another message in the batch. This allows you to mutate events based on the contents of other messages. Functions that support this behaviour are `content`, `json` and `meta`. - -#### Parameters - -**`index`** <integer> The message index to use as a perspective. - -#### Examples - - -For example, the following map extracts the contents of the JSON field `foo` specifically from message index `1` of a batch, effectively overriding the field `foo` for all messages of a batch to that of message 1: - -```coffee -root = this -root.foo = json("foo").from(1) -``` - -### `from_all` - -Modifies a target query such that certain functions are executed from the perspective of each message in the batch, and returns the set of results as an array. Functions that support this behaviour are `content`, `json` and `meta`. - -#### Examples - - -```coffee -root = this -root.foo_summed = json("foo").from_all().sum() -``` - -### `or` - -If the result of the target query fails or resolves to `null`, returns the argument instead. This is an explicit method alternative to the coalesce pipe operator `|`. - -#### Parameters - -**`fallback`** <query expression> A value to yield, or query to execute, if the target query fails or resolves to `null`. - -#### Examples - - -```coffee -root.doc.id = this.thing.id.or(uuid_v4()) -``` - -## String Manipulation - -### `capitalize` - -Takes a string value and returns a copy with all Unicode letters that begin words mapped to their Unicode title case. - -#### Examples - - -```coffee -root.title = this.title.capitalize() - -# In: {"title":"the foo bar"} -# Out: {"title":"The Foo Bar"} -``` - -### `compare_argon2` - -Checks whether a string matches a hashed secret using Argon2. - -#### Parameters - -**`hashed_secret`** <string> The hashed secret to compare with the input. This must be a fully-qualified string which encodes the Argon2 options used to generate the hash. - -#### Examples - - -```coffee -root.match = this.secret.compare_argon2("$argon2id$v=19$m=4096,t=3,p=1$c2FsdHktbWNzYWx0ZmFjZQ$RMUMwgtS32/mbszd+ke4o4Ej1jFpYiUqY6MHWa69X7Y") - -# In: {"secret":"there-are-many-blobs-in-the-sea"} -# Out: {"match":true} -``` - -```coffee -root.match = this.secret.compare_argon2("$argon2id$v=19$m=4096,t=3,p=1$c2FsdHktbWNzYWx0ZmFjZQ$RMUMwgtS32/mbszd+ke4o4Ej1jFpYiUqY6MHWa69X7Y") - -# In: {"secret":"will-i-ever-find-love"} -# Out: {"match":false} -``` - -### `compare_bcrypt` - -Checks whether a string matches a hashed secret using bcrypt. - -#### Parameters - -**`hashed_secret`** <string> The hashed secret value to compare with the input. - -#### Examples - - -```coffee -root.match = this.secret.compare_bcrypt("$2y$10$Dtnt5NNzVtMCOZONT705tOcS8It6krJX8bEjnDJnwxiFKsz1C.3Ay") - -# In: {"secret":"there-are-many-blobs-in-the-sea"} -# Out: {"match":true} -``` - -```coffee -root.match = this.secret.compare_bcrypt("$2y$10$Dtnt5NNzVtMCOZONT705tOcS8It6krJX8bEjnDJnwxiFKsz1C.3Ay") - -# In: {"secret":"will-i-ever-find-love"} -# Out: {"match":false} -``` - -### `contains` - -Checks whether a string contains a substring and returns a boolean result. - -#### Parameters - -**`value`** <unknown> A value to test against elements of the target. - -#### Examples - - -```coffee -root.has_foo = this.thing.contains("foo") - -# In: {"thing":"this foo that"} -# Out: {"has_foo":true} - -# In: {"thing":"this bar that"} -# Out: {"has_foo":false} -``` - -### `escape_html` - -Escapes a string so that special characters like `<` to become `<`. It escapes only five such characters: `<`, `>`, `&`, `'` and `"` so that it can be safely placed within an HTML entity. - -#### Examples - - -```coffee -root.escaped = this.value.escape_html() - -# In: {"value":"foo & bar"} -# Out: {"escaped":"foo & bar"} -``` - -### `escape_url_query` - -Escapes a string so that it can be safely placed within a URL query. - -#### Examples - - -```coffee -root.escaped = this.value.escape_url_query() - -# In: {"value":"foo & bar"} -# Out: {"escaped":"foo+%26+bar"} -``` - -### `filepath_join` - -Joins an array of path elements into a single file path. The separator depends on the operating system of the machine. - -#### Examples - - -```coffee -root.path = this.path_elements.filepath_join() - -# In: {"path_elements":["/foo/","bar.txt"]} -# Out: {"path":"/foo/bar.txt"} -``` - -### `filepath_split` - -Splits a file path immediately following the final Separator, separating it into a directory and file name component returned as a two element array of strings. If there is no Separator in the path, the first element will be empty and the second will contain the path. The separator depends on the operating system of the machine. - -#### Examples - - -```coffee -root.path_sep = this.path.filepath_split() - -# In: {"path":"/foo/bar.txt"} -# Out: {"path_sep":["/foo/","bar.txt"]} - -# In: {"path":"baz.txt"} -# Out: {"path_sep":["","baz.txt"]} -``` - -### `format` - -Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go [`fmt` package documentation](https://pkg.go.dev/fmt) for the list of valid format verbs. - -#### Examples - - -```coffee -root.foo = "%s(%v): %v".format(this.name, this.age, this.fingers) - -# In: {"name":"lance","age":37,"fingers":13} -# Out: {"foo":"lance(37): 13"} -``` - -### `has_prefix` - -Checks whether a string has a prefix argument and returns a bool. - -#### Parameters - -**`value`** <string> The string to test. - -#### Examples - - -```coffee -root.t1 = this.v1.has_prefix("foo") -root.t2 = this.v2.has_prefix("foo") - -# In: {"v1":"foobar","v2":"barfoo"} -# Out: {"t1":true,"t2":false} -``` - -### `has_suffix` - -Checks whether a string has a suffix argument and returns a bool. - -#### Parameters - -**`value`** <string> The string to test. - -#### Examples - - -```coffee -root.t1 = this.v1.has_suffix("foo") -root.t2 = this.v2.has_suffix("foo") - -# In: {"v1":"foobar","v2":"barfoo"} -# Out: {"t1":false,"t2":true} -``` - -### `index_of` - -Returns the starting index of the argument substring in a string target, or `-1` if the target doesn't contain the argument. - -#### Parameters - -**`value`** <string> A string to search for. - -#### Examples - - -```coffee -root.index = this.thing.index_of("bar") - -# In: {"thing":"foobar"} -# Out: {"index":3} -``` - -```coffee -root.index = content().index_of("meow") - -# In: the cat meowed, the dog woofed -# Out: {"index":8} -``` - -### `length` - -Returns the length of a string. - -#### Examples - - -```coffee -root.foo_len = this.foo.length() - -# In: {"foo":"hello world"} -# Out: {"foo_len":11} -``` - -### `lowercase` - -Convert a string value into lowercase. - -#### Examples - - -```coffee -root.foo = this.foo.lowercase() - -# In: {"foo":"HELLO WORLD"} -# Out: {"foo":"hello world"} -``` - -### `quote` - -Quotes a target string using escape sequences (`\t`, `\n`, `\xFF`, `\u0100`) for control characters and non-printable characters. - -#### Examples - - -```coffee -root.quoted = this.thing.quote() - -# In: {"thing":"foo\nbar"} -# Out: {"quoted":"\"foo\\nbar\""} -``` - -### `replace_all` - -Replaces all occurrences of the first argument in a target string with the second argument. - -#### Parameters - -**`old`** <string> A string to match against. -**`new`** <string> A string to replace with. - -#### Examples - - -```coffee -root.new_value = this.value.replace_all("foo","dog") - -# In: {"value":"The foo ate my homework"} -# Out: {"new_value":"The dog ate my homework"} -``` - -### `replace_all_many` - -For each pair of strings in an argument array, replaces all occurrences of the first item of the pair with the second. This is a more compact way of chaining a series of `replace_all` methods. - -#### Parameters - -**`values`** <array> An array of values, each even value will be replaced with the following odd value. - -#### Examples - - -```coffee -root.new_value = this.value.replace_all_many([ - "", "<b>", - "", "</b>", - "", "<i>", - "", "</i>", -]) - -# In: {"value":"Hello World"} -# Out: {"new_value":"<i>Hello</i> <b>World</b>"} -``` - -### `reverse` - -Returns the target string in reverse order. - -#### Examples - - -```coffee -root.reversed = this.thing.reverse() - -# In: {"thing":"backwards"} -# Out: {"reversed":"sdrawkcab"} -``` - -```coffee -root = content().reverse() - -# In: {"thing":"backwards"} -# Out: }"sdrawkcab":"gniht"{ -``` - -### `slice` - -Extract a slice from a string by specifying two indices, a low and high bound, which selects a half-open range that includes the first character, but excludes the last one. If the second index is omitted then it defaults to the length of the input sequence. - -#### Parameters - -**`low`** <integer> The low bound, which is the first element of the selection, or if negative selects from the end. -**`high`** <(optional) integer> An optional high bound. - -#### Examples - - -```coffee -root.beginning = this.value.slice(0, 2) -root.end = this.value.slice(4) - -# In: {"value":"foo bar"} -# Out: {"beginning":"fo","end":"bar"} -``` - -A negative low index can be used, indicating an offset from the end of the sequence. If the low index is greater than the length of the sequence then an empty result is returned. - -```coffee -root.last_chunk = this.value.slice(-4) -root.the_rest = this.value.slice(0, -4) - -# In: {"value":"foo bar"} -# Out: {"last_chunk":" bar","the_rest":"foo"} -``` - -### `slug` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its [docs](https://pkg.go.dev/github.com/gosimple/slug) for more information. - -Introduced in version 4.2.0. - - -#### Parameters - -**`lang`** <(optional) string, default `"en"`> - -#### Examples - - -Creates a slug from an English string - -```coffee -root.slug = this.value.slug() - -# In: {"value":"Gopher & Benthos"} -# Out: {"slug":"gopher-and-benthos"} -``` - -Creates a slug from a French string - -```coffee -root.slug = this.value.slug("fr") - -# In: {"value":"Gaufre & Poisson d'Eau Profonde"} -# Out: {"slug":"gaufre-et-poisson-deau-profonde"} -``` - -### `split` - -Split a string value into an array of strings by splitting it on a string separator. - -#### Parameters - -**`delimiter`** <string> The delimiter to split with. - -#### Examples - - -```coffee -root.new_value = this.value.split(",") - -# In: {"value":"foo,bar,baz"} -# Out: {"new_value":["foo","bar","baz"]} -``` - -### `strip_html` - -Attempts to remove all HTML tags from a target string. - -#### Parameters - -**`preserve`** <(optional) array> An optional array of element types to preserve in the output. - -#### Examples - - -```coffee -root.stripped = this.value.strip_html() - -# In: {"value":"

the plain old text

"} -# Out: {"stripped":"the plain old text"} -``` - -It's also possible to provide an explicit list of element types to preserve in the output. - -```coffee -root.stripped = this.value.strip_html(["article"]) - -# In: {"value":"

the plain old text

"} -# Out: {"stripped":"
the plain old text
"} -``` - -### `trim` - -Remove all leading and trailing characters from a string that are contained within an argument cutset. If no arguments are provided then whitespace is removed. - -#### Parameters - -**`cutset`** <(optional) string> An optional string of characters to trim from the target value. - -#### Examples - - -```coffee -root.title = this.title.trim("!?") -root.description = this.description.trim() - -# In: {"description":" something happened and its amazing! ","title":"!!!watch out!?"} -# Out: {"description":"something happened and its amazing!","title":"watch out"} -``` - -### `trim_prefix` - -Remove the provided leading prefix substring from a string. If the string does not have the prefix substring, it is returned unchanged. - -Introduced in version 4.12.0. - - -#### Parameters - -**`prefix`** <string> The leading prefix substring to trim from the string. - -#### Examples - - -```coffee -root.name = this.name.trim_prefix("foobar_") -root.description = this.description.trim_prefix("foobar_") - -# In: {"description":"unchanged","name":"foobar_blobton"} -# Out: {"description":"unchanged","name":"blobton"} -``` - -### `trim_suffix` - -Remove the provided trailing suffix substring from a string. If the string does not have the suffix substring, it is returned unchanged. - -Introduced in version 4.12.0. - - -#### Parameters - -**`suffix`** <string> The trailing suffix substring to trim from the string. - -#### Examples - - -```coffee -root.name = this.name.trim_suffix("_foobar") -root.description = this.description.trim_suffix("_foobar") - -# In: {"description":"unchanged","name":"blobton_foobar"} -# Out: {"description":"unchanged","name":"blobton"} -``` - -### `unescape_html` - -Unescapes a string so that entities like `<` become `<`. It unescapes a larger range of entities than `escape_html` escapes. For example, `á` unescapes to `á`, as does `á` and `&xE1;`. - -#### Examples - - -```coffee -root.unescaped = this.value.unescape_html() - -# In: {"value":"foo & bar"} -# Out: {"unescaped":"foo & bar"} -``` - -### `unescape_url_query` - -Expands escape sequences from a URL query string. - -#### Examples - - -```coffee -root.unescaped = this.value.unescape_url_query() - -# In: {"value":"foo+%26+bar"} -# Out: {"unescaped":"foo & bar"} -``` - -### `unquote` - -Unquotes a target string, expanding any escape sequences (`\t`, `\n`, `\xFF`, `\u0100`) for control characters and non-printable characters. - -#### Examples - - -```coffee -root.unquoted = this.thing.unquote() - -# In: {"thing":"\"foo\\nbar\""} -# Out: {"unquoted":"foo\nbar"} -``` - -### `uppercase` - -Convert a string value into uppercase. - -#### Examples - - -```coffee -root.foo = this.foo.uppercase() - -# In: {"foo":"hello world"} -# Out: {"foo":"HELLO WORLD"} -``` - -## Regular Expressions - -### `re_find_all` - -Returns an array containing all successive matches of a regular expression in a string. - -#### Parameters - -**`pattern`** <string> The pattern to match against. - -#### Examples - - -```coffee -root.matches = this.value.re_find_all("a.") - -# In: {"value":"paranormal"} -# Out: {"matches":["ar","an","al"]} -``` - -### `re_find_all_object` - -Returns an array of objects containing all matches of the regular expression and the matches of its subexpressions. The key of each match value is the name of the group when specified, otherwise it is the index of the matching group, starting with the expression as a whole at 0. - -#### Parameters - -**`pattern`** <string> The pattern to match against. - -#### Examples - - -```coffee -root.matches = this.value.re_find_all_object("a(?Px*)b") - -# In: {"value":"-axxb-ab-"} -# Out: {"matches":[{"0":"axxb","foo":"xx"},{"0":"ab","foo":""}]} -``` - -```coffee -root.matches = this.value.re_find_all_object("(?m)(?P\\w+):\\s+(?P\\w+)$") - -# In: {"value":"option1: value1\noption2: value2\noption3: value3"} -# Out: {"matches":[{"0":"option1: value1","key":"option1","value":"value1"},{"0":"option2: value2","key":"option2","value":"value2"},{"0":"option3: value3","key":"option3","value":"value3"}]} -``` - -### `re_find_all_submatch` - -Returns an array of arrays containing all successive matches of the regular expression in a string and the matches, if any, of its subexpressions. - -#### Parameters - -**`pattern`** <string> The pattern to match against. - -#### Examples - - -```coffee -root.matches = this.value.re_find_all_submatch("a(x*)b") - -# In: {"value":"-axxb-ab-"} -# Out: {"matches":[["axxb","xx"],["ab",""]]} -``` - -### `re_find_object` - -Returns an object containing the first match of the regular expression and the matches of its subexpressions. The key of each match value is the name of the group when specified, otherwise it is the index of the matching group, starting with the expression as a whole at 0. - -#### Parameters - -**`pattern`** <string> The pattern to match against. - -#### Examples - - -```coffee -root.matches = this.value.re_find_object("a(?Px*)b") - -# In: {"value":"-axxb-ab-"} -# Out: {"matches":{"0":"axxb","foo":"xx"}} -``` - -```coffee -root.matches = this.value.re_find_object("(?P\\w+):\\s+(?P\\w+)") - -# In: {"value":"option1: value1"} -# Out: {"matches":{"0":"option1: value1","key":"option1","value":"value1"}} -``` - -### `re_match` - -Checks whether a regular expression matches against any part of a string and returns a boolean. - -#### Parameters - -**`pattern`** <string> The pattern to match against. - -#### Examples - - -```coffee -root.matches = this.value.re_match("[0-9]") - -# In: {"value":"there are 10 puppies"} -# Out: {"matches":true} - -# In: {"value":"there are ten puppies"} -# Out: {"matches":false} -``` - -### `re_replace_all` - -Replaces all occurrences of the argument regular expression in a string with a value. Inside the value $ signs are interpreted as submatch expansions, e.g. `$1` represents the text of the first submatch. - -#### Parameters - -**`pattern`** <string> The pattern to match against. -**`value`** <string> The value to replace with. - -#### Examples - - -```coffee -root.new_value = this.value.re_replace_all("ADD ([0-9]+)","+($1)") - -# In: {"value":"foo ADD 70"} -# Out: {"new_value":"foo +(70)"} -``` - -## Number Manipulation - -### `abs` - -Returns the absolute value of an int64 or float64 number. As a special case, when an integer is provided that is the minimum value it is converted to the maximum value. - -#### Examples - - -```coffee - -root.outs = this.ins.map_each(ele -> ele.abs()) - - -# In: {"ins":[9,-18,1.23,-4.56]} -# Out: {"outs":[9,18,1.23,4.56]} -``` - -### `ceil` - -Returns the least integer value greater than or equal to a number. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned. - -#### Examples - - -```coffee -root.new_value = this.value.ceil() - -# In: {"value":5.3} -# Out: {"new_value":6} - -# In: {"value":-5.9} -# Out: {"new_value":-5} -``` - -### `float32` - - -Converts a numerical type into a 32-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the [`strconv.ParseFloat` documentation](https://pkg.go.dev/strconv#ParseFloat) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.out = this.in.float32() - - -# In: {"in":"6.674282313423543523453425345e-11"} -# Out: {"out":6.674283e-11} -``` - -### `float64` - - -Converts a numerical type into a 64-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the [`strconv.ParseFloat` documentation](https://pkg.go.dev/strconv#ParseFloat) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.out = this.in.float64() - - -# In: {"in":"6.674282313423543523453425345e-11"} -# Out: {"out":6.674282313423544e-11} -``` - -### `floor` - -Returns the greatest integer value less than or equal to the target number. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned. - -#### Examples - - -```coffee -root.new_value = this.value.floor() - -# In: {"value":5.7} -# Out: {"new_value":5} -``` - -### `int16` - - -Converts a numerical type into a 16-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 16-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.int16() -root.b = this.b.round().int16() -root.c = this.c.int16() -root.d = this.d.int16().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":-12} -``` - -```coffee - -root = this.int16() - - -# In: "0xDE" -# Out: 222 -``` - -### `int32` - - -Converts a numerical type into a 32-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 32-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.int32() -root.b = this.b.round().int32() -root.c = this.c.int32() -root.d = this.d.int32().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":-12} -``` - -```coffee - -root = this.int32() - - -# In: "0xDEAD" -# Out: 57005 -``` - -### `int64` - - -Converts a numerical type into a 64-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 64-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.int64() -root.b = this.b.round().int64() -root.c = this.c.int64() -root.d = this.d.int64().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":-12} -``` - -```coffee - -root = this.int64() - - -# In: "0xDEADBEEF" -# Out: 3735928559 -``` - -### `int8` - - -Converts a numerical type into a 8-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 8-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.int8() -root.b = this.b.round().int8() -root.c = this.c.int8() -root.d = this.d.int8().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":-12} -``` - -```coffee - -root = this.int8() - - -# In: "0xD" -# Out: 13 -``` - -### `log` - -Returns the natural logarithm of a number. - -#### Examples - - -```coffee -root.new_value = this.value.log().round() - -# In: {"value":1} -# Out: {"new_value":0} - -# In: {"value":2.7183} -# Out: {"new_value":1} -``` - -### `log10` - -Returns the decimal logarithm of a number. - -#### Examples - - -```coffee -root.new_value = this.value.log10() - -# In: {"value":100} -# Out: {"new_value":2} - -# In: {"value":1000} -# Out: {"new_value":3} -``` - -### `max` - -Returns the largest numerical value found within an array. All values must be numerical and the array must not be empty, otherwise an error is returned. - -#### Examples - - -```coffee -root.biggest = this.values.max() - -# In: {"values":[0,3,2.5,7,5]} -# Out: {"biggest":7} -``` - -```coffee -root.new_value = [0,this.value].max() - -# In: {"value":-1} -# Out: {"new_value":0} - -# In: {"value":7} -# Out: {"new_value":7} -``` - -### `min` - -Returns the smallest numerical value found within an array. All values must be numerical and the array must not be empty, otherwise an error is returned. - -#### Examples - - -```coffee -root.smallest = this.values.min() - -# In: {"values":[0,3,-2.5,7,5]} -# Out: {"smallest":-2.5} -``` - -```coffee -root.new_value = [10,this.value].min() - -# In: {"value":2} -# Out: {"new_value":2} - -# In: {"value":23} -# Out: {"new_value":10} -``` - -### `round` - -Rounds numbers to the nearest integer, rounding half away from zero. If the resulting value fits within a 64-bit integer then that is returned, otherwise a new floating point number is returned. - -#### Examples - - -```coffee -root.new_value = this.value.round() - -# In: {"value":5.3} -# Out: {"new_value":5} - -# In: {"value":5.9} -# Out: {"new_value":6} -``` - -### `uint16` - - -Converts a numerical type into a 16-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 16-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.uint16() -root.b = this.b.round().uint16() -root.c = this.c.uint16() -root.d = this.d.uint16().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":0} -``` - -```coffee - -root = this.uint16() - - -# In: "0xDE" -# Out: 222 -``` - -### `uint32` - - -Converts a numerical type into a 32-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 32-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.uint32() -root.b = this.b.round().uint32() -root.c = this.c.uint32() -root.d = this.d.uint32().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":0} -``` - -```coffee - -root = this.uint32() - - -# In: "0xDEAD" -# Out: 57005 -``` - -### `uint64` - - -Converts a numerical type into a 64-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 64-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.uint64() -root.b = this.b.round().uint64() -root.c = this.c.uint64() -root.d = this.d.uint64().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":0} -``` - -```coffee - -root = this.uint64() - - -# In: "0xDEADBEEF" -# Out: 3735928559 -``` - -### `uint8` - - -Converts a numerical type into a 8-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). - -If the value is a string then an attempt will be made to parse it as a 8-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use [`.round()`](#round) on the value. Please refer to the [`strconv.ParseInt` documentation](https://pkg.go.dev/strconv#ParseInt) for details regarding the supported formats. - -#### Examples - - -```coffee - -root.a = this.a.uint8() -root.b = this.b.round().uint8() -root.c = this.c.uint8() -root.d = this.d.uint8().catch(0) - - -# In: {"a":12,"b":12.34,"c":"12","d":-12} -# Out: {"a":12,"b":12,"c":12,"d":0} -``` - -```coffee - -root = this.uint8() - - -# In: "0xD" -# Out: 13 -``` - -## Timestamp Manipulation - -### `parse_duration` - -Attempts to parse a string as a duration and returns an integer of nanoseconds. A duration string is a possibly signed sequence of decimal numbers, each with an optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". - -#### Examples - - -```coffee -root.delay_for_ns = this.delay_for.parse_duration() - -# In: {"delay_for":"50us"} -# Out: {"delay_for_ns":50000} -``` - -```coffee -root.delay_for_s = this.delay_for.parse_duration() / 1000000000 - -# In: {"delay_for":"2h"} -# Out: {"delay_for_s":7200} -``` - -### `parse_duration_iso8601` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to parse a string using ISO-8601 rules as a duration and returns an integer of nanoseconds. A duration string is represented by the format "P[n]Y[n]M[n]DT[n]H[n]M[n]S" or "P[n]W". In these representations, the "[n]" is replaced by the value for each of the date and time elements that follow the "[n]". For example, "P3Y6M4DT12H30M5S" represents a duration of "three years, six months, four days, twelve hours, thirty minutes, and five seconds". The last field of the format allows fractions with one decimal place, so "P3.5S" will return 3500000000ns. Any additional decimals will be truncated. - -#### Examples - - -Arbitrary ISO-8601 duration string to nanoseconds: - -```coffee -root.delay_for_ns = this.delay_for.parse_duration_iso8601() - -# In: {"delay_for":"P3Y6M4DT12H30M5S"} -# Out: {"delay_for_ns":110839937000000000} -``` - -Two hours ISO-8601 duration string to seconds: - -```coffee -root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000 - -# In: {"delay_for":"PT2H"} -# Out: {"delay_for_s":7200} -``` - -Two and a half seconds ISO-8601 duration string to seconds: - -```coffee -root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000 - -# In: {"delay_for":"PT2.5S"} -# Out: {"delay_for_s":2.5} -``` - -### `ts_add_iso8601` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Parse parameter string as ISO 8601 period and add it to value with high precision for units larger than an hour. - -#### Parameters - -**`duration`** <string> Duration in ISO 8601 format - -### `ts_format` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to format a timestamp value as a string according to a specified format, or RFC 3339 by default. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. - -The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the [`ts_strftime`](#ts_strftime) method. - -#### Parameters - -**`format`** <string, default `"2006-01-02T15:04:05.999999999Z07:00"`> The output format to use. -**`tz`** <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. - -#### Examples - - -```coffee -root.something_at = (this.created_at + 300).ts_format() -``` - -An optional string argument can be used in order to specify the output format of the timestamp. The format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. - -```coffee -root.something_at = (this.created_at + 300).ts_format("2006-Jan-02 15:04:05") -``` - -A second optional string argument can also be used in order to specify a timezone, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. - -```coffee -root.something_at = this.created_at.ts_format(format: "2006-Jan-02 15:04:05", tz: "UTC") - -# In: {"created_at":1597405526} -# Out: {"something_at":"2020-Aug-14 11:45:26"} - -# In: {"created_at":"2020-08-14T11:50:26.371Z"} -# Out: {"something_at":"2020-Aug-14 11:50:26"} -``` - -And `ts_format` supports up to nanosecond precision with floating point timestamp values. - -```coffee -root.something_at = this.created_at.ts_format("2006-Jan-02 15:04:05.999999", "UTC") - -# In: {"created_at":1597405526.123456} -# Out: {"something_at":"2020-Aug-14 11:45:26.123456"} - -# In: {"created_at":"2020-08-14T11:50:26.371Z"} -# Out: {"something_at":"2020-Aug-14 11:50:26.371"} -``` - -### `ts_parse` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as [`ts_format`](#ts_format). - -The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the [`ts_strptime`](#ts_strptime) method. - -#### Parameters - -**`format`** <string> The format of the target string. - -#### Examples - - -```coffee -root.doc.timestamp = this.doc.timestamp.ts_parse("2006-Jan-02") - -# In: {"doc":{"timestamp":"2020-Aug-14"}} -# Out: {"doc":{"timestamp":"2020-08-14T00:00:00Z"}} -``` - -### `ts_round` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns the result of rounding a timestamp to the nearest multiple of the argument duration (nanoseconds). The rounding behavior for halfway values is to round up. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -Introduced in version 4.2.0. - - -#### Parameters - -**`duration`** <integer> A duration measured in nanoseconds to round by. - -#### Examples - - -Use the method `parse_duration` to convert a duration string into an integer argument. - -```coffee -root.created_at_hour = this.created_at.ts_round("1h".parse_duration()) - -# In: {"created_at":"2020-08-14T05:54:23Z"} -# Out: {"created_at_hour":"2020-08-14T06:00:00Z"} -``` - -### `ts_strftime` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to format a timestamp value as a string according to a specified strftime-compatible format. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. - -#### Parameters - -**`format`** <string> The output format to use. -**`tz`** <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used. - -#### Examples - - -The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behaviour of the specifier. Please refer to [man 3 strftime](https://linux.die.net/man/3/strftime) for the list of format specifiers. - -```coffee -root.something_at = (this.created_at + 300).ts_strftime("%Y-%b-%d %H:%M:%S") -``` - -A second optional string argument can also be used in order to specify a timezone, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. - -```coffee -root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S", "UTC") - -# In: {"created_at":1597405526} -# Out: {"something_at":"2020-Aug-14 11:45:26"} - -# In: {"created_at":"2020-08-14T11:50:26.371Z"} -# Out: {"something_at":"2020-Aug-14 11:50:26"} -``` - -As an extension provided by the underlying formatting library, [itchyny/timefmt-go](https://github.com/itchyny/timefmt-go), the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. - -```coffee -root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S.%f", "UTC") - -# In: {"created_at":1597405526} -# Out: {"something_at":"2020-Aug-14 11:45:26.000000"} - -# In: {"created_at":"2020-08-14T11:50:26.371Z"} -# Out: {"something_at":"2020-Aug-14 11:50:26.371000"} -``` - -### `ts_strptime` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into [`ts_format`](#ts_format). - -#### Parameters - -**`format`** <string> The format of the target string. - -#### Examples - - -The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behaviour of the specifier. Please refer to [man 3 strptime](https://linux.die.net/man/3/strptime) for the list of format specifiers. - -```coffee -root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d") - -# In: {"doc":{"timestamp":"2020-Aug-14"}} -# Out: {"doc":{"timestamp":"2020-08-14T00:00:00Z"}} -``` - -As an extension provided by the underlying formatting library, [itchyny/timefmt-go](https://github.com/itchyny/timefmt-go), the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. - -```coffee -root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d %H:%M:%S.%f") - -# In: {"doc":{"timestamp":"2020-Aug-14 11:50:26.371000"}} -# Out: {"doc":{"timestamp":"2020-08-14T11:50:26.371Z"}} -``` - -### `ts_sub` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns the difference in nanoseconds between the target timestamp (t1) and the timestamp provided as a parameter (t2). The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -Introduced in version 4.23.0. - - -#### Parameters - -**`t2`** <timestamp> The second timestamp to be subtracted from the method target. - -#### Examples - - -Use the `.abs()` method in order to calculate an absolute duration between two timestamps. - -```coffee -root.between = this.started_at.ts_sub("2020-08-14T05:54:23Z").abs() - -# In: {"started_at":"2020-08-13T05:54:23Z"} -# Out: {"between":86400000000000} -``` - -### `ts_sub_iso8601` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Parse parameter string as ISO 8601 period and subtract it from value with high precision for units larger than an hour. - -#### Parameters - -**`duration`** <string> Duration in ISO 8601 format - -### `ts_tz` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns the result of converting a timestamp to a specified timezone. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -Introduced in version 4.3.0. - - -#### Parameters - -**`tz`** <string> The timezone to change to. If set to "UTC" then the timezone will be UTC. If set to "Local" then the local timezone will be used. Otherwise, the argument is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York". - -#### Examples - - -```coffee -root.created_at_utc = this.created_at.ts_tz("UTC") - -# In: {"created_at":"2021-02-03T17:05:06+01:00"} -# Out: {"created_at_utc":"2021-02-03T16:05:06Z"} -``` - -### `ts_unix` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -#### Examples - - -```coffee -root.created_at_unix = this.created_at.ts_unix() - -# In: {"created_at":"2009-11-10T23:00:00Z"} -# Out: {"created_at_unix":1257894000} -``` - -### `ts_unix_micro` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -#### Examples - - -```coffee -root.created_at_unix = this.created_at.ts_unix_micro() - -# In: {"created_at":"2009-11-10T23:00:00Z"} -# Out: {"created_at_unix":1257894000000000} -``` - -### `ts_unix_milli` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -#### Examples - - -```coffee -root.created_at_unix = this.created_at.ts_unix_milli() - -# In: {"created_at":"2009-11-10T23:00:00Z"} -# Out: {"created_at_unix":1257894000000} -``` - -### `ts_unix_nano` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -#### Examples - - -```coffee -root.created_at_unix = this.created_at.ts_unix_nano() - -# In: {"created_at":"2009-11-10T23:00:00Z"} -# Out: {"created_at_unix":1257894000000000000} -``` - -## Type Coercion - -### `bool` - -Attempt to parse a value into a boolean. An optional argument can be provided, in which case if the value cannot be parsed the argument will be returned instead. If the value is a number then any non-zero value will resolve to `true`, if the value is a string then any of the following values are considered valid: `1, t, T, TRUE, true, True, 0, f, F, FALSE`. - -#### Parameters - -**`default`** <(optional) bool> An optional value to yield if the target cannot be parsed as a boolean. - -#### Examples - - -```coffee -root.foo = this.thing.bool() -root.bar = this.thing.bool(true) -``` - -### `bytes` - -Marshal a value into a byte array. If the value is already a byte array it is unchanged. - -#### Examples - - -```coffee -root.first_byte = this.name.bytes().index(0) - -# In: {"name":"foobar bazson"} -# Out: {"first_byte":102} -``` - -### `not_empty` - -Ensures that the given string, array or object value is not empty, and if so returns it, otherwise an error is returned. - -#### Examples - - -```coffee -root.a = this.a.not_empty() - -# In: {"a":"foo"} -# Out: {"a":"foo"} - -# In: {"a":""} -# Out: Error("failed assignment (line 1): field `this.a`: string value is empty") - -# In: {"a":["foo","bar"]} -# Out: {"a":["foo","bar"]} - -# In: {"a":[]} -# Out: Error("failed assignment (line 1): field `this.a`: array value is empty") - -# In: {"a":{"b":"foo","c":"bar"}} -# Out: {"a":{"b":"foo","c":"bar"}} - -# In: {"a":{}} -# Out: Error("failed assignment (line 1): field `this.a`: object value is empty") -``` - -### `not_null` - -Ensures that the given value is not `null`, and if so returns it, otherwise an error is returned. - -#### Examples - - -```coffee -root.a = this.a.not_null() - -# In: {"a":"foobar","b":"barbaz"} -# Out: {"a":"foobar"} - -# In: {"b":"barbaz"} -# Out: Error("failed assignment (line 1): field `this.a`: value is null") -``` - -### `number` - -Attempt to parse a value into a number. An optional argument can be provided, in which case if the value cannot be parsed into a number the argument will be returned instead. - -#### Parameters - -**`default`** <(optional) float> An optional value to yield if the target cannot be parsed as a number. - -#### Examples - - -```coffee -root.foo = this.thing.number() + 10 -root.bar = this.thing.number(5) * 10 -``` - -### `string` - -Marshal a value into a string. If the value is already a string it is unchanged. - -#### Examples - - -```coffee -root.nested_json = this.string() - -# In: {"foo":"bar"} -# Out: {"nested_json":"{\"foo\":\"bar\"}"} -``` - -```coffee -root.id = this.id.string() - -# In: {"id":228930314431312345} -# Out: {"id":"228930314431312345"} -``` - -### `type` - -Returns the type of a value as a string, providing one of the following values: `string`, `bytes`, `number`, `bool`, `timestamp`, `array`, `object` or `null`. - -#### Examples - - -```coffee -root.bar_type = this.bar.type() -root.foo_type = this.foo.type() - -# In: {"bar":10,"foo":"is a string"} -# Out: {"bar_type":"number","foo_type":"string"} -``` - -```coffee -root.type = this.type() - -# In: "foobar" -# Out: {"type":"string"} - -# In: 666 -# Out: {"type":"number"} - -# In: false -# Out: {"type":"bool"} - -# In: ["foo", "bar"] -# Out: {"type":"array"} - -# In: {"foo": "bar"} -# Out: {"type":"object"} - -# In: null -# Out: {"type":"null"} -``` - -```coffee -root.type = content().type() - -# In: foobar -# Out: {"type":"bytes"} -``` - -```coffee -root.type = this.ts_parse("2006-01-02").type() - -# In: "2022-06-06" -# Out: {"type":"timestamp"} -``` - -## Object & Array Manipulation - -### `all` - -Checks each element of an array against a query and returns true if all elements passed. An error occurs if the target is not an array, or if any element results in the provided query returning a non-boolean result. Returns false if the target array is empty. - -#### Parameters - -**`test`** <query expression> A test query to apply to each element. - -#### Examples - - -```coffee -root.all_over_21 = this.patrons.all(patron -> patron.age >= 21) - -# In: {"patrons":[{"id":"1","age":18},{"id":"2","age":23}]} -# Out: {"all_over_21":false} - -# In: {"patrons":[{"id":"1","age":45},{"id":"2","age":23}]} -# Out: {"all_over_21":true} -``` - -### `any` - -Checks the elements of an array against a query and returns true if any element passes. An error occurs if the target is not an array, or if an element results in the provided query returning a non-boolean result. Returns false if the target array is empty. - -#### Parameters - -**`test`** <query expression> A test query to apply to each element. - -#### Examples - - -```coffee -root.any_over_21 = this.patrons.any(patron -> patron.age >= 21) - -# In: {"patrons":[{"id":"1","age":18},{"id":"2","age":23}]} -# Out: {"any_over_21":true} - -# In: {"patrons":[{"id":"1","age":10},{"id":"2","age":12}]} -# Out: {"any_over_21":false} -``` - -### `append` - -Returns an array with new elements appended to the end. - -#### Examples - - -```coffee -root.foo = this.foo.append("and", "this") - -# In: {"foo":["bar","baz"]} -# Out: {"foo":["bar","baz","and","this"]} -``` - -### `assign` - -Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the value in the destination object will be overwritten by that of source object. In order to preserve both values on collision use the [`merge`](#merge) method. - -#### Parameters - -**`with`** <unknown> A value to merge the target value with. - -#### Examples - - -```coffee -root = this.foo.assign(this.bar) - -# In: {"foo":{"first_name":"fooer","likes":"bars"},"bar":{"second_name":"barer","likes":"foos"}} -# Out: {"first_name":"fooer","likes":"foos","second_name":"barer"} -``` - -### `collapse` - -Collapse an array or object into an object of key/value pairs for each field, where the key is the full path of the structured field in dot path notation. Empty arrays an objects are ignored by default. - -#### Parameters - -**`include_empty`** <bool, default `false`> Whether to include empty objects and arrays in the resulting object. - -#### Examples - - -```coffee -root.result = this.collapse() - -# In: {"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]} -# Out: {"result":{"foo.0.bar":"1","foo.2.bar":"2"}} -``` - -An optional boolean parameter can be set to true in order to include empty objects and arrays. - -```coffee -root.result = this.collapse(include_empty: true) - -# In: {"foo":[{"bar":"1"},{"bar":{}},{"bar":"2"},{"bar":[]}]} -# Out: {"result":{"foo.0.bar":"1","foo.1.bar":{},"foo.2.bar":"2","foo.3.bar":[]}} -``` - -### `concat` - -Concatenates an array value with one or more argument arrays. - -#### Examples - - -```coffee -root.foo = this.foo.concat(this.bar, this.baz) - -# In: {"foo":["a","b"],"bar":["c"],"baz":["d","e","f"]} -# Out: {"foo":["a","b","c","d","e","f"]} -``` - -### `contains` - -Checks whether an array contains an element matching the argument, or an object contains a value matching the argument, and returns a boolean result. Numerical comparisons are made irrespective of the representation type (float versus integer). - -#### Parameters - -**`value`** <unknown> A value to test against elements of the target. - -#### Examples - - -```coffee -root.has_foo = this.thing.contains("foo") - -# In: {"thing":["this","foo","that"]} -# Out: {"has_foo":true} - -# In: {"thing":["this","bar","that"]} -# Out: {"has_foo":false} -``` - -```coffee -root.has_bar = this.thing.contains(20) - -# In: {"thing":[10.3,20.0,"huh",3]} -# Out: {"has_bar":true} - -# In: {"thing":[2,3,40,67]} -# Out: {"has_bar":false} -``` - -### `diff` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its [docs](https://pkg.go.dev/github.com/r3labs/diff/v3) for more information. - -Introduced in version 4.25.0. - - -#### Parameters - -**`other`** <unknown> The value to compare against. - -### `enumerated` - -Converts an array into a new array of objects, where each object has a field index containing the `index` of the element and a field `value` containing the original value of the element. - -#### Examples - - -```coffee -root.foo = this.foo.enumerated() - -# In: {"foo":["bar","baz"]} -# Out: {"foo":[{"index":0,"value":"bar"},{"index":1,"value":"baz"}]} -``` - -### `explode` - -Explodes an array or object at a [field path][field_paths]. - -#### Parameters - -**`path`** <string> A [dot path][field_paths] to a field to explode. - -#### Examples - - -##### On arrays - -Exploding arrays results in an array containing elements matching the original document, where the target field of each element is an element of the exploded array: - -```coffee -root = this.explode("value") - -# In: {"id":1,"value":["foo","bar","baz"]} -# Out: [{"id":1,"value":"foo"},{"id":1,"value":"bar"},{"id":1,"value":"baz"}] -``` - -##### On objects - -Exploding objects results in an object where the keys match the target object, and the values match the original document but with the target field replaced by the exploded value: - -```coffee -root = this.explode("value") - -# In: {"id":1,"value":{"foo":2,"bar":[3,4],"baz":{"bev":5}}} -# Out: {"bar":{"id":1,"value":[3,4]},"baz":{"id":1,"value":{"bev":5}},"foo":{"id":1,"value":2}} -``` - -### `filter` - -Executes a mapping query argument for each element of an array or key/value pair of an object. If the query returns `false` the item is removed from the resulting array or object. The item will also be removed if the query returns any non-boolean value. - -#### Parameters - -**`test`** <query expression> A query to apply to each element, if this query resolves to any value other than a boolean `true` the element will be removed from the result. - -#### Examples - - -```coffee -root.new_nums = this.nums.filter(num -> num > 10) - -# In: {"nums":[3,11,4,17]} -# Out: {"new_nums":[11,17]} -``` - -##### On objects - -When filtering objects the mapping query argument is provided a context with a field `key` containing the value key, and a field `value` containing the value. - -```coffee -root.new_dict = this.dict.filter(item -> item.value.contains("foo")) - -# In: {"dict":{"first":"hello foo","second":"world","third":"this foo is great"}} -# Out: {"new_dict":{"first":"hello foo","third":"this foo is great"}} -``` - -### `find` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns the index of the first occurrence of a value in an array. `-1` is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer). - -#### Parameters - -**`value`** <unknown> A value to find. - -#### Examples - - -```coffee -root.index = this.find("bar") - -# In: ["foo", "bar", "baz"] -# Out: {"index":1} -``` - -```coffee -root.index = this.things.find(this.goal) - -# In: {"goal":"bar","things":["foo", "bar", "baz"]} -# Out: {"index":1} -``` - -### `find_all` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns an array containing the indexes of all occurrences of a value in an array. An empty array is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer). - -#### Parameters - -**`value`** <unknown> A value to find. - -#### Examples - - -```coffee -root.index = this.find_all("bar") - -# In: ["foo", "bar", "baz", "bar"] -# Out: {"index":[1,3]} -``` - -```coffee -root.indexes = this.things.find_all(this.goal) - -# In: {"goal":"bar","things":["foo", "bar", "baz", "bar", "buz"]} -# Out: {"indexes":[1,3]} -``` - -### `find_all_by` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns an array containing the indexes of all occurrences of an array where the provided query resolves to a boolean `true`. An empty array is returned if there are no matches. Numerical comparisons are made irrespective of the representation type (float versus integer). - -#### Parameters - -**`query`** <query expression> A query to execute for each element. - -#### Examples - - -```coffee -root.index = this.find_all_by(v -> v != "bar") - -# In: ["foo", "bar", "baz"] -# Out: {"index":[0,2]} -``` - -### `find_by` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Returns the index of the first occurrence of an array where the provided query resolves to a boolean `true`. `-1` is returned if there are no matches. - -#### Parameters - -**`query`** <query expression> A query to execute for each element. - -#### Examples - - -```coffee -root.index = this.find_by(v -> v != "bar") - -# In: ["foo", "bar", "baz"] -# Out: {"index":0} -``` - -### `flatten` - -Iterates an array and any element that is itself an array is removed and has its elements inserted directly in the resulting array. - -#### Examples - - -```coffee -root.result = this.flatten() - -# In: ["foo",["bar","baz"],"buz"] -# Out: {"result":["foo","bar","baz","buz"]} -``` - -### `fold` - -Takes two arguments: an initial value, and a mapping query. For each element of an array the mapping context is an object with two fields `tally` and `value`, where `tally` contains the current accumulated value and `value` is the value of the current element. The mapping must return the result of adding the value to the tally. - -The first argument is the value that `tally` will have on the first call. - -#### Parameters - -**`initial`** <unknown> The initial value to start the fold with. For example, an empty object `{}`, a zero count `0`, or an empty string `""`. -**`query`** <query expression> A query to apply for each element. The query is provided an object with two fields; `tally` containing the current tally, and `value` containing the value of the current element. The query should result in a new tally to be passed to the next element query. - -#### Examples - - -```coffee -root.sum = this.foo.fold(0, item -> item.tally + item.value) - -# In: {"foo":[3,8,11]} -# Out: {"sum":22} -``` - -```coffee -root.result = this.foo.fold("", item -> "%v%v".format(item.tally, item.value)) - -# In: {"foo":["hello ", "world"]} -# Out: {"result":"hello world"} -``` - -You can use fold to merge an array of objects together: - -```coffee -root.smoothie = this.fruits.fold({}, item -> item.tally.merge(item.value)) - -# In: {"fruits":[{"apple":5},{"banana":3},{"orange":8}]} -# Out: {"smoothie":{"apple":5,"banana":3,"orange":8}} -``` - -### `get` - -Extract a field value, identified via a [dot path][field_paths], from an object. - -#### Parameters - -**`path`** <string> A [dot path][field_paths] identifying a field to obtain. - -#### Examples - - -```coffee -root.result = this.foo.get(this.target) - -# In: {"foo":{"bar":"from bar","baz":"from baz"},"target":"bar"} -# Out: {"result":"from bar"} - -# In: {"foo":{"bar":"from bar","baz":"from baz"},"target":"baz"} -# Out: {"result":"from baz"} -``` - -### `index` - -Extract an element from an array by an index. The index can be negative, and if so the element will be selected from the end counting backwards starting from -1. E.g. an index of -1 returns the last element, an index of -2 returns the element before the last, and so on. - -#### Parameters - -**`index`** <integer> The index to obtain from an array. - -#### Examples - - -```coffee -root.last_name = this.names.index(-1) - -# In: {"names":["rachel","stevens"]} -# Out: {"last_name":"stevens"} -``` - -It is also possible to use this method on byte arrays, in which case the selected element will be returned as an integer. - -```coffee -root.last_byte = this.name.bytes().index(-1) - -# In: {"name":"foobar bazson"} -# Out: {"last_byte":110} -``` - -### `join` - -Join an array of strings with an optional delimiter into a single string. - -#### Parameters - -**`delimiter`** <(optional) string> An optional delimiter to add between each string. - -#### Examples - - -```coffee -root.joined_words = this.words.join() -root.joined_numbers = this.numbers.map_each(this.string()).join(",") - -# In: {"words":["hello","world"],"numbers":[3,8,11]} -# Out: {"joined_numbers":"3,8,11","joined_words":"helloworld"} -``` - -### `json_path` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Executes the given JSONPath expression on an object or array and returns the result. The JSONPath expression syntax can be found at https://goessner.net/articles/JsonPath/. For more complex logic, you can use Gval expressions (https://github.com/PaesslerAG/gval). - -#### Parameters - -**`expression`** <string> The JSONPath expression to execute. - -#### Examples - - -```coffee -root.all_names = this.json_path("$..name") - -# In: {"name":"alice","foo":{"name":"bob"}} -# Out: {"all_names":["alice","bob"]} - -# In: {"thing":["this","bar",{"name":"alice"}]} -# Out: {"all_names":["alice"]} -``` - -```coffee -root.text_objects = this.json_path("$.body[?(@.type=='text')]") - -# In: {"body":[{"type":"image","id":"foo"},{"type":"text","id":"bar"}]} -# Out: {"text_objects":[{"id":"bar","type":"text"}]} -``` - -### `json_schema` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Checks a [JSON schema](https://json-schema.org/) against a value and returns the value if it matches or throws and error if it does not. - -#### Parameters - -**`schema`** <string> The schema to check values against. - -#### Examples - - -```coffee -root = this.json_schema("""{ - "type":"object", - "properties":{ - "foo":{ - "type":"string" - } - } -}""") - -# In: {"foo":"bar"} -# Out: {"foo":"bar"} - -# In: {"foo":5} -# Out: Error("failed assignment (line 1): field `this`: foo invalid type. expected: string, given: integer") -``` - -In order to load a schema from a file use the `file` function. - -```coffee -root = this.json_schema(file(env("BENTHOS_TEST_BLOBLANG_SCHEMA_FILE"))) -``` - -### `key_values` - -Returns the key/value pairs of an object as an array, where each element is an object with a `key` field and a `value` field. The order of the resulting array will be random. - -#### Examples - - -```coffee -root.foo_key_values = this.foo.key_values().sort_by(pair -> pair.key) - -# In: {"foo":{"bar":1,"baz":2}} -# Out: {"foo_key_values":[{"key":"bar","value":1},{"key":"baz","value":2}]} -``` - -### `keys` - -Returns the keys of an object as an array. - -#### Examples - - -```coffee -root.foo_keys = this.foo.keys() - -# In: {"foo":{"bar":1,"baz":2}} -# Out: {"foo_keys":["bar","baz"]} -``` - -### `length` - -Returns the length of an array or object (number of keys). - -#### Examples - - -```coffee -root.foo_len = this.foo.length() - -# In: {"foo":["first","second"]} -# Out: {"foo_len":2} - -# In: {"foo":{"first":"bar","second":"baz"}} -# Out: {"foo_len":2} -``` - -### `map_each` - - - -#### Parameters - -**`query`** <query expression> A query that will be used to map each element. - -#### Examples - - -##### On arrays - -Apply a mapping to each element of an array and replace the element with the result. Within the argument mapping the context is the value of the element being mapped. - -```coffee -root.new_nums = this.nums.map_each(num -> if num < 10 { - deleted() -} else { - num - 10 -}) - -# In: {"nums":[3,11,4,17]} -# Out: {"new_nums":[1,7]} -``` - -##### On objects - -Apply a mapping to each value of an object and replace the value with the result. Within the argument mapping the context is an object with a field `key` containing the value key, and a field `value`. - -```coffee -root.new_dict = this.dict.map_each(item -> item.value.uppercase()) - -# In: {"dict":{"foo":"hello","bar":"world"}} -# Out: {"new_dict":{"bar":"WORLD","foo":"HELLO"}} -``` - -### `map_each_key` - -Apply a mapping to each key of an object, and replace the key with the result, which must be a string. - -#### Parameters - -**`query`** <query expression> A query that will be used to map each key. - -#### Examples - - -```coffee -root.new_dict = this.dict.map_each_key(key -> key.uppercase()) - -# In: {"dict":{"keya":"hello","keyb":"world"}} -# Out: {"new_dict":{"KEYA":"hello","KEYB":"world"}} -``` - -```coffee -root = this.map_each_key(key -> if key.contains("kafka") { "_" + key }) - -# In: {"amqp_key":"foo","kafka_key":"bar","kafka_topic":"baz"} -# Out: {"_kafka_key":"bar","_kafka_topic":"baz","amqp_key":"foo"} -``` - -### `merge` - -Merge a source object into an existing destination object. When a collision is found within the merged structures (both a source and destination object contain the same non-object keys) the result will be an array containing both values, where values that are already arrays will be expanded into the resulting array. In order to simply override destination fields on collision use the [`assign`](#assign) method. - -#### Parameters - -**`with`** <unknown> A value to merge the target value with. - -#### Examples - - -```coffee -root = this.foo.merge(this.bar) - -# In: {"foo":{"first_name":"fooer","likes":"bars"},"bar":{"second_name":"barer","likes":"foos"}} -# Out: {"first_name":"fooer","likes":["bars","foos"],"second_name":"barer"} -``` - -### `patch` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its [docs](https://pkg.go.dev/github.com/r3labs/diff/v3) for more information. - -Introduced in version 4.25.0. - - -#### Parameters - -**`changelog`** <unknown> The changelog to apply. - -### `slice` - -Extract a slice from an array by specifying two indices, a low and high bound, which selects a half-open range that includes the first element, but excludes the last one. If the second index is omitted then it defaults to the length of the input sequence. - -#### Parameters - -**`low`** <integer> The low bound, which is the first element of the selection, or if negative selects from the end. -**`high`** <(optional) integer> An optional high bound. - -#### Examples - - -```coffee -root.beginning = this.value.slice(0, 2) -root.end = this.value.slice(4) - -# In: {"value":["foo","bar","baz","buz","bev"]} -# Out: {"beginning":["foo","bar"],"end":["bev"]} -``` - -A negative low index can be used, indicating an offset from the end of the sequence. If the low index is greater than the length of the sequence then an empty result is returned. - -```coffee -root.last_chunk = this.value.slice(-2) -root.the_rest = this.value.slice(0, -2) - -# In: {"value":["foo","bar","baz","buz","bev"]} -# Out: {"last_chunk":["buz","bev"],"the_rest":["foo","bar","baz"]} -``` - -### `sort` - -Attempts to sort the values of an array in increasing order. The type of all values must match in order for the ordering to succeed. Supports string and number values. - -#### Parameters - -**`compare`** <(optional) query expression> An optional query that should explicitly compare elements `left` and `right` and provide a boolean result. - -#### Examples - - -```coffee -root.sorted = this.foo.sort() - -# In: {"foo":["bbb","ccc","aaa"]} -# Out: {"sorted":["aaa","bbb","ccc"]} -``` - -It's also possible to specify a mapping argument, which is provided an object context with fields `left` and `right`, the mapping must return a boolean indicating whether the `left` value is less than `right`. This allows you to sort arrays containing non-string or non-number values. - -```coffee -root.sorted = this.foo.sort(item -> item.left.v < item.right.v) - -# In: {"foo":[{"id":"foo","v":"bbb"},{"id":"bar","v":"ccc"},{"id":"baz","v":"aaa"}]} -# Out: {"sorted":[{"id":"baz","v":"aaa"},{"id":"foo","v":"bbb"},{"id":"bar","v":"ccc"}]} -``` - -### `sort_by` - -Attempts to sort the elements of an array, in increasing order, by a value emitted by an argument query applied to each element. The type of all values must match in order for the ordering to succeed. Supports string and number values. - -#### Parameters - -**`query`** <query expression> A query to apply to each element that yields a value used for sorting. - -#### Examples - - -```coffee -root.sorted = this.foo.sort_by(ele -> ele.id) - -# In: {"foo":[{"id":"bbb","message":"bar"},{"id":"aaa","message":"foo"},{"id":"ccc","message":"baz"}]} -# Out: {"sorted":[{"id":"aaa","message":"foo"},{"id":"bbb","message":"bar"},{"id":"ccc","message":"baz"}]} -``` - -### `squash` - -Squashes an array of objects into a single object, where key collisions result in the values being merged (following similar rules as the `.merge()` method) - -#### Examples - - -```coffee -root.locations = this.locations.map_each(loc -> {loc.state: [loc.name]}).squash() - -# In: {"locations":[{"name":"Seattle","state":"WA"},{"name":"New York","state":"NY"},{"name":"Bellevue","state":"WA"},{"name":"Olympia","state":"WA"}]} -# Out: {"locations":{"NY":["New York"],"WA":["Seattle","Bellevue","Olympia"]}} -``` - -### `sum` - -Sum the numerical values of an array. - -#### Examples - - -```coffee -root.sum = this.foo.sum() - -# In: {"foo":[3,8,4]} -# Out: {"sum":15} -``` - -### `unique` - -Attempts to remove duplicate values from an array. The array may contain a combination of different value types, but numbers and strings are checked separately (`"5"` is a different element to `5`). - -#### Parameters - -**`emit`** <(optional) query expression> An optional query that can be used in order to yield a value for each element to determine uniqueness. - -#### Examples - - -```coffee -root.uniques = this.foo.unique() - -# In: {"foo":["a","b","a","c"]} -# Out: {"uniques":["a","b","c"]} -``` - -### `values` - -Returns the values of an object as an array. The order of the resulting array will be random. - -#### Examples - - -```coffee -root.foo_vals = this.foo.values().sort() - -# In: {"foo":{"bar":1,"baz":2}} -# Out: {"foo_vals":[1,2]} -``` - -### `with` - -Returns an object where all but one or more [field path][field_paths] arguments are removed. Each path specifies a specific field to be retained from the input object, allowing for nested fields. - -If a key within a nested path does not exist then it is ignored. - -#### Examples - - -```coffee -root = this.with("inner.a","inner.c","d") - -# In: {"inner":{"a":"first","b":"second","c":"third"},"d":"fourth","e":"fifth"} -# Out: {"d":"fourth","inner":{"a":"first","c":"third"}} -``` - -### `without` - -Returns an object where one or more [field path][field_paths] arguments are removed. Each path specifies a specific field to be deleted from the input object, allowing for nested fields. - -If a key within a nested path does not exist or is not an object then it is not removed. - -#### Examples - - -```coffee -root = this.without("inner.a","inner.c","d") - -# In: {"inner":{"a":"first","b":"second","c":"third"},"d":"fourth","e":"fifth"} -# Out: {"e":"fifth","inner":{"b":"second"}} -``` - -### `zip` - -Zip an array value with one or more argument arrays. Each array must match in length. - -#### Examples - - -```coffee -root.foo = this.foo.zip(this.bar, this.baz) - -# In: {"foo":["a","b","c"],"bar":[1,2,3],"baz":[4,5,6]} -# Out: {"foo":[["a",1,4],["b",2,5],["c",3,6]]} -``` - -## Parsing - -### `bloblang` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Executes an argument Bloblang mapping on the target. This method can be used in order to execute dynamic mappings. Imports and functions that interact with the environment, such as `file` and `env`, or that access message information directly, such as `content` or `json`, are not enabled for dynamic Bloblang mappings. - -#### Parameters - -**`mapping`** <string> The mapping to execute. - -#### Examples - - -```coffee -root.body = this.body.bloblang(this.mapping) - -# In: {"body":{"foo":"hello world"},"mapping":"root.foo = this.foo.uppercase()"} -# Out: {"body":{"foo":"HELLO WORLD"}} - -# In: {"body":{"foo":"hello world 2"},"mapping":"root.foo = this.foo.capitalize()"} -# Out: {"body":{"foo":"Hello World 2"}} -``` - -### `format_json` - -:::caution BETA -This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. -::: -Serializes a target value into a pretty-printed JSON byte array (with 4 space indentation by default). - -#### Parameters - -**`indent`** <string, default `" "`> Indentation string. Each element in a JSON object or array will begin on a new, indented line followed by one or more copies of indent according to the indentation nesting. -**`no_indent`** <bool, default `false`> Disable indentation. - -#### Examples - - -```coffee -root = this.doc.format_json() - -# In: {"doc":{"foo":"bar"}} -# Out: { -# "foo": "bar" -# } -``` - -Pass a string to the `indent` parameter in order to customise the indentation. - -```coffee -root = this.format_json(" ") - -# In: {"doc":{"foo":"bar"}} -# Out: { -# "doc": { -# "foo": "bar" -# } -# } -``` - -Use the `.string()` method in order to coerce the result into a string. - -```coffee -root.doc = this.doc.format_json().string() - -# In: {"doc":{"foo":"bar"}} -# Out: {"doc":"{\n \"foo\": \"bar\"\n}"} -``` - -Set the `no_indent` parameter to true to disable indentation. The result is equivalent to calling `bytes()`. - -```coffee -root = this.doc.format_json(no_indent: true) - -# In: {"doc":{"foo":"bar"}} -# Out: {"foo":"bar"} -``` - -### `format_msgpack` - -Formats data as a [MessagePack](https://msgpack.org/) message in bytes format. - -#### Examples - - -```coffee -root = this.format_msgpack().encode("hex") - -# In: {"foo":"bar"} -# Out: 81a3666f6fa3626172 -``` - -```coffee -root.encoded = this.format_msgpack().encode("base64") - -# In: {"foo":"bar"} -# Out: {"encoded":"gaNmb2+jYmFy"} -``` - -### `format_xml` - - -Serializes a target value into an XML byte array. - - -#### Parameters - -**`indent`** <string, default `" "`> Indentation string. Each element in an XML object or array will begin on a new, indented line followed by one or more copies of indent according to the indentation nesting. -**`no_indent`** <bool, default `false`> Disable indentation. - -#### Examples - - -Serializes a target value into a pretty-printed XML byte array (with 4 space indentation by default). - -```coffee -root = this.format_xml() - -# In: {"foo":{"bar":{"baz":"foo bar baz"}}} -# Out: -# -# foo bar baz -# -# -``` - -Pass a string to the `indent` parameter in order to customise the indentation. - -```coffee -root = this.format_xml(" ") - -# In: {"foo":{"bar":{"baz":"foo bar baz"}}} -# Out: -# -# foo bar baz -# -# -``` - -Use the `.string()` method in order to coerce the result into a string. - -```coffee -root.doc = this.format_xml("").string() - -# In: {"foo":{"bar":{"baz":"foo bar baz"}}} -# Out: {"doc":"\n\nfoo bar baz\n\n"} -``` - -Set the `no_indent` parameter to true to disable indentation. - -```coffee -root = this.format_xml(no_indent: true) - -# In: {"foo":{"bar":{"baz":"foo bar baz"}}} -# Out: foo bar baz -``` - -### `format_yaml` - -Serializes a target value into a YAML byte array. - -#### Examples - - -```coffee -root = this.doc.format_yaml() - -# In: {"doc":{"foo":"bar"}} -# Out: foo: bar -``` - -Use the `.string()` method in order to coerce the result into a string. - -```coffee -root.doc = this.doc.format_yaml().string() - -# In: {"doc":{"foo":"bar"}} -# Out: {"doc":"foo: bar\n"} -``` - -### `parse_csv` - -Attempts to parse a string into an array of objects by following the CSV format described in RFC 4180. - -#### Parameters - -**`parse_header_row`** <bool, default `true`> Whether to reference the first row as a header row. If set to true the output structure for messages will be an object where field keys are determined by the header row. Otherwise, the output will be an array of row arrays. -**`delimiter`** <string, default `","`> The delimiter to use for splitting values in each record. It must be a single character. -**`lazy_quotes`** <bool, default `false`> If set to `true`, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field. - -#### Examples - - -Parses CSV data with a header row - -```coffee -root.orders = this.orders.parse_csv() - -# In: {"orders":"foo,bar\nfoo 1,bar 1\nfoo 2,bar 2"} -# Out: {"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]} -``` - -Parses CSV data without a header row - -```coffee -root.orders = this.orders.parse_csv(false) - -# In: {"orders":"foo 1,bar 1\nfoo 2,bar 2"} -# Out: {"orders":[["foo 1","bar 1"],["foo 2","bar 2"]]} -``` - -Parses CSV data delimited by dots - -```coffee -root.orders = this.orders.parse_csv(delimiter:".") - -# In: {"orders":"foo.bar\nfoo 1.bar 1\nfoo 2.bar 2"} -# Out: {"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar 2","foo":"foo 2"}]} -``` - -Parses CSV data containing a quote in an unquoted field - -```coffee -root.orders = this.orders.parse_csv(lazy_quotes:true) - -# In: {"orders":"foo,bar\nfoo 1,bar 1\nfoo\" \"2,bar\" \"2"} -# Out: {"orders":[{"bar":"bar 1","foo":"foo 1"},{"bar":"bar\" \"2","foo":"foo\" \"2"}]} -``` - -### `parse_form_url_encoded` - -Attempts to parse a url-encoded query string (from an x-www-form-urlencoded request body) and returns a structured result. - -#### Examples - - -```coffee -root.values = this.body.parse_form_url_encoded() - -# In: {"body":"noise=meow&animal=cat&fur=orange&fur=fluffy"} -# Out: {"values":{"animal":"cat","fur":["orange","fluffy"],"noise":"meow"}} -``` - -### `parse_json` - -Attempts to parse a string as a JSON document and returns the result. - -#### Parameters - -**`use_number`** <(optional) bool> An optional flag that when set makes parsing numbers as json.Number instead of the default float64. - -#### Examples - - -```coffee -root.doc = this.doc.parse_json() - -# In: {"doc":"{\"foo\":\"bar\"}"} -# Out: {"doc":{"foo":"bar"}} -``` - -```coffee -root.doc = this.doc.parse_json(use_number: true) - -# In: {"doc":"{\"foo\":\"11380878173205700000000000000000000000000000000\"}"} -# Out: {"doc":{"foo":"11380878173205700000000000000000000000000000000"}} -``` - -### `parse_msgpack` - -Parses a [MessagePack](https://msgpack.org/) message into a structured document. - -#### Examples - - -```coffee -root = content().decode("hex").parse_msgpack() - -# In: 81a3666f6fa3626172 -# Out: {"foo":"bar"} -``` - -```coffee -root = this.encoded.decode("base64").parse_msgpack() - -# In: {"encoded":"gaNmb2+jYmFy"} -# Out: {"foo":"bar"} -``` - -### `parse_parquet` - -Decodes a [Parquet file](https://parquet.apache.org/docs/) into an array of objects, one for each row within the file. - -#### Parameters - -**`byte_array_as_string`** <bool, default `false`> Deprecated: This parameter is no longer used. - -#### Examples - - -```coffee -root = content().parse_parquet() -``` - -### `parse_url` - -Attempts to parse a URL from a string value, returning a structured result that describes the various facets of the URL. The fields returned within the structured result roughly follow https://pkg.go.dev/net/url#URL, and may be expanded in future in order to present more information. - -#### Examples - - -```coffee -root.foo_url = this.foo_url.parse_url() - -# In: {"foo_url":"https://www.benthos.dev/docs/guides/bloblang/about"} -# Out: {"foo_url":{"fragment":"","host":"www.benthos.dev","opaque":"","path":"/docs/guides/bloblang/about","raw_fragment":"","raw_path":"","raw_query":"","scheme":"https"}} -``` - -```coffee -root.username = this.url.parse_url().user.name | "unknown" - -# In: {"url":"amqp://foo:bar@127.0.0.1:5672/"} -# Out: {"username":"foo"} - -# In: {"url":"redis://localhost:6379"} -# Out: {"username":"unknown"} -``` - -### `parse_xml` - - -Attempts to parse a string as an XML document and returns a structured result, where elements appear as keys of an object according to the following rules: - -- If an element contains attributes they are parsed by prefixing a hyphen, `-`, to the attribute label. -- If the element is a simple element and has attributes, the element value is given the key `#text`. -- XML comments, directives, and process instructions are ignored. -- When elements are repeated the resulting JSON value is an array. -- If cast is true, try to cast values to numbers and booleans instead of returning strings. - - -#### Parameters - -**`cast`** <(optional) bool, default `false`> whether to try to cast values that are numbers and booleans to the right type. - -#### Examples - - -```coffee -root.doc = this.doc.parse_xml() - -# In: {"doc":"This is a titleThis is some content"} -# Out: {"doc":{"root":{"content":"This is some content","title":"This is a title"}}} -``` - -```coffee -root.doc = this.doc.parse_xml(cast: false) - -# In: {"doc":"This is a title123True"} -# Out: {"doc":{"root":{"bool":"True","number":{"#text":"123","-id":"99"},"title":"This is a title"}}} -``` - -```coffee -root.doc = this.doc.parse_xml(cast: true) - -# In: {"doc":"This is a title123True"} -# Out: {"doc":{"root":{"bool":true,"number":{"#text":123,"-id":99},"title":"This is a title"}}} -``` - -### `parse_yaml` - -Attempts to parse a string as a single YAML document and returns the result. - -#### Examples - - -```coffee -root.doc = this.doc.parse_yaml() - -# In: {"doc":"foo: bar"} -# Out: {"doc":{"foo":"bar"}} -``` - -## Encoding and Encryption - -### `compress` - -Compresses a string or byte array value according to a specified algorithm. - -#### Parameters - -**`algorithm`** <string> One of `flate`, `gzip`, `pgzip`, `lz4`, `snappy`, `zlib`, `zstd`. -**`level`** <integer, default `-1`> The level of compression to use. May not be applicable to all algorithms. - -#### Examples - - -```coffee -let long_content = range(0, 1000).map_each(content()).join(" ") -root.a_len = $long_content.length() -root.b_len = $long_content.compress("gzip").length() - - -# In: hello world this is some content -# Out: {"a_len":32999,"b_len":161} -``` - -```coffee -root.compressed = content().compress("lz4").encode("base64") - -# In: hello world I love space -# Out: {"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="} -``` - -### `decode` - -Decodes an encoded string target according to a chosen scheme and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method [`string`][methods.string], or encoded using the method [`encode`][methods.encode], otherwise it will be base64 encoded by default. - -Available schemes are: `base64`, `base64url` [(RFC 4648 with padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `base64rawurl` [(RFC 4648 without padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `hex`, `ascii85`. - -#### Parameters - -**`scheme`** <string> The decoding scheme to use. - -#### Examples - - -```coffee -root.decoded = this.value.decode("hex").string() - -# In: {"value":"68656c6c6f20776f726c64"} -# Out: {"decoded":"hello world"} -``` - -```coffee -root = this.encoded.decode("ascii85") - -# In: {"encoded":"FD,B0+DGm>FDl80Ci\"A>F`)8BEckl6F`M&(+Cno&@/"} -# Out: this is totally unstructured data -``` - -### `decompress` - -Decompresses a string or byte array value according to a specified algorithm. The result of decompression - -#### Parameters - -**`algorithm`** <string> One of `gzip`, `pgzip`, `zlib`, `bzip2`, `flate`, `snappy`, `lz4`, `zstd`. - -#### Examples - - -```coffee -root = this.compressed.decode("base64").decompress("lz4") - -# In: {"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="} -# Out: hello world I love space -``` - -Use the `.string()` method in order to coerce the result into a string, this makes it possible to place the data within a JSON document without automatic base64 encoding. - -```coffee -root.result = this.compressed.decode("base64").decompress("lz4").string() - -# In: {"compressed":"BCJNGGRwuRgAAIBoZWxsbyB3b3JsZCBJIGxvdmUgc3BhY2UAAAAAGoETLg=="} -# Out: {"result":"hello world I love space"} -``` - -### `decrypt_aes` - -Decrypts an encrypted string or byte array target according to a chosen AES encryption method and returns the result as a byte array. The algorithms require a key and an initialization vector / nonce. Available schemes are: `ctr`, `ofb`, `cbc`. - -#### Parameters - -**`scheme`** <string> The scheme to use for decryption, one of `ctr`, `ofb`, `cbc`. -**`key`** <string> A key to decrypt with. -**`iv`** <string> An initialization vector / nonce. - -#### Examples - - -```coffee -let key = "2b7e151628aed2a6abf7158809cf4f3c".decode("hex") -let vector = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff".decode("hex") -root.decrypted = this.value.decode("hex").decrypt_aes("ctr", $key, $vector).string() - -# In: {"value":"84e9b31ff7400bdf80be7254"} -# Out: {"decrypted":"hello world!"} -``` - -### `encode` - -Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` [(RFC 4648 with padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `base64rawurl` [(RFC 4648 without padding characters)](https://rfc-editor.org/rfc/rfc4648.html), `hex`, `ascii85`. - -#### Parameters - -**`scheme`** <string> The encoding scheme to use. - -#### Examples - - -```coffee -root.encoded = this.value.encode("hex") - -# In: {"value":"hello world"} -# Out: {"encoded":"68656c6c6f20776f726c64"} -``` - -```coffee -root.encoded = content().encode("ascii85") - -# In: this is totally unstructured data -# Out: {"encoded":"FD,B0+DGm>FDl80Ci\"A>F`)8BEckl6F`M&(+Cno&@/"} -``` - -### `encrypt_aes` - -Encrypts a string or byte array target according to a chosen AES encryption method and returns a string result. The algorithms require a key and an initialization vector / nonce. Available schemes are: `ctr`, `ofb`, `cbc`. - -#### Parameters - -**`scheme`** <string> The scheme to use for encryption, one of `ctr`, `ofb`, `cbc`. -**`key`** <string> A key to encrypt with. -**`iv`** <string> An initialization vector / nonce. - -#### Examples - - -```coffee -let key = "2b7e151628aed2a6abf7158809cf4f3c".decode("hex") -let vector = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff".decode("hex") -root.encrypted = this.value.encrypt_aes("ctr", $key, $vector).encode("hex") - -# In: {"value":"hello world!"} -# Out: {"encrypted":"84e9b31ff7400bdf80be7254"} -``` - -### `hash` - -Hashes a string or byte array according to a chosen algorithm and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method [`string`][methods.string], or encoded using the method [`encode`][methods.encode], otherwise it will be base64 encoded by default. - -Available algorithms are: `hmac_sha1`, `hmac_sha256`, `hmac_sha512`, `md5`, `sha1`, `sha256`, `sha512`, `xxhash64`, `crc32`. - -The following algorithms require a key, which is specified as a second argument: `hmac_sha1`, `hmac_sha256`, `hmac_sha512`. - -#### Parameters - -**`algorithm`** <string> The hasing algorithm to use. -**`key`** <(optional) string> An optional key to use. -**`polynomial`** <string, default `"IEEE"`> An optional polynomial key to use when selecting the `crc32` algorithm, otherwise ignored. Options are `IEEE` (default), `Castagnoli` and `Koopman` - -#### Examples - - -```coffee -root.h1 = this.value.hash("sha1").encode("hex") -root.h2 = this.value.hash("hmac_sha1","static-key").encode("hex") - -# In: {"value":"hello world"} -# Out: {"h1":"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed","h2":"d87e5f068fa08fe90bb95bc7c8344cb809179d76"} -``` - -The `crc32` algorithm supports options for the polynomial. - -```coffee -root.h1 = this.value.hash(algorithm: "crc32", polynomial: "Castagnoli").encode("hex") -root.h2 = this.value.hash(algorithm: "crc32", polynomial: "Koopman").encode("hex") - -# In: {"value":"hello world"} -# Out: {"h1":"c99465aa","h2":"df373d3c"} -``` - -## JSON Web Tokens - -### `parse_jwt_es256` - -Parses a claims object from a JWT string encoded with ES256. This method does not validate JWT claims. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The ES256 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_es256("""-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGtLqIBePHmIhQcf0JLgc+F/4W/oI -dp0Gta53G35VerNDgUUXmp78J2kfh4qLdh0XtmOMI587tCaqjvDAXfs//w== ------END PUBLIC KEY-----""") - -# In: {"signed":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.GIRajP9JJbpTlqSCdNEz4qpQkRvzX4Q51YnTwVyxLDM9tKjR_a8ggHWn9CWj7KG0x8J56OWtmUxn112SRTZVhQ"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_es384` - -Parses a claims object from a JWT string encoded with ES384. This method does not validate JWT claims. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The ES384 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_es384("""-----BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERoz74/B6SwmLhs8X7CWhnrWyRrB13AuU -8OYeqy0qHRu9JWNw8NIavqpTmu6XPT4xcFanYjq8FbeuM11eq06C52mNmS4LLwzA -2imlFEgn85bvJoC3bnkuq4mQjwt9VxdH ------END PUBLIC KEY-----""") - -# In: {"signed":"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.H2HBSlrvQBaov2tdreGonbBexxtQB-xzaPL4-tNQZ6TVh7VH8VBcSwcWHYa1lBAHmdsKOFcB2Wk0SB7QWeGT3ptSgr-_EhDMaZ8bA5spgdpq5DsKfaKHrd7DbbQlmxNq"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_es512` - -Parses a claims object from a JWT string encoded with ES512. This method does not validate JWT claims. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The ES512 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_es512("""-----BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAkHLdts9P56fFkyhpYQ31M/Stwt3w -vpaxhlfudxnXgTO1IP4RQRgryRxZ19EUzhvWDcG3GQIckoNMY5PelsnCGnIBT2Xh -9NQkjWF5K6xS4upFsbGSAwQ+GIyyk5IPJ2LHgOyMSCVh5gRZXV3CZLzXujx/umC9 -UeYyTt05zRRWuD+p5bY= ------END PUBLIC KEY-----""") - -# In: {"signed":"eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.ACrpLuU7TKpAnncDCpN9m85nkL55MJ45NFOBl6-nEXmNT1eIxWjiP4pwWVbFH9et_BgN14119jbL_KqEJInPYc9nAXC6dDLq0aBU-dalvNl4-O5YWpP43-Y-TBGAsWnbMTrchILJ4-AEiICe73Ck5yWPleKg9c3LtkEFWfGs7BoPRguZ"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_hs256` - -Parses a claims object from a JWT string encoded with HS256. This method does not validate JWT claims. - -Introduced in version v4.12.0. - - -#### Parameters - -**`signing_secret`** <string> The HS256 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_hs256("""dont-tell-anyone""") - -# In: {"signed":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.YwXOM8v3gHVWcQRRRQc_zDlhmLnM62fwhFYGpiA0J1A"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_hs384` - -Parses a claims object from a JWT string encoded with HS384. This method does not validate JWT claims. - -Introduced in version v4.12.0. - - -#### Parameters - -**`signing_secret`** <string> The HS384 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_hs384("""dont-tell-anyone""") - -# In: {"signed":"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.2Y8rf_ijwN4t8hOGGViON_GrirLkCQVbCOuax6EoZ3nluX0tCGezcJxbctlIfsQ2"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_hs512` - -Parses a claims object from a JWT string encoded with HS512. This method does not validate JWT claims. - -Introduced in version v4.12.0. - - -#### Parameters - -**`signing_secret`** <string> The HS512 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_hs512("""dont-tell-anyone""") - -# In: {"signed":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.utRb0urG6LGGyranZJVo5Dk0Fns1QNcSUYPN0TObQ-YzsGGB8jrxHwM5NAJccjJZzKectEUqmmKCaETZvuX4Fg"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_rs256` - -Parses a claims object from a JWT string encoded with RS256. This method does not validate JWT claims. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The RS256 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_rs256("""-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ibN8r68pLMR6gRzg4S -8v8l6Q7yi8qURjkEbcNeM1rkokC7xh0I4JVTwxYSVv/JIW8qJdyspl5NIfuAVi32 -WfKvSAs+NIs+DMsNPYw3yuQals4AX8hith1YDvYpr8SD44jxhz/DR9lYKZFGhXGB -+7NqQ7vpTWp3BceLYocazWJgusZt7CgecIq57ycM5hjM93BvlrUJ8nQ1a46wfL/8 -Cy4P0et70hzZrsjjN41KFhKY0iUwlyU41yEiDHvHDDsTMBxAZosWjSREGfJL6Mfp -XOInTHs/Gg6DZMkbxjQu6L06EdJ+Q/NwglJdAXM7Zo9rNELqRig6DdvG5JesdMsO -+QIDAQAB ------END PUBLIC KEY-----""") - -# In: {"signed":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.b0lH3jEupZZ4zoaly4Y_GCvu94HH6UKdKY96zfGNsIkPZpQLHIkZ7jMWlLlNOAd8qXlsBGP_i8H2qCKI4zlWJBGyPZgxXDzNRPVrTDfFpn4t4nBcA1WK2-ntXP3ehQxsaHcQU8Z_nsogId7Pme5iJRnoHWEnWtbwz5DLSXL3ZZNnRdrHM9MdI7QSDz9mojKDCaMpGN9sG7Xl-tGdBp1XzXuUOzG8S03mtZ1IgVR1uiBL2N6oohHIAunk8DIAmNWI-zgycTgzUGU7mvPkKH43qO8Ua1-13tCUBKKa8VxcotZ67Mxm1QAvBGoDnTKwWMwghLzs6d6WViXQg6eWlJcpBA"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_rs384` - -Parses a claims object from a JWT string encoded with RS384. This method does not validate JWT claims. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The RS384 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_rs384("""-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ibN8r68pLMR6gRzg4S -8v8l6Q7yi8qURjkEbcNeM1rkokC7xh0I4JVTwxYSVv/JIW8qJdyspl5NIfuAVi32 -WfKvSAs+NIs+DMsNPYw3yuQals4AX8hith1YDvYpr8SD44jxhz/DR9lYKZFGhXGB -+7NqQ7vpTWp3BceLYocazWJgusZt7CgecIq57ycM5hjM93BvlrUJ8nQ1a46wfL/8 -Cy4P0et70hzZrsjjN41KFhKY0iUwlyU41yEiDHvHDDsTMBxAZosWjSREGfJL6Mfp -XOInTHs/Gg6DZMkbxjQu6L06EdJ+Q/NwglJdAXM7Zo9rNELqRig6DdvG5JesdMsO -+QIDAQAB ------END PUBLIC KEY-----""") - -# In: {"signed":"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.orcXYBcjVE5DU7mvq4KKWFfNdXR4nEY_xupzWoETRpYmQZIozlZnM_nHxEk2dySvpXlAzVm7kgOPK2RFtGlOVaNRIa3x-pMMr-bhZTno4L8Hl4sYxOks3bWtjK7wql4uqUbqThSJB12psAXw2-S-I_FMngOPGIn4jDT9b802ottJSvTpXcy0-eKTjrV2PSkRRu-EYJh0CJZW55MNhqlt6kCGhAXfbhNazN3ASX-dmpd_JixyBKphrngr_zRA-FCn_Xf3QQDA-5INopb4Yp5QiJ7UxVqQEKI80X_JvJqz9WE1qiAw8pq5-xTen1t7zTP-HT1NbbD3kltcNa3G8acmNg"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `parse_jwt_rs512` - -Parses a claims object from a JWT string encoded with RS512. This method does not validate JWT claims. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The RS512 secret that was used for signing the token. - -#### Examples - - -```coffee -root.claims = this.signed.parse_jwt_rs512("""-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ibN8r68pLMR6gRzg4S -8v8l6Q7yi8qURjkEbcNeM1rkokC7xh0I4JVTwxYSVv/JIW8qJdyspl5NIfuAVi32 -WfKvSAs+NIs+DMsNPYw3yuQals4AX8hith1YDvYpr8SD44jxhz/DR9lYKZFGhXGB -+7NqQ7vpTWp3BceLYocazWJgusZt7CgecIq57ycM5hjM93BvlrUJ8nQ1a46wfL/8 -Cy4P0et70hzZrsjjN41KFhKY0iUwlyU41yEiDHvHDDsTMBxAZosWjSREGfJL6Mfp -XOInTHs/Gg6DZMkbxjQu6L06EdJ+Q/NwglJdAXM7Zo9rNELqRig6DdvG5JesdMsO -+QIDAQAB ------END PUBLIC KEY-----""") - -# In: {"signed":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.rsMp_X5HMrUqKnZJIxo27aAoscovRA6SSQYR9rq7pifIj0YHXxMyNyOBDGnvVALHKTi25VUGHpfNUW0VVMmae0A4t_ObNU6hVZHguWvetKZZq4FZpW1lgWHCMqgPGwT5_uOqwYCH6r8tJuZT3pqXeL0CY4putb1AN2w6CVp620nh3l8d3XWb4jaifycd_4CEVCqHuWDmohfug4VhmoVKlIXZkYoAQowgHlozATDssBSWdYtv107Wd2AzEoiXPu6e3pflsuXULlyqQnS4ELEKPYThFLafh1NqvZDPddqozcPZ-iODBW-xf3A4DYDdivnMYLrh73AZOGHexxu8ay6nDA"} -# Out: {"claims":{"iat":1516239022,"mood":"Disdainful","sub":"1234567890"}} -``` - -### `sign_jwt_es256` - -Hash and sign an object representing JSON Web Token (JWT) claims using ES256. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_es256("""-----BEGIN EC PRIVATE KEY----- -... signature data ... ------END EC PRIVATE KEY-----""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.-8LrOdkEiv_44ADWW08lpbq41ZmHCel58NMORPq1q4Dyw0zFhqDVLrRoSvCvuyyvgXAFb9IHfR-9MlJ_2ShA9A"} -``` - -### `sign_jwt_es384` - -Hash and sign an object representing JSON Web Token (JWT) claims using ES384. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_es384("""-----BEGIN EC PRIVATE KEY----- -... signature data ... ------END EC PRIVATE KEY-----""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.8FmTKH08dl7dyxrNu0rmvhegiIBCy-O9cddGco2e9lpZtgv5mS5qHgPkgBC5eRw1d7SRJsHwHZeehzdqT5Ba7aZJIhz9ds0sn37YQ60L7jT0j2gxCzccrt4kECHnUnLw"} -``` - -### `sign_jwt_es512` - -Hash and sign an object representing JSON Web Token (JWT) claims using ES512. - -Introduced in version v4.20.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_es512("""-----BEGIN EC PRIVATE KEY----- -... signature data ... ------END EC PRIVATE KEY-----""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.AQbEWymoRZxDJEJtKSFFG2k2VbDCTYSuBwAZyMqexCspr3If8aERTVGif8HXG3S7TzMBCCzxkcKr3eIU441l3DlpAMNfQbkcOlBqMvNBn-CX481WyKf3K5rFHQ-6wRonz05aIsWAxCDvAozI_9J0OWllxdQ2MBAuTPbPJ38OqXsYkCQs"} -``` - -### `sign_jwt_hs256` - -Hash and sign an object representing JSON Web Token (JWT) claims using HS256. - -Introduced in version v4.12.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_hs256("""dont-tell-anyone""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.hUl-nngPMY_3h9vveWJUPsCcO5PeL6k9hWLnMYeFbFQ"} -``` - -### `sign_jwt_hs384` - -Hash and sign an object representing JSON Web Token (JWT) claims using HS384. - -Introduced in version v4.12.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_hs384("""dont-tell-anyone""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.zGYLr83aToon1efUNq-hw7XgT20lPvZb8sYei8x6S6mpHwb433SJdXJXx0Oio8AZ"} -``` - -### `sign_jwt_hs512` - -Hash and sign an object representing JSON Web Token (JWT) claims using HS512. - -Introduced in version v4.12.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_hs512("""dont-tell-anyone""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.zBNR9o_6EDwXXKkpKLNJhG26j8Dc-mV-YahBwmEdCrmiWt5les8I9rgmNlWIowpq6Yxs4kLNAdFhqoRz3NXT3w"} -``` - -### `sign_jwt_rs256` - -Hash and sign an object representing JSON Web Token (JWT) claims using RS256. - -Introduced in version v4.18.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_rs256("""-----BEGIN RSA PRIVATE KEY----- -... signature data ... ------END RSA PRIVATE KEY-----""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.b0lH3jEupZZ4zoaly4Y_GCvu94HH6UKdKY96zfGNsIkPZpQLHIkZ7jMWlLlNOAd8qXlsBGP_i8H2qCKI4zlWJBGyPZgxXDzNRPVrTDfFpn4t4nBcA1WK2-ntXP3ehQxsaHcQU8Z_nsogId7Pme5iJRnoHWEnWtbwz5DLSXL3ZZNnRdrHM9MdI7QSDz9mojKDCaMpGN9sG7Xl-tGdBp1XzXuUOzG8S03mtZ1IgVR1uiBL2N6oohHIAunk8DIAmNWI-zgycTgzUGU7mvPkKH43qO8Ua1-13tCUBKKa8VxcotZ67Mxm1QAvBGoDnTKwWMwghLzs6d6WViXQg6eWlJcpBA"} -``` - -### `sign_jwt_rs384` - -Hash and sign an object representing JSON Web Token (JWT) claims using RS384. - -Introduced in version v4.18.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_rs384("""-----BEGIN RSA PRIVATE KEY----- -... signature data ... ------END RSA PRIVATE KEY-----""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.orcXYBcjVE5DU7mvq4KKWFfNdXR4nEY_xupzWoETRpYmQZIozlZnM_nHxEk2dySvpXlAzVm7kgOPK2RFtGlOVaNRIa3x-pMMr-bhZTno4L8Hl4sYxOks3bWtjK7wql4uqUbqThSJB12psAXw2-S-I_FMngOPGIn4jDT9b802ottJSvTpXcy0-eKTjrV2PSkRRu-EYJh0CJZW55MNhqlt6kCGhAXfbhNazN3ASX-dmpd_JixyBKphrngr_zRA-FCn_Xf3QQDA-5INopb4Yp5QiJ7UxVqQEKI80X_JvJqz9WE1qiAw8pq5-xTen1t7zTP-HT1NbbD3kltcNa3G8acmNg"} -``` - -### `sign_jwt_rs512` - -Hash and sign an object representing JSON Web Token (JWT) claims using RS512. - -Introduced in version v4.18.0. - - -#### Parameters - -**`signing_secret`** <string> The secret to use for signing the token. - -#### Examples - - -```coffee -root.signed = this.claims.sign_jwt_rs512("""-----BEGIN RSA PRIVATE KEY----- -... signature data ... ------END RSA PRIVATE KEY-----""") - -# In: {"claims":{"sub":"user123"}} -# Out: {"signed":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm1vb2QiOiJEaXNkYWluZnVsIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.rsMp_X5HMrUqKnZJIxo27aAoscovRA6SSQYR9rq7pifIj0YHXxMyNyOBDGnvVALHKTi25VUGHpfNUW0VVMmae0A4t_ObNU6hVZHguWvetKZZq4FZpW1lgWHCMqgPGwT5_uOqwYCH6r8tJuZT3pqXeL0CY4putb1AN2w6CVp620nh3l8d3XWb4jaifycd_4CEVCqHuWDmohfug4VhmoVKlIXZkYoAQowgHlozATDssBSWdYtv107Wd2AzEoiXPu6e3pflsuXULlyqQnS4ELEKPYThFLafh1NqvZDPddqozcPZ-iODBW-xf3A4DYDdivnMYLrh73AZOGHexxu8ay6nDA"} -``` - -## GeoIP - -### `geoip_anonymous_ip` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the anonymous IP associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_asn` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the ASN associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_city` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the city associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_connection_type` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the connection type associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_country` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the country associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_domain` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the domain associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_enterprise` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the enterprise associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -### `geoip_isp` - -:::caution EXPERIMENTAL -This method is experimental and therefore breaking changes could be made to it outside of major version releases. -::: -Looks up an IP address against a [MaxMind database file](https://www.maxmind.com/en/home) and, if found, returns an object describing the ISP associated with it. - -#### Parameters - -**`path`** <string> A path to an mmdb (maxmind) file. - -## Deprecated - -### `format_timestamp` - -Attempts to format a timestamp value as a string according to a specified format, or RFC 3339 by default. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. - -The output format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the [`ts_strftime`](#ts_strftime) method. - -#### Parameters - -**`format`** <string, default `"2006-01-02T15:04:05.999999999Z07:00"`> The output format to use. -**`tz`** <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used, or in the case of unix timestamps the local timezone is used. - -### `format_timestamp_strftime` - -Attempts to format a timestamp value as a string according to a specified strftime-compatible format. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. - -#### Parameters - -**`format`** <string> The output format to use. -**`tz`** <(optional) string> An optional timezone to use, otherwise the timezone of the input string is used. - -### `format_timestamp_unix` - -Attempts to format a timestamp value as a unix timestamp. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -### `format_timestamp_unix_micro` - -Attempts to format a timestamp value as a unix timestamp with microsecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -### `format_timestamp_unix_milli` - -Attempts to format a timestamp value as a unix timestamp with millisecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -### `format_timestamp_unix_nano` - -Attempts to format a timestamp value as a unix timestamp with nanosecond precision. Timestamp values can either be a numerical unix time in seconds (with up to nanosecond precision via decimals), or a string in RFC 3339 format. The [`ts_parse`](#ts_parse) method can be used in order to parse different timestamp formats. - -### `parse_timestamp` - -Attempts to parse a string as a timestamp following a specified format and outputs a timestamp, which can then be fed into methods such as [`ts_format`](#ts_format). - -The input format is defined by showing how the reference time, defined to be Mon Jan 2 15:04:05 -0700 MST 2006, would be displayed if it were the value. For an alternative way to specify formats check out the [`ts_strptime`](#ts_strptime) method. - -#### Parameters - -**`format`** <string> The format of the target string. - -### `parse_timestamp_strptime` - -Attempts to parse a string as a timestamp following a specified strptime-compatible format and outputs a timestamp, which can then be fed into [`ts_format`](#ts_format). - -#### Parameters - -**`format`** <string> The format of the target string. - -[field_paths]: /docs/configuration/field_paths -[methods.encode]: #encode -[methods.string]: #string diff --git a/website/docs/guides/bloblang/walkthrough.md b/website/docs/guides/bloblang/walkthrough.md deleted file mode 100644 index 179a7cade2..0000000000 --- a/website/docs/guides/bloblang/walkthrough.md +++ /dev/null @@ -1,766 +0,0 @@ ---- -title: Bloblang Walkthrough -sidebar_label: Walkthrough -description: A step by step introduction to Bloblang ---- - -Bloblang is the most advanced mapping language that you'll learn from this walkthrough (probably). It is designed for readability, the power to shape even the most outrageous input documents, and to easily make erratic schemas bend to your will. Bloblang is the native mapping language of Benthos, but it has been designed as a general purpose technology ready to be adopted by other tools. - -In this walkthrough you'll learn how to make new friends by mapping their documents, and lose old friends as they grow jealous and bitter of your mapping abilities. There are a few ways to execute Bloblang but the way we'll do it in this guide is to pull a Benthos docker image and run the command `benthos blobl server`, which opens up an interactive Bloblang editor: - -```sh -docker pull ghcr.io/benthosdev/benthos:latest -docker run -p 4195:4195 --rm ghcr.io/benthosdev/benthos blobl server --no-open --host 0.0.0.0 -``` - -:::note Alternatives -For alternative Benthos installation options check out the [getting started guide][guides.getting_started]. -::: - -Next, open your browser at `http://localhost:4195` and you should see an app with three panels, the top-left is where you paste an input document, the bottom is your Bloblang mapping and on the top-right is the output. - -## Your first assignment - -The primary goal of a Bloblang mapping is to construct a brand new document by using an input document as a reference, which we achieve through a series of assignments. Bloblang is traditionally used to map JSON documents and that's mostly what we'll be doing in this walkthrough. The first mapping you'll see when you open the editor is a single assignment: - -```coffee -root = this -``` - -On the left-hand side of the assignment is our assignment target, where `root` is a keyword referring to the root of the new document being constructed. On the right-hand side is a query which determines the value to be assigned, where `this` is a keyword that refers to the context of the mapping which begins as the root of the input document. - -As you can see the input document in the editor begins as a JSON object `{"message":"hello world"}`, and the output panel should show the result as: - -```json -{ - "message": "hello world" -} -``` - -Which is a (neatly formatted) replica of the input document. This is the result of our mapping because we assigned the entire input document to the root of our new thing. However, you won't get far in life by trapping yourself in the past, let's create a brand new document by assigning a fresh object to the root: - -```coffee -root = {} -root.foo = this.message -``` - -Bloblang supports a bunch of [literal types][blobl.literals], and the first line of this mapping assigns an empty object literal to the root. The second line then creates a new field `foo` on that object by assigning it the value of `message` from the input document. You should see that our output has changed to: - -```json -{ - "foo": "hello world" -} -``` - -In Bloblang, when the path that we assign to contains fields that are themselves unset then they are created as empty objects. This rule also applies to `root` itself, which means the mapping: - -```coffee -root.foo.bar = this.message -root.foo."buz me".baz = "I like mapping" -``` - -Will automatically create the objects required to produce the output document: - -```json -{ - "foo": { - "bar": "hello world", - "buz me": { - "baz": "I like mapping" - } - } -} -``` - -Also note that we can use quotes in order to express path segments that contain symbols or whitespace. Great, let's move on quick before our self-satisfaction gets in the way of progress. - -## Basic Methods and Functions - -Nothing is ever good enough for you, why should the input document be any different? Usually in our mappings it's necessary to mutate values whilst we map them over, this is almost always done with methods, of which [there are many][blobl.methods]. To demonstrate we're going to change our mapping to [uppercase][blobl.methods.uppercase] the field `message` from our input document: - -```coffee -root.foo.bar = this.message.uppercase() -root.foo."buz me".baz = "I like mapping" -``` - -As you can see the syntax for a method is similar to many languages, simply add a dot on the target value followed by the method name and arguments within brackets. With this method added our output document should look like this: - -```json -{ - "foo": { - "bar": "HELLO WORLD", - "buz me": { - "baz": "I like mapping" - } - } -} -``` - -Since the result of any Bloblang query is a value you can use methods on anything, including other methods. For example, we could expand our mapping of `message` to also replace `WORLD` with `EARTH` using the [`replace_all` method][blobl.methods.replace_all]: - -```coffee -root.foo.bar = this.message.uppercase().replace_all("WORLD", "EARTH") -root.foo."buz me".baz = "I like mapping" -``` - -As you can see this method required some arguments. Methods support both nameless (like above) and named arguments, which are often literal values but can also be queries themselves. For example try out the following mapping using both named style and a dynamic argument: - -```coffee -root.foo.bar = this.message.uppercase().replace_all(old: "WORLD", new: this.message.capitalize()) -root.foo."buz me".baz = "I like mapping" -``` - -Woah, I think that's the plot to Inception, let's move onto functions. Functions are just boring methods that don't have a target, and there are [plenty of them as well][blobl.functions]. Functions are often used to extract information unrelated to the input document, such as [environment variables][blobl.functions.env], or to generate data such as [timestamps][blobl.functions.now] or [UUIDs][blobl.functions.uuid_v4]. - -Since we're completionists let's add one to our mapping: - -```coffee -root.foo.bar = this.message.uppercase().replace_all("WORLD", "EARTH") -root.foo."buz me".baz = "I like mapping" -root.foo.id = uuid_v4() -``` - -Now I can't tell you what the output looks like since it will be different each time it's mapped, how fun! - -### Deletions - -Everything in Bloblang is an expression to be assigned, including deletions, which is a [function `deleted()`][blobl.functions.deleted]. To illustrate let's create a field we want to delete by changing our input to the following: - -```json -{ - "name": "fooman barson", - "age": 7, - "opinions": ["trucks are cool","trains are cool","chores are bad"] -} -``` - -If we wanted a full copy of this document without the field `name` then we can assign `deleted()` to it: - -```coffee -root = this -root.name = deleted() -``` - -And it won't be included in the output: - -```json -{ - "age": 7, - "opinions": [ - "trucks are cool", - "trains are cool", - "chores are bad" - ] -} -``` - -An alternative way to delete fields is the [method `without`][blobl.methods.without], our above example could be rewritten as a single assignment `root = this.without("name")`. However, `deleted()` is generally more powerful and will come into play more later on. - -## Variables - -Sometimes it's necessary to capture a value for later, but we might not want it to be added to the resulting document. In Bloblang we can achieve this with variables which are created using the `let` keyword, and can be referenced within subsequent queries with a dollar sign prefix: - -```coffee -let id = uuid_v4() -root.id_sha1 = $id.hash("sha1").encode("hex") -root.id_md5 = $id.hash("md5").encode("hex") -``` - -Variables can be assigned any value type, including objects and arrays. - -## Unstructured and Binary Data - -So far in all of our examples both the input document and our newly mapped document are structured, but this does not need to be so. Try assigning some literal value types directly to the `root`, such as a string `root = "hello world"`, or a number `root = 5`. - -You should notice that when a value type is assigned to the root the output is the raw value, and therefore strings are not quoted. This is what makes it possible to output data of any format, including encrypted, encoded or otherwise binary data. - -Unstructured mapping is not limited to the output. Rather than referencing the input document with `this`, where it must be structured, it is possible to reference it as a binary string with the [function `content`][blobl.functions.content], try changing your mapping to: - -```coffee -root = content().uppercase() -``` - -And then put any old gibberish in the input panel, the output panel should be the same gibberish but all uppercase. - -## Conditionals - -In order to play around with conditionals let's set our input to something structured: - -```json -{ - "pet": { - "type": "cat", - "is_cute": true, - "treats": 5, - "toys": 3 - } -} -``` - -In Bloblang all conditionals are expressions, this is a core principal of Bloblang and will be important later on when we're mapping deeply nested structures. - -### If Expression - -The simplest conditional is the `if` expression, where the boolean condition does not need to be in parentheses. Let's create a map that modifies the number of treats our pet receives based on a field: - -```coffee -root = this -root.pet.treats = if this.pet.is_cute { - this.pet.treats + 10 -} -``` - -Try that mapping out and you should see the number of treats in the output increased to 15. Now try changing the input field `pet.is_cute` to `false` and the output treats count should go back to the original 5. - -When a conditional expression doesn't have a branch to execute then the assignment is skipped entirely, which means when the pet is not cute the value of `pet.treats` is unchanged (and remains the value set in the `root = this` assignment). - -We can add an `else` block to our `if` expression to remove treats entirely when the pet is not cute: - -```coffee -root = this -root.pet.treats = if this.pet.is_cute { - this.pet.treats + 10 -} else { - deleted() -} -``` - -This is possible because field deletions are expressed as assigned values created with the `deleted()` function. This is cool but also in poor taste, treats should be allocated based on need, not cuteness! - -### If Statement - -The `if` keyword can also be used as a statement in order to conditionally apply a series of mapping assignments, the previous example can be rewritten as: - -```coffee -root = this -if this.pet.is_cute { - root.pet.treats = this.pet.treats + 10 -} else { - root.pet.treats = deleted() -} -``` - -Converting this mapping to use a statement has resulted in a more verbose mapping as we had to specify `root.pet.treats` multiple times as an assignment target. However, using `if` as a statement can be beneficial when multiple assignments rely on the same logic: - -```coffee -root = this -if this.pet.is_cute { - root.pet.treats = this.pet.treats + 10 - root.pet.toys = this.pet.toys + 10 -} -``` - -More treats *and* more toys! Lucky Spot! - -### Match Expression - -Another conditional expression is `match` which allows you to list many branches consisting of a condition and a query to execute separated with `=>`, where the first condition to pass is the one that is executed: - -```coffee -root = this -root.pet.toys = match { - this.pet.treats > 5 => this.pet.treats - 5, - this.pet.type == "cat" => 3, - this.pet.type == "dog" => this.pet.toys - 3, - this.pet.type == "horse" => this.pet.toys + 10, - _ => 0, -} -``` - -Try executing that mapping with different values for `pet.type` and `pet.treats`. Match expressions can also specify a new context for the keyword `this` which can help reduce some of the boilerplate in your boolean conditions. The following mapping is equivalent to the previous: - -```coffee -root = this -root.pet.toys = match this.pet { - this.treats > 5 => this.treats - 5, - this.type == "cat" => 3, - this.type == "dog" => this.toys - 3, - this.type == "horse" => this.toys + 10, - _ => 0, -} -``` - -Your boolean conditions can also be expressed as value types, in which case the context being matched will be compared to the value: - -```coffee -root = this -root.pet.toys = match this.pet.type { - "cat" => 3, - "dog" => 5, - "rabbit" => 8, - "horse" => 20, - _ => 0, -} -``` - -## Error Handling - -Are you feeling relaxed? Well don't, because in the world of mapping anything can happen, at ANY TIME, and there are plenty of ways that a mapping can fail due to variations in the input data. Are you feeling stressed? Well don't, because Bloblang makes handling errors easy. - -First, let's take a look at what happens when errors _aren't_ handled, change your input to the following: - -```json -{ - "palace_guards": 10, - "angry_peasants": "I couldn't be bothered to ask them" -} -``` - -And change your mapping to something simple like a number comparison: - -```coffee -root.in_trouble = this.angry_peasants > this.palace_guards -``` - -Uh oh! It looks like our canvasser was too lazy and our `angry_peasants` count was incorrectly set for this document. You should see an error in the output window that mentions something like `cannot compare types string (from field this.angry_peasants) and number (from field this.palace_guards)`, which means the mapping was abandoned. - -So what if we want to try and map something, but don't care if it fails? In this case if we are unable to compare our angry peasants with palace guards then I would still consider us in trouble just to be safe. - -For that we have a special [method `catch`][blobl.methods.catch], which if we add to any query allows us to specify an argument to be returned when an error occurs. Since methods can be added to any query we can surround our arithmetic with brackets and catch the whole thing: - -```coffee -root.in_trouble = (this.angry_peasants > this.palace_guards).catch(true) -``` - -Now instead of an error we should see an output with `in_trouble` set to `true`. Try changing to value of `angry_peasants` to a few different values, including some numbers. - -One of the powerful features of `catch` is that when it is added at the end of a series of expressions and methods it will capture errors at any part of the series, allowing you to capture errors at any granularity. For example, the mapping: - -```coffee -root.abort_mission = if this.mission.type == "impossible" { - !this.user.motives.contains("must clear name") -} else { - this.mission.difficulty > 10 -}.catch(false) -``` - -Will catch errors caused by: - -- `this.mission.type` not being a string -- `this.user.motives` not being an array -- `this.mission.difficulty` not being a number - -But will always return `false` if any of those errors occur. Try it out with this input and play around by breaking some of the fields: - -```json -{ - "mission": { - "type": "impossible", - "difficulty": 5 - }, - "user": { - "motives": ["must clear name"] - } -} -``` - -Now try out this mapping: - -```coffee -root.abort_mission = if (this.mission.type == "impossible").catch(true) { - !this.user.motives.contains("must clear name").catch(false) -} else { - (this.mission.difficulty > 10).catch(true) -} -``` - -This version is more granular and will capture each of the errors individually, with each error given a unique `true` or `false` fallback. - -## Validation - -I'm worried that I've turned you into some sort of error hating thug, hell-bent on eliminating all errors from existence. However, sometimes errors are what we want. Failing a mapping with an error allows us to handle the bad document in other ways, such as routing it to a dead-letter queue or filtering it entirely. - -You can read about common Benthos error handling patterns for bad data in the [error handling guide][configuration.error_handling], but the first step is to create the error. Luckily, Bloblang has a range of ways of creating errors under certain circumstances, which can be used in order to validate the data being mapped. - -There are [a few helper methods][blobl.methods.coercion] that make validating and coercing fields nice and easy, try this mapping out: - -```coffee -root.foo = this.foo.number() -root.bar = this.bar.not_null() -root.baz = this.baz.not_empty() -``` - -With some of these sample inputs: - -```json -{"foo":"nope","bar":"hello world","baz":[1,2,3]} -{"foo":5,"baz":[1,2,3]} -{"foo":10,"bar":"hello world","baz":[]} -``` - -However, these methods don't cover all use cases. The general purpose error throwing technique is the [`throw` function][blobl.functions.throw], which takes an argument string that describes the error. When it's called it will throw a mapping error that abandons the mapping (unless it's caught, psych!) - -For example, we can check the type of a field with the [method `type`][blobl.methods.type], and then throw an error if it's not the type we expected: - -```coffee -root.foos = if this.user.foos.type() == "array" { - this.user.foos -} else { - throw("foos must be an array, but it ain't, what gives?") -} -``` - -Try this mapping out with a few sample inputs: - -```json -{"user":{"foos":[1,2,3]}} -{"user":{"foos":"1,2,3"}} -``` - -## Context - -In Bloblang, when we refer to the context we're talking about the value returned with the keyword `this`. At the beginning of a mapping the context starts off as a reference to the root of a structured input document, which is why the mapping `root = this` will result in the same document coming out as you put in. - -However, in Bloblang there are mechanisms whereby the context might change, we've already seen how this can happen within a `match` expression. Another useful way to change the context is by adding a bracketed query expression as a method to a query, which looks like this: - -```coffee -root = this.foo.bar.(this.baz + this.buz) -``` - -Within the bracketed query expression the context becomes the result of the query that it's a method of, so within the brackets in the above mapping the value of `this` points to the result of `this.foo.bar`, and the mapping is therefore equivalent to: - -```coffee -root = this.foo.bar.baz + this.foo.bar.buz -``` - -With this handy trick the `throw` mapping from the validation section above could be rewritten as: - -```coffee -root.foos = this.user.foos.(if this.type() == "array" { this } else { - throw("foos must be an array, but it ain't, what gives?") -}) -``` - -### Naming the Context - -Shadowing the keyword `this` with new contexts can look confusing in your mappings, and it also limits you to only being able to reference one context at any given time. As an alternative, Bloblang supports context capture expressions that look similar to lambda functions from other languages, where you can name the new context with the syntax ` -> `, which looks like this: - -```coffee -root = this.foo.bar.(thing -> thing.baz + thing.buz) -``` - -Within the brackets we now have a new field `thing`, which returns the context that would have otherwise been captured as `this`. This also means the value returned from `this` hasn't changed and will continue to return the root of the input document. - -## Coalescing - -Being able to open up bracketed query expressions on fields leads us onto another cool trick in Bloblang referred to as coalescing. It's very common in the world of document mapping that due to structural deviations a value that we wish to obtain could come from one of multiple possible paths. - -To illustrate this problem change the input document to the following: - -```json -{ - "thing": { - "article": { - "id": "foo", - "contents": "Some people did some stuff" - } - } -} -``` - -Let's say we wish to flatten this structure with the following mapping: - -```coffee -root.contents = this.thing.article.contents -``` - -But articles are only one of many document types we expect to receive, where the field `contents` remains the same but the field `article` could instead be `comment` or `share`. In this case we could expand our map of `contents` to use a `match` expression where we check for the existence of `article`, `comment`, etc in the input document. - -However, a much cleaner way of approaching this is with the pipe operator (`|`), which in Bloblang can be used to join multiple queries, where the first to yield a non-null result is selected. Change your mapping to the following: - -```coffee -root.contents = this.thing.article.contents | this.thing.comment.contents -``` - -And now try changing the field `article` in your input document to `comment`. You should see that the value of `contents` remains as `Some people did some stuff` in the output document. - -Now, rather than write out the full path prefix `this.thing` each time we can use a bracketed query expression to change the context, giving us more space for adding other fields: - -```coffee -root.contents = this.thing.(this.article | this.comment | this.share).contents -``` - -And by the way, the keyword `this` within queries can be omitted and made implicit, which allows us to reduce this even further: - -```coffee -root.contents = this.thing.(article | comment | share).contents -``` - -Finally, we can also add a pipe operator at the end to fallback to a literal value when none of our candidates exists: - -```coffee -root.contents = this.thing.(article | comment | share).contents | "nothing" -``` - -Neat. - -## Advanced Methods - -Congratulations for making it this far, but if you take your current level of knowledge to a map-off you'll be laughed off the stage. What happens when you need to map all of the elements of an array? Or filter the keys of an object by their values? What if the fellowship just used the eagles to fly to mount doom? - -Bloblang offers a bunch of advanced methods for [manipulating structured data types][blobl.methods.object-array-manipulation], let's take a quick tour of some of the cooler ones. Set your input document to this list of things: - -```json -{ - "num_friends": 5, - "things": [ - { - "name": "yo-yo", - "quantity": 10, - "is_cool": true - }, - { - "name": "dish soap", - "quantity": 50, - "is_cool": false - }, - { - "name": "scooter", - "quantity": 1, - "is_cool": true - }, - { - "name": "pirate hat", - "quantity": 7, - "is_cool": true - } - ] -} -``` - -Let's say we wanted to reduce the `things` in our input document to only those that are cool and where we have enough of them to share with our friends. We can do this with a [`filter` method][blobl.methods.filter]: - -```coffee -root = this.things.filter(thing -> thing.is_cool && thing.quantity > this.num_friends) -``` - -Try running that mapping and you'll see that the output is reduced. What is happening here is that the `filter` method takes an argument that is a query, and that query will be mapped for each individual element of the array (where the context is changed to the element itself). We have captured the context into a field `thing` which allows us to continue referencing the root of the input with `this`. - -The `filter` method requires the query parameter to resolve to a boolean `true` or `false`, and if it resolves to `true` the element will be present in the resulting array, otherwise it is removed. - -Being able to express a query argument to be applied to a range in this way is one of the more powerful features of Bloblang, and when mapping complex structured data these advanced methods will likely be a common tool that you'll reach for. - -Another such method is [`map_each`][blobl.methods.map_each], which allows you to mutate each element of an array, or each value of an object. Change your input document to the following: - -```json -{ - "talking_heads": [ - "1:E.T. is a bad film,Pokemon corrupted an entire generation", - "2:Digimon ripped off Pokemon,Cats are boring", - "3:I'm important", - "4:Science is just made up,The Pokemon films are good,The weather is good" - ] -} -``` - -Here we have an array of talking heads, where each element is a string containing an identifer, a colon, and a comma separated list of their opinions. We wish to map each string into a structured object, which we can do with the following mapping: - -```coffee -root = this.talking_heads.map_each(raw -> { - "id": raw.split(":").index(0), - "opinions": raw.split(":").index(1).split(",") -}) -``` - -The argument to `map_each` is a query where the context is the element, which we capture into the field `raw`. The result of the query argument will become the value of the element in the resulting array, and in this case we return an object literal. - -In order to separate the identifier from opinions we perform a `split` by colon on the raw string element and get the first substring with the `index` method. We then do the split again and extract the remainder, and split that by comma in order to extract all of the opinions to an array field. - -However, one problem with this mapping is that the split by colon is written out twice and executed twice. A more efficient way of performing the same thing is with the bracketed query expressions we've played with before: - -```coffee -root = this.talking_heads.map_each(raw -> raw.split(":").(split_string -> { - "id": split_string.index(0), - "opinions": split_string.index(1).split(",") -})) -``` - -:::note Challenge! -Try updating that map so that only opinions that mention Pokemon are kept -::: - - -Cool. To find more methods for manipulating structured data types check out the [methods page][blobl.methods.object-array-manipulation]. - -## Reusable Mappings - -Bloblang has cool methods, sure, but there's nothing cooler than methods you've made yourself. When the going gets tough in the mapping world the best solution is often to create a named mapping, which you can do with the keyword `map`: - -```coffee -map parse_talking_head { - let split_string = this.split(":") - - root.id = $split_string.index(0) - root.opinions = $split_string.index(1).split(",") -} - -root = this.talking_heads.map_each(raw -> raw.apply("parse_talking_head")) -``` - -The body of a named map, encapsulated with squiggly brackets, is a totally isolated mapping where `root` now refers to a new value being created for each invocation of the map, and `this` refers to the root of the context provided to the map. - -Named maps are executed with the [method `apply`][blobl.methods.apply], which has a string parameter identifying the map to execute, this means it's possible to dynamically select the target map. - -As you can see in the above example we were able to use a custom map in order to create our talking head objects without the object literal. Within a named map we can also create variables that exist only within the scope of the map. - -A cool feature of named mappings is that they can invoke themselves recursively, allowing you to define mappings that walk deeply nested structures. The following mapping will scrub all values from a document that contain the word "Voldemort" (case insensitive): - -```coffee -map remove_naughty_man { - root = match { - this.type() == "object" => this.map_each(item -> item.value.apply("remove_naughty_man")), - this.type() == "array" => this.map_each(ele -> ele.apply("remove_naughty_man")), - this.type() == "string" => if this.lowercase().contains("voldemort") { deleted() }, - this.type() == "bytes" => if this.lowercase().contains("voldemort") { deleted() }, - _ => this, - } -} - -root = this.apply("remove_naughty_man") -``` - -Try running that mapping with the following input document: - -```json -{ - "summer_party": { - "theme": "the woman in black", - "guests": [ - "Emma Bunton", - "the seal I spotted in Trebarwith", - "Voldemort", - "The cast of Swiss Army Man", - "Richard" - ], - "notes": { - "lisa": "I don't think voldemort eats fish", - "monty": "Seals hate dance music" - } - }, - "crushes": [ - "Richard is nice but he hates pokemon", - "Victoria Beckham but I think she's taken", - "Charlie but they're totally into Voldemort" - ] -} -``` - -Charlie will be upset but at least we'll be safe. - -## Unit Testing - -You are truly a champion of mappings, and you're probably feeling pretty confident right now. Maybe you even have a mapping that you're particularly proud of. Well, I'm sorry to inform you that your mapping is DOOMED, as a mapping without unit tests is like a Twitter session, with the progression of time it will inevitably descend into madness. - -However, if you act now there is still time to spare your mapping from this fate, as Benthos has it's own [unit testing capabilities][configuration.unit_testing] that you can also use for your mappings. To start with save a mapping into a file called something like `naughty_man.blobl`, we can use the example above from the reusable mappings section: - -```coffee -map remove_naughty_man { - root = match { - this.type() == "object" => this.map_each(item -> item.value.apply("remove_naughty_man")), - this.type() == "array" => this.map_each(ele -> ele.apply("remove_naughty_man")), - this.type() == "string" => if this.lowercase().contains("voldemort") { deleted() }, - this.type() == "bytes" => if this.lowercase().contains("voldemort") { deleted() }, - _ => this, - } -} - -root = this.apply("remove_naughty_man") -``` - -Next, we can define our unit tests in an accompanying YAML file in the same directory, let's call this `naughty_man_test.yaml`: - -```yaml -tests: - - name: test naughty man scrubber - target_mapping: './naughty_man.blobl' - environment: {} - input_batch: - - content: | - { - "summer_party": { - "theme": "the woman in black", - "guests": [ - "Emma Bunton", - "the seal I spotted in Trebarwith", - "Voldemort", - "The cast of Swiss Army Man", - "Richard" - ] - } - } - output_batches: - - - - json_equals: { - "summer_party": { - "theme": "the woman in black", - "guests": [ - "Emma Bunton", - "the dolphin I spotted in Trebarwith", - "The cast of Swiss Army Man", - "Richard" - ] - } - } -``` - -As you can see we've defined a single test, where we point to our mapping file which will be executed in our test. We then specify an input message which is a reduced version of the document we tried out before, and finally we specify output predicates, which is a JSON comparison against the output document. - -We can execute these tests with `benthos test ./naughty_man_test.yaml`, Benthos will also automatically find our tests if you simply run `benthos test ./...`. You should see an output something like: - -```text -Test 'naughty_man_test.yaml' failed - -Failures: - ---- naughty_man_test.yaml --- - -test naughty man scrubber [line 2]: -batch 0 message 0: json_equals: JSON content mismatch -{ - "summer_party": { - "guests": [ - "Emma Bunton", - "the seal I spotted in Trebarwith" => "the dolphin I spotted in Trebarwith", - "The cast of Swiss Army Man", - "Richard" - ], - "theme": "the woman in black" - } -} -``` - -Because in actual fact our expected output is wrong, I'll leave it to you to spot the error. Once the test is fixed you should see: - -```text -Test 'naughty_man_test.yaml' succeeded -``` - -And now our mapping, should we need to expand it in the future, is better protected against regressions. You can read more about the Benthos unit test specification, including alternative output predicates, in [this document][configuration.unit_testing]. - -## Final Words - -That's it for this walkthrough, if you're hungry for more then I suggest you re-evaluate your priorities in life. If you have feedback then please [get in touch][community], despite being terrible people the Benthos community are very welcoming. - -[guides.getting_started]: /docs/guides/getting_started -[blobl.methods]: /docs/guides/bloblang/methods -[blobl.methods.uppercase]: /docs/guides/bloblang/methods#uppercase -[blobl.methods.replace_all]: /docs/guides/bloblang/methods#replace_all -[blobl.methods.catch]: /docs/guides/bloblang/methods#catch -[blobl.methods.without]: /docs/guides/bloblang/methods#without -[blobl.methods.type]: /docs/guides/bloblang/methods#type -[blobl.methods.coercion]: /docs/guides/bloblang/methods#type-coercion -[blobl.methods.object-array-manipulation]: /docs/guides/bloblang/methods#object--array-manipulation -[blobl.methods.filter]: /docs/guides/bloblang/methods#filter -[blobl.methods.map_each]: /docs/guides/bloblang/methods#map_each -[blobl.methods.apply]: /docs/guides/bloblang/methods#apply -[blobl.functions]: /docs/guides/bloblang/functions -[blobl.functions.deleted]: /docs/guides/bloblang/functions#deleted -[blobl.functions.content]: /docs/guides/bloblang/functions#content -[blobl.functions.env]: /docs/guides/bloblang/functions#env -[blobl.functions.now]: /docs/guides/bloblang/functions#now -[blobl.functions.uuid_v4]: /docs/guides/bloblang/functions#uuid_v4 -[blobl.functions.throw]: /docs/guides/bloblang/functions#throw -[blobl.literals]: /docs/guides/bloblang/about#literals -[configuration.error_handling]: /docs/configuration/error_handling -[configuration.unit_testing]: /docs/configuration/unit_testing -[community]: /community diff --git a/website/docs/guides/cloud/aws.md b/website/docs/guides/cloud/aws.md deleted file mode 100644 index 84019d5af3..0000000000 --- a/website/docs/guides/cloud/aws.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Amazon Web Services -description: Find out about AWS components in Benthos ---- - -There are many components within Benthos which utilise AWS services. You will find that each of these components contains a configuration section under the field `credentials`, of the format: - -```yml -credentials: - profile: "" - id: "" - secret: "" - token: "" - role: "" - role_external_id: "" -``` - -This section contains many fields and it isn't immediately clear which of them are compulsory and which aren't. This document aims to make it clear what each field is responsible for and how it might be used. - -### None of these fields are compulsory - -The first thing to make clear is that _all_ of these fields are optional. When all fields are left blank Benthos will attempt to load credentials from a shared credentials file (`~/.aws/credentials`). The profile loaded will be `default` unless the `AWS_PROFILE` environment variable is set. - -## Explicit Credentials - -By explicitly setting the credentials you are using at the component level it's possible to connect to components using different accounts within the same Benthos process. - -### Selecting a Profile - -If you are using your shared credentials file but wish to explicitly select a profile set the `profile` field: - -```yml -credentials: - profile: foo -``` - -### Manual - -If you are using long term credentials for your account you only need to set the fields `id` and `secret`: - -```yml -credentials: - id: foo # aws_access_key_id - secret: bar # aws_secret_access_key -``` - -If you are using short term credentials then you will also need to set the field `token`: - -```yml -credentials: - id: foo # aws_access_key_id - secret: bar # aws_secret_access_key - token: baz # aws_session_token -``` - -## Assuming a Role - -It's also possible to configure Benthos to [assume a role][assuming-role] using your credentials by setting the field `role` to your target role ARN. - -```yml -credentials: - role: fooarn # Role ARN -``` - -This does NOT require explicit credentials, but it's possible to use both. - -If you need to assume a role owned by another organisation they might require you to [provide an external ID][role-external-id], in which case place it in the field `role_external_id`: - -```yml -credentials: - role: fooarn # Role ARN - role_external_id: bar_id -``` - -[temporary-creds]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html -[assuming-role]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html -[role-external-id]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html diff --git a/website/docs/guides/cloud/gcp.md b/website/docs/guides/cloud/gcp.md deleted file mode 100644 index 1a78640586..0000000000 --- a/website/docs/guides/cloud/gcp.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Google Cloud Platform -description: Find out about GCP components in Benthos ---- - -There are many components within Benthos which utilise Google Cloud Platform (GCP) services. You will find that each of -these components require valid credentials. - -When running Benthos inside a Google Cloud environment that has a -[default service account](https://cloud.google.com/iam/docs/service-accounts#default), it can automatically retrieve the -service account credentials to call Google Cloud APIs through a library called Application Default Credentials (ADC). - -Otherwise, if your application runs outside Google Cloud environments that provide a default service account, you need -to manually create one. Once you have a service account set up which has the required permissions, you can -[create](https://console.cloud.google.com/apis/credentials/serviceaccountkey) a new Service Account Key and download it -as a JSON file. Then all you need to do set the path to this JSON file in the `GOOGLE_APPLICATION_CREDENTIALS` -environment variable. - -Please refer to [this document](https://cloud.google.com/docs/authentication/production) for details. diff --git a/website/docs/guides/getting_started.md b/website/docs/guides/getting_started.md deleted file mode 100644 index 7f963686b5..0000000000 --- a/website/docs/guides/getting_started.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: Getting Started -description: Getting started with Benthos ---- - -Woops! You fell for the marketing hype. Let's try and get through this together. - -
- -## Install - -The easiest way to install Benthos is with this handy script: - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - - - - -```sh -curl -Lsf https://www.benthos.dev/sh/install | bash -``` - - - - -```sh -curl -Lsf https://www.benthos.dev/sh/install | bash -s -- 3.56.0 -``` - - - - -Or you can grab an archive containing Benthos from the [releases page][releases]. - -### Docker - -If you have docker installed you can pull the latest official Benthos image with: - -```sh -docker pull ghcr.io/benthosdev/benthos -docker run --rm -v /path/to/your/config.yaml:/benthos.yaml ghcr.io/benthosdev/benthos -``` - -### Homebrew - -On macOS, Benthos can be installed via Homebrew: - -```sh -brew install benthos -``` - -### Asdf - -If you use the [asdf](https://asdf-vm.com/) tool version manager you can install Benthos with: - -```sh -asdf plugin add benthos -asdf install benthos latest -asdf global benthos latest -``` - -### Serverless - -For information about serverless deployments of Benthos check out the serverless section [here][serverless]. - -## Run - -A Benthos stream pipeline is configured with a single [config file][configuration], you can generate a fresh one with: - -```shell -benthos create > config.yaml -``` - -The main sections that make up a config are `input`, `pipeline` and `output`. When you generate a fresh config it'll simply pipe `stdin` to `stdout` like this: - -```yaml -input: - stdin: {} - -pipeline: - processors: [] - -output: - stdout: {} -``` - -Eventually we'll want to configure a more useful [input][inputs] and [output][outputs], but for now this is useful for quickly testing processors. You can execute this config with: - -```sh -benthos -c ./config.yaml -``` - -Anything you write to stdin will get written unchanged to stdout, cool! Resist the temptation to play with this for hours, there's more stuff to try out. - -Next, let's add some processing steps in order to mutate messages. The most powerful one is the [`mapping` processor][processors.mapping] which allows us to perform mappings, let's add a mapping to uppercase our messages: - -```yaml -input: - stdin: {} - -pipeline: - processors: - - mapping: root = content().uppercase() - -output: - stdout: {} -``` - -Now your messages should come out in all caps, how whacky! IT'S LIKE BENTHOS IS SHOUTING BACK AT YOU! - -You can add as many [processing steps][processors] as you like, and since processors are what make Benthos powerful they are worth experimenting with. Let's create a more advanced pipeline that works with JSON documents: - -```yaml -input: - stdin: {} - -pipeline: - processors: - - sleep: - duration: 500ms - - mapping: | - root.doc = this - root.first_name = this.names.index(0).uppercase() - root.last_name = this.names.index(-1).hash("sha256").encode("base64") - -output: - stdout: {} -``` - -First, we sleep for 500 milliseconds just to keep the suspense going. Next, we restructure our input JSON document by nesting it within a field `doc`, we map the upper-cased first element of `names` to a new field `first_name`. Finally, we map the hashed and base64 encoded value of the last element of `names` to a new field `last_name`. - -Try running that config with some sample documents: - -```sh -echo '{"id":"1","names":["celine","dion"]} -{"id":"2","names":["chad","robert","kroeger"]}' | benthos -c ./config.yaml -``` - -You should see (amongst some logs): - -```sh -{"doc":{"id":"1","names":["celine","dion"]},"first_name":"CELINE","last_name":"1VvPgCW9sityz5XAMGdI2BTA7/44Wb3cANKxqhiCo50="} -{"doc":{"id":"2","names":["chad","robert","kroeger"]},"first_name":"CHAD","last_name":"uXXg5wCKPjpyj/qbivPbD9H9CZ5DH/F0Q1Twytnt2hQ="} -``` - -How exciting! I don't know about you but I'm going to need to lie down for a while. Now that you are a Benthos expert might I suggest you peruse these sections to see if anything tickles your fancy? - -- [Bloblang Walkthrough][bloblang.walkthrough] -- [Inputs][inputs] -- [Processors][processors] -- [Outputs][outputs] -- [Monitoring][monitoring] -- [Cookbooks][cookbooks] -- [More about configuration][configuration] - -[proc_proc_field]: /docs/components/processors/process_field -[proc_text]: /docs/components/processors/text -[processors]: /docs/components/processors/about -[processors.mapping]: /docs/components/processors/mapping -[inputs]: /docs/components/inputs/about -[outputs]: /docs/components/outputs/about -[jmespath]: http://jmespath.org/ -[releases]: https://github.com/benthosdev/benthos/releases -[serverless]: /docs/guides/serverless/about -[configuration]: /docs/configuration/about -[monitoring]: /docs/guides/monitoring -[cookbooks]: /cookbooks -[bloblang.walkthrough]: /docs/guides/bloblang/walkthrough diff --git a/website/docs/guides/migration/v2.md b/website/docs/guides/migration/v2.md deleted file mode 100644 index 6a72305ae9..0000000000 --- a/website/docs/guides/migration/v2.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Migrating to Version 2 ---- - -Benthos version 2 comes with a small number of backwards incompatible changes -that are organised into three sections; [configuration](#configuration), -[service](#service) and [Go API](#go-api). - -These changes are considered minor and therefore upgrading is not expected to -cause significant problems for any users. - -## Configuration - -### Type Inference - -Version 2 comes with the ability to infer the `type` of components in -configuration files whenever the field is omitted. You can read more about this -behaviour [here](/docs/configuration/about#concise-configuration). - -This feature is not expected to impact the vast majority of users. However, -there is one exception where a malformed section containing unused type -parameters but a missing `type` field will be interpreted differently. For -example, the following config: - -```yml -pipeline: - processors: - - # type: text - text: - operator: set - value: "delete all your content" -``` - -In V1 would be interpreted as a `bounds_check` processor as it is the default -processor type, whereas V2 would infer this to be a `text` processor based on -its fields. - -#### Migration Guide - -Most users should not be impacted by this change, and a config file that is -vulnerable to the regression would report linting errors in V1. - -You can quickly verify that your configs are interpreted without regression by -comparing the output of `benthos -c ./yourconfig.yaml --print-yaml` with V1 and -V2. If they are the same then you are not affected. - -### Field Default Value Changes - -In version 2 the field `unsubscribe_on_close` of the `nats_stream` input is now -`false` by default. - -## Service - -The recommended way to create plugins for Benthos is outlined in -[this repository](https://github.com/benthosdev/benthos-plugin-example). -Therefore the following experimental plugin related flags have been removed from -the service: `swap-envs`, `plugins-dir`, `list-input-plugins`, -`list-output-plugins`, `list-processor-plugins`, `list-condition-plugins`. - -The flag `swap-envs` has also been removed for clarity, as it had no impact on -JSON reference resolution. If this flag is being used please open an issue and -it can be reimplemented to be fully compliant. - -## Go API - -### Condition Package Moved - -The package `github.com/Jeffail/benthos/lib/processor/condition` has been -changed to `github.com/Jeffail/benthos/lib/condition`. Migrating should be a -simple case of applying a find/replace on your codebase: - -```sh -find . -name "*.go" | \ - xargs sed -i 's/benthos\/lib\/processor\/condition/benthos\/lib\/condition/g' -``` - -### Interface Changes - -The following interface changes have occurred to core Benthos components: - -- `types.Cache` now has `types.Closable` embedded. -- `types.RateLimit` now has `types.Closable` embedded. -- `types.Manager` has new method `GetPlugin`. -- `log.Modular` has new method `WithFields`. \ No newline at end of file diff --git a/website/docs/guides/migration/v3.md b/website/docs/guides/migration/v3.md deleted file mode 100644 index 9034655714..0000000000 --- a/website/docs/guides/migration/v3.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: Migrating to Version 3 ---- - -Benthos version 3 comes with some breaking [service](#service) and [configuration](#configuration) changes as well as a few breaking [API changes](#go-api). - -## Service - -### Memory Map File Buffer Removed - -The long deprecated `mmap_file` buffer has been removed. If you were still relying on this buffer implementation then please [raise an issue](https://github.com/Jeffail/benthos/issues). - -### Old Metrics Paths Removed - -The following undocumented metrics paths have been removed: - -- `input.parts.count` -- `input.read.success` -- `input.read.error` -- `input.send.success` -- `input.send.error` -- `input.ack.success` -- `input.ack.error` -- `output.running` -- `output.parts.count` -- `output.send.success` -- `output.parts.send.success` -- `output.send.error` - -All of these paths have [remaining equivalents](/docs/components/metrics/about#paths). - -### Metric Path Changes - -The `http_client` output client metrics have been renamed from `output.*.output.http_client` to `output.*.client`. - -## Configuration - -### Metrics Prefix - -The configuration field `prefix` within `metrics` has been moved from the root -of the config object to individual types. E.g. when using `statsd` the field -`metrics.prefix` should be replaced with `metrics.statsd.prefix`. - -### JSON Paths - -Many components within Benthos use an unspecified "JSON dot path" syntax for querying and setting fields within JSON documents. The format of these paths has been formalised to make them clearer and more generally useful, but this potentially breaks your paths when they query against hierarchies that contain arrays. - -The formal specification for v3 can be found [in this document](/docs/configuration/field_paths). - -The following components are affected: - -- `awk` processor (all of the `json_*` functions) -- `json` processor (`path` field) -- `process_field` processor (`path` field) -- `process_map` processor (`premap`, `premap_optional`, `postmap` and `postmap_optional` fields) -- `check_field` condition (`path` field) -- `json_field` function interpolation -- `s3` input (`sqs_body_path`, `sqs_bucket_path` and `sqs_envelope_path` fields) -- `dynamodb` output (`json_map_columns` field values) - -#### Migration Guide - -In order to replicate the exact same behaviour as currently exists your paths should be updated to include the character `*` wherever an array exists. For example, the default value of `sqs_body_path` for the `s3` input has been updated from `Records.s3.object.key` to `Records.*.s3.object.key`. - -### Process DAG Stage Names - -The `process_dag` processor now only permits workflow stage names matching the following regular expression: `[a-zA-Z0-9_-]+`. The reasoning for this restriction is to potentially expand the features of `process_dag` in the future with custom root fields (e.g. `$on_error`). - -## Go API - -### Modules - -Benthos now fully adheres to [Go Modules](https://github.com/golang/go/wiki/Modules), import paths must therefore now contain the major version (v3) like so: - -```go -import "github.com/Jeffail/benthos/v3/lib/processor" -``` - -It should be pretty quick to update your imports, either using a tool or just: - -```sh -grep "Jeffail/benthos" . -Rl | grep -e "\.go$" | xargs -I{} sed -i 's/Jeffail\/benthos/Jeffail\/benthos\/v3/g' {} -``` - -### Other - -- Constructors for buffer components now require a `types.Manager`, giving them parity with other components: `buffer.New(conf Config, mgr types.Manager, log log.Modular, stats metrics.Type) (Type, error)` \ No newline at end of file diff --git a/website/docs/guides/migration/v4.md b/website/docs/guides/migration/v4.md deleted file mode 100644 index 2a646f9e8a..0000000000 --- a/website/docs/guides/migration/v4.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: Migrating to Version 4 ---- - -Benthos has been at major version 3 [for more than two years][blog.v4roadmap], during which time it has gained a huge amount of functionality without introducing any breaking changes. However, the number of components, APIs and features that have been deprecated in favour of better solutions has grown steadily and the time has finally come to purge them. There are also some areas of functionality that have been improved with breaking changes. - -This document outlines the changes made to Benthos since V3 and tips for how to migrate to V4 in places where those changes are significant. - -## Deprecated Components Removed - -All components, features and configuration fields that were marked as deprecated in the latest release of V3 have been removed in V4. In order to detect deprecated components or fields within your existing configuration files you can run the linter from a later release of V3 Benthos with the `--deprecated` flag: - -```sh -benthos lint --deprecated ./configs/*.yaml -``` - -This should report all remaining deprecated components. All deprecated components have favoured alternative solutions in V3, so it should be possible to slowly eliminate deprecated aspects of your config using V3 before upgrading. - -### Unit test directories - -The `benthos test` subcommand no longer walks paths when they are directories. Instead use explicit triple-dot syntax (`./dir/...`) or wildcard patterns. - -## New Go Module Name - -For users of the Go plugin APIs the import path of this module needs to be updated to `github.com/benthosdev/benthos/v4`, like so: - -```go -import "github.com/benthosdev/benthos/v4/public/service" -``` - -## Pulsar Components Disabled (for now) - -There have been multiple issues with the Go Pulsar client libraries. Since some are still outstanding and causing problems with unrelated components the decision has been made to remove the `pulsar` input and output from standard builds. However, it is still possible to build custom versions of Benthos with them included by importing the package `./public/components/pulsar`: - -```go -package main - -import ( - "context" - - "github.com/Jeffail/benthos/v3/public/service" - - // Import all plugins defined within the repo. - _ "github.com/benthosdev/benthos/v4/public/components/all" - _ "github.com/benthosdev/benthos/v4/public/components/pulsar" -) - -func main() { - service.RunCLI(context.Background()) -} -``` - -## Pipeline Threads Behaviour Change - -https://github.com/benthosdev/benthos/issues/399 - -In V3 the `pipeline.threads` field defaults to 1. If this field is explicitly set to `0` it will automatically match the number of CPUs on the host machine. In V4 this will change so that the default value of `pipeline.threads` is `-1`, where this value indicates we should match the number of host CPUs. An explicit value of `0` is still considered valid and functionally equivalent to `-1`. - -## Old Style Interpolation Functions Removed - -The original style of interpolation functions, where you specify a function name followed by a colon and then any arguments (`${!json:foo,1}`) has been deprecated (and undocumented) for a while now. What we've had instead is a subset of Bloblang allowing you to use functions directly (`${! json("foo").from(1) }`), but with the old style still supported for backwards compatibility. - -However, supporting the old style means our parsing capabilities are weakened and so it is now removed in order to allow more powerful interpolations in the future. - -## Bloblang Changes - -https://github.com/benthosdev/benthos/issues/571 - -The functions `meta`, `root_meta`, `error` and `env` now return `null` when the target value does not exist. This is in order to improve consistency across different functions and query types. In cases where a default empty string is preferred you can add `.or("")` onto the function. In cases where you want to throw an error when the value does not exist you can add `.not_null()` onto the function. - -### Root referencing - -It is now possible to reference the `root` of the document being created within a mapping query, i.e. `root.hash = root.string().hash("xxhash64")`. - -## Env Var Docker Configuration - -Docker builds will no longer come with a default config that contains generated environment variables. This system doesn't scale at all for complex configuration files and was becoming a challenge to maintain (and also huge). Instead, the new `-s` flag has been the preferred way to configure Benthos through arguments and will need to be used exclusively in V4. - -It's worth noting that this does not prevent you from defining your own env var based configuration and adding that to your docker image. It's entirely possible to copy the config from V3 and have that work, it just won't be present by default any more. - -In order to migrate to the `-s` flag use the path of the fields you're setting instead of the generated environment variables, so: - -```sh -docker run --rm -p 4195:4195 jeffail/benthos \ - -e "INPUT_TYPE=http_server" \ - -e "OUTPUT_TYPE=kafka" \ - -e "OUTPUT_KAFKA_ADDRESSES=kafka-server:9092" \ - -e "OUTPUT_KAFKA_TOPIC=benthos_topic" -``` - -Becomes: - -```sh -docker run --rm -p 4195:4195 jeffail/benthos \ - -s "input.type=http_server" \ - -s "output.type=kafka" \ - -s "output.kafka.addresses=kafka-server:9092" \ - -s "output.kafka.topic=benthos_topic" -``` - -## Old Plugin APIs Removed - -Any packages from within the `lib` directory have been removed. Please use only the APIs within the `public` directory, the API docs count be found on [pkg.go.dev][plugins.docs], and examples can be found in the [`benthos-plugin-example` repository][plugins.repo]. These new APIs can be found in V3 so if you have many components you can migrate them incrementally by sticking with V3 until completion. - -Many of the old packages within `lib` can also still be found within `internal`, if you're in a pickle you can find some of those APIs and copy/paste them into your own repository. - -## Caches - -All caches that support retries have had their retry/backoff configuration fields modified in order to be more consistent. The new common format is: - -```yml -retries: - initial_interval: 1s - max_interval: 5s - max_elapsed_time: 30s -``` - -In cases where it might be desirable to disable retries altogether (the `ristretto` cache) there is also an `enabled` field. - -### TTL changes - -Caches that support TTLs have had their `ttl` fields renamed to `default_ttl` in order to make it clearer that their purpose is to provide a fallback. All of these values are now duration string types, i.e. a cache with an integer seconds based field with a previous value of `60` should now be defined as `60s`. - -## Field Default Changes - -https://github.com/benthosdev/benthos/issues/392 - -Lots of fields have had default values removed in cases where they were deemed unlikely to be useful and likely to cause frustration. This specifically applies to any `url`, `urls`, `address` or `addresses` fields that may have once had a default value containing a common example for the particular service. In most cases this should cause minimal disruption as the field is non-optional and therefore not specifying it explicitly will result in config errors. - -However, there are the following exceptions that are worth noting: - -### The `http` processor and `http_client` output no longer create multipart requests by default - -The `http` processor and `http_client` output now execute message batch requests as individual requests by default. This behaviour can be disabled by explicitly setting `batch_as_multipart` to `true`. - -### Output `lines` codec no longer adds extra batch newlines - -All outputs that traditionally wrote empty newlines at the end of batches with >1 message when using the `lines` codec (`socket`, `stdout`, `file`, `sftp`) no longer do this by default. This was originally kept for backwards compatibility but was often seen as an unexpected and annoying behaviour. - -It is still possible to add these end-of-batch newlines in a more consistent way by either adding an empty message to the end of batches, or by adding a newline to the last message of the batch. - -### The `switch` output `retry_until_success` - -By default the `switch` output continues retrying switch case outputs until success. This default was sensible at the time as we didn't have a concept of intentionally nacking messages, and therefore a nacked message was likely a recoverable problem and retrying internally means that messages matching multiple cases wouldn't produce duplicates. - -However, since then Benthos has evolved and a very common pattern with the `switch` output is to reject messages that failed during processing using the `reject` output. But because of the default value of `retry_until_success` many users end up in a confusing situation where using a `reject` output results in the pipeline blocking indefinitely until they discover this field. - -Therefore the default value of `retry_until_success` will now be `false`, which means users that aren't using a `reject` flow in one of their switch cases, and have a configuration where messages could match multiple cases, should explicitly set this field to `true` in order to avoid potential duplicates during downstream outages. - -### AWS `region` fields - -https://github.com/benthosdev/benthos/issues/696 - -Any configuration sections containing AWS fields no longer have a default `region` of `eu-west-1`. Instead, the field will be empty by default, where unless explicitly set the environment variable `AWS_REGION` will be used. This will cause problems for users where they expect the region `eu-west-1` to be targeted when neither the field nor the environment variable `AWS_REGION` are set. - -## Clickhouse Driver Changes - -The `clickhouse` SQL driver Data Source Name format parameters have been changed due to a client library update (details can be found at https://github.com/ClickHouse/clickhouse-go). A compatibility layer has been added that makes a best attempt to translate the old DSN format to the new one, but some parameters may not carry over exactly. - -This update also means placeholders in `sql_raw` queries should be in dollar syntax. - -## Serverless Default Output - -The default output of the serverless distribution of Benthos is now the following config: - -```yml -output: - switch: - retry_until_success: false - cases: - - check: errored() - output: - reject: "processing failed due to: ${! error() }" - - output: - sync_response: {} -``` - -This change was made in order to return processing errors directly to the invoker by default. - -## Metrics Changes - -https://github.com/benthosdev/benthos/issues/1066 - -The metrics produced by a Benthos stream have been greatly simplified and now make better use of labels/tags in order to provide component-specific insights. The configuration and behaviour of metrics types has also been made more consistent, with metric names being the same throughout and `mapping` now being a general top-level field. - -For a full overview of the new system check out the [metrics about page][metrics.about]. - -### Field `prefix` is gone - -Some metrics components such as `prometheus` had a `prefix` field for setting a prefix to all metric names. These fields are now gone, if you want to reintroduce these prefixes you can use the general purpose `mapping` field. For example, if we previously had a config: - -```yml -metrics: - prometheus: - prefix: ${METRICS_PREFIX:benthos} -``` - -We need to delete that prefix and add a mapping that renames metric names: - -```yaml -metrics: - mapping: 'root = env("METRICS_PREFIX").or("benthos") + "_" + this' - prometheus: {} -``` - -### The `http_server` type renamed to `json_api` - -The name given to the generic JSON API metrics type was `http_server`, which was confusing as it isn't the only metrics output type that presents as an HTTP server endpoint. This type was also only originally intended for local debugging, which the `prometheus` type is also good for. - -In order to distinguish this metrics type by its unique feature, which is that it exposes metrics as a JSON object, it has been renamed to `json_api`. - -### The `stdout` type renamed to `logger` - -The `stdout` metrics type now emits metrics using the Benthos logger, and therefore also matches the logger format. As such, it has been renamed to `logger` in order to reflect that. - -### No more dots - -In V3 metrics names contained dots in order to represent pseudo-paths of the source component. In V4 all metric names produced by Benthos have been changed to contain only alpha-numeric characters and underscores. It is recommended that any custom metric names produced by your `metric` processors and custom plugins should match this new format for consistency. - -Since dots were invalid characters in Prometheus metric names, in V3 the `prometheus` metrics type made some automatic modifications to all names before registering them. This rewrite first replaced all `-` and `_` characters to a double underscore (`__`), and then replaced all `.` characters with `_`. This was an ugly work around and has been removed in V4, but means in previous cases where custom metrics containing dots were automatically converted you will instead see error logs reporting that the names were invalid and therefore ignored. - -If you wish to retain the old rewrite behaviour you can reproduce it with the new `mapping` field: - -```yml -metrics: - mapping: 'root = this.replace("_", "__").replace("-", "__").replace(".", "_")' - prometheus: {} -``` - -However, it's recommended to change your metric names instead. - -## Tracing Changes - -https://github.com/benthosdev/benthos/issues/872 - -Distributed tracing within Benthos is now done via the Open Telemetry client library. Unfortunately, this client library does not support the full breadth of options as we had before. As such, the `jaeger` tracing type now only supports the `const` sampling type, and the field `service_name` has been removed. - -This will likely mean tracing output will appear different in this release, and if you were relying on code that extracts and interacts with spans from messages in your custom plugins then it will need to be converted to use the official Open Telemetry APIs. - -## Logging Changes - -https://github.com/benthosdev/benthos/issues/589 - -The `logger` config section has been simplified, the new default set to `logfmt`, and the `classic` format removed. The default value of `add_timestamp` has also been changed to `false`. - -## Automatic Max In Flight - -Outputs that compose other outputs (`broker`, `switch`, etc) no longer require their own `max_in_flight` settings as they will automatically saturate their composed outputs. This includes outputs that compose resources. - -## Processor Batch Behaviour Changes - -https://github.com/benthosdev/benthos/issues/408 - -Some processors that once executed only once per batch have been updated to execute upon each message individually by default. This change has been made because it was felt the individual message case was considerably more common (and intuitive) and that it is possible to satisfy the batch-wide behaviour in other ways that are opt-in, such as by placing the processors within a `branch` and having your `request_map` explicit for a single `batch_index` (i.e. `request_map: root = if batch_index() != 0 { deleted() }`). - -### Processor `parts` field removed - -Many processors previously had a `parts` field, which allowed you to explicitly list the indexes of a batch to apply the processor to. This field had confusing naming and was rarely used (or even known about). Since that same behaviour can be reproduced by placing the processor within a `branch` (or `switch`) all `parts` fields have been removed. - -### `dedupe` - -The `dedupe` processor has been reworked so that it now acts upon individual messages by default. It's now mandatory to specify a `key`, and the `parts` and `hash` fields have been removed. Instead, specify full-content hashing with interpolation functions in the `key` field, e.g. `${! content().hash("xxhash64") }`. - -In order to deduplicate an entire batch it is likely easier to use a `cache` processor with the `add` operator: - -```yml -pipeline: - processors: - # Try and add one message to a cache that identifies the whole batch - - branch: - request_map: | - root = if batch_index() == 0 { - this.id - } else { deleted() } - processors: - - cache: - operator: add - key: ${! content() } - value: t - # Delete all messages if we failed - - mapping: | - root = if errored().from(0) { - deleted() - } -``` - -### `log` - -The `log` processor now executes for every message of batches by default. - -### `sleep` - -The `sleep` processor now executes for every message of batches by default. - -## Broker Ditto Macro Gone - -The hidden macro `ditto` for broker configs is now removed. Use the `copies` field instead. For some edge cases where `copies` does not satisfy your requirements you may be better served using [configuration templates][configuration.templates]. If all else fails then please [reach out][community] and we can look into other solutions. - -[processor.branch]: /docs/components/processors/branch -[blog.v4roadmap]: /blog/2021/01/04/v4-roadmap -[v3.docs]: https://v3.benthos.dev -[plugins.repo]: https://github.com/benthosdev/benthos-plugin-example -[plugins.docs]: https://pkg.go.dev/github.com/benthosdev/benthos/v4/public -[metrics.about]: /docs/components/metrics/about -[configuration.templates]: /docs/configuration/templating -[community]: /community diff --git a/website/docs/guides/monitoring.md b/website/docs/guides/monitoring.md deleted file mode 100644 index efdb188647..0000000000 --- a/website/docs/guides/monitoring.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Monitoring ---- - -## Health Checks - -Benthos serves two HTTP endpoints for health checks: - -- `/ping` can be used as a liveness probe as it always returns a 200. -- `/ready` can be used as a readiness probe as it serves a 200 only when both the input and output are connected, otherwise a 503 is returned. - -## Metrics - -Benthos [exposes lots of metrics][metrics.names] either to Statsd, Prometheus, Cloudwatch or for debugging purposes an HTTP endpoint that returns a JSON formatted object. - -The target destination of Benthos metrics is configurable from the [metrics section][metrics.about], where it's also possible to rename and restrict the metrics that are emitted with mappings. - -## Tracing - -Benthos also [emits opentracing events][tracing.about] to a tracer of your choice, which can be used to visualise the processors within a pipeline. - -[metrics.about]: /docs/components/metrics/about -[metrics.names]: /docs/components/metrics/about#metric_names -[tracing.about]: /docs/components/tracers/about \ No newline at end of file diff --git a/website/docs/guides/performance_tuning.md b/website/docs/guides/performance_tuning.md deleted file mode 100644 index 15311fb845..0000000000 --- a/website/docs/guides/performance_tuning.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Performance Tuning ---- - -## Maximising IO Throughput - -This section outlines a few common throughput issues and ways in which they can be solved within Benthos. - -It is assumed here that your Benthos instance is performing only minor processing steps, and therefore has minimal reliance on your CPU resource. If this is not the case the following still applies to an extent, but you should also refer to [the next section regarding CPU utilisation](#maximising-cpu-utilisation). - -Firstly, before venturing into Benthos configurations, you should take an in-depth look at your sources and sinks. Benthos is generally much simpler architecturally than the inputs and outputs it supports. Spend some time understanding how to squeeze the most out of these services and it will make it easier (or unnecessary) to tune your Benthos configuration. - -### Benthos Reads Too Slowly - -If Benthos isn't reading fast enough from your source it might not necessarily be due to a slow consumer. If the sink is slow this can cause back pressure that throttles the amount Benthos can read. Try consuming a test feed with the output replaced with `drop`. If you notice that the input consumption suddenly speeds up then the issue is likely with the output, in which case [try the next section](#benthos-writes-too-slowly). - -If the `drop` output pipe didn't help then take a quick look at the basic configuration fields for the input source type. Sometimes there are fields for setting a number of background prefetches or similar concepts that can increase your throughput. For example, increasing the value of `prefetch_count` for an AMQP consumer can greatly increase the rate at which it is consumed. - -Next, if your source supports multiple parallel consumers then you can try doing that within Benthos by using a [broker][broker-input]. For example, if you started with: - -```yaml -input: - http_client: - url: http://localhost:4195/get - verb: GET -``` - -You could change to: - -```yaml -input: - broker: - copies: 4 - inputs: - - http_client: - url: http://localhost:4195/get - verb: GET -``` - -Which would create the exact same consumer as before with four connections in total. Try increasing the number of copies to see how that affects the throughput. If your multiple consumers would require different configurations then set copies to `1` and write each consumer as a separate object in the `inputs` array. - -Read the [broker documentation][broker-input] for more tips on simplifying broker configs. - -If your source doesn't support multiple parallel consumers then unfortunately your options are more limited. A logical next step might be to look at your network/disk configuration to see if that's a potential cause of contention. - -### Benthos Writes Too Slowly - -If you have an output sink that regularly places back pressure on your source there are a few solutions depending on the details of the issue. - -Firstly, you should check the config parameters of your output sink. There are often fields specifically for controlling the level of acknowledgement to expect before moving onto the next message, if these levels of guarantee are overkill you can disable them for greater throughput. For example, setting the `ack_replicas` field to `false` in the Kafka sink can have a high impact on throughput. - -If the config parameters for an output sink aren't enough then you can try the following: - -#### Increase in flight messages - -Most outputs have a field `max_in_flight` that allows you to specify how many messages can be in flight at the same time. Increasing this value can improve throughput significantly. - -#### Send messages in batches - -Most outputs will send data quicker when messages are batched, this is often done automatically in the background. However, for a few outputs your batches need to be configured. Read the [batching documentation][batching] for more guidance on how to tune message batches within Benthos. - -#### Level out input spikes with a buffer - -There are many reasons why an input source might have spikes or inconsistent throughput rates. It is possible that your output is capable of keeping up with -the long term average flow of data, but fails to keep up when an intermittent spike occurs. - -In situations like these it is sometimes a better use of your hardware and resources to level out the flow of data rather than try and match the peak throughput. This would depend on the frequency and duration of the spikes as well as your latency requirements, and is therefore a matter of judgement. - -Leveling out the flow of data can be done within Benthos using a [buffer][buffers]. Buffers allow an input source to store a bounded amount of data temporarily, which a consumer can work through at its own pace. Buffers always have a fixed capacity, which when full will proceed to block the input just like a busy output would. - -Therefore, it's still important to have an output that can keep up with the flow of data, the difference that a buffer makes is that the output only needs to keep up with the _average_ flow of data versus the instantaneous flow of data. - -For example, if your input usually produces 10 msgs/s, but occasionally spikes to 100 msgs/s, and your output can handle up to 50 msgs/s, it might be possible to configure a buffer large enough to store spikes in their entirety. As long as the average flow of messages from the input remains below 50 msgs/s then your service should be able to continue indefinitely without ever blocking the input source. - -## Maximising CPU Utilisation - -Some [processors][processors] within Benthos are relatively heavy on your CPU, and can potentially become the bottleneck of a service. In these circumstances it is worth configuring Benthos so that your processors are running on each available core of your machine without contention. - -An array of processors in any section of a Benthos config becomes a single logical pipeline of steps running on a single logical thread. The easiest way to create parallel processor threads is to configure them inside the [pipeline][pipeline] configuration block, where we can explicitly set any number of parallel processor threads independent of how many inputs or outputs we want to use. - -Please refer [to the documentation regarding pipelines][pipeline] for some examples. - -[pipeline]: /docs/configuration/processing_pipelines -[batching]: /docs/configuration/batching -[processors]: /docs/components/processors/about -[buffers]: /docs/components/buffers/about -[broker-input]: /docs/components/inputs/broker -[broker-output]: /docs/components/outputs/broker diff --git a/website/docs/guides/serverless/about.md b/website/docs/guides/serverless/about.md deleted file mode 100644 index ac7da54463..0000000000 --- a/website/docs/guides/serverless/about.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Serverless -sidebar_label: About ---- - -Currently the only supported serverless deployment of Benthos is for -[AWS Lambda][lambda], if you are interested in other platforms please -[raise an issue](https://github.com/benthosdev/benthos/issues). - -:::info Looking for something less manual? -Rather than bundle the distribution and configs yourself, -check out [makenew/serverless-benthos], which makes quick work of deploying -a Benthos serverless project on AWS Lambda. -For building and deploying distributions with custom plugins, -look at [makenew/benthos-plugin]. -::: - -## Platforms - -- [AWS Lambda][lambda] - -[lambda]: /docs/guides/serverless/lambda -[makenew/serverless-benthos]: https://github.com/makenew/serverless-benthos -[makenew/benthos-plugin]: https://github.com/makenew/benthos-plugin diff --git a/website/docs/guides/serverless/lambda.md b/website/docs/guides/serverless/lambda.md deleted file mode 100644 index e5c9822e42..0000000000 --- a/website/docs/guides/serverless/lambda.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Lambda -description: Deploying Benthos as an AWS Lambda function ---- - -The `benthos-lambda` distribution is a version of Benthos specifically tailored -for deployment as an AWS Lambda function on the `go1.x` runtime, -which runs Amazon Linux on the `x86_64` architecture. -The `benthos-lambda-al2` distribution supports the `provided.al2` runtime, -which runs Amazon Linux 2 on either the `x86_64` or `arm64` architecture. - -:::info Looking for something less manual? -Rather than bundle the distribution and configs yourself, -check out [makenew/serverless-benthos], which makes quick work of deploying -a Benthos serverless project on AWS Lambda. -For building and deploying distributions with custom plugins, -look at [makenew/benthos-plugin]. -::: - -It uses the same configuration format as a regular Benthos instance, which can be -provided in 1 of 2 ways: - -1. Inline via the `BENTHOS_CONFIG` environment variable (YAML format). -2. Via the filesystem using a layer, extension, or container image. By default, - the `benthos-lambda` distribution will look for a valid configuration file in - the locations listed below. Alternatively, the configuration file path can be - set explicity by passing a `BENTHOS_CONFIG_PATH` environment variable. - - `./benthos.yaml` - - `./config.yaml` - - `/benthos.yaml` - - `/etc/benthos/config.yaml` - - `/etc/benthos.yaml` - -Also, the `http`, `input` and `buffer` sections are ignored as the service wide -HTTP server is not used, and messages are inserted via function invocations. - -If the `output` section is omitted in your config then the result of the -processing pipeline is returned back to the caller, otherwise the resulting data -is sent to the output destination. - -### Running with an output - -The flow of a Benthos lambda function with an output configured looks like this: - -```text - benthos-lambda - +------------------------------+ - | | - -------> Processors ----> Output -----> Somewhere -invoke | | | - <-------------------------------------------/ - | | - | | - +------------------------------+ -``` - -Where the call will block until the output target has confirmed receipt of the -resulting payload. When the message is successfully propagated a JSON payload is -returned of the form `{"message":"request successful"}`, otherwise an error is -returned containing the reason for the failure. - -### Running without an output - -The flow when an output is not configured looks like this: - -```text - benthos-lambda - +--------------------+ - | | - -------> Processors --\ | -invoke | | | - <---------------------/ | - | | - | | - +--------------------+ -``` - -Where the function returns the result of processing directly back to the caller. -The format of the result differs depending on the number of batches and messages -of a batch that resulted from the invocation: - -- Single message of a single batch: `{}` (JSON object) -- Multiple messages of a single batch: `[{},{}]` (Array of JSON objects) -- Multiple batches: `[[{},{}],[{}]]` (Array of arrays of JSON objects, batches - of size one are a single object array in this case) - -#### Processing Errors - -The default behaviour of a Benthos lambda is that the handler will not return an -error unless the output fails. This means that errors that occur within your -processors will not result in the handler failing, which will instead return the -final state of the message. - -In the next major version release (V4) this will change and the handler will -fail if messages have encountered an uncaught error during execution. However, -in the meantime it is possible to configure your output to use the new -[`reject` output][output.reject] in order to trigger a handler error on -processor errors: - -```yaml -output: - switch: - retry_until_success: false - cases: - - check: '!errored()' - output: - sync_response: {} - - output: - reject: "processing failed due to: ${! error() }" -``` - -### Running a combination - -It's possible to configure pipelines that send messages to third party -destinations and also return a result back to the caller. This is done by -configuring an output block and including an output of the type -`sync_response`. - -For example, if we wished for our lambda function to send a payload to Kafka -and also return the same payload back to the caller we could use a -[broker][output-broker]: - -```yml -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: - - todo:9092 - client_id: benthos_serverless - topic: example_topic - - sync_response: {} -``` - -## Upload to AWS - -### go1.x on x86_64 - -Grab an archive labelled `benthos-lambda` from the [releases page][releases] -page and then create your function: - -```sh -LAMBDA_ENV=`cat yourconfig.yaml | jq -csR {Variables:{BENTHOS_CONFIG:.}}` -aws lambda create-function \ - --runtime go1.x \ - --handler benthos-lambda \ - --role benthos-example-role \ - --zip-file fileb://benthos-lambda.zip \ - --environment "$LAMBDA_ENV" \ - --function-name benthos-example -``` - -There is also an example [SAM template][sam-template] and -[Terraform resource][tf-example] in the repo to copy from. - -### provided.al2 on amd64 - -Grab an archive labelled `benthos-lambda-al2` for `arm64` from the [releases page][releases] -page and then create your function (AWS CLI v2 only): - -```sh -LAMBDA_ENV=`cat yourconfig.yaml | jq -csR {Variables:{BENTHOS_CONFIG:.}}` -aws lambda create-function \ - --runtime provided.al2 \ - --architectures arm64 \ - --handler not.used.for.provided.al2.runtime \ - --role benthos-example-role \ - --zip-file fileb://benthos-lambda.zip \ - --environment "$LAMBDA_ENV" \ - --function-name benthos-example -``` - -There is also an example [SAM template][sam-template-al2] and -[Terraform resource][tf-example-al2] in the repo to copy from. - -Note that you can also run `benthos-lambda-al2` on x86_64, just use the `amd64` zip instead. - -## Invoke - -```sh -aws lambda invoke \ - --function-name benthos-example \ - --payload '{"your":"document"}' \ - out.txt && cat out.txt && rm out.txt -``` - -## Build - -You can build and archive the function yourself with: - -```sh -go build github.com/benthosdev/benthos/v4/cmd/serverless/benthos-lambda -zip benthos-lambda.zip benthos-lambda -``` - -[releases]: https://github.com/benthosdev/benthos/releases -[sam-template]: https://github.com/benthosdev/benthos/tree/main/resources/serverless/lambda/benthos-lambda-sam.yaml -[tf-example]: https://github.com/benthosdev/benthos/tree/main/resources/serverless/lambda/benthos-lambda.tf -[sam-template-al2]: https://github.com/benthosdev/benthos/tree/main/resources/serverless/lambda/benthos-lambda-al2-sam.yaml -[tf-example-al2]: https://github.com/benthosdev/benthos/tree/main/resources/serverless/lambda/benthos-lambda-al2.tf -[output-broker]: /docs/components/outputs/broker -[output.reject]: /docs/components/outputs/reject -[makenew/serverless-benthos]: https://github.com/makenew/serverless-benthos -[makenew/benthos-plugin]: https://github.com/makenew/benthos-plugin diff --git a/website/docs/guides/streams_mode/about.md b/website/docs/guides/streams_mode/about.md deleted file mode 100644 index ee010ede4a..0000000000 --- a/website/docs/guides/streams_mode/about.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Streams Mode -sidebar_label: About -description: Find out about Benthos Streams mode ---- - -A Benthos stream consists of four components; an input, an optional buffer, processor pipelines and an output. Under normal use a Benthos instance is a single stream, and these components are configured within the service config file. - -Alternatively, Benthos can be run in `streams` mode, where a single running Benthos instance is able to run multiple entirely isolated streams. Adding streams in this mode can be done in two ways: - -1. [Static configuration files][static-files] allows you to maintain a directory of static stream configuration files that will be traversed by Benthos. - -2. An [HTTP REST API][rest-api] allows you to dynamically create, read the status of, update, and delete streams at runtime. - -These two methods can be used in combination, i.e. it's possible to update and delete streams that were created with static files. - -When running Benthos in streams mode it is still necessary to provide a general service wide configuration with the `-c`/`--config` flag that specifies observability configuration such as the `metrics`, `logger` and `tracing` sections, as well the `http` section for configuring how the HTTP server should behave. - -You can import resources either in the general configuration, or using the `-r`/`--resources` flag, the same as when running Benthos in regular mode. - -```sh -benthos -r "./prod/*.yaml" -c ./config.yaml streams -``` - -## HTTP Endpoints - -A Benthos config can contain components such as an `http_server` input that register endpoints to the service-wide HTTP server. When these components are created from within a named stream in streams mode the endpoint will be prefixed with the streams identifier by default. For example, a stream with the identifier `foo` and the config: - -```yaml -input: - http_server: - path: /meow -pipeline: - processors: - - mapping: 'root = "meow " + content()' -output: - sync_response: {} -``` - -Will register an endpoint `/meow`, which will be prefixed with the name `foo` to become `/foo/meow`. This behaviour is intended to make a clearer distinction between endpoints registered by different streams, and prevent collisions of those endpoints. However, you can disable this behaviour by setting the flag `--prefix-stream-endpoints` to `false` (`benthos streams --prefix-stream-endpoints=false ./streams/*.yaml`). - -## Resources - -When running Benthos in streams mode [resource components][resources] are shared across all streams. The streams mode HTTP API also provides an endpoint for modifying and adding resource configurations dynamically. - -## Metrics - -Metrics from all streams are aggregated and exposed via the method specified in [the config][metrics] of the Benthos instance running in `streams` mode, with their metrics enriched with the tag `stream` containing the stream name. - -For example, a Benthos instance running in streams mode running a stream named `foo` would have metrics from `foo` registered with the label `stream` with the value of `foo`. - -This can cause problems if your streams are short lived and uniquely named as the number of metrics registered will continue to climb indefinitely. In order to avoid this you can use the `mapping` field to filter metric names. - -```yaml -# Only register metrics for the stream `foo`. Others will be ignored. -metrics: - mapping: if meta("stream") != "foo" { deleted() } - prometheus: {} -``` - -[static-files]: /docs/guides/streams_mode/using_config_files -[rest-api]: /docs/guides/streams_mode/using_rest_api -[metrics]: /docs/components/metrics/about -[resources]: /docs/configuration/resources diff --git a/website/docs/guides/streams_mode/streams_api.md b/website/docs/guides/streams_mode/streams_api.md deleted file mode 100644 index dfdf5ecd56..0000000000 --- a/website/docs/guides/streams_mode/streams_api.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: Streams API ---- - -When Benthos is run in `streams` mode it will open up an HTTP REST API for creating and managing independent streams of data instead of creating a single stream. - -Each stream has its own input, buffer, pipeline and output sections which contains an isolated stream of data with its own lifetime. A stream config cannot include [resources][resources], and instead these should be created and modified using the `/resources/{type}/{id}` endpoint. - -A walkthrough on using this API [can be found here][streams-api-walkthrough]. - -## API - -### GET `/ready` - -Returns a 200 OK response if all active streams are connected to their respective inputs and outputs at the time of the request. Otherwise, a 503 response is returned along with a message naming the faulty stream. - -If zero streams are active this endpoint still returns a 200 OK response. - -### GET `/streams` - -Returns a map of existing streams by their unique identifiers to an object showing their status and uptime. - -#### Response 200 - -```json -{ - "": { - "active": "", - "uptime": "", - "uptime_str": "" - } -} -``` - -### POST `/streams` - -Sets the entire collection of streams to the body of the request. Streams that exist but aren't within the request body are *removed*, streams that exist already and are in the request body are updated, other streams within the request body are created. - -```json -{ - "": "" -} -``` - -#### Response 200 - -The streams were updated successfully. - -#### Response 400 - -A configuration was invalid, or has linting errors. If linting errors were detected then a JSON response is provided of the form: - -```json -{ - "linting_errors": [ - "", - "uptime": "", - "uptime_str": "", - "config": "" -} -``` - -### PUT `/streams/{id}` - -Update an existing stream identified by `id` by posting a body containing the new stream configuration in either JSON or YAML format. The configuration should be a standard Benthos configuration containing the sections `input`, `buffer`, `pipeline` and `output`. - -The previous stream will be shut down before and a new stream will take its place. - -#### Response 200 - -The stream was updated successfully. - -#### Response 400 - -The configuration was invalid, or has linting errors. If linting errors were detected then a JSON response is provided of the form: - -```json -{ - "linting_errors": [ - " ./streams/foo.yaml < ./streams/bar.yaml < - -Input (AMQP) -> Processors -> Output (AMQP) - - <------- Acknowledgement --------- -``` - -However, Benthos has support for a number of protocols where this limitation is not the case. - -For example, HTTP is a request/response protocol, and so our `http_server` input is capable of returning a response payload after consuming a message from a request. - -When using these protocols it's possible to configure Benthos stream pipelines that allow messages to pass in the opposite direction, resulting in response messages at the input level: - -```text - --------- Request Body --------> - -Input (HTTP Server) -> Processors -> Output (Sync Response) - - <--- Response Body (and ack) --- -``` - -## Routing Processed Messages Back - -It's possible to route the result of any Benthos processing pipeline directly back to an input with a [`sync_response`][sync-res] output: - -```yaml -input: - http_server: - path: /post -pipeline: - processors: - - mapping: root = content().uppercase() -output: - sync_response: {} -``` - -Using the above example, sending a request 'foo bar' to the path `/post` returns the response 'FOO BAR'. - -It's also possible to combine a `sync_response` output with other outputs using a [`broker`][output-broker]: - -```yaml -input: - http_server: - path: /post -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ TODO:9092 ] - topic: foo_topic - - sync_response: {} - processors: - - mapping: root = content().uppercase() -``` - -Using the above example, sending a request 'foo bar' to the path `/post` passes the message unchanged to the Kafka topic `foo_topic` and also returns the response 'FOO BAR'. - -:::note -It's safe to use these mechanisms even when combining multiple inputs with a broker, a response payload will always be routed back to the original source of the message. -::: - -## Returning Partially Processed Messages - -It's possible to set the state of a message to be the synchronous response before processing is finished by using the [`sync_response` processor][sync-res-proc]. This allows you to further mutate the payload without changing the response returned to the input: - -```yaml -input: - http_server: - path: /post - -pipeline: - processors: - - mapping: root = "%v baz".format(content().string()) - - sync_response: {} - - mapping: root = content().uppercase() - -output: - kafka: - addresses: [ TODO:9092 ] - topic: foo_topic -``` - -Using the above example, sending a request 'foo bar' to the path `/post` passes the message 'FOO BAR BAZ' to the Kafka topic `foo_topic`, and also returns the response 'foo bar baz'. - -However, it is important to keep in mind that due to Benthos' strict delivery guarantees the response message will not actually be returned until the message has reached its output destination and an acknowledgement can be made. - -## Routing Output Responses Back - -Some outputs, such as [`http_client`][http-client-output], have the potential to propagate payloads received from their destination after sending a message back to the input: - -```yaml -input: - http_server: - path: /post -output: - http_client: - url: http://localhost:4196/post - verb: POST - propagate_response: true -``` - -With the above example a message received from the endpoint `/post` would be sent unchanged to the address `http://localhost:4196/post`, and then the response from that request would get returned back. This basically turns Benthos into a proxy server with the potential to mutate payloads between requests. - -The following config turns Benthos into an HTTP proxy server that also sends all request payloads to a Kafka topic: - -```yaml -input: - http_server: - path: /post -output: - broker: - pattern: fan_out - outputs: - - kafka: - addresses: [ TODO:9092 ] - topic: foo_topic - - http_client: - url: http://localhost:4196/post - verb: POST - propagate_response: true -``` - -[sync-res]: /docs/components/outputs/sync_response -[sync-res-proc]: /docs/components/processors/sync_response -[http-client-output]: /docs/components/outputs/http_client -[output-broker]: /docs/components/outputs/broker \ No newline at end of file diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js deleted file mode 100755 index 6bd3b61833..0000000000 --- a/website/docusaurus.config.js +++ /dev/null @@ -1,167 +0,0 @@ -const path = require('path'); -const {components} = require('./src/plugins/components'); - -module.exports = { - title: 'Benthos', - tagline: 'Fancy stream processing made operationally mundane', - url: 'https://www.benthos.dev', - baseUrl: '/', - favicon: 'img/favicon.ico', - organizationName: 'benthosdev', - projectName: 'benthos', - customFields: { - components: { - inputs: components("inputs"), - processors: components("processors"), - outputs: components("outputs"), - caches: components("caches"), - rate_limits: components("rate_limits"), - buffers: components("buffers"), - metrics: components("metrics"), - tracers: components("tracers"), - scanners: components("scanners"), - }, - }, - themeConfig: { - prism: { - theme: require('./src/plugins/prism_themes/github'), - darkTheme: require('./src/plugins/prism_themes/monokai'), - }, - colorMode: { - defaultMode: 'light', - }, - image: 'img/og_img.png', - metadata: [ - {name: 'keywords', content: 'benthos, stream processor, data engineering, ETL, ELT, event processor, go, golang'}, - {name: 'twitter:card', content: 'summary'}, - ], - navbar: { - title: 'Benthos', - logo: { - alt: 'Benthos Blobfish', - src: 'img/logo.svg', - }, - items: [ - {to: 'docs/about', label: 'Docs', position: 'left'}, - {to: 'cookbooks', label: 'Cookbooks', position: 'left'}, - {to: 'https://studio.benthos.dev', label: 'Studio', position: 'left'}, - {to: 'blog', label: 'Blog', position: 'left'}, - {to: 'community', label: 'Community', position: 'right'}, - {to: 'support', label: 'Support', position: 'right'}, - { - href: 'https://github.com/benthosdev/benthos/releases/latest', - position: 'right', - className: 'header-download-link header-icon-link', - 'aria-label': 'Download', - }, - { - href: 'https://github.com/benthosdev/benthos', - position: 'right', - className: 'header-github-link header-icon-link', - 'aria-label': 'GitHub repository', - }, - ], - }, - footer: { - style: 'dark', - links: [ - { - title: 'Help', - items: [ - { - label: 'Support', - to: 'support', - }, - { - label: 'Documentation', - to: 'docs/guides/getting_started', - }, - { - label: 'Videos', - to: 'videos', - }, - ], - }, - { - title: 'Swag', - items: [ - { - label: 'Meet the Mascot', - to: 'blobfish', - }, - { - label: 'Purchase Stickers', - href: 'https://www.redbubble.com/people/earzola/shop', - }, - ], - }, - { - title: 'Community', - items: [ - { - label: 'Join the chat', - to: 'community', - }, - { - label: 'See the Code', - href: 'https://github.com/benthosdev/benthos', - }, - { - label: 'Sponsor the Developers', - href: 'https://github.com/sponsors/Jeffail', - }, - ], - }, - ], - copyright: `Copyright © ${new Date().getFullYear()} Ashley Jeffs.`, - }, - announcementBar: { - id: 'star_the_dang_repo', - content: `Hey, 🫵 you, make sure you've ⭐ starred the repo ⭐ otherwise you won't be entered into our daily prize draw for silent admiration.`, - backgroundColor: 'var(--ifm-color-primary)', - textColor: 'var(--ifm-background-color)', - isCloseable: true, - }, - algolia: { - appId: 'WBY9Z65YR4', - apiKey: 'a6c476911e6ecef76049a55d9798a51b', - indexName: 'benthos', - contextualSearch: true - } - }, - presets: [ - [ - '@docusaurus/preset-classic', - { - docs: { - sidebarPath: require.resolve('./sidebars.js'), - editUrl: - 'https://github.com/benthosdev/benthos/edit/main/website/', - }, - theme: { - customCss: require.resolve('./src/css/custom.css'), - }, - blog: { - feedOptions: { - type: 'all', - }, - }, - }, - ], - ], - plugins: [ - path.resolve(__dirname, './src/plugins/analytics'), - [ - require.resolve("./src/plugins/cookbooks/compiled/index"), - { - path: 'cookbooks', - routeBasePath: 'cookbooks', - include: ['*.md', '*.mdx'], - exclude: [], - guideListComponent: '@theme/CookbookListPage', - guidePostComponent: '@theme/CookbookPage', - }, - ], - ], -}; - diff --git a/website/package.json b/website/package.json deleted file mode 100755 index c6d0a4ea4d..0000000000 --- a/website/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "benthos", - "version": "0.0.0", - "private": true, - "engines": { - "node": ">=14.18.3" - }, - "scripts": { - "prestart": "sh build_plugins.sh", - "start": "docusaurus start -h 0.0.0.0", - "prebuild": "sh build_plugins.sh", - "build": "docusaurus build", - "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy" - }, - "dependencies": { - "@algolia/client-search": "^4.17.0", - "@docusaurus/core": "^3.0.0", - "@docusaurus/preset-classic": "^3.0.0", - "@types/node": "^18.15.13", - "classnames": "^2.3.2", - "react": "18", - "react-dom": "18", - "react-loadable": "^5.5.0", - "react-player": "^2.12.0", - "ts-node": "^10.9.1", - "typescript": "^5" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@docusaurus/module-type-aliases": "^3.0.0", - "@docusaurus/tsconfig": "^3.0.0", - "@types/fs-extra": "^11.0.1" - } -} diff --git a/website/sidebars.js b/website/sidebars.js deleted file mode 100755 index e5cce10f13..0000000000 --- a/website/sidebars.js +++ /dev/null @@ -1,151 +0,0 @@ -const {listPaths} = require('./src/plugins/components'); - -let inputs_docs = listPaths("inputs"); -let processors_docs = listPaths("processors"); -let outputs_docs = listPaths("outputs"); -let caches_docs = listPaths("caches"); -let rate_limits_docs = listPaths("rate_limits"); -let buffers_docs = listPaths("buffers"); -let metrics_docs = listPaths("metrics"); -let tracers_docs = listPaths("tracers"); -let scanners_docs = listPaths("scanners"); - -module.exports = { - docs: [ - { - type: 'doc', - id: 'about', - }, - { - type: 'category', - label: 'Configuration', - items: [ - 'configuration/about', - 'configuration/resources', - 'configuration/batching', - 'configuration/windowed_processing', - 'configuration/metadata', - 'configuration/error_handling', - 'configuration/interpolation', - 'configuration/secrets', - 'configuration/field_paths', - 'configuration/processing_pipelines', - 'configuration/unit_testing', - 'configuration/templating', - 'configuration/dynamic_inputs_and_outputs', - 'configuration/using_cue', - ], - }, - { - type: 'category', - label: 'Components', - items: [ - 'components/about', - 'components/http/about', - { - type: 'category', - label: 'Inputs', - items: inputs_docs, - }, - { - type: 'category', - label: 'Scanners', - items: scanners_docs, - }, - { - type: 'category', - label: 'Processors', - items: processors_docs, - }, - { - type: 'category', - label: 'Outputs', - items: outputs_docs, - }, - { - type: 'category', - label: 'Caches', - items: caches_docs, - }, - { - type: 'category', - label: 'Rate Limits', - items: rate_limits_docs, - }, - { - type: 'category', - label: 'Buffers', - items: buffers_docs, - }, - { - type: 'category', - label: 'Metrics', - items: metrics_docs, - }, - { - type: 'category', - label: 'Tracers', - items: tracers_docs, - }, - 'components/logger/about' - ], - }, - { - type: 'category', - label: 'Guides', - items: [ - 'guides/getting_started', - { - type: 'category', - label: 'Bloblang', - items: [ - 'guides/bloblang/about', - 'guides/bloblang/walkthrough', - 'guides/bloblang/functions', - 'guides/bloblang/methods', - 'guides/bloblang/arithmetic', - 'guides/bloblang/advanced', - ], - }, - 'guides/monitoring', - 'guides/performance_tuning', - 'guides/sync_responses', - { - type: 'category', - label: 'Cloud Credentials', - items: [ - 'guides/cloud/aws', - 'guides/cloud/gcp', - ], - }, - { - type: 'category', - label: 'Serverless', - items: [ - 'guides/serverless/about', - 'guides/serverless/lambda', - ], - }, - { - type: 'category', - label: 'Streams Mode', - items: [ - 'guides/streams_mode/about', - 'guides/streams_mode/using_config_files', - 'guides/streams_mode/using_rest_api', - 'guides/streams_mode/streams_api', - ], - }, - { - type: 'category', - label: 'Migration', - items: [ - 'guides/migration/v4', - 'guides/migration/v3', - 'guides/migration/v2', - ] - } - ], - }, - ], -}; diff --git a/website/src/css/custom.css b/website/src/css/custom.css deleted file mode 100755 index b5004fb27f..0000000000 --- a/website/src/css/custom.css +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - -:root { - --ifm-color-primary: #3578e5; - --ifm-color-primary-dark: #1d68e1; - --ifm-color-primary-darker: #1b62d4; - --ifm-color-primary-darkest: #1751af; - --ifm-color-primary-light: #4e89e8; - --ifm-color-primary-lighter: #5a91ea; - --ifm-color-primary-lightest: #80aaef; - - --ifm-color-danger: #ff6188; - --ifm-color-danger-dark: #ff3e6d; - --ifm-color-danger-darker: #ff2c60; - --ifm-color-danger-darkest: #f6003d; - --ifm-color-danger-light: #ff84a3; - --ifm-color-danger-lighter: #ff96b0; - --ifm-color-danger-lightest: #ffcbd8; - - --ifm-color-success: #a9dc76; - --ifm-color-success-dark: #98d55b; - --ifm-color-success-darker: #90d24e; - --ifm-color-success-darkest: #76bc30; - --ifm-color-success-light: #bae391; - --ifm-color-success-lighter: #c2e69e; - --ifm-color-success-lightest: #dcf1c7; - - --ifm-color-info: #78dce8; - --ifm-color-info-dark: #5ad4e3; - --ifm-color-info-darker: #4bd0e0; - --ifm-color-info-darkest: #24c0d3; - --ifm-color-info-light: #96e4ed; - --ifm-color-info-lighter: #a5e8f0; - --ifm-color-info-lightest: #d2f3f7; - - --ifm-color-warning: #ffd866; - --ifm-color-warning-dark: #ffcf42; - --ifm-color-warning-darker: #ffca30; - --ifm-color-warning-darkest: #faba00; - --ifm-color-warning-light: #ffe18a; - --ifm-color-warning-lighter: #ffe69c; - --ifm-color-warning-lightest: #fff3d1; - - --ifm-navbar-background-color: #f9f9f9; - --ifm-background-surface-color: #f9f9f9; - --ifm-background-surface-color-secondary: #f3f3f3; - --ifm-background-color: #ffffff; - - --ifm-heading-color: #000000; - --ifm-hero-text-color: #000000; - --ifm-blockquote-color: #3c3c3c; - --ifm-alert-color: #000000; - - --ifm-code-font-size: 95%; -} - -.footer.footer--dark { - --ifm-footer-background-color: #272822; -} - -html[data-theme="dark"] { - --ifm-color-primary: #3bd1ff; - --ifm-color-primary-dark: #1ccaff; - --ifm-color-primary-darker: #0cc6ff; - --ifm-color-primary-darkest: #00a8dc; - --ifm-color-primary-light: #5ad8ff; - --ifm-color-primary-lighter: #6adcff; - --ifm-color-primary-lightest: #99e7ff; - - --ifm-background-color: #1f201c; - --ifm-background-surface-color: #272822; - --ifm-background-surface-color-secondary: #2a2b25; - --ifm-navbar-background-color: #272822; - - --ifm-code-background: #2e2f28; - --ifm-code-color: #ffffff; - - background-color: #1f201c; - - --ifm-heading-color: #ffffff; - --ifm-hero-text-color: #ffffff; - --ifm-color-secondary: #f0f0f0; - --ifm-blockquote-color: #c0c0c0; -} - -code { - background-color: var(--ifm-code-background) !important; - color: var(--ifm-code-color) !important; -} - -a > code { - color: var(--ifm-link-color) !important; -} - -a.contents__link > code { - color: var(--ifm-code-color); -} - -.menu--responsive > button { - background-color: var(--ifm-color-primary); - border-color: var(--ifm-color-primary); -} - -.header-icon-link { - padding: var(--ifm-navbar-item-padding-vertical) calc(var(--ifm-navbar-item-padding-horizontal) * 0.8); -} - -.header-icon-link:hover { - opacity: 0.6; -} - -.header-download-link:before { - content: ''; - width: 24px; - height: 24px; - display: flex; - - background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 0 24 24' width='24'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z'/%3E%3C/svg%3E") no-repeat; -} - -html[data-theme='dark'] .header-download-link:before { - background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 0 24 24' width='24'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z' style='fill:%23ffffff'/%3E%3C/svg%3E") no-repeat; -} - -.header-github-link:before { - content: ''; - width: 24px; - height: 24px; - display: flex; - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} - -html[data-theme='dark'] .header-github-link:before { - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} - -html[data-theme='dark'] .DocSearch { - --docsearch-text-color: var(--ifm-font-color-base); - --docsearch-muted-color: var(--ifm-color-secondary-darkest); - --docsearch-container-background: #2e2f28; - --docsearch-modal-background: var(--ifm-background-color); - --docsearch-searchbox-background: var(--ifm-background-color); - --docsearch-searchbox-focus-background: var(--ifm-color-black); - --docsearch-hit-color: var(--ifm-font-color-base); - --docsearch-hit-active-color: var(--ifm-color-white); - --docsearch-hit-background: var(--ifm-color-emphasis-100); - --docsearch-footer-background: var(--ifm-background-surface-color); - --docsearch-key-gradient: linear-gradient( - -26.5deg, - var(--ifm-color-emphasis-200) 0%, - var(--ifm-color-emphasis-100) 100% - ); -} - -.markdown > table { - border: none; - min-width: 160px; -} - -.markdown > table td { - min-width: 160px; -} diff --git a/website/src/exports/redirect.js b/website/src/exports/redirect.js deleted file mode 100644 index 41f52ae364..0000000000 --- a/website/src/exports/redirect.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import {Redirect} from '@docusaurus/router'; - -function To(props) { - let {location} = props; - let {to, forComponent} = props.dest; - - if ( forComponent ) { - if ( location.hash && location.hash.length > 1 ) { - to = to + location.hash.slice(1); - } else { - to = to + '/about'; - } - } - - return -} - -export default To; diff --git a/website/src/pages/blobfish.module.css b/website/src/pages/blobfish.module.css deleted file mode 100755 index c45e8b194e..0000000000 --- a/website/src/pages/blobfish.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.emoji { - height: 64px; - width: 64px; - max-width: unset; -} - -.headerImg { - height: 180px; -} - -.emojiContainer { - display: flex; - justify-content: center; - flex-wrap: wrap; -} \ No newline at end of file diff --git a/website/src/pages/blobfish.tsx b/website/src/pages/blobfish.tsx deleted file mode 100644 index 6fdf845290..0000000000 --- a/website/src/pages/blobfish.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; - -import Layout from '@theme/Layout'; - -import classnames from 'classnames'; -import styles from './blobfish.module.css'; - -const emojis = [ - "blob.png", - "blobok.png", - "blobyes.png", - "blobno.png", - "blobcool.png", - "blobkiss.png", - "blobhug.png", - "blobthanks.png", - "blobthinking.png", - "blobpalm.png", - "blobwave.png", - "blobshrug.png", - "blobheart.png", - "blobheartpuke.png", - "blobpuke.png", - "blobsweat.png", - "blobsweatsmile.png", - "blobnervous.png", - "blobcrying.png", - "blobcrylaugh.png", - "blobnaughty.png", - "blobmad.png", - "blobbot.png", - "blobemo.png", - "blobnerd.png", - "cowblob.png", - "blobbug.png", - "blobswag.png", - "blobpirate.png", - "blobbounce.gif", - "blobtrance.gif", -]; - -function Blobfish() { - return ( - -
-
-
-
- -
-

The Benthos Blobfish

-
- The official Benthos mascot. -
-
-
-
-
-
-
-
-
-
-
-

-The Benthos blobfish began its life as a dumb placeholder logo, and since nothing has changed this is still the case. -

-

-Variations of the mascot such as the dapper captain blobfish and the utterly flawless chef blobish were created with love by Esther Arzola. Sticker packs and other swag can be purchased directly from https://www.redbubble.com/people/earzola/shop. -

-
-
-
-
-
-
-
-
-

Emojis

-

Use these with great care.

-
-
-
-
- {emojis.map((emoji, idx) => ( - - - - ))} -
-
-
-
-
-
- ); -} - -export default Blobfish; \ No newline at end of file diff --git a/website/src/pages/community.module.css b/website/src/pages/community.module.css deleted file mode 100644 index be3f4c8d33..0000000000 --- a/website/src/pages/community.module.css +++ /dev/null @@ -1,72 +0,0 @@ -.icon { - color: var(--ifm-navbar-link-color); - font-weight: var(--ifm-font-weight-semibold); - padding: var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal); - display: inline-block; -} - -.headerImg { - padding-top: 20px; - height: 180px; -} - -.twitter:before { - content: ''; - width: 48px; - height: 48px; - display: flex; - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z'/%3E%3C/svg%3E") no-repeat; -} - -html[data-theme='dark'] .twitter:before { - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z'/%3E%3C/svg%3E") no-repeat; -} - -.chat:before { - content: ''; - width: 48px; - height: 48px; - display: flex; - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z'/%3E%3C/svg%3E") no-repeat; -} - -html[data-theme='dark'] .chat:before { - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z'/%3E%3C/svg%3E") no-repeat; -} - -.email:before { - content: ''; - width: 48px; - height: 48px; - display: flex; - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z'/%3E%3Cpolyline points='22,6 12,13 2,6'/%3E%3C/svg%3E") no-repeat; -} - -html[data-theme='dark'] .email:before { - background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z'/%3E%3Cpolyline points='22,6 12,13 2,6'/%3E%3C/svg%3E") no-repeat; -} - -.slack:before { - content: ''; - width: 48px; - height: 48px; - display: flex; - background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-slack'%3E%3Cpath d='M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z'%3E%3C/path%3E%3Cpath d='M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z'%3E%3C/path%3E%3Cpath d='M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z'%3E%3C/path%3E%3Cpath d='M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z'%3E%3C/path%3E%3Cpath d='M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z'%3E%3C/path%3E%3Cpath d='M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z'%3E%3C/path%3E%3Cpath d='M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z'%3E%3C/path%3E%3Cpath d='M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z'%3E%3C/path%3E%3C/svg%3E") no-repeat; -} - -html[data-theme='dark'] .slack:before { - background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-slack'%3E%3Cpath d='M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z'%3E%3C/path%3E%3Cpath d='M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z'%3E%3C/path%3E%3Cpath d='M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z'%3E%3C/path%3E%3Cpath d='M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z'%3E%3C/path%3E%3Cpath d='M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z'%3E%3C/path%3E%3Cpath d='M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z'%3E%3C/path%3E%3Cpath d='M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z'%3E%3C/path%3E%3Cpath d='M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z'%3E%3C/path%3E%3C/svg%3E") no-repeat; -} - -.discord:before { - content: ''; - width: 48px; - height: 48px; - display: flex; - - background: url("data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='UTF-8'?%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='mdi-discord' viewBox='0 0 24 24'%3E%3Cpath d='M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z' /%3E%3C/svg%3E") no-repeat; -} - -html[data-theme='dark'] .discord:before { - background: url("data:image/svg+xml;charset=utf7,%3C?xml version='1.0' encoding='UTF-9'?%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='mdi-discord' fill='white' viewBox='0 0 24 24'%3E%3Cpath d='M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z' /%3E%3C/svg%3E") no-repeat; -} diff --git a/website/src/pages/community.tsx b/website/src/pages/community.tsx deleted file mode 100644 index 4dd940902f..0000000000 --- a/website/src/pages/community.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; - -import styles from './community.module.css'; -import classnames from 'classnames'; -import Layout from '@theme/Layout'; -import Link from '@docusaurus/Link'; - -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; - -function Community() { - const context = useDocusaurusContext(); - - return ( - -
-
-

Community

-
- These are places where you can ask questions and find your soul mate (no promises). -
- -
-
-
-
-
-
-
-
- -
-
-

Join the official Benthos discord server

-
-
- Join -
-
-
- -
-
-
- -
-
-

Aggressively @mention Ash on Twitter

-
-
- Follow @Jeffail -
-
-
- -
-
-
- -
-
-

Join us on the #benthos channel in the Gophers slack

-
-
- Get an invite -
-
- Open -
-
-
- -
-
-
- -
-
-

For sensitive or security related queries pop us an email

-
-
- hello@benthos.dev -
-
-
-
-
-
-
- ); -} - -export default Community; \ No newline at end of file diff --git a/website/src/pages/index.module.css b/website/src/pages/index.module.css deleted file mode 100755 index b434c43dfc..0000000000 --- a/website/src/pages/index.module.css +++ /dev/null @@ -1,106 +0,0 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 966px) { - .heroBanner { - padding: 2rem; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} - -.features { - display: flex; - align-items: center; - padding: 2rem 0; - width: 100%; -} - -.featureImage { - height: 180px; -} - -.heroImg { - height: 200px; - margin: 10px 0; -} - -.configSnippets { - margin: 10px 0; -} - -.pitch { - margin: 40px 0; -} - -.loveSection { - padding: 40px 0; - background-color: #602541; - - --ifm-heading-color: #ffffff; -} - -.loveSectionPlea { - text-align: center; -} - -.loveImg { - height: 200px; -} - -.goldSponsors { - --logo-height: 100px; - --basis: calc(100% - 10px); - margin: 10px 0 50px 0; -} - -.silverSponsors { - --logo-height: 50px; - --basis: calc(50% - 10px); -} - -.sponsorsBox { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: center; - gap: 20px; -} - -.sponsorsBox > a { - flex-basis: var(--basis); - flex-grow: 0; -} - -.goldSponsors > a > img { - width: 100%; -} - -.sponsorsBox > a > img { - max-width: 100%; - max-height: var(--logo-height); -} - -.synadiaImg { - height: 90px; - margin: 10px 0 50px 0; -} - -.furtherButton { - position: absolute; - right: calc(var(--ifm-pre-padding) / 2); - bottom: calc(var(--ifm-pre-padding) / 2); -} diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx deleted file mode 100755 index d14e44059b..0000000000 --- a/website/src/pages/index.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import ReactPlayer from 'react-player/youtube' -import Layout from '@theme/Layout'; -import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import useBaseUrl from '@docusaurus/useBaseUrl'; -import styles from './index.module.css'; -import CodeBlock from "@theme/CodeBlock"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -const installs = [ - { - label: 'Curl', - language: 'bash', - children: `# Install -curl -Lsf https://www.benthos.dev/sh/install | bash - -# Make a config -benthos create nats/protobuf/aws_sqs > ./config.yaml - -# Run -benthos -c ./config.yaml` - }, - { - label: 'Homebrew', - language: 'bash', - children: `# Install -brew install benthos - -# Make a config -benthos create nats/protobuf/aws_sqs > ./config.yaml - -# Run -benthos -c ./config.yaml` - }, - { - label: 'Docker', - language: 'bash', - children: `# Pull -docker pull ghcr.io/benthosdev/benthos - -# Make a config -docker run --rm ghcr.io/benthosdev/benthos create nats/protobuf/aws_sqs > ./config.yaml - -# Run -docker run --rm -v $(pwd)/config.yaml:/benthos.yaml ghcr.io/benthosdev/benthos` - }, - { - label: 'Asdf', - language: 'bash', - children: `# Install -asdf plugin add benthos -asdf install benthos latest -asdf global benthos latest - -# Make a config -benthos create nats/protobuf/aws_sqs > ./config.yaml - -# Run -benthos -c ./config.yaml` - }, -] - -const snippets = [ - { - label: 'Mapping', - further: '/docs/guides/bloblang/about', - language: 'yaml', - children: `input: - gcp_pubsub: - project: foo - subscription: bar - -pipeline: - processors: - - mapping: | - root.message = this - root.meta.link_count = this.links.length() - root.user.age = this.user.age.number() - -output: - redis_streams: - url: tcp://TODO:6379 - stream: baz - max_in_flight: 20`, - }, - { - label: 'Multiplexing', - further: '/docs/components/outputs/about#multiplexing-outputs', - language: 'yaml', - children: `input: - kafka: - addresses: [ TODO ] - topics: [ foo, bar ] - consumer_group: foogroup - -output: - switch: - cases: - - check: doc.tags.contains("AWS") - output: - aws_sqs: - url: https://sqs.us-west-2.amazonaws.com/TODO/TODO - max_in_flight: 20 - - - output: - redis_pubsub: - url: tcp://TODO:6379 - channel: baz - max_in_flight: 20`, - }, - { - label: 'Windowing', - further: '/docs/configuration/windowed_processing', - language: 'yaml', - children: `input: - nats_jetstream: - urls: [ nats://TODO:4222 ] - queue: myqueue - subject: traffic.light.events - deliver: all - -buffer: - system_window: - timestamp_mapping: root = this.created_at - size: 1h - -pipeline: - processors: - - group_by_value: - value: '\${! json("traffic_light_id") }' - - mapping: | - root = if batch_index() == 0 { - { - "traffic_light_id": this.traffic_light_id, - "created_at": @window_end_timestamp, - "total_cars": json("registration_plate").from_all().unique().length(), - "passengers": json("passengers").from_all().sum(), - } - } else { deleted() } - -output: - http_client: - url: https://example.com/traffic_data - verb: POST - max_in_flight: 64`, - }, - { - label: 'Enrichments', - further: '/cookbooks/enrichments', - language: 'yaml', - children: `input: - mqtt: - urls: [ tcp://TODO:1883 ] - topics: [ foo ] - -pipeline: - processors: - - branch: - request_map: | - root.id = this.doc.id - root.content = this.doc.body - processors: - - aws_lambda: - function: sentiment_analysis - result_map: root.results.sentiment = this - -output: - aws_s3: - bucket: TODO - path: '\${! meta("partition") }/\${! timestamp_unix_nano() }.tar.gz' - batching: - count: 100 - period: 10s - processors: - - archive: - format: tar - - compress: - algorithm: gzip`, - }, -]; - -const features = [ - { - title: 'Takes Care of the Dull Stuff', - imageUrl: 'img/Blobboring.svg', - description: ( - <> -

- Benthos solves common data engineering tasks such as transformations, integrations, and multiplexing with declarative and unit testable configuration. This allows you to easily and incrementally adapt your data pipelines as requirements change, letting you focus on the more exciting stuff. -

-

- It comes armed with a wide range of processors, a lit mapping language, stateless windowed processing capabilities and an industry leading mascot. -

- - ), - }, - { - title: 'Well Connected', - imageUrl: 'img/Blobborg.svg', - description: ( - <> -

- Benthos is able to glue a wide range of sources and sinks together and hook into a variety of databases, caches, HTTP APIs, lambdas and more, enabling you to seamlessly drop it into your existing infrastructure. -

-

- Working with disparate APIs and services can be a daunting task, doubly so in a streaming data context. With Benthos it's possible to break these tasks down and automatically parallelize them as a streaming workflow. -

- - ), - }, - { - description: ( - - ), - }, - { - title: 'Create, Test and Deploy Configs Visually', - imageUrl: 'img/Blobartist.svg', - description: ( - <> -

Declarative YAML is great for seamlessly integrating with version control tools, but creating and editing large configs can become a right bother.

-

Benthos Studio is a visual web application that allows you to create/import configs, edit, test, share and deploy them. It's so fun you'll be making configs just for the heck of it! Loser.

- - ), - }, - { - title: 'Reliable and Operationally Simple', - imageUrl: 'img/Blobscales.svg', - description: ( - <> -

- Delivery guarantees can be a dodgy subject. Benthos processes and acknowledges messages using an in-process transaction model with no need for any disk persisted state, so when connecting to at-least-once sources and sinks it's able to guarantee at-least-once delivery even in the event of crashes, disk corruption, or other unexpected server faults. -

-

- This behaviour is the default and free of caveats, which also makes deploying and scaling Benthos much simpler. However, simplicity doesn't negate the need for observability, so it also exposes metrics and tracing events to targets of your choice. -

- - ), - }, - { - title: 'Extendable', - imageUrl: 'img/Blobextended.svg', - description: ( - <> -

- Sometimes the components that come with Benthos aren't enough. Luckily, Benthos has been designed to be easily plugged with whatever components you need. -

-

- You can either write plugins directly in Go (recommended) or you can have Benthos run your plugin as a subprocess. -

- - ), - }, -]; - -interface FeatureArgs { - imageUrl?: string; - title?: string; - description: JSX.Element; -}; - -function Feature({imageUrl, title, description}: FeatureArgs) { - const imgUrl = useBaseUrl(imageUrl); - return ( -
- {imgUrl && ( -
- {title} -
- )} -

{title}

- {description} -
- ); -} - -function Home() { - const context = useDocusaurusContext(); - const siteConfig = context.siteConfig; - return ( - -
-
-
-
-

{siteConfig.title}

-

{siteConfig.tagline}

-
- - Get Started - -
-
-
- -
-
-
-
-
-
-
-
-

It's boringly easy to use

-

- Written in Go, deployed as a static binary, declarative configuration. Open source and cloud native as utter heck. -

- {installs && installs.length && ( - { - return {label:props.label, value:props.label}; - })}> - {installs.map((props, idx) => ( - - - - ))} - - )} -
-
- {snippets && snippets.length && ( -
- { - return {label:props.label, value:props.label}; - })}> - {snippets.map((props, idx) => ( - -
- - {props.further && - Read about - } -
-
- ))} -
-
- )} -
-
-
- {features && features.length && ( -
-
-
- {features.map((props, idx) => ( - - ))} -
-
-
- )} -
-
-
-
- -
-
- -
-
- -
-
- - - - - - - - -
-
-
-
-
- - Blob Heart - -
- - Become a sponsor - -
-
-
-
-
-
- ); -} - -export default Home; diff --git a/website/src/pages/support.module.css b/website/src/pages/support.module.css deleted file mode 100755 index ada3cb2517..0000000000 --- a/website/src/pages/support.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -.heroImg { - height: 200px; - margin: 10px 0; -} - -@media screen and (max-width: 966px) { - .heroBanner { - padding: 2rem; - } -} diff --git a/website/src/pages/support.tsx b/website/src/pages/support.tsx deleted file mode 100644 index 4ee4d2190b..0000000000 --- a/website/src/pages/support.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; - -import Layout from '@theme/Layout'; -import Link from '@docusaurus/Link'; - -import classnames from 'classnames'; -import styles from './support.module.css'; - -function Support() { - return ( - -
-
-
-
-

Benthos Support

-

When Benthos hands you lemons this page lists people to angrily throw them at.

- (That was just a metaphor - lemons not provided) -
-
- -
-
-
-
-
-
-
-
-
-

-Benthos has a helpful and conveniently global community so if you have quick questions, are in the market for ideas, or just want to make some friends, it's worth trying your luck with our community spaces. However, for organisations that want quicker action, guaranteed attention or some one-on-one consulting time there are some paid services available that are worth considering. -

-
-
-
-
-
-
-
-
- -

-Paid services options range from ad hoc consultations to long term help desk contracts. These can be tailored to your particular needs and are subject to availability. Please reach out to business@benthos.dev to get started. -

-

-It's also worth noting that some subscription offerings of Benthos Studio include support and may offer enough for your particular use case. -

-
-
-
-
-
-
-
-
-

Community Support -

-

-Nothing makes the Benthos community happier than welcoming new blobs into our blobosphere, and that includes answering questions, helping you to fit Benthos around your use cases, and general chit chat about the project, and there are multiple spaces where you can find us. However, we are a finite number of entities (for now) and there are limits to how much of our free time and energy we can realistically spend on these activities. -

-

-Keeping the community cogs turning is therefore a balancing act between encouraging shy users to seek help when they need it, and gently teaching others (the noisy ones) how to reduce their reliance on our help. Here's some tips on how to get the best of us. -

-
-
-
-
-

Before Asking for Help -

-

Try the Documentation

-

-Give the Benthos documentation a try, no one expects you to read the whole thing but it's there for a reason. Try using the search functionality and if there's a section that covers the area you're looking at then try your best to understand it. However, with that said, you will not be judged for missing something, if you're not seeing the answers you need then asking the community is definitely the reasonable thing to do. -

-

Consider a Simpler Approach

-

-When you have a particular solution in mind it's easy to slip into a mindset that blocks you from considering other approaches even when it's not working out. If you're struggling to find out how to make your solution work with Benthos then take a deep breath, think about ponies or something for a few minutes, and consider if there's a more Benthosy way in which your problem could be solved. -

-

-If not then we still want to hear from you as maybe it's an interesting use case we can implement a proper solution for, but it's good to have considered other approaches before it gets to us. -

-

Test Things Yourself

-

-Benthos has lots of ways to try solutions out, it has unit tests, a generate input, a benthos blobl server subcommand, and many more ways to get some trial and error done before asking for help. It's frustrating for us to spend our time reading and understanding configuration files and use cases that make up part of your question when it could have been answered with a few seconds of testing. -

-
-
-
-
-

When Asking for Help -

-

Focus on the Problem

-

-When asking us for help try and focus on the specific problem you have rather than the higher level details. Ideally we want to see things like concise examples of data you have versus what you want to get out, or a description of how you want data to flow through your pipeline. Please also reduce config examples down into the basics that are relevant to your question. Any details that you provide that isn't specifically part of the problem is only a hurdle that we will need to overcome before we can understand your question. -

-

-If your particular question requires knowledge of private and commercial applications that you have built or are building then the public support channels are not the right place to ask it. -

-

Reproduce Your Issue

-

-It's common for users to point out an issue they're seemingly having with Benthos and it then turns out to be an unrelated issue with cached builds, configs not being saved, docker images not being pulled fresh, etc. These encounters may sound rare but anecdotally they occur more often in our support channels than actual bugs, and we've spent considerable measures of time trying to reproduce issues that don't exist (yet). By attempting to isolate and reproduce issues yourself you can significantly reduce the burden on us. -

-

Provide Context

-

-Things that a typical community member cannot do include mind-reading, assuming control of your PC (I hope) and time travelling into the past in order to obtain context of the problem on your behalf. In order to work around these limitations please make sure that when you are asking us for help regarding a problem you've had that at the very least you are able to give us any error messages/logs that were emitted by Benthos when it happened. -

-

Never Direct Message

-

-Unless you are explicitly instructed to do so please never direct message maintainers or community members for support, and avoid directly pinging them. If you aren't receiving responses in a public support channel it is because they currently do not have time to address your issue, please be patient or consider a paid support option instead. Direct messages only serve to add extra pressure on volunteers, and answering questions via direct messages denies other users the opportunity to read the answers for themselves. -

-

-If there is sensitive information within your question that you do not want to expose publicly then please take the time to scrub that information from the material you are sharing. -

-
-
-
-
-
-
- ); -} - -export default Support; \ No newline at end of file diff --git a/website/src/pages/videos.module.css b/website/src/pages/videos.module.css deleted file mode 100644 index 32c8e645b7..0000000000 --- a/website/src/pages/videos.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.videoContainer { - text-align: center; - width: 100%; - margin: 40px 0; -} - -.videosTitle { - margin-top: 20px; -} \ No newline at end of file diff --git a/website/src/pages/videos.tsx b/website/src/pages/videos.tsx deleted file mode 100755 index 1c6359807c..0000000000 --- a/website/src/pages/videos.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import Layout from '@theme/Layout'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import styles from './videos.module.css'; -import ReactPlayer from 'react-player/youtube' - -function Videos() { - const context = useDocusaurusContext(); - const siteConfig = context.siteConfig; - return ( - -
-
-
-
-

Benthos Videos

-

All videos are rated U for Underwhelming. If you'd rather access these videos directly from YouTube you can find them on the Jeffail channel.

-
-
-
-
-
-
-
-
-

Tutorials

-

This playlist contains tutorial videos covering various aspects of Benthos use.

-
- -
-
-
-
- ); -} - -export default Videos; diff --git a/website/src/plugins/analytics/client.js b/website/src/plugins/analytics/client.js deleted file mode 100644 index 45b1158159..0000000000 --- a/website/src/plugins/analytics/client.js +++ /dev/null @@ -1,25 +0,0 @@ -function poke(host, path) { - // TODO: Allow this to be configured - fetch(`https://poke.benthos.dev/poke?h=${encodeURIComponent(host)}&p=${encodeURIComponent(path)}`, { method: "POST" }) - .catch((error) => console.error(error)) -} - -module.exports = (() => { - if (typeof window !== "object") { - return {}; - } - - const host = window.location.hostname; - let path = window.location.pathname; - - poke(host, path); - return { - onRouteUpdate({location}) { - if ( path === location.pathname ) { - return; - } - path = location.pathname; - poke(host, path); - }, - }; -})(); diff --git a/website/src/plugins/analytics/index.js b/website/src/plugins/analytics/index.js deleted file mode 100644 index 6b8222b532..0000000000 --- a/website/src/plugins/analytics/index.js +++ /dev/null @@ -1,12 +0,0 @@ -const path = require('path'); - -const isProd = process.env.NODE_ENV === 'production'; - -module.exports = function(context, options) { - return { - name: 'blob-analytics', - getClientModules() { - return isProd ? [path.resolve(__dirname, './client')] : []; - }, - }; -}; diff --git a/website/src/plugins/components/index.js b/website/src/plugins/components/index.js deleted file mode 100644 index 5a9b75f3ec..0000000000 --- a/website/src/plugins/components/index.js +++ /dev/null @@ -1,53 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const {parseMarkdownString} = require('@docusaurus/utils'); - -function components(type) { - return all_components(type).filter(c => c.status != "deprecated") -} - -function all_components(type) { - let components = []; - let dir = path.join(__dirname, `../../../docs/components/${type}`); - fs.readdirSync(dir).forEach(function (file) { - if ( !/about\.mdx?/.test(file) ) { - let name = file.split('.').slice(0, -1).join('.'); - let data = fs.readFileSync(path.join(dir, file)); - const {frontMatter} = parseMarkdownString(data); - frontMatter["name"] = name; - components.push(frontMatter); - } - }); - return components; -} - -function listPaths(type) { - let paths = [`components/${type}/about`]; - - let components = all_components(type); - - components - .filter(c => c.status != "deprecated") - .forEach(function (info) { - paths.push(`components/${type}/${info.name}`); - }); - - let deprecatedPaths = components - .filter(c => c.status == "deprecated") - .map(c => `components/${type}/${c.name}`); - - if ( deprecatedPaths.length > 0 ) { - paths.push({ - type: 'category', - label: 'Deprecated', - items: deprecatedPaths, - }); - } - - return paths; -} - -module.exports = { - components: components, - listPaths: listPaths, -}; diff --git a/website/src/plugins/cookbooks/.gitignore b/website/src/plugins/cookbooks/.gitignore deleted file mode 100644 index a13427f2df..0000000000 --- a/website/src/plugins/cookbooks/.gitignore +++ /dev/null @@ -1 +0,0 @@ -compiled \ No newline at end of file diff --git a/website/src/plugins/cookbooks/cookbookUtils.ts b/website/src/plugins/cookbooks/cookbookUtils.ts deleted file mode 100644 index 11dc07beb8..0000000000 --- a/website/src/plugins/cookbooks/cookbookUtils.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import fs from 'fs-extra'; -import path from 'path'; -import logger from '@docusaurus/logger'; -import { - parseMarkdownString, - normalizeUrl, - aliasedSitePath, - getFolderContainingFile, - posixPath, - replaceMarkdownLinks, - Globby, - getContentPathList, -} from '@docusaurus/utils'; -import type {LoadContext} from '@docusaurus/types'; -import { validateCookbookPostFrontMatter } from './frontMatter'; -import type { - PluginOptions, - CookbookPost, - CookbookContentPaths, - CookbookMarkdownLoaderOptions, -} from './types'; - -export function getSourceToPermalink(cookbookPosts: CookbookPost[]): { - [aliasedPath: string]: string; -} { - return Object.fromEntries( - cookbookPosts.map(({metadata: {source, permalink}}) => [source, permalink]), - ); -} - -const DATE_FILENAME_REGEX = - /^(?.*)(?\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?.*?)(?:\/index)?.mdx?$/; - -type ParsedCookbookFileName = { - date: Date | undefined; - text: string; - slug: string; -}; - -export function parseCookbookFileName( - blogSourceRelative: string, -): ParsedCookbookFileName { - const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX); - if (dateFilenameMatch) { - const {folder, text, date: dateString} = dateFilenameMatch.groups!; - // Always treat dates as UTC by adding the `Z` - const date = new Date(`${dateString!}Z`); - const slugDate = dateString!.replace(/-/g, '/'); - const slug = `/${slugDate}/${folder!}${text!}`; - return {date, text: text!, slug}; - } - const text = blogSourceRelative.replace(/(?:\/index)?\.mdx?$/, ''); - const slug = `/${text}`; - return {date: undefined, text, slug}; -} - -async function parseCookbookPostMarkdownFile(blogSourceAbsolute: string) { - const markdownString = await fs.readFile(blogSourceAbsolute, 'utf-8'); - try { - const result = parseMarkdownString(markdownString, { - removeContentTitle: true, - }); - return { - ...result, - frontMatter: validateCookbookPostFrontMatter(result.frontMatter), - }; - } catch (err) { - logger.error`Error while parsing blog post file path=${blogSourceAbsolute}.`; - throw err; - } -} - -async function processCookbookSourceFile( - cookbookSourceRelative: string, - contentPaths: CookbookContentPaths, - context: LoadContext, - options: PluginOptions, -): Promise { - const { - siteConfig: {baseUrl}, - siteDir, - i18n, - } = context; - const { - routeBasePath, - } = options; - - // Lookup in localized folder in priority - const cookbookDirPath = await getFolderContainingFile( - getContentPathList(contentPaths), - cookbookSourceRelative, - ); - - const cookbookSourceAbsolute = path.join(cookbookDirPath, cookbookSourceRelative); - - const {frontMatter, content, contentTitle, excerpt} = - await parseCookbookPostMarkdownFile(cookbookSourceAbsolute); - - const aliasedSource = aliasedSitePath(cookbookSourceAbsolute, siteDir); - - if (frontMatter.draft && process.env.NODE_ENV === 'production') { - return undefined; - } - - if (frontMatter.id) { - logger.warn`name=${'id'} header option is deprecated in path=${cookbookSourceRelative} file. Please use name=${'slug'} option instead.`; - } - - const parsedCookbookFileName = parseCookbookFileName(cookbookSourceRelative); - - const title = frontMatter.title ?? contentTitle ?? parsedCookbookFileName.text; - const description = frontMatter.description ?? excerpt ?? ''; - - const slug = frontMatter.slug ?? parsedCookbookFileName.slug; - - const permalink = normalizeUrl([baseUrl, routeBasePath, slug]); - - return { - id: slug, - metadata: { - permalink, - source: aliasedSource, - title, - description, - }, - content, - }; -} - -export async function generateCookbookPosts( - contentPaths: CookbookContentPaths, - context: LoadContext, - options: PluginOptions, -): Promise { - const {include, exclude} = options; - - if (!(await fs.pathExists(contentPaths.contentPath))) { - return []; - } - - const cookbookSourceFiles = await Globby(include, { - cwd: contentPaths.contentPath, - ignore: exclude, - }); - - const cookbookPosts = ( - await Promise.all( - cookbookSourceFiles.map(async (cookbookSourceFile: string) => { - try { - return await processCookbookSourceFile( - cookbookSourceFile, - contentPaths, - context, - options, - ); - } catch (err) { - logger.error`Processing of cookbook source file path=${cookbookSourceFile} failed.`; - throw err; - } - }), - ) - ).filter(Boolean) as CookbookPost[]; - return cookbookPosts; -} - -export type LinkifyParams = { - filePath: string; - fileString: string; -} & Pick< - CookbookMarkdownLoaderOptions, - 'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink' ->; - -export function linkify({ - filePath, - contentPaths, - fileString, - siteDir, - sourceToPermalink, - onBrokenMarkdownLink, -}: LinkifyParams): string { - const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({ - siteDir, - fileString, - filePath, - contentPaths, - sourceToPermalink, - }); - - brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l)); - - return newContent; -} diff --git a/website/src/plugins/cookbooks/frontMatter.ts b/website/src/plugins/cookbooks/frontMatter.ts deleted file mode 100644 index a4a2cb1aff..0000000000 --- a/website/src/plugins/cookbooks/frontMatter.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { - JoiFrontMatter as Joi, // Custom instance for front matter - validateFrontMatter, -} from '@docusaurus/utils-validation'; -import type {CookbookPostFrontMatter} from './types'; - -const CookbookFrontMatterSchema = Joi.object({ - id: Joi.string(), - title: Joi.string().allow(''), - description: Joi.string().allow(''), - draft: Joi.boolean(), - slug: Joi.string(), - keywords: Joi.array().items(Joi.string().required()), -}).messages({ - 'deprecate.error': - '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.', -}); - -export function validateCookbookPostFrontMatter(frontMatter: { - [key: string]: unknown; -}): CookbookPostFrontMatter { - return validateFrontMatter(frontMatter, CookbookFrontMatterSchema); -} diff --git a/website/src/plugins/cookbooks/index.ts b/website/src/plugins/cookbooks/index.ts deleted file mode 100644 index b493f58736..0000000000 --- a/website/src/plugins/cookbooks/index.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import path from 'path'; -import logger from '@docusaurus/logger'; -import { - normalizeUrl, - docuHash, - aliasedSitePath, - addTrailingPathSeparator, - getContentPathList, - DEFAULT_PLUGIN_ID, -} from '@docusaurus/utils'; -import { - generateCookbookPosts, - getSourceToPermalink, -} from './cookbookUtils'; -import type {LoadContext, Plugin} from '@docusaurus/types'; - -import type { - PluginOptions, - CookbookContent, - CookbookContentPaths, - CookbookMarkdownLoaderOptions, -} from './types'; - -export default async function pluginContentCookbook( - context: LoadContext, - options: PluginOptions, -): Promise> { - const { - siteDir, - siteConfig, - generatedFilesDir, - } = context; - const {onBrokenMarkdownLinks, baseUrl} = siteConfig; - - const contentPaths: CookbookContentPaths = { - contentPath: path.resolve(siteDir, options.path), - contentPathLocalized: path.resolve(siteDir, options.path), - }; - const pluginId = options.id ?? DEFAULT_PLUGIN_ID; - - const pluginDataDirRoot = path.join( - generatedFilesDir, - 'cookbooks', - ); - const dataDir = path.join(pluginDataDirRoot, pluginId); - - return { - name: 'cookbooks', - - getPathsToWatch() { - const {include} = options; - const contentMarkdownGlobs = getContentPathList(contentPaths).flatMap( - (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`), - ); - - return [...contentMarkdownGlobs].filter( - Boolean, - ) as string[]; - }, - - // Fetches guide contents and returns metadata for the necessary routes. - async loadContent() { - const guidePosts = await generateCookbookPosts(contentPaths, context, options); - return { - cookbookPosts: guidePosts, - }; - }, - - async contentLoaded({content: cookbookContents, actions}) { - const { - routeBasePath, - guideListComponent, - guidePostComponent, - } = options; - - const aliasedSource = (source: string) => - `~cookbooks/${path.relative(pluginDataDirRoot, source)}`; - - const {addRoute, createData} = actions; - const { - cookbookPosts, - } = cookbookContents; - - // Create routes for guide entries. - await Promise.all( - cookbookPosts.map(async guidePost => { - const {metadata} = guidePost; - await createData( - // Note that this created data path must be in sync with metadataPath provided to mdx-loader - `${docuHash(metadata.source)}.json`, - JSON.stringify(metadata, null, 2), - ); - - addRoute({ - path: metadata.permalink, - component: guidePostComponent, - exact: true, - modules: { - content: metadata.source, - }, - }); - }), - ); - - const basePageUrl = normalizeUrl([baseUrl, routeBasePath]); - - const listPageMetadataPath = await createData( - `${docuHash(`${basePageUrl}`)}.json`, - JSON.stringify({}, null, 2), - ); - - let basePageItems = cookbookPosts.map(cookbookPost => { - const {metadata} = cookbookPost; - // To tell routes.js this is an import and not a nested object to recurse. - return { - content: { - __import: true, - path: metadata.source, - query: { - truncated: true, - }, - }, - }; - }); - - addRoute({ - path: basePageUrl, - component: guideListComponent, - exact: true, - modules: { - items: basePageItems, - metadata: aliasedSource(listPageMetadataPath), - }, - }); - }, - - configureWebpack(_config, isServer, {getJSLoader}, content) { - const { - rehypePlugins, - remarkPlugins, - } = options; - - const markdownLoaderOptions: CookbookMarkdownLoaderOptions = { - siteDir, - contentPaths, - sourceToPermalink: getSourceToPermalink(content.cookbookPosts), - onBrokenMarkdownLink: (brokenMarkdownLink) => { - if (onBrokenMarkdownLinks === 'ignore') { - return; - } - logger.report( - onBrokenMarkdownLinks, - )`Blog markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath}`; - }, - }; - - const contentDirs = getContentPathList(contentPaths); - return { - resolve: { - alias: { - '~cookbooks': pluginDataDirRoot, - }, - }, - module: { - rules: [ - { - test: /(\.mdx?)$/, - include: contentDirs.map(addTrailingPathSeparator), - use: [ - getJSLoader({isServer}), - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: { - remarkPlugins, - rehypePlugins, - staticDirs: siteConfig.staticDirectories.map((dir) => - path.resolve(siteDir, dir), - ), - // Note that metadataPath must be the same/ in-sync as the path from createData for each MDX - metadataPath: (mdxPath: string) => { - // Note that metadataPath must be the same/in-sync as - // the path from createData for each MDX. - const aliasedPath = aliasedSitePath(mdxPath, siteDir); - return path.join( - dataDir, - `${docuHash(aliasedPath)}.json`, - ); - }, - markdownConfig: siteConfig.markdown, - }, - }, - { - loader: path.resolve(__dirname, './markdownLoader.js'), - options: markdownLoaderOptions, - }, - ].filter(Boolean), - }, - ], - }, - }; - }, - }; -} diff --git a/website/src/plugins/cookbooks/markdownLoader.ts b/website/src/plugins/cookbooks/markdownLoader.ts deleted file mode 100644 index c7aeb603cb..0000000000 --- a/website/src/plugins/cookbooks/markdownLoader.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {linkify} from './cookbookUtils'; -import type {CookbookMarkdownLoaderOptions} from './types'; -import type {LoaderContext} from 'webpack'; - -export default function markdownLoader( - this: LoaderContext, - source: string, -): void { - const filePath = this.resourcePath; - const fileString = source; - const callback = this.async(); - const markdownLoaderOptions = this.getOptions(); - - // Linkify blog posts - let finalContent = linkify({ - fileString, - filePath, - ...markdownLoaderOptions, - }); - - return callback(null, finalContent); -} diff --git a/website/src/plugins/cookbooks/tsconfig.json b/website/src/plugins/cookbooks/tsconfig.json deleted file mode 100644 index af2fa94858..0000000000 --- a/website/src/plugins/cookbooks/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "outDir": "compiled", - "esModuleInterop": true, - }, - "include": ["index.ts","markdownLoader.ts"] -} diff --git a/website/src/plugins/cookbooks/types.ts b/website/src/plugins/cookbooks/types.ts deleted file mode 100644 index 2b348ba2c8..0000000000 --- a/website/src/plugins/cookbooks/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import type {BrokenMarkdownLink, ContentPaths} from '@docusaurus/utils'; -import type {MDXOptions} from '@docusaurus/mdx-loader'; - -export type CookbookContentPaths = ContentPaths; - -export type CookbookBrokenMarkdownLink = BrokenMarkdownLink; -export type CookbookMarkdownLoaderOptions = { - siteDir: string; - contentPaths: CookbookContentPaths; - sourceToPermalink: {[aliasedPath: string]: string}; - onBrokenMarkdownLink: (brokenMarkdownLink: CookbookBrokenMarkdownLink) => void; -}; - -export type CookbookPostMetadata = { - readonly source: string; - readonly title: string; - readonly permalink: string; - readonly description: string; -}; - -export type CookbookPost = { - id: string; - metadata: CookbookPostMetadata; - content: string; -}; - -export type PluginOptions = MDXOptions & { - id?: string; - path: string; - routeBasePath: string; - include: string[]; - exclude: string[]; - guideListComponent: string; - guidePostComponent: string; -}; - -export type CookbookContent = { - cookbookPosts: CookbookPost[]; -}; - -export type CookbookPostFrontMatter = { - id?: string; - title?: string; - description?: string; - slug?: string; - draft?: boolean; - keywords?: string[]; -}; diff --git a/website/src/plugins/prism_themes/github/index.js b/website/src/plugins/prism_themes/github/index.js deleted file mode 100644 index 4ad08d5fd2..0000000000 --- a/website/src/plugins/prism_themes/github/index.js +++ /dev/null @@ -1,75 +0,0 @@ -var theme = { - plain: { - color: "#393A34", - backgroundColor: "#f6f8fa" - }, - styles: [ - { - types: ["comment", "prolog", "doctype", "cdata"], - style: { - color: "#999988", - fontStyle: "italic" - } - }, - { - types: ["namespace"], - style: { - opacity: 0.7 - } - }, - { - types: ["string", "attr-value"], - style: { - color: "#e3116c" - } - }, - { - types: ["punctuation", "operator"], - style: { - color: "#393A34" - } - }, - { - types: [ - "entity", - "url", - "symbol", - "number", - "boolean", - "variable", - "constant", - "property", - "regex", - "inserted" - ], - style: { - color: "#36acaa" - } - }, - { - types: ["atrule", "keyword", "attr-name", "selector"], - style: { - color: "#00a4db" - } - }, - { - types: ["function", "deleted", "tag"], - style: { - color: "#d73a49" - } - }, - { - types: ["function-variable"], - style: { - color: "#6f42c1" - } - }, - { - types: ["tag", "selector", "keyword"], - style: { - color: "#00009f" - } - } - ] -}; -module.exports = theme; diff --git a/website/src/plugins/prism_themes/monokai/index.js b/website/src/plugins/prism_themes/monokai/index.js deleted file mode 100644 index 1d50688393..0000000000 --- a/website/src/plugins/prism_themes/monokai/index.js +++ /dev/null @@ -1,39 +0,0 @@ -// Converted automatically using ./tools/themeFromVsCode -var theme = { - "plain": { - "color": "#f8f8f2", - "backgroundColor": "#272822" - }, - "styles": [{ - "types": ["comment"], - "style": { - "color": "rgb(136, 132, 111)" - } - }, { - "types": ["string", "changed"], - "style": { - "color": "rgb(230, 219, 116)" - } - }, { - "types": ["punctuation", "tag", "deleted"], - "style": { - "color": "rgb(249, 38, 114)" - } - }, { - "types": ["number", "builtin"], - "style": { - "color": "rgb(174, 129, 255)" - } - }, { - "types": ["variable"], - "style": { - "color": "rgb(248, 248, 242)" - } - }, { - "types": ["function", "attr-name", "inserted"], - "style": { - "color": "rgb(166, 226, 46)" - } - }] -}; -module.exports = theme; diff --git a/website/src/theme/ComponentCard/index.js b/website/src/theme/ComponentCard/index.js deleted file mode 100755 index 4f4672cdb5..0000000000 --- a/website/src/theme/ComponentCard/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import Link from '@docusaurus/Link'; -import styles from './styles.module.css'; - -function ComponentCard(props) { - const { - type, - component, - } = props; - - return ( - - {component.name} - - ); -} - -export default ComponentCard; diff --git a/website/src/theme/ComponentCard/styles.module.css b/website/src/theme/ComponentCard/styles.module.css deleted file mode 100644 index 09d29116ad..0000000000 --- a/website/src/theme/ComponentCard/styles.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.componentCard { - background-color: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-background-surface-color-secondary); - border-radius: var(--ifm-global-radius); - box-shadow: var(--ifm-panel-box-shadow); - color: var(--ifm-font-base-color); - display: inline-block; - margin: 3px; - padding: calc( var(--ifm-spacing-vertical) * 1 ) calc( var(--ifm-spacing-horizontal) * 1.5 ); -} - -.componentCard:hover { - text-decoration: none; - background-color: var(--ifm-background-surface-color-secondary); -} - -.componentCard > * { - color: var(--ifm-font-base-color); -} \ No newline at end of file diff --git a/website/src/theme/ComponentSelect/index.js b/website/src/theme/ComponentSelect/index.js deleted file mode 100644 index 0100fac8c1..0000000000 --- a/website/src/theme/ComponentSelect/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; - -import classnames from 'classnames'; - -import styles from './styles.module.css'; - -function ComponentSelect(props) { - let {type, singular} = props; - const context = useDocusaurusContext(); - const types = context.siteConfig.customFields.components[type]; - - if ( typeof(singular) !== 'string' ) { - singular = type; - if (/s$/.test(singular)) { - singular = type.slice(0, -1); - } - } - - return
- -
    - {types.map((info) => { - return
  • - {info.name} -
  • - })} -
-
-} - -export default ComponentSelect; \ No newline at end of file diff --git a/website/src/theme/ComponentSelect/styles.module.css b/website/src/theme/ComponentSelect/styles.module.css deleted file mode 100644 index 3cb2b6c219..0000000000 --- a/website/src/theme/ComponentSelect/styles.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.componentList { - overflow: auto; - max-height: 200px; -} \ No newline at end of file diff --git a/website/src/theme/ComponentsByCategory/index.js b/website/src/theme/ComponentsByCategory/index.js deleted file mode 100644 index fcb9b80af7..0000000000 --- a/website/src/theme/ComponentsByCategory/index.js +++ /dev/null @@ -1,163 +0,0 @@ -import React from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import ComponentCard from '@theme/ComponentCard'; - -let descriptions = { - inputs: [ - { - name: "Services", - description: "Inputs that consume from storage or message streaming services.", - }, - { - name: "Network", - description: "Inputs that consume directly from low level network protocols.", - }, - { - name: "AWS", - description: "Inputs that consume from Amazon Web Services products.", - }, - { - name: "GCP", - description: "Inputs that consume from Google Cloud Platform services.", - }, - { - name: "Azure", - description: "Inputs that consume from Microsoft Azure services.", - }, - { - name: "Social", - description: "Inputs that consume from social applications and services.", - }, - { - name: "Local", - description: "Inputs that consume from the local machine/filesystem.", - }, - { - name: "Utility", - description: "Inputs that provide utility by generating data or combining/wrapping other inputs.", - }, - ], - buffers: [ - { - name: "Windowing", - description: "Buffers that provide message windowing capabilities.", - }, - { - name: "Utility", - description: "Buffers that are intended for niche but general use.", - }, - ], - processors: [ - { - name: "Mapping", - description: "Processors that specialize in restructuring messages.", - }, - { - name: "Integration", - description: "Processors that interact with external services.", - }, - { - name: "Parsing", - description: "Processors that specialize in translating messages from one format to another.", - }, - { - name: "Composition", - description: "Higher level processors that compose other processors and modify their behavior.", - }, - { - name: "Utility", - description: "Processors that provide general utility or do not fit in another category.", - }, - ], - outputs: [ - { - name: "Services", - description: "Outputs that write to storage or message streaming services.", - }, - { - name: "Network", - description: "Outputs that write directly to low level network protocols.", - }, - { - name: "AWS", - description: "Outputs that write to Amazon Web Services products.", - }, - { - name: "GCP", - description: "Outputs that write to Google Cloud Platform services.", - }, - { - name: "Azure", - description: "Outputs that write to Microsoft Azure services.", - }, - { - name: "Social", - description: "Outputs that write to social applications and services.", - }, - { - name: "Local", - description: "Outputs that write to the local machine/filesystem.", - }, - { - name: "Utility", - description: "Outputs that provide utility by combining/wrapping other outputs.", - }, - ], -}; - -function ComponentsByCategory(props) { - let {type} = props; - const context = useDocusaurusContext(); - const types = context.siteConfig.customFields.components[type]; - - let summaries = descriptions[type] || []; - - let categories = {}; - let categoryList = []; - for (let i = 0; i < summaries.length; i++) { - categoryList.push(summaries[i].name); - categories[summaries[i].name.toLowerCase()] = { - summary: summaries[i].description, - items: [], - } - } - - for (let i = 0; i < types.length; i++) { - let cats = types[i].categories; - if ( Array.isArray(cats) ) { - for (let j = 0; j < cats.length; j++) { - let catLower = cats[j].toLowerCase(); - if ( categories[catLower] === undefined ) { - categoryList.push(catLower.charAt(0).toUpperCase() + catLower.slice(1)); - categories[catLower] = { - summary: "", - items: [types[i]], - }; - } else { - categories[catLower].items.push(types[i]); - } - } - } - } - - return ( - ( - { label: cat, value: cat.toLowerCase() } - ))}> - {categoryList.map((cat) => ( - -

{categories[cat.toLowerCase()].summary}

- {categories[cat.toLowerCase()].items.map((data, idx) => ( - - ))} -
- ))} -
- ); -} - -export default ComponentsByCategory; \ No newline at end of file diff --git a/website/src/theme/CookbookItem/index.js b/website/src/theme/CookbookItem/index.js deleted file mode 100755 index 4b6a8e5128..0000000000 --- a/website/src/theme/CookbookItem/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -import Link from '@docusaurus/Link'; - -import classnames from 'classnames'; - -import styles from './styles.module.css'; - -function CookbookItem(props) { - const { - frontMatter, - metadata, - } = props; - const {description, permalink} = metadata; - const {title} = frontMatter; - - return ( -
- -
-

{title}

-
{description}
-
- -
- ); -} - -export default CookbookItem; diff --git a/website/src/theme/CookbookItem/styles.module.css b/website/src/theme/CookbookItem/styles.module.css deleted file mode 100644 index b0104a51b6..0000000000 --- a/website/src/theme/CookbookItem/styles.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.cookbookPostItem { - background-color: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-background-surface-color); - border-radius: var(--ifm-global-radius); - box-shadow: var(--ifm-global-shadow-lw); - color: var(--ifm-font-color-base); - display: block; - margin-bottom: var(--ifm-spacing-vertical); - padding: calc( var(--ifm-spacing-vertical) * 1.5 ) calc( var(--ifm-spacing-horizontal) * 2 ); - - transition: box-shadow var(--ifm-transition-fast), background-color var(--ifm-transition-fast); -} - -.cookbookPostItem:hover { - text-decoration: none; - background-color: var(--ifm-background-color); - box-shadow: var(--ifm-global-shadow-md); -} - -.cookbookPostItem > * { - color: var(--ifm-font-color-base); -} diff --git a/website/src/theme/CookbookListPage/index.js b/website/src/theme/CookbookListPage/index.js deleted file mode 100755 index 86bb09de0e..0000000000 --- a/website/src/theme/CookbookListPage/index.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, {useState} from 'react'; - -import CookbookItem from '@theme/CookbookItem'; -import Layout from '@theme/Layout'; -import qs from 'qs'; -import classnames from 'classnames'; - -import styles from './styles.module.css'; - -function CookbookListPage(props) { - const {items} = props; - - const queryObj = props.location ? qs.parse(props.location.search, {ignoreQueryPrefix: true}) : {}; - - let itemsFiltered = items.slice(0); - itemsFiltered.sort((a, b) => (b.content.metadata.featured === true && 1) || -1); - - // - // State - // - - const [onlyFeatured, setOnlyFeatured] = useState(queryObj['featured'] == 'true'); - const [searchTerm, setSearchTerm] = useState(null); - const [searchLimit, setSearchLimit] = useState(20); - - let filteredCap = itemsFiltered.length; - let increaseSearchLimit = function() { - if ( searchLimit > filteredCap ) { - return - } - let newLimit = searchLimit + 10; - setSearchLimit(newLimit); - }; - - // - // Filtering - // - - if (searchTerm) { - itemsFiltered = itemsFiltered.filter(item => { - let searchTerms = searchTerm.split(" "); - let content = `${item.content.metadata.title.toLowerCase()} ${item.content.metadata.description.toLowerCase()}`; - return searchTerms.every(term => { - return content.includes(term.toLowerCase()) - }) - }); - } - - if (onlyFeatured) { - itemsFiltered = itemsFiltered.filter(item => item.content.metadata.featured == true); - } - - filteredCap = itemsFiltered.length; - itemsFiltered = itemsFiltered.slice(0, searchLimit); - - return ( - -
-
-
-
- -
-

Benthos Cookbooks

-

A collection of guides to walk you through more advanced Benthos applications.

-
-
- setSearchTerm(event.currentTarget.value)} - placeholder="🔍 Search..." /> -
-
-
- -
-
-
-
-
-
- {itemsFiltered.map(({content: CookbookContent}) => ( - - - - ))} - {itemsFiltered.length > 0 && itemsFiltered.length < items.length && itemsFiltered.length > searchLimit && -
- -
} - {itemsFiltered.length == 0 && -
-

Whoops, looks like your search hasn't got any results. If the cookbook you want doesn't exist please ask for it.

-
} -
-
-
- ); -} - -export default CookbookListPage; diff --git a/website/src/theme/CookbookListPage/styles.module.css b/website/src/theme/CookbookListPage/styles.module.css deleted file mode 100644 index 5494028d9d..0000000000 --- a/website/src/theme/CookbookListPage/styles.module.css +++ /dev/null @@ -1,45 +0,0 @@ -.cookbookListHeader { - text-align: center; - background-color: var(--ifm-background-surface-color); - padding-top: 60px; - padding-bottom: 40px; -} - -.headerImg { - height: 200px; -} - -.headerImgMobile { - margin-bottom: 20px; - max-width: 200px; - display: none; -} - -@media only screen and (max-width: 996px) { - .cookbookListHeader { - padding-top: 20px; - padding-bottom: 20px; - } - - .headerImg { - display: none; - } - - .headerImgMobile { - display: inline; - } -} - -.cookbookItemsContainer { - --ifm-container-width: 800px; - --ifm-container-width-xl: 800px; -} - -.cookbookSearch { - font-size: 12pt; - border-radius: var(--ifm-global-radius); - border: none; - padding: 5px; - color: var(--ifm-primary-color); - background-color: var(--ifm-background-color); -} \ No newline at end of file diff --git a/website/src/theme/CookbookPage/index.js b/website/src/theme/CookbookPage/index.js deleted file mode 100755 index f4366e75e9..0000000000 --- a/website/src/theme/CookbookPage/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -import Layout from '@theme/Layout'; -import Link from '@docusaurus/Link'; -import MDXComponents from '@theme/MDXComponents'; -import {MDXProvider} from '@mdx-js/react'; -import TOC from '@theme/TOC'; - -import classnames from 'classnames'; -import styles from './styles.module.css'; - -function CookbookPage(props) { - const {content: CookbookContents} = props; - const {frontMatter, metadata} = CookbookContents; - const {title} = frontMatter; - const {keywords} = metadata; - - return ( - -
-
-
-
-
-

{title}

-

{metadata.description}

-
-
-
- -
- Find more cookbooks -
-
-
- {CookbookContents.toc && ( -
- -
- )} -
-
-
- ); -} - -export default CookbookPage; diff --git a/website/src/theme/CookbookPage/styles.module.css b/website/src/theme/CookbookPage/styles.module.css deleted file mode 100644 index 8152c039f0..0000000000 --- a/website/src/theme/CookbookPage/styles.module.css +++ /dev/null @@ -1,52 +0,0 @@ -.tableOfContents { - display: inherit; - max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); - overflow-y: auto; - position: sticky; - top: calc(var(--ifm-navbar-height) + 2rem); -} - -.tableOfContents::-webkit-scrollbar { - width: 7px; -} - -.tableOfContents::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 10px; -} - -.tableOfContents::-webkit-scrollbar-thumb { - background: #888; - border-radius: 10px; -} - -.tableOfContents::-webkit-scrollbar-thumb:hover { - background: #555; -} - -@media only screen and (max-width: 996px) { - .tableOfContents { - display: none; - } -} - -.header { - text-align: center; -} - -.cookbookTitle { - font-size: 32pt; -} - -.cookbookDescription { - margin: 0; -} - -.cookbookTimeToRead { - color: var(--ifm-blockquote-color); -} - -.cookbookContainer { - --ifm-container-width: 800px; - --ifm-container-width-xl: 1000px; -} \ No newline at end of file diff --git a/website/src/theme/NotFound/index.tsx b/website/src/theme/NotFound/index.tsx deleted file mode 100644 index 84f000694d..0000000000 --- a/website/src/theme/NotFound/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from 'react'; -import Layout from '@theme/Layout'; -import classnames from 'classnames'; - -import styles from './styles.module.css'; - -function NotFound() { - return ( - -
-
-
- -

Woops! Page Not Found

-

The documentation site has recently moved, chances are that the page you're looking for is in the new docs section.

-
-
-
-
- ); -} - -export default NotFound; \ No newline at end of file diff --git a/website/src/theme/NotFound/styles.module.css b/website/src/theme/NotFound/styles.module.css deleted file mode 100644 index f7c15b98ee..0000000000 --- a/website/src/theme/NotFound/styles.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.notFoundContainer { - text-align: center; -} - -.notFoundBlob { - max-width: 300px; -} \ No newline at end of file diff --git a/website/static/img/Blobartist.svg b/website/static/img/Blobartist.svg deleted file mode 100644 index bab6269121..0000000000 --- a/website/static/img/Blobartist.svg +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - - - - -image/svg+xml - - - - - diff --git a/website/static/img/Blobborg.svg b/website/static/img/Blobborg.svg deleted file mode 100644 index f2005563be..0000000000 --- a/website/static/img/Blobborg.svg +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/website/static/img/Blobboring.svg b/website/static/img/Blobboring.svg deleted file mode 100644 index 1e6a0af2a1..0000000000 --- a/website/static/img/Blobboring.svg +++ /dev/null @@ -1,449 +0,0 @@ - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/Blobchef.svg b/website/static/img/Blobchef.svg deleted file mode 100644 index f9a96e6bf8..0000000000 --- a/website/static/img/Blobchef.svg +++ /dev/null @@ -1,233 +0,0 @@ - - - -image/svg+xml diff --git a/website/static/img/Blobextended.svg b/website/static/img/Blobextended.svg deleted file mode 100644 index b0cd45b76b..0000000000 --- a/website/static/img/Blobextended.svg +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/website/static/img/Blobgangsta.svg b/website/static/img/Blobgangsta.svg deleted file mode 100644 index c227f15ade..0000000000 --- a/website/static/img/Blobgangsta.svg +++ /dev/null @@ -1,607 +0,0 @@ - - - -image/svg+xml diff --git a/website/static/img/Blobninja.svg b/website/static/img/Blobninja.svg deleted file mode 100644 index c53e1516bc..0000000000 --- a/website/static/img/Blobninja.svg +++ /dev/null @@ -1,264 +0,0 @@ - - - -image/svg+xml diff --git a/website/static/img/Blobpirate.svg b/website/static/img/Blobpirate.svg deleted file mode 100644 index 966b06079b..0000000000 --- a/website/static/img/Blobpirate.svg +++ /dev/null @@ -1,218 +0,0 @@ - - - -image/svg+xml diff --git a/website/static/img/Blobscales.svg b/website/static/img/Blobscales.svg deleted file mode 100644 index 9fb7ef0a8c..0000000000 --- a/website/static/img/Blobscales.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/Blobsherlock.svg b/website/static/img/Blobsherlock.svg deleted file mode 100644 index b873cc4402..0000000000 --- a/website/static/img/Blobsherlock.svg +++ /dev/null @@ -1,266 +0,0 @@ - - - -image/svg+xml diff --git a/website/static/img/Blobsocial.svg b/website/static/img/Blobsocial.svg deleted file mode 100644 index c412ee6ab1..0000000000 --- a/website/static/img/Blobsocial.svg +++ /dev/null @@ -1,227 +0,0 @@ - -image/svg+xml diff --git a/website/static/img/Blobviking.svg b/website/static/img/Blobviking.svg deleted file mode 100644 index e006504f98..0000000000 --- a/website/static/img/Blobviking.svg +++ /dev/null @@ -1,286 +0,0 @@ - - - -image/svg+xml diff --git a/website/static/img/ash.jpg b/website/static/img/ash.jpg deleted file mode 100644 index e1778b7fc0d997887cce18b757889a895daa0dd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38487 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 diff --git a/website/static/img/blobheart.svg b/website/static/img/blobheart.svg deleted file mode 100644 index 15666c3999..0000000000 --- a/website/static/img/blobheart.svg +++ /dev/null @@ -1,236 +0,0 @@ - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/emojis/blob.png b/website/static/img/emojis/blob.png deleted file mode 100644 index 32a987c83068c86c8301e06c108779fb3fe7c749..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7341 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 diff --git a/website/static/img/emojis/blobbot.png b/website/static/img/emojis/blobbot.png deleted file mode 100644 index ed873a70ff1ef5b29102505c594645d28c505808..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9243 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 diff --git a/website/static/img/emojis/blobbounce.gif b/website/static/img/emojis/blobbounce.gif deleted file mode 100644 index b51e96e6ff99258232bae57ac4715e3f42196a59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22348 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

^}Tm5WGD<5KZhY1so*a!|2PBCP8zmW%@OTU7?$( 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 diff --git a/website/static/img/emojis/blobbug.png b/website/static/img/emojis/blobbug.png deleted file mode 100644 index 60e6da913cc2c9c2cf429024201314bc82066966..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7717 zcma)Bg;N~O)4szcxVyVsaCg_>?hc2$ySoK<0t9yn5L|-<$>C0dyYsvEANcC4+S%FK z+Un`vdAgsT?pQSySrjBfBme+_A}=SU0hvSo8whZa`%e+GDaeHADyQcT0FXrgH-O0p zS5}ah1Rm159-1!J9^PhdRse5rZ&o{J2X_lIS1VQ*H=D2LLWGd7o8_g%wS4l=3Vb~b zM{~(x#CWnpTtE6%mqE|Sysjn6anH555JgO6^T zzI2@TdAbC4{H<;1e?}^mip8W+$e>sMzaOq=#ChNz`qc+rj-(<-k;7uP2=Mrtds0K=x z-?^ekE*DM+?H2ucXgS%WDQW}_ArOx4(!lhZFUvtU;ckT&dEUeNrJ?}-Iws+;4z?1o z7&^51QVy*b!^+UaYhr%!b{c|m448%bB1W^zw7KHHUIeqvE%D=I58IW*l^N>b2X#)8 zTH5Q6V$YwwSp<)C+aEw#biz)QUqIF#Y58l6>Bj zmsC;*a*3QW|8(HFVJhC7$5lyTLpY&KEYVDf$$wyW5#jwSVAMbkv+FSCgF5&wv5jB~ z?W%h5VG;6zj@$OB3puCd))J34C9_D(+E}RR&$w>;hBI3tkC%OIomQqnBRA@|q9EWa zQ>nqTK1U&xEAnOD)|O|?dmv(|O97*28o#3I)w(5;b&ec+Vj@XR*Wm2GAHq>RHUrv& z@VziXFs;e*gOnkD#20TI60!eY99u%XL;r1^*{Q~3wC}!e54b-A5^bikIcucn8 zqwrbOl^i@BVkS5Ix{&yV=KSQe7k3}28GbSzuk0#lbY zpJf2plLM-we+Wyp5}8QE4-nsKqijL7(Ee`LM2c)yE8dltyLjGwrs7_O9IBgD8CG=Pst_A{vBBz3pNz!9Y!4^ZG)nRv&>%-0`+v{ou2 ziTJRI|Ik6q-68OowE>Y({Wlw7ilx^;4?iB>4IO5yxg40@>DA7|Db7ES(fs*SDAF<2y%hrUPHU%48oj4LrM$fHp#utM6r^Eh z^FOQEwzn{*q(iuya|^$_hDT(L4vtI;NV@&3WKM?T+|$Yf-e<@X9Il?sHF_oW2UgZk3{K4vqk4&*iFf||lI#bJsRIm?_gcPy+q-nE z8jrhUDF`eqJPdh`w6kUnWE~TFPgWK{Nob-UTrbIQ&?XW2&TWLyI&(b*(k&mcx^3E= z$!KIIU(QT#3Pl%e`LR-8CYx?H2vN@j$nswWhHTuHV#@(_vOdpo$V{ehsy30GZZt%6 zt`8_5@_7t+fasrt%k>8QZueT26dIR2UtT6t&KHIuB>D~Ox{@YRD@ehd7Hy*!C~1hPQk7m+=ohyo@<6Oz zn>jQj!A*#QgM-6YpmKe4<9mzuV>=ShSgb7CXK|cSRMh?RL*28gxrN8-nOEWC0oz)m z88mKu=TF)F-Q4@suNNFI-a&uh~dJtL#}+3#f27_O$fcsyB;V15YF-5VxN9}WdR zEMYG%{-0O2OvSTVqsx1RM#FYOBR+fFd+KM>vA|8 zic6?4t6^LIpMUio09|!IG_LCg;vgWL*>b4|cQ<-q0F61x zK{X}=uoL`&maZr3C0^WQF|lrto1zgib+9YQr0u#r5c+Q6>+AdWv0}?zr{5MwBIsqx znVX}RC19SsO_cqL05KpjG10Kg60u4JvDQIB0co)+-n&kXK1aZlFda7^cuTkNEucM_`_nb(kQr;qdV;4_Q66;|s>4?!AYp9HG zH(?mP90vD=jyi{Xm_Q=4{eG4fP^prO>Bfv-G&dH9!!$lKQ_*xs9FHGV^zY91%-7$^)_TU*0VEX5(J=MQ6RUutnNfEH$*pgE2m z=s=HGp_-J_55a^gvr1)cu`T^}P7YWx_GclS*^b-zFH-C!@;0`6EozbNY6w%S{0>GO zxLYy8(|*7Ew{kPd)Npw`HrmeHFji z^42Qp^Hk%`=*17uashmMe$Jf>5s5$>=uD4{V>>VZC?2-0*!=a-bg zoja^zj7-DSs0m_wmVY3@`F{!fmP%Evtr;s@KH>LXPvnG|nVI_s2RmLpYq$5f3+}t} zN(8-V*`;0eQE5XJ_@o*dUZQ?rfzVr5BvBibBRcJF+B zpMJ_V36@7UQI%Y$M7AF`hS!V%T0&ninh;Pa%t=t=cWm|CL%h*{rURCRg~FE*7dX=H z^r=3)z*E%6n$>l6`vy8D%0@;7md3%&EiEUFZJ@UeUuv6AbJzwsp8O9D?ev zgiWSWjjg^e5H5xS2 za~}Zf;A}J)YGjiWXeDryr6N{E<^$OC^5<`^iWUC)kKN{9mvbNnFLPhA1MYe(6f9|S zwDQ-_S65jq5VEo808dO!S*Ys~IIMi9;ZUyw)>W@f#eY2VquLyWJjBC@kgfj7Z_WQ_ zzSRylTR?CvtV$K8k&pNg;RfkT^Ii!i!u9$@TWJ3m!zyn8acxY zqV`U<2eR$B3G%s@78G18n>x6wb2H_h4#6zAe;OuO3W2ie__{pmY|m7uMQT3+Z`jWq zpNl;6rYgYFkU)g{hFl|GQmF9GXXw>_4j#XUZny=5j%w-j+}vT`A3v-|E^Q4>xdb0d0q>De|dgHETNCu`dm*Jzl1 zL>kZ>YO!iZ`Tho~k(9+>SKVedr@1ed zV9$Z%_|d6qk74p(tB;C;@%26;QRCAW$H4sVy#?KWlLkj8uIUGC7t#vLY|K+r^Cx6J zjf(xu(gS6ZY9Lb$p@5oTpms0gbIgNZw;RO1Fm=(B8=4G*P?AfZp4c~fR(uw+f}5wpNOIAVGm7M_eo$N#CXu5VsRwx zNG8qBCar>RSc%?P4&V4&`g6Cut0eyLnN^^k^-}zef#|Nv@id?#$_`m0H_)nkNy%{7 z?psUC+u7n9D7x2}tSwX9o~i!SuOTX}ncJF8MYilqiCV}opvDEojHQ&v2dl{>6#D@h z?0+LiNe58a=Xpns=&%p*j+o*QlAhSRzy5trd++2_T&t-SAoD#GlD_XNs}gE~$rzoK zQ6ZAGvN|+^C!?kywtTvs^&p!ddB9_JVWrE6%>$TihtqQ2^W4D%vJwHf>Jhx6m zP_3n~ULs!mmgu5lic%VOKr9y={cDH1O7dXyT;LUoANHzoX(oR|5-%hViAnI?E2`ZZ zo-s3w0C{HGI6sNO>2_$CI=2mSWn@&$n}OW+Z)hh)9fsD<4j)`flZKt$-LE(3HJ$0phv@jkaV{e+t7nh#bU$tsd*8>uqA;|%yjfixpVO8N3>;&iot`V z9oa?~znlN{5t^HSF?3fXwPK=Ect%UWgCKoLNtjD z2ve(JOx8QwpuLM1-y*l4JhSFTbN*PXl}GRd6txjq2>;IL?nqYZuaeD|1$_!sd8UKQ zc7+uWCGi7M)rAJe4QIx1SOfH)5Sz_5H$N0{#vVG??pov-#l~xhc+ZeYK=Bhn=$zfk8Zjzx@3_V3Wa$Zrv(wId$_j6O3}E zkSzbxsIER?_mFxe7rJ1roNwhLx))JbfnGdf9ybzh$VWWB4cv|2uUOcDw!vHt&fW4- z6M$=fd%B*LT}BX{AhYzbxmb->N~(pabqztLzWAFR5gl5Gdhn!-wXwvwZr@!1?}c;q zanUc3mw$Er>GowIZGZ^7&1i{6G*XyG4LOU!+;pVfV*sVV6M$>ket7H=bs_IfFCyr( z@~$rJC4Uf8@|y!Ck(p5%p=EH980=8YDlx^rc;$3@K0S2@Un+KP&2BsiWaqWTp)`-G z7WS?+0#}E2jA=Nic>9Cb^nYjLs*oY`kom8R%xZhOdbqTxF}K{Nb*E`mm_kD1>xsNp zkR5@o^{`Y@!wb#~;On{2hOmV+^g(i{;m(F!MU@Y~ zppzYQxdf|cD6UG6CPBTTo$;K~gZ125acE<(lQ8iHSSdk9TE@AWb>GW(?Mekj;gjUSdzmN!oHzji zwIw4Z96=UJ^9y#KW)~^;1+XGT>=B;2hWjcUaI1v&jCI6YWLXbQP*)X-iAeewCARU@ zNjQ|*wllSY1 zgM@Lu?sbilJ)o@vZq{I>pWt1UW!2yVdgqNF*;>rFxkk4b<-%kj>SY`fhha*d=4Q<5 z&QOuG98sKvt?38VLe z9qmpJw`Xqxp5OIE0za7PP+Lu%K$u0?FNtcbdssZ>NBGsx3zTb^a@)Xjm^}YrZ-xC> zaUW>16n?L8sc!jDSI$qLiTdS_)AnHzxXjtx5GD*6jJz;t{3jB-phuri{+^b6TOW#r zW`XXOwAm-j@zP_IZb^&o)oUHwD|@$uKbrM0ckteE^Z7-v1gUE0vQLO<8xQ52R|Y`H zslA_~qBr^yGxX1^dijHP7C3i0LRSOV5IBZC;EgWX*B@RV(=W10a~BMqEN}R_wz&nA zD8PAEufg8Orc2{mi{a1<*Qh84pN}rAD!<7?7~g3JUxd|AwVs+uwD44d>adP4ZPD}i zmg7y50X>V?AwK+B^D_jNKra=r$E0(BjwR@qynBAtY~xM>?^Q^|j5>;jfI?Fw`>XXknKwDM4}&&UHx&HP zbr0P-oNBTw3`*gr3z>ksK*|87p_6TgE%mR{0$KG_bS#3+J((}PB8*ck5W-#C7Ms@y z2yuPPz)Y1KV2-`jLAUs5M9=|zmpZ*CNYE;3`Bc(``Wtg!+>!Q;L899NU4?Fi%4r|^=kwNXs zX;ri9)BTVW6aIBpHb~W&a1N<#cI{hI3XYPQ8|*byBaR(VPL@8#%t@QX8Ts;aDQ4Ko zzmM=I!IH8tMyV15jGvir2p5?p%nuW=&XTtl?)Z7L#_{YKy~+49FtT-=lo?xK^yifF z;t~5V<(9){O3THGnq?YHCm`QrkY8h4Ph(qGqkg`mhAq4){DQU+I#~nkk3NFePWV;y zS(Kr`&`dema(0TdOgGGu>c<=8VHu57N|br&>A!v11UpmJ=Pc>aX2_yZmpLSp_h(!f zwWV4H2NHWQNB}@Jsw}pAKs< z!seqcI|L^pwW){+trp-p6$_=6z>2U0g;BS?Vt^fl0qWdaq+E}hNFb?H%dPTYxEf=0 zsV-`ip<~e?F>^8{3qa$$DMbWVW!6jSfl{gl6uQ@^R>hnhq=8toLih+eKo89VD%I$X zh8#;NGI;O|Q$_^Z%I3Sd>^G5C~WCgRCYng8%CnXux$)++rLUP`#uTwK0Ge zjA0!I++(_bFz^C_Xk!2CNU6JjY=DOp-g5fhT5h)9eioiKAU{7p4hL5!FDnao8xA*5 zyZlp8N)U+5Tv1j^+rQuh8ep6;e-H1&8SLeJ^CsdARz&-|Dm`TnMXwDbw(M?0$2GR5 zP=|a23*FX$xdpRU4d21?=AV2weA@2lC~|TdlGo79CyxyCJ(`?dC}G#x%|&i%=#-Gx z6vg_!*tl2WzEJW1-`s<&ylOjC{uf>-Nlbu=nr~po;*PwC`y2Hd)M2|&^_r*^E{SkKkZ-BfH=I{e-qPHX z%?$jN(2z<9g*zfxv%(O#A=O|PI71ZIy6&p07WkdJ#5sngzW|n-pP%9F{g2fZs?20u z*Qu#5F5h_Hv9gypcNYw#zvQs9gXBV8K0f~lZv(4=Ewxx-@7oeV?MN5lFTiS6(SwnZ zhqIqiXk?`0a4h0^A#ZB2d3tYg5- zX#wUtPU`4*d6fK=yeO`#|JCl^;Uh8(LqBFgcWiDx)5dF$E9mzk(*vSqDZgN%o8+Bf zUS4$@1{V@H>#AnnK<_-}Z5YC%c%7)%?Lhr~y?#IVbWLN4kI}#-r+9BNSICorH8UY( zIOX%Ky>|lW2Gj*suJME6wcUR`!hJ-Q{iSm0>9rjxmzYCL7337iNUxx!)nPaKQng5e zGt&ek2`RM2_3P7?ZVKoL1K04TGhR&C+kE)pY>s)%UQ7)#v@slu9)r7m+2N%rjQ}Yl zsjl7XLyZ_gNO6&wcs@jxIIonYjj4NIy=tq_BOT~pFC2|Tv#8W<+CH~9apvd36O0se zZ)9zg1_cw!nCh<$Mc=FqqpOKULk&Z)JFNqpot@(je?-4WLULYiLRa}`5`o1{!tf@g zs4k}E1)O9N%ey|m-Tp{>Yj5h?-h0D?+>iUdCkLdY@&j&t4mFuB;SF#%j0cn&^U1z; ztlL*~hb?Tm+>zO~lZ}=Fd1yxXudH_@O+=9ok!|v;$^29? z+ix6vG|R2dirOo!wJQxWLBj5+XeeeNo2=8=7F2U$>91xYTR5lliC?ohdg`ukJc(XW zD;F2hQu&@OO#PLk_6v)QR++7(1BJ)UaqZ`0_CGy6c6&0`tE%eS_d3kAgamyY9|rPfZEeGET{~;-@4L*` z85^r)QzuFu|Voo+WzgGmNl9b)~DsO_KSfB1hn;L(=lsZRkb|ppkJzWOsxv| zdn!3_*m{o!MT0mCz*(^NoF9KZB@Z6N)`rq6>qk3SCc?IH*Nxg#MLKXW&>5z)RHR&y zU~>NZKV2WXS63IGXBDVb6gRB{h3$BF1ihX__rDr%23mYxPgpfE#=yjQ4Mad11tmTu zsm!fXmG9=dqNFiB^Uoi)=Phi{zZW*g!Cks_{?0!b0H7uJKxvi&0vHN~1`oaVKa5W> zi19+2CzJsS_3DAa=$bg4byy!mf-$adDs7rE)eINEr%qTnX;>1_+h4m0WrGB4{=(t#++D_win< z5O*MK3*BoaWBlPW>jJuELR_e4kJ=@!n-<{I=Gr6*g1{7zZH_*@DDnUwMuf1lL z>^JtJThuG8-A(47s-uoO{{#@^i|3fg) zxM;nTgKP-$8&gk};=8!y3+T-rSXqOfBg|DRD#vvzt3<UVb3 zwX`CkhV11Bj%Z0}z5FdILa63gx_s(5a&mM9=#@V(87ueX~vZ&2tY#Ld((J+ zT5zf2*(Up^^S!_?J4Yboql32K;hLij;8B^94rBxvW?mP|IQ>xLjsIbyLQfVIy3zva zF|}&%a@&v}SI!{)nZLKgWN`}VM5&|yD8b2j?|Woz+j6hiV@`4W<&KEiu*_5@m&abc z)Mg+eBF>(^%AUXDVh;-rI*B?M`S6NDVtvB#3Sy=`#J`5}vMIoPk!$%V&)Ogw0Pf|% z%;!H5aDqb`^{^<8@AlSYU{gZEBw8-Xv0jz;?@yNKgP^{S{9i>MVZW~^2efWB+}xKI z3&qOQ=X2VLc;!H=1mP59^lt)x((^E&w$!nY?@#fUOQ8MH4epjZf)Z1W-y#X~xM%<@ z#48o(!Tg^vxVlrr<1P=8c>$-$2|wSRmd|13%)AeU^+|xX(Q(W_csKjDvQdi!Wtk|X z8kU{vV3Yl*4)99+S(p5=+;V;(TAn>`tpIvehoZbZ#<$Q&wIyW+5=VNf+BzR+hMr=k=V}J$8ns#}j|O3Xr`HEcdsWq7IakCI8oR`os3hBF9{O{Szoj zQt&N@yzJkZM8ooumNtAD`qJ~N?Ul3r-EW%Fx#zZL)tcWL> z_SYSfl1DZ5hzTARqF==)SZ*hfBpJ4U0%V%ieMMwZHd$ZyzxOAR5sQ$5iM%LOV`>^3 zFRq>f=p<~Imie7P_M}^nochHc@Qu2eKN%!fBvkZ_P5(SwB-n@Fc$_3v-3Hsy2&NU; zBlcy!xZ16ikm6;@*Q3!m_RX8Pvp%B0SsQ-9;2(R#l;;q5)c0RNMy3tovNL=PC+7hx7B3@zm(%oJ#(!?XfP9 z4H&pMe5^CFp&Epop^w~iW}&%`$uYF1LCM_zp8wivtt|7vg8VA*sA8aolcSg3W2{No zfA$ZL#tIs&?g9@rpJqE$os)j>2`YIhn_3GAI6-B=)5NIZ?0ux7u)UdgCGzg zzdKJcs!%gEwR>4P!j)3=n++emQm;Q1JwdrwM>WQyqo08Qq^jl(#r1|M~% zx4O*2b_cJHS>AHkYWIcK0@wkHH2A)z!e#NJu0Kq0915L!Q+5 z`2Hmgr@-3U2ndV1YiRe#5H>}3>xtbH#9LNm0j|P9=Zh&U#oSzU5E2JRy}I2VwL4fJ z1EJt>;@jKhsn`ScoS&cHzq2!sn6;v~JttMbb*IPY_(PY4>=M&!FvrRA=I&mMVcma~ z^>RWvrLsH?Qk0sUj4M-GYQX2i>gRiGtJx=YE3mV(Z{_5a7@L@Aas6VS13pg;S;^Rf zcX-i`;-XHgn1{)iF`AH-8PJ4f$e^$l$|e+I%bg?aS2oZ$%Ttnc2L1rTB}5o`$Rs za){_o%w=RQTK`H{X>crhwrSubY(Z@Gb#2|}kbg#=42;oAzv{V58s>T+RqEEcB8(qd zSr6{-c{J-y=cmS`$j^Ur2saP2LSf}X-@au~9BCV{KWm(vNPm<^gOG7nJTyBvFZK2P z;cRxJ?{LXNu?q@XC+C%79Y_Bn@XG;PGW}X$tfpEp$72eXHMz>PmSWr;n7QQPA-f*L z`MlHt*|(iQoV>uMd@eC$mse7_!!?#;-X7Z!)|#hl=}*w?;kg>!S>FLv-S5L%rUs)D!%3Sn%tbk*_utp?238UXrEFJe z7!|;Q_#ZxkbU*v#N_of$O*<=pdRwiensRuhicmnQPN-t(bt`3G^+k4J+!HI))}wB zX~M9$5?dc$clo~kdgiQvgQYAnK1~wPOp4PJJ6D{2JEgc(U+U{CwCvyFk&?f`Wd!7z zoy0BJK`FcO_&|fTrrO=p9T6S-%JSr5dfe{T(hmL9^-~aywIHIps>)*aJ8c>1`MWPf zKFUQS|L~OM#wYAAMv0o8_7sLmk%Z0s-ELHEhr{GP^+^DNYhhFX74Xx3Hz!JcQ_!Wf zoDYw!F11ga=n3OvagHks-E9*eQp(ECe+Hd#i*TewGOI0gX5z=E>FM+o z2RHR~4eS$p{PlXoE z1Z*2m*op5C{8O?~>SjB?jLpOfqj+dENd;xXieIaSTqMhFJ(Fc+5cIiFnK>^FoR%?E zVdpAWxQu5drNnRcZw?+3Q?eTM*rsLT?)ylO9PLW4`*aSm)3?bgOg>@`B-g(BF8Y}x zE^Z?Pho#%&Tf+CNOG&Cl8l4LVi!d%JP~D{c1oug*5mhm@2Y z%yhQMJvi~`)l125_tkwY-JJ5YIwgP_sfJ)*hOW# z@f|@`Z(e5zYe*e?B8D(+Q>X)!NQA=3uo4pEL$^GFp#{e*+vyx8)?1ZMi|)pMyP5DU z#=d%S1QQAAI%vdc$&Pl$$SYiI=ew~I4oq768dE#HdmDkD>h!PAsL5d;Ee!pHBps?8+CgZC&NyD9xl0dHmVuihlM z+k;*NpEm1p)6e_&$V3`_uBbhM>r`Mqe_T>6+mva$WT*!>M@q`=!z}?NxxLPOlXyx+ z1rH)kVT4z7`N${A4el$0HQu3NmZa};^bI)u8D>)3E2NY0E(x}=wQD*aA$)4`VwN~v zQBcgaAhQ(`5$5SMlaL=C;`(AY9ayM%|Mo4%-mf9q*54b`Ktb>DKFL$15NXK+oOFZ# zy?0!OXGxZ)Q7aC7j;sn6t9k<{saPoMFW!2pY1XVeUOU77};{XPSf z*)Odwn3or&xs`S_4DRhQfP)X) zqeM>1mqZA=+7Hs|K2r*pA@uL*=|0fGtO6>2pl`ahQr zctt{kg=Gv{XY{uOe=Kv)v|_F}Qg!KFdmSTl-i#3}S7}S`?+i#%6DFi+#6?$fr@c)e zz)HWu`0O;IP{X|SG+4z~EY#A_KsPguZ-$Q_EGqh?wz}j?bH-`}GKWbQ({QxTH#kw8 zmci1kh{-BdgnOWUELX@iimU0MMm)K}kML4BExAOnJhBIT&C`1U!;zp3CMvUCS{gw@ z9^Xz;{(Ydx2}6duX05;2Tu>E5Vhm>|2theCx^^5FXO`tbq4|j<$ei+=*HXiF0)YY6JB=dMv#=2Zs~3iq@+8fV`<5yn`J>jx}-#6>F!2aq`N^7sipJd{RiKi zGq=yo+?X@-oExK|rig<{jtKw&aFpK5X};9K|1mo1OW7x3G5S&=d&nqhqrVUY-75N} zkKy{>zykoFjQWp}5_Zq5Uk1rM<@G(aTx>kOE!?dE-rnAv_RfwTmKLtooG$LRS;u1J z0Kl8~N^;WLKH2|reDzawp1LKyk&YuqzXu1S(_u#IZ>{ZQw$$qj+uMA%+}2-|05=*5 zOjz+-s~IkSwKQ1bVw-m_9}6IHN0RSxwO-!@dfz^}0Zz_Mo3GuavwS5d`wBbv+fV#= zAnSFICeXY8pSoO#^jeX?ax}j%qJh1CpJ$K^rL>+cfn~w&->{ic)qok@2Hh)raA(jH z1qux^Ddq*p4=5g-PApMJE`;iY)(8|0E+CfB8kGGJOc8t?Odrj)xH2NCV9s@lj6Zev zEKkOQ3Zh2xtq+V6ytD)~0=#L%&%6gtz?i>~u7P;8k=|)`B=}Nt@6joj8e~f~HctLR z4H@n#3R*J{Gs#EF%bN8yx+6k?-b-mX?N|^@?N&r^ZFqr_7xH`x}K!-Am7|J*!R70+qtk`GpPyX z5^yeRtep7R@Sz*jo!RJUfG`FFxzTU1qZ>G)VR1VxEL!P-%V6+RgXpMB8t2)_)&Qi_ z0%TJs+cAX}{m&A-+%U$YmkbyK?tQ9D!Tf!Ye!meW#_hbKvwYTg^l+(1<$Dj-WYi`Uf9ibJ__{RUr3iWXIeGb7l?YA-!0TIrzm?-3Wp9` z)s_QQfQ?8bOYI@V#s}*Tq8t9^!Mg+IK|vDP{LnUqxaN28bDGkO3>Cjy>vw_os%6b@ znn5?EV583&Y{I~BdOo`VcCM>`YdLu&oI7~pCUXeml<+2($(bx?y6rJC7TQy!W7OY& zgO;+{7yyareahaWQ165{!)3k*bPVkFcW?&=NP9v6NiD2R_dPTW9pRBR zH3$3#5&+E`nr8D`VgF#d|+iSsyy5V=t7=^m1~x%)d& ziMv1EmNWK_Uew|vU(l$(o{QYQNnavAaT;gYkkmiA)=_3CUxy_B#E*&>e@1v>V)Z!* z)Xkb2`EXQY{XD-AHTtXdCVo8ddGl;rDJH>N!{mDiNr`Hq`lkZzDZ238nZo)(qHl6P z`26{e(Ps{R-tAE=p&;`Glr#y}JE9rFZ`urTP(d@Z($)-Lze=g9vhgh{d5WA~qj7Of zNfdspSFIue^ZE=vBg_?0LEqJf>|7LbvD$YM(`Kg0W3OZMJtfe^gOYQo@c|z{Zl(Q= z{-tqd>nofsie_z1WmiV{V-}wPiI%otn$ki5+Gx@YC@$vkv(D?6vw1p8Xzy|_22W{q z4+R&27b(TWfj{u4gkj5_FJ9oTTa~(o@b{H=Vn=T4{0Sc(-<`fiWQe(2{k=u?`a^>} ztpn1rXR)%DhTof-6~BJ%nZq3tn)GV9U@};;<zAfvM$$y)cyQKe0_cG_{iz#>B%IVNCnp+ycQM~1)t1mE{-rVGMp2%LqsUK z)UpQ6_9vS-T1jr0%u9sb(bN+FXd!Dj&l)T#R)LTFHHnFH4G;*8aLd_f*^eI^yLzS; zQw8!fb8`i{9x(Sifg~xbA2i8SkH*q~7}bW6QmpGj_j27QAN?{7`!y zR`5!5Vo7dbrsYt0%i8iXg`}jU@`n!;s~z=*9MdK0wiAw31S_sp!^mW0pB!AnmRd!U z7*)3}ZV`?0js%C7#M{Fr-5Ts7)EjfsVMn57r9@pDcN3BUcYO0#hXW6{h*CJb*#G+Y zKU=nk5|T4BmF?|W?ypbwW0-QKO0{dvdw}1cce(n#j(%q0iSP96%@e+!UAR>!OhmJt zuo_bpbURw$(Pn-9A&s-RpdhrMfbPY-+k=w4bYTxh<>I94^`Jac7M7#cw)wl~!|iQd zt@e&~AB!cv(gGtle6CH_Tp~c*n$JUJ+mWNZd`J~HmX)pcT0vnU zm!L*zk;{5#`~7j-%QOKsb;6XL)meo>ZW9ACvQ)=QBJhsqcFq6xPhkQ%w5sh`YR&4gPri{&6bnD;fU!m)v~ z&9pU-a6`qdY)_tDgJf}?3o`QsxoPrP+Q7hI z1%epY)?GrIuQcn*ZEqLLZNGQM<6jQ#mf`^dGBPsSA4b@`bjr%gNJYKaQB>t{CGU^w z;;1G6TWceJy|N2jF8zga+_jBcI;94K1x1MZC<7RmO-Thke+0nqT4v3;xt}|(-$*#U z_>M}p5J4F1ll4n8_>|#P=-(3m(`DXCyKGJvepR7j!aKhUyQo5z@YgiZC+oxMzET=&)L?1(_(G$*?LP$ zi=1M;i~=!NmBOZqsw(*Wue15|ch!f#X3y^Wx%yR;>!y+pNL%!-$xlj3#?uYu+EboQ z)Ume6<`w@UK7vpe=F7~4K~|#r@0EBQ=PCxJK6IF#XeObR)Yus*C`3*-jpvA_z4BM3 zl-mB|`fzhLN@BEdg(0{?^Vt`9?57z4 zV>WfGt=;tcKm3Y`^WvFVS+~OQB&yp?jf|FmU{#Z=8j>QorA4$WdvaDd3|Rr78lI1l zC}@O)RL*A8y~ENa(=8WCdS(Hy3#Z=8$sr$b#!;zRP6<$xqzGUUvsY7~nEL zI<3>zYSQo_EGSe;nqyIMz0TlhK*4V6;QpqFcaTXOnJ7nM(tCBZ*cVMeuTiSKLrnC= zOjG7Z*8QQ)G9wu3{j>N(8Y!<0wz}9EvR_B;?r54|ygtCB)w9KT(d`_)glx7;a;UBj z%!Mca=aaF3SiL85HDU0}f&S(n9r^YR;NVCm=2Jb&H8L{daFqQy6^l(>LvQ}(^NUJC zGqZIY=jy%g5YWSEAD(f$&sptxNiJ>b^Zel)pds$q-4g9!(Vv_*-7pIdZgEhxDVel97`$9!kPhpusHUw}Y;PLoM&%0Hrj1 z$+GPv{rEm#O%Ka#5a{LY9}#Up7mi_y6(W?Jwha40Mi+h~B6ypMdHT`hOt1O?ftd8P zv8?rj4t#?SkT}1u4JkeuIP7Bc8C|T(x6{pTka~#lvZfZn5S;1GmcuIP9V%s%xj#Ym%^=Q-a)1ELTOmuvk%@O4UAKu&hyCfot519d1 z1WidSe1gCm&?FtU_`l3jEUBJIoO*A%cK_Z*AKg@u%byBYLzJ|&=YmNmEm6NV%rzzN zvEf;%0=7Tr%9|zF$fygcU@fRzvJELim71k;r5fLU-)a5ZRhQ0*mD%2PDNFmF)Mt|5 z({3`oKe0Rg`L7dK7J)Dk)Wla;%Q4u904bJhN@c%1MOmcDaq zTplME8Gibb*Ll2SielQEbYUgag2v2}^03Tie?(5s$KH?jWuTT3!1^Q?)J8 znJy%BbMRd22h$vq_aB`p`*oP{F@6{uT8L%>sT7JFl07N(*izz@RX)ir6A9?(JHwmw%}pnO-r{UAorwrCC%Ds~s6Mb52uw*Mk;t3ORn6(?Ze!Ec(AFIc zWy*6(bX?d>W@>qc2`*f$`6C+%l{Cr$0JysM{2YK;zn^2M0fc`+u!NiDdh%05vE%fi zD}xOBvE0QrG}`kz>HXz`gWMAp+p+RN-(D28rB4&GXZ)TC3{@sGZ+@LKddCOmXauK8 z3E7w7y2pLNAcHuRBLlrMsTD>;vEx}v(ed9MwZDFdVN_;g_4${q-hAQ`RM=h?D;kMS z+ejwAP`N@<|%UYtt4F`UJIuhlG~b} z+CYhNDtx&mne6!nEe#V(>G*fV&~Yo&sH!U$9qr?L#^jjCg9FIpv^u2SkKZ~aWktY; z8=C7I8INB`3j?ftDugo#p!hHXCgmE~&f z195-$d)ktTj@3v^-eRTuGp!{jrpfGxcA0~$6|2b%r>BQq+oFBty*x3@3zUjI@@4+u z97IN5Q-VhhUT;n-S)iNc6B-Z7!Y96r_oHJwKM$tgOinOXHK*@13x#@$#Ey=#uya^H zR+^B)6cVj_oCb%MDPReBJe!-ye>`9ASmJ&YEl|Sei*t_16q`hDe|q|$2QuK!j%}3~ zK#z&5chB>4agKfQpP zLR4&X?dPWpXX;^Z>&gRmcHwXHi&(+Bpx3#QS~C0FeVJsuO1_SqOo$`C@&uaRQS$r6 ztK<&C#HDru>W0iD!8Pga-TrJiBCN<67y5S$Q|9-*rj5Hevu}kk<#?>(&4fgoZu#`n zT?~<$(dO0THR|hA->|`27@y_wiox!Q?mnnuwH7I`cH#&5(TVyCU~N^9C!nNK`<7=`=ZjkI;P9gqUR+cV zDiRh_JEF|_n%JF_DlirW?OsONJ@|_CtdD~?uVET zB-T_$(bPWjyPLJg{;n zCz&{SlR;!;56wZ6Z3$Gxm15K*?zG*`F*`&|4)sLjv#@D>`Zd!p3p!3HM)-=%5oyZu zO~1IW_7`qV4Sn&H9qbk;y|UA*2hJ% zH|nNy!sY!@0pGlBD9`q@x5&TnjZMQgEoxc31qfarFYe{weVcZ;-LAG+ne7Yc3JhMI zRb7zallVfqU(Yyez*+=P&@KPSg47%w5zbd_b>f_Qw7R;3CAr>})^PtXOdKGfgWSNp znWHw_ZqUeoa6J9aHhxuSyt(JrVIWg#mux$6(P_^ruu?m5)?sF9d5@-EQCI9{HmyR) zQ|dhOy2)i%-Hd(rbT~O_cfHY^0_B@l{lAg*)VD74jaiLH@ZT0r6L*z}*RgzfZ>L0Z z{bN4ebJSu`s8$sUrqJMmYwzIm!Y}cI?m>3g{OeZ2QM(vN)_jX)gVxJEbeZ66~hoced&r*4`xk_OA8Mz#r zh+_4+aMh%^uw*bM@hvm{mPlVIFWqGvRC{jj^yvz&Der~Vvgfx;*^i;;1#f-{Ccgvx zD@3}GmRFjnUpI(-{SXj=0x*=oY;RzKezHxLM@Exzs@FRFY((t*93ggpJwtPjW4JkH zdrL)!K6M~I(luXUe=#Fmac!BEb@yBj<6Ps=TF5?!(cAxi+qv30+d?4|O1s4AZ3}lNs)wQ1^s7qp zZR&>`kA1`>@8$r>us6Q0jeHb-Nkq5#J0AN3CL*21MbeAthz`n1CowMC+8`=fc3dt% z6wzwAG-1cTA^GB1@Vr=W z(w0UWowqP^vAT=pk#>tSJ`aS||M}4c)x@8}cVHkcXcNU9T9jg|D<|*zO>QaZKeH_d z-VvW9VW$hn{W@0utvRcO^6T|z{}*Ncl`bIa(r%m`f$brm*a9cX30pJiO3AgY3DyG$ zX=3K~RJS1H-lZd>XhDC3m5nT@59h`qQ=lzPNB7-EY1gZQO9qBAt?kQt#D03U6^~=Q zvJH$4$r<=YVa2C6={KQ;JkcciGh zS_Nyu`ydx>Z`|^%Oy|c}Wh%;dah0?dM?XadMrLG5i^B9A2=ecf*m_aL5}D7@Pbs&K zL{GsDGglB}qQRPNd&mMqUT(Zep%ILlP23}QbNyWO9ZxkrJb46uDe8TGb~qb+8k*VF zV1V$X?3HgHo6<_n^<&Jyk7a_wZ-a5C(`d;UJ`c_ zg`@}V*$H!R*t?x930Es1n-nBr*gsjJbUB7w<6)g-GoQh3wyj1ex0GM zYcQ;ihF5EG?L7ByW%w3yzq#-MZe#WQ)b6<+;HjiyVW<0w$w53T&Gn;|0PnIAxf4Jr zH)k9tL2PSb*&Y2k~VIV@Ej3I+(W4`baD;d57x z#$wiE(=DO0__n}hTZ2=)h*%kJqcMNw*LI-%-wM62F@Hr!wK!riBqZ~_vI(SD;Q7WT zuYy~?1x>^SN{=wcH2$Ql{exiOVf)*495>D0M$`(HRNY_=(w;DpsoN9OY^4$=$9IsE_X!>46dT z{;Eb-8-~TaMUJtXVrb#~aE_^JN<^mp;C1k-PVy=J27~bCwCyDO;K3T5#HC5JEB_H5 zLVfBTexJxDV}p7{Uu)q~{@g~D7#jHY)OLeB%G;kDO!F2yj8Ibca`&`6y4i|Tf+XsW zR@&)wPuPT~AIK4(YK3D?rQxb3rz3a>|I9ufQs7PyMgJ71FS|gQYbz@$=iD9>%ycoP zz)8Jo8i(JEF9#gUy$ouyz1zQ;Cq@MOQa2fUQ+5!((r|o?5t+&|tV=31nf_z!B1Z1P zm1ZokF@vqaWELXYer)a#im#rK@GDFIO& z%S`vZUAL-ksG__CDiS^t002Ohk`z;h%)b8(1USgMSI}S-G9lVYYB~Y{gdzV8sObGG zW5^<&lemV{4_gx_R|5xQfUB!3$lS)#(a^xo7-Z{UntslY4*&okrNq9exMiGWx@#oN zzjq0-p)jMMwEFszAdofZm!!U=wP>HGrMa`+nWSk68n5Sg%4px}Rykx-gGEi$M|k-3#Xg zStmjJ03>{SyOg%7f~>k{r&6`nF@7cVzIBw!meJ-GTlUYwP<|3h7oL+W0)0H`00e04y6GB4 zi02Eaw{teO>hwkl4a@Ucta@}~ry)1VrDNQ;Q~t6^z%6_lOOAzw%jGoUu8&t!ti)^d zDXk803&RN@L{X)~Mk+!D4(ySN9xXP|b1Iw1yHvIPjT1mbv|6eScnb|hCG3E4Lo+Zk zQ)8EaviqJgJ;|xE3}TMyRLB^L7$oXlp01VRQ^P&1`e}H!Q0diRON^bE=uDV|$d7z# z0nXx(b;R@)?7|g}u@R4{W@4Jm7+zyBOnWuwO43eEZELhA*E?UAk(0~x$p0W^Af`{N zGIgHKfZd|p%06N!)XB_vJbZoP=kxZeYrg{bWebevxo+;?Cek|-LGy<))A6gM^@8W2 z9)vrMZ=ISlXWQEeijqmSHmsC_89xI9^Mf4t7VX za$JzuHFeris?6b(B3W_-4qF0GF}?Iojs!Q$V4$Hk#pT-AuJ z)BKN5O`Biat?>4r!}7gyaC_);r_Dz&NZ=mRO}g7^4c&dNcBoE8ZgL~Di3wNh4F+>X z!PPK4%veh(2^=m$Y0`iJlA}9IiHuwUk4tCSv22Uqw$>}3;C3$d`>V($oXYtz#@4mX zjnwOl&;dTMmMJ^l-*TcqzbsVt4tIY$>AKAzK%=I#=cWyX2@Tb0c8?#bsp-63%duQ- zOh`(`^trFFQC;=?AhH=d;ksv{7f-o*=+x6UwUp@@B*w&}Vt}qHEUYycMC%Z6`jb$> z&<+Q(T9@ly4HH?QRcUhj*Z5RQa;jY9@ocBkg@q8`bG!i%ng03m zd96h(I4D+KrElc3;8y(4){>55I%ijxBJEa}z{i)}(<4cZtD<8>yPi$rG=go!DdROdSOn<;syB@eSq^OoPr(rc=m; z0WlF<<1oV2XoTF)(-IVVtpiKg^v#K>p_!G83XxyCHwPlg>G<2u7t#>ZT`o+PpYPTf z8Rp7wTwG0$=Smr}_?H>JNlMBWZN5GQw?%cpX0ev{qE4kiUlQWW2m(tJ`sC7U5{JLm z=tH%9`gHqm%1^GkW92m1&2X5JW(#s*C^Gm4qeMfv9Ved`IZ1pgSy-H`R~!9sZryih z6`lsEv%i+%2j_^^(nV4NV5V8@_=U{B&}VyuRn?iqrlyZRzP~NwV+Lg6DM&jS`}`u4 zS*Rp`dUhr_JzKt;;DaC!YiT3|p*#d`P^g)7=PJQSP9lGlYVad#kz%0vFd_s1&D=a! z>#MeG4|*SD$1{|0&KDD|%l5? zg^)ma;o}ycsO)%O=#^`s0Yw2t0AvniweeKZRkccu$Me=^+Qd`6P9m;CW)8*j`Lzzm zkk+6e?veeaC3bk+pkP{+QpwSYqPD+Zs3WocA|p}rUv5d+xRL|+jP9u@m`&$sAp3~4 zSzVlsKYjJ`=JU~QDueg;FX_VHgsS*Js`>V3qBL{7g*LIEZy>Lx>2Q2fs962w-pOh< z_~Z8jp%4+@z!Q**hLH(ZK1mwUc0KJ12JUjN%g+f3vGhh{KQYk@2Y2vxyEna$t46Ex z00PT=XheGQc__wKM+~5+E^)gIII;cCVtg@ zy&u?UQK4c*aK1lse^!5WQzw6Mq7RATWTj^B{M;N|^qrmk_NGh{^Y2GHP;tSO z!doJDNs(N?6LTulsyEtF4=pA`&g?tr zr}WOJ*;gV+0ju!263@b8D{8b_eSeeHYzRA~lHCf}AFW()|5-L~fzxq=M*RDuefUsN zzyb@XUR6I7U7Pne?oa8DmZC}^;E$+cwRQxDUh|t@V%jIe=llH@M9d+2x?hkpapskJ z_VCy#Q&HB@Dm#?ZR{c4C{^-`Ww)O|<^P#*hQGOC*?_h60rfuiCS@|D}hO}iC7OR!^ zRsD9zErSvil%$MXQBR>N2I(Dmc<}DNJaB0zS{oV$#ETdVsZHT@|MeKG2E4lw2c%#V%3a@`XgPoD=xAg0K!U54A_fEDBxL`Jf&kduAP$Vm?@#W`w zN2cv(!e2Y|)n`Jlw(o89rJibj0vNvBi<8A(UV+YpQ#J_suGjy~qfV@46R`ieP_O=8c7@iS zmL~0@OB9~vH6y*wIrfjKa}N9@NJK>Nz7^nMD=4fX41iy#9u(~@uB~s&V0zHN!GCPB z-$jXZVRd??AM4hFX8zfv@8#1Cx3`%mZk@U|5%uAD#*vRLEb4iy|NeLdHwp~Ti~G{} zdK<5{49U^1&C$`48|@~m;b&aEIXX6R3?qa>k?w?CQB+ldfGtfc&V>}1dRRC!95*m{ zru7FkV1YqPtU+shJOV{+Ealsu&Q7Vw>J*lMkFzyRJ|3&>e-^jn%pn1tAM%?00VS23 z-rlkV>_Y{dMQDW5E17(KL_9qZcruClYGB`a5J@;n-Fn-Dqzy3ivmLWu#D`}7lsVs* zBKX@PS%?S(^6IciT#kf)ew?CYPxz6{1k)3i%fjP%SGKU3|9m^`fr>Z!_jp#5^QWyo zdpZ#Y1_UyuPl%aukzu>=t9_@zpL#v+*_{a7=UJ1CU|Goz=NA*TxV~(VcIp+wm0I0eUF&R(e=&kND@oKbZ(zi`)Fs4Tf@`;1?jIK@E zH_QDB&D-NOyi&1W3$tcOHv>_|PT7Jq*hnS(7VQqt8rvtaviir`t09@)+_xfcR1dEd@Af>4?K>N z?T#U1#NIVIqjIAmuI=uipMPAM66b0LsDt{6@Jv1~&x+-CUn7Cwet2KfgL>2BU3SH$ z9MhNkh7Tqs89Yb&hzAZELE)Urj)SqUyg`96&JUpFXSlfUq?GjGK^GmCR=Oksux4J1 zBlw6itk9|1L=h{pdYcBp>cVdrPApW0&QM1f7*N8wE5R6CtF=u_suvW*n!Uj!0l48< zV<@fBL>wnxKiu7W;RK^1IY#VMaXT8H4{h2``p4$b%PeGx;o*QT%(1z3U)XRQVTD6u zQm#}3BZ#!>Kcf|CSocl=w_mk>Y0wm!MEO8aO5DbDa9~N}2A|ol{?y8afVZ#N`%Q6Z zA2nMMK8|G}7Dg!8sF zZLpdJ)xjYS4_iezcX$-rV5b9p$S<5=c)D z8$9mxOHzLMwOCQRv}&eTTi)#+LnTzIbDU2@HZ;NQCl5&#;=2Gz3Smpw=n5Uf7xiQ+ zJ5rG{&H!k#cxk)^kbk#z?-9mQn&bz-X1M(rD#GL8iO~2PSkdabT<21$sxnbLijR|1hL6bh!=LBm9hr!8o}>;+8*gdZPqv85 z+=7@uNBu{#Ey?B$$fiZNHr7jlkK zM7m;+Yj6C)_={LY zvx^kzvW*7;q0?FE-ddXno+7u>Hc(BaFZz|`r zfl1_SQ+>0q^1T0+qKz=D62#{gX&G6|&fKQiV1UTqZAw)}Ak=JlXxw=AeBJ8p$+QO= z8sX}J>o`$SQD+R8?LCsgwFx1>2dzz;CK*91oiA%u^}F*7yP~TTQUXWnnco!UVqL^l zIT8{@W0B4ec6CaA8NHyvf<0rj^T^7Fa+Kwu`Tb*}&fx>wVBkK_6p+JZpoTPsZw)?$ zqXawPN?9Q$DNsxNT2UU>vCbiN860H0sB9V<8!r^KW1dt<>no8UE;J?#CU4M^+X+Ip z5^{@)HZ+6~vahpEOvDU&QQ5&yE6VH*z5_FwnWLqD&!`yD6gK3y&)!~^+z%3Ls{CFG zNGrbF#>pS)n)3Q@w%~L!X-hS!1yep{H^#PESb~3hcX;A_+|2(lPm{})?k~2te{~&< z(u&R?(bEG52JaN9ki`!}xY6Es58mO$>UeEMUokQRx5=zFh-iW#BL_D7AEUHkbQ>fd zBY|y4A6Rl%ZZ{7e$ASFq`^X?8yfL9v6esw4lQb}N4VIfiUDxzP){g6~PCOLNV6Yk* z+fY)4zbAb@j+X2Gqul-4O%`Z2oOj;xhZdGR6lPdsieUb5kMxIYs@ z&N(i4z#TUVpv`GVeBHp9sHS_t6FUFfnWTdJ(EwAYNt)`iou-8JTg&`2^wPK? zZi)s5GHQ_C@W~b5@y^LNF-7$5jt-N3L(Mokwhd_b6R(C@e*knS`g#!OIJ-z z>(oLORaK=NWhF%&S#NumAUf>SmKLnC^Rq9Q=!oSEvvPe>Fq)>{e#LB`k=Z%hk?4KA zz>!+HeL6z!QVndst|vNMax}ez-NG$pGR7H5EGcBoo5bqz*?~&Y4uq zT9=@tG7>eHu}xBTT1KIq323{08tRT;JX~yBLvIB9MlGWPnymW8PQ55H3PZlQAA$+>$tG~QU8e>HmcBRiKCQB zBC!mnHKM1Z$WY1X3FxQpnEAJ$=edHvNpz{n$Z#%<`KZ$tC)K2?Mi`;EHKX!Fk+ki=vh<`j?E4ae zl1##(?W%u3CF2jW)_CskHe~WbL%e=Ui-sY+-tO zT1-M>F^TrV{K?`)L&;D24*kLEmUVo;UC@Z*fPUB|i^sU+kvpeh@#k+e(icB$aL~Ur z{Edyy=+VW>z6g~gq!>V$J4d+73Cf)`n zsX+69(Z7JbDDV&1Uu~rb%E&L*@jQ};6!!nB&PHof6>4%C0TUM&(aC)Hpy$KE)kZt& z0rE!LMfcp?bMH_j=~_pEkjNwg7Y!Rw`F5u7NU2bbP5YqWunashmaT2-pY{3cvJFCG*wMTH7Y zxxvSBp&&m4@dJkbVdPsT+5=2ylr?MK zETh(&t5&hT*1kpTvsG5e95&y*c=1NY>04$Tj7VvXtG}y%{N*Cf2@wejm*ap}0hL60 zO=ztq))n802Vf2#E>`Ui9nYS=?-IBdQtFHj8=P&!wADEBHC>~;M`0sW|J(f6TGVh` zwmC!Q1|7c<0=gUg6rp%<(=o}IAMyq~K7O@3{R#-C;YUSQB0}04943L53;-J4r9)+F5h|}p zSn;zN1`3D8P`kL9GY&DD#W+cIFns3DjPfs^w?u{A9qx<-qs#CIh}qSWFTv(MgdXmC zAvrv<^Y3gGt3(Cj|2(?6q3>MSD#_+hadcJFnY!SB9gAckmDkfK4u54Oab~qP#T>-r z{9QJ?!y(8I87D?h$Es!n;ErIk4fi^FS6gJfuNt^)lG)75Q|oqZRD=dBYQmKCkVosx z;_aX44SCvLVyV2fyUUFk#anG+GJLr+lvGJj3$6mwsFZTMT~l~sepw3++b;B z#e-HzTFGePSIpa9|hFdEOXCOrO!^?6&u znC$f}hq?*I-igd0r}gE-bzV@prL}bG-!LSbf?`LAfZt@&Vdg#1gOCB+v9D@OI%ZSf z3Tu~~_Yl}N-FE$wmDcA-+734*F87(uuMDhL%cy&@y3Q)r&P9uzN0Y2Vqik-cN%(}a z7ddT=46Q11tq+|;p%$P_CX0Z*&F4`yayq+|H=dG1R#tBIE9?;0Eg(N}c5Y*2Y!5>7 z-*3jb20AXd$O|HyhN?Si+vo}Wd`O!~)8hj3;3UO{g5OTsKaxpLg_yBOCH#!9R@5MR zJ$xP+m(~1Izs?#5;+$l(dn7^{4W|O$83EJ^_&0rI34E?+N}9M+ZOe9Ot96=1vw6^{0xFb#IO$E8ybt>*$nJ zl;uJCJ`m5bz}Qqf!SslTZcBWNTn0F^S6xqS$t*G@O5=Z0hifM!AVJwP9ykArVWZ&u&M!J-~iI4b%uHaU&Z?f#13iMp|gywtyX7=R?5j z#OI=}`lDJSYL~Sh!c5DyAB;;X>%B-ab_8Vl;JpEMn>kN^iEvHXGfiWL`eW?6au8g<;AK*^!@(_5ppWL diff --git a/website/static/img/emojis/blobemo.png b/website/static/img/emojis/blobemo.png deleted file mode 100644 index 639fec9f83a0d68cded774e8b8346b2dfe49ba43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7411 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 ZJ?%+cM&izqfj=xE@1&Ka%H9}<{12o-gZBUc diff --git a/website/static/img/emojis/blobheart.png b/website/static/img/emojis/blobheart.png deleted file mode 100644 index 0e0b72b4f5a816c80a5934b0697a9dcd077b79a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7044 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 diff --git a/website/static/img/emojis/blobhug.png b/website/static/img/emojis/blobhug.png deleted file mode 100644 index 92245090c4fe9049a5ecae552188151a72cac801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9311 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 XAo7s~SJi*RK_F#$b-4-|)6f3{pgcwr diff --git a/website/static/img/emojis/blobkiss.png b/website/static/img/emojis/blobkiss.png deleted file mode 100644 index c4e9f547afdd81e55dc4095afff27868bd90c853..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6891 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 diff --git a/website/static/img/emojis/blobno.png b/website/static/img/emojis/blobno.png deleted file mode 100644 index 955b3d16fa0879adedb2c397ed7bd1f898e03fe6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7860 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 diff --git a/website/static/img/emojis/blobok.png b/website/static/img/emojis/blobok.png deleted file mode 100644 index fc0d31a90039ef54a566a5159c914875c29a7b00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8155 zcmai3WmgJv!3KASJMS;JwN`bl z?hjp6yKC=9I!aYp9t)im9RL7eDSVMphmD~BbyQ^7wNK1!3^pLTODSlg!kRy-WhCq# z&E<=pI{-it@xKn2uy<(%dq@J5)dgxeTLZn!+^hgzUS8~WP7dxCW-eCj&TckYXCkBk z0F|zSjHIS__GylfZZhPpN1T)MBqs8|$REQr!SL)E7(3e0y8G(;@jIzj?59R!)hT;! zbKeZcPj%Q^{b+gHt@lzf&Pob@fNZ1CX@R9KB(gd{F$XyqVF}m&g^W9|0(JH*(QHUoLa;!hJOnCuL0)?a3NCPB7^ksmUjRXsTw8x>D~mtlkIxKQb2P%^mOFC(^E z8{F9Lm1Lr;s|PuaEI+nhZy5K6#sG8y=kVvr?U@F98SG ze^NPY25`hx)%7cCR?Pe33tGw1Cd5sRZX8((Wo#5pxx!(=BFw6`oXh zJ+4~{TT(eZ73h?MsHVuQ0P8X$W;V7A(Lj+J94g~umZz2f*hF)jk{FV_U)!i57@z`F zrG4q&)kqS-s_C&>96&Euvtx$5>2fWwkW;+f@Wj-?<&y3j&o`a?N#=g`g4wE2#3m3y zFTEOVfV7b^+R zq`ncI$-NF12jJD?QV`s84F7-7~VTd#RMApziGCttN zknD17Y|f>=DY4SQ#!6NOL!92@?5lzvlf1?+CN>s?85{zYy99}-6|1*MhP9tm%;a3X zXL7o1;9_<;kYo@2?UX$cet(T(nz=LWtF$Pyp@Jf<%P=lvuG?Zq@g{(^f9qJqM8)D? zCM3JFpw49tM+Cz%`TdoEb7r_?T@qE(UyH!O578E*AFg7eBWSMV#Hc zR55UP9(_#!NsNwejh-5;T!*un8`TkP9~Gyqki(teVUWE?!^;Z`$5-k~DmtI*zbv)U z`p+|_m>uRYJYT4Fzd^bdXJ5OyzaPup%I=8CFl->ZG&EVi9_o(Z?I-;EBp2A6*}vM{ zuqhN#3;qXvURzmn35wG{$jZp5+sW~ID(9FSgRXe znO0ekPuC>8RY~cI`?98@L&ilu^bGx`o{DI$y3zMSk@r;g&!AVHTygG3;L)W$~`%%b+^BqJ;9W3wro7-b1?KHy6g zgqITQhp5OU(2f3VjReq!g7|>|-*LK8j2){rHDhF?!2voD7ku55w{oi_3J*jHFC+SP zxS2OZr6I1%6VNs)UQpbS97VP;>wZLoZN#fjSJ~GRpW{=fr~^17A}(dhT?_)8 zVGE5mGO2qg3Bso%WT{<`czJkQudcPi7DWGwGKzVO)NIh6CkxYp#%(VR+Fz*}3|Kq4ZNL}zMzXdxHKT^MwngjMG5v$C&t_tWNdbWppa(+(165|K zSY<*&Q!Bca^)ftke*Tq*M=n9;=u|PWY{hSyXi{=#0Sg0EMIm19`Yq5~SBN6UqFkjo zo(e2^NLP+lh;GaZR8_T+-xa4_gfC>4$TZXKv2bH*wae4rdNF3DyE8JpS3*JtaLVMA zm9a}z^7daIuMHQQzy^Af*ZY-1p0Pwktek3p=v>@>Osi4@^aIg+e0dyu4X#FE8=JPiNKKZDJ~_7rDc_(Uf|l8UutS^-tS-MtS5-Y z&ESLHn7UqaMg55RJ(y+b=-Tf2ker-wi5cZ+^NWi^bA-LFUVc(P@Hofkbc$jb8gl9| z+CtiRHA92%;W<$Yy0y95oK5UL*3@taV93gu)}1g6sOm#^&3I{|*kWVe#l6 z7%1P^X!BrseY%}%`NYkg1Oe-Cdz~3BRO%u0+_k#9dMwO%LNE;sW*+za@7KLz7@P@P z%A240V62*ln8d+kj-b~WxvQ&drG8_y)mUns`+@Sy%Zu(GYpf6y9C|vsu#JGHpV`^0 z?DdVmWses~soYPUeuoQo;wLv-tzTNmnK)Qm59Uw1obHGbyq6!La(-(!^*-NdYs}

TgzzeHMXc3v$1Y%z!#_;M-F`pFY3i{H7Vp&>bFpntx$^+QWbWEJDjA&r*P1dz!z2yyZ@VIafWkfPfc16PC!ouL(H!ZNXs4lBQ zZQ!e~vCkesT>bO?xjS^8sVzJ#qkpF)`1kK=Za`F2l;f(kKF4Pb;COfR@qPx+EY-;*kL3hW11r;ZPO0QvlYUr@M+u2LxY)d6bS< zT-?MhFoi}~ILqgHA5K-Z8YZ{OOcfQse*H>JMy60+>wc9-?9&oTBg%7Uv)_C!9?x22%76(nD!uD!U4 zDSr0375+{CzV`F3nEH<0^Zj(OZMpIc5-y9uG9V^_E;BwW+2?E+*}65<<8As%mn7t6 z1>0v+j-c%9=_;lpg?5#n75LSYFEy2?BAg{4Fj3i@o4c$F0Gwl=W&wnSnc77a714Tn zdam)SW~JohOn_^;Mq^26LNBJkI^bo^*KYyGY$KDKgu+pWrhV(}EPaN)R8IKINH~aF zuFiS(Y~dQ8xf^1fByURqbbHqkLKHu4F`f5Fc*Z9t&@eHRtBWF1Sad$SY)P=Pvcgb5 zb_S~|R+fyf))K-T_t~ABTN{V!d;KIoQW)I;vn;M=5UJW6qP7cO1p^>~wh51!6(7A9 z0lwG)jJ3W}u~AW5Tf5b`XiO_4l=*-`^hI&;@dIS1`6Gk(>-Ch!s5se;NZ-oBkLm*C z^sEeUt~Z9edwBEQNRb&XF9C=UHLZ~MODGS&-8?Ex(&WSdhP^p&Vo3$nrSpslU?s)+ z<>9iwzaOT9M%~zMT|HW|lKRS)|L}_V!qI!iZ|y}o@pg5W*KzU>O&F(740)VLX&fMH z8B#0x+edrO2$mTF$npMWmj<(-%Kz9#tuev+r1MVj??C#oW3x~egI+1KxVxH=;Q7iX z*C#xc&7l6``TX&Dk|LxgZKvwDVymYHx1_v_1r8zBPfk>iC?9 z(B;WD(^n18g}7MsM8I3%Zgp6)_{E-}3aN>S zNj!z<=mj3_!|VMfxQp=O{@!MJ(d7Dk`HzqLkvcv)QVlkR{d6Nhc3{B&z5D$rS>BN$!49$!{jP}$BB+Inn)Iq`w9wA89Tb^STkSr9+ebc8jEUKvr9(xBS5(a8 zvFqjVyQ?+pL*Adv8X^&06@kq?I57D6`wEYvqoeI+muSHA-CpBBj9|dy=NOyuo@Kzm z@FQ4fWPEag@lSnietWm8!JK?tyN?gKl7cozTLP*i3MHyUP(NzEU#wYQc;c^Ly?;YcT46XtM@Q%F?T!24gOs;-OZZOT%@2%}jQzb|!Q7^sVV9oA^S#y=SY-`6(*O z3&7t!4+mN8(=AyLdraCFG}uB=f!v&~Np&V24fwOOTl-2bUPOP1nuG??B_-i3t)nLw zqtOo63lP%yoc?t`-_?2V%840u=e}1OphN;M2+@(S`S@N^5+%E2Ory9|f?yPc%b^>> zS6*@9YBzl2B$_eXXwZolx-+XDC;;Ye{E7~WuWD?x`FKh*r=j@?k(y1(drn_3DrjOt zc8G{Dnyr|0(RuxUMMJ>%O(ck(F46l{$g947c;Q2Q``-2IcP8;Sx6x16TngM)*T|hY zI5eLep$l;*Ds8>t>8=oL6mGh1qb?b_>hRM&RvK`6G&o6gZ%xg0X4;OdcxGngLVl?@+SQtDocwu0 ze+xGQ&E)Ql=^bo5mOb~AJrm=Z8>Ijr@FLjCTP~XMP)zW%#NJ(970_H!uxo+W)_0WR z%6e>wF(sCkFazVyY ztCe`ubuLg%tplBDI-X>>#Kix_L!!)haga(Y%u5_IzSwXtyoAV2ND84$G7Rtn7@sMF zA&?5uGXJ)sP_!n0jXN$fqN>O555`~(((_69}<^mep z<_el&|=T*XivGkLZ{=~1bzJZiydk5SBtMuBG3qXskw`H4b73KZg?LJqX zRqD92RUJA2fJ}uls0_aLwqbIv8)4c<9YS?(fvUqn@I8xfcM0sQC5kQ}ECu>|Ab3>k zb8ft$+m;NrPvoZf5fT;fWM}Msr9jT?(}uMdXiSMRnN3Yi7xw6|2u)AzG%_Tf7Q%#y z|LV22fuO0}h07z2p~=B;rYX^(nbrrDy+XM23^jCHT zFCI5pY?_;63{#5zn6v;pNrqh2g|+ zR6@zvb#>X=!YYhMojA5m`5XmIf`M!)o)}`<9Ly5Bt{$KC186(&%&r$N?tEUm4bblG zzb+CARlrO98$5cxRc9CPA`nJc&y9(Wd3;!P&iLcYi?o+>1x#ayxs1Q7qSm{K(vH#W zrDSx8#Zx1S&*KtoHZpln*ojsuv&lE!t`xQV2af(p)dqfvFme>}F>F5s_|`{*n_Dow z(Okb(FxPgM6^EA9q}JMvd*_IF45@2GK0)2j*1D%)mK#lPg{QAb&@FS3_=Cjlr^cH* z>+J@+8y_i#Xvn9NmtC9@H;w~jOmwW$_8>s8(^s`2~sg(zm40tM%4 zz1k=?Ho8#0-SftFF`iqey4G}EXyN^Zdw`D4P+_jNd2V_VTce`CnjkKpH~%XPj40?*qPxT5)z#MD#)U^f zIlnU$80hs5u)d9|R>!K;)V)x<37M}Uxj&n++K~}6Y>Y0xI-!GQqv2PjJ@c>8atEu- zDIIz#2{LT1dW-w6RCN<+GMhCQpL@T1r{0mlWJoczjo6)9Fe8SE=~ua_$>WU%-|2cx zNEI7KsS@qoQR!_E!pC1%_nSgigXuxE`VU+%*Q;tz+Fn~3`<&fiuzf7mtV$Cq%I&hR zp~mB2YKT}<{?p~H17!W=gLQMNHVV2AJ|cF_9{Ht|^=T~UWWnAuf1Pv35n_ZPdt@CicOLOUQ0c~uu zTU9JcFQ5mJ4l~#}QuGFWXX%bpM#hewhRT%aG`jWik@e-xPpmSITnzG`4h?x#8)4XF z7u(^<>JKXp%mbGzE%x7~NjjR535wE`4kv0=_%4lqjn+SY^LY6S;Toja*L=k2KS~pV z2h^IL>dVQ>;$7fQ-Bn50r&l|r#-k^rW3)A0EP{!AmcF2Zs#%grArK9x8L!`bLD=cKN*^6)`{kikw^hq3r|mFc2^9W(6e3?Dw|w)^pmQwjFA7JniY62v2+oGdG2 zEEM+Q)AgEGrHSfIOa|kprE0Sbc6B9bsP>3Xc&MP$pnu`u#RK_o@iGe%zV>N>1+vnI zZRSdKD8<5EZdV$-I5yALMn)9VcsAOTQU(?_11*l7ySKNYqp)|I7Z!m<0_o>XymBYU zC#-fyc5m5H=kX1vW?h4XjUc=wOEr#?>7=*jRhpmw`~2(8sQtsg-H^^WtgwNBPe$F! zF9R_rf}XJeX1r2DJ^R-hUx(N?hl$91w9$qg0`j5}I>?|`= zo+i&?4vwyW501Zx?TpKHrb_`%@O-W3mYf|xML86dSY)$h!IAkBER0*$a^aS`KAW+=AfQS4+8(2Yj*3i2gy!bKz>%T8kU9~D?CUP1jl858!^-wu*7?0HDX~*C5z1Z z9(`)VD;GdjF^!OJPX|ORAeu�wbfLnysK%>Zmo#!KmZ@<@@;Fj*?N<-y%6!cEey= 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 I^EvGQ0M7*``Tzg` diff --git a/website/static/img/emojis/blobpirate.png b/website/static/img/emojis/blobpirate.png deleted file mode 100644 index 46fa657a44921786de79f263e52e23975788893a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10572 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 diff --git a/website/static/img/emojis/blobpuke.png b/website/static/img/emojis/blobpuke.png deleted file mode 100644 index 1ad2c226ab368b98eb88ca48a4185ef176a72589..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8808 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 R5vNiB%JS-RwK5iw{{#I1TF3wZ diff --git a/website/static/img/emojis/blobswag.png b/website/static/img/emojis/blobswag.png deleted file mode 100644 index 778b3ce1016e5ff4f16fd0ed5e0a1ba7c1a01278..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8208 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 KzDmw2@_zuFCgKnP diff --git a/website/static/img/emojis/blobsweat.png b/website/static/img/emojis/blobsweat.png deleted file mode 100644 index bae08f46421375aeb97c5d86bde1409930a74612..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7748 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 a7Z?~ktoEr}S{YFR2FOdRNL5Rig!~Wl$q_05 diff --git a/website/static/img/emojis/blobsweatsmile.png b/website/static/img/emojis/blobsweatsmile.png deleted file mode 100644 index e748869cf5b4b5d72caf82592db4ffdd69bb5278..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7304 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 diff --git a/website/static/img/emojis/blobthanks.png b/website/static/img/emojis/blobthanks.png deleted file mode 100644 index 8f274e6738c12927c36bbf4d8d0f7c0b0d633247..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7921 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 diff --git a/website/static/img/emojis/blobthinking.png b/website/static/img/emojis/blobthinking.png deleted file mode 100644 index 45a68a6d1a3d16e1a51b08d78f37a184ffc96e30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10296 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 diff --git a/website/static/img/emojis/blobtrance.gif b/website/static/img/emojis/blobtrance.gif deleted file mode 100644 index 2aae118a51eb3d51874e0803300fe8f5cbc21a42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20442 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 diff --git a/website/static/img/emojis/blobwave.png b/website/static/img/emojis/blobwave.png deleted file mode 100644 index a5b2174cc1938922d11239498a9408dec8ad0532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7907 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 A)&Kwi diff --git a/website/static/img/emojis/blobyes.png b/website/static/img/emojis/blobyes.png deleted file mode 100644 index 4d758cc50d1be0cf0da730c292524ba4aa9e56b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7031 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 diff --git a/website/static/img/emojis/cowblob.png b/website/static/img/emojis/cowblob.png deleted file mode 100644 index 56fb79b629d80e11ab51571901ff2fc6c6d50d1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8157 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 diff --git a/website/static/img/introducing-benthos-lab/banner.svg b/website/static/img/introducing-benthos-lab/banner.svg deleted file mode 100644 index 7c9c86be44..0000000000 --- a/website/static/img/introducing-benthos-lab/banner.svg +++ /dev/null @@ -1,506 +0,0 @@ - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/introducing-benthos-lab/genteel.jpg b/website/static/img/introducing-benthos-lab/genteel.jpg deleted file mode 100644 index 84a4436a105e07cd44a9a6cfb93804c245fc2951..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106013 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 diff --git a/website/static/img/introducing-benthos-lab/slamslack.jpg b/website/static/img/introducing-benthos-lab/slamslack.jpg deleted file mode 100644 index 22601b1c43ae7170a143fa5aa294159f58e0f393..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79875 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 - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/logo_hero.svg b/website/static/img/logo_hero.svg deleted file mode 100644 index 28142bca56..0000000000 --- a/website/static/img/logo_hero.svg +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - -image/svg+xml - - - - - diff --git a/website/static/img/love-bg-dark.svg b/website/static/img/love-bg-dark.svg deleted file mode 100644 index c06d631ebb..0000000000 --- a/website/static/img/love-bg-dark.svg +++ /dev/null @@ -1,410 +0,0 @@ - - - - - - image/svg+xml - - Group - - - - - - Group - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/nav-logo.svg b/website/static/img/nav-logo.svg deleted file mode 100644 index 448b1706f2..0000000000 --- a/website/static/img/nav-logo.svg +++ /dev/null @@ -1,466 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/og_img.png b/website/static/img/og_img.png deleted file mode 100644 index e12786c6f5cb7fddba1b5507c8eb20fce2c7afca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33808 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�hHQrZg!J+(~V{3HpqkPkzQjR=SeLd2jSV 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 Ls!X+%Y4HC59g3@v diff --git a/website/static/img/og_img.svg b/website/static/img/og_img.svg deleted file mode 100644 index fc8db45021..0000000000 --- a/website/static/img/og_img.svg +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - - - - -image/svg+xml - - - - -benthos.dev diff --git a/website/static/img/sponsors/HUMAN_logo.png b/website/static/img/sponsors/HUMAN_logo.png deleted file mode 100644 index 716446cd7a2cb4761a76728401340b24d27c4412..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29925 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и2e3d+2h_c>v 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 - - - - - - - - - - - - - - - diff --git a/website/static/img/sponsors/community.svg b/website/static/img/sponsors/community.svg deleted file mode 100644 index f19034ccb8..0000000000 --- a/website/static/img/sponsors/community.svg +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/website/static/img/sponsors/formance.svg b/website/static/img/sponsors/formance.svg deleted file mode 100644 index c12bf4b933..0000000000 --- a/website/static/img/sponsors/formance.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/website/static/img/sponsors/mw_logo.png b/website/static/img/sponsors/mw_logo.png deleted file mode 100644 index 770cdc84cae13db0a5de9d19ca4f338b773b31d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14218 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/sponsors/optum_logo.png b/website/static/img/sponsors/optum_logo.png deleted file mode 100644 index 78a294700424bee28d0aa366b6dc0c450131bc8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7908 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 diff --git a/website/static/img/sponsors/synadia.svg b/website/static/img/sponsors/synadia.svg deleted file mode 100644 index 464ccc51cc..0000000000 --- a/website/static/img/sponsors/synadia.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/sponsors/umh_logo.svg b/website/static/img/sponsors/umh_logo.svg deleted file mode 100644 index 496f0593b8..0000000000 --- a/website/static/img/sponsors/umh_logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/website/static/img/sponsors/warpstream_logo.svg b/website/static/img/sponsors/warpstream_logo.svg deleted file mode 100644 index 3b57d17ef6..0000000000 --- a/website/static/img/sponsors/warpstream_logo.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/teacher-blob.svg b/website/static/img/teacher-blob.svg deleted file mode 100644 index 92624a9798..0000000000 --- a/website/static/img/teacher-blob.svg +++ /dev/null @@ -1,645 +0,0 @@ - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/website/static/img/what-is-blob.svg b/website/static/img/what-is-blob.svg deleted file mode 100644 index a55954a06f..0000000000 --- a/website/static/img/what-is-blob.svg +++ /dev/null @@ -1,2427 +0,0 @@ - - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/website/static/img/write-a-benthos-plugin/benthos-plugged.png b/website/static/img/write-a-benthos-plugin/benthos-plugged.png deleted file mode 100644 index 8d7029f0546c4b8fa31d7a50377342c6ad7f819b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51404 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 VIE)xDV8NpU0@zr$&2 < /dev/null 2>&1 -} - -check_tools() { - Tools=("curl" "grep" "cut" "tar" "uname" "chmod" "mv" "rm") - - for tool in ${Tools[*]}; do - if ! check_cmd $tool; then - echo "Aborted, missing $tool, sorry!" - exit 6 - fi - done -} - -install_benthos() -{ - trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; exit 1' ERR - - # Process the command line - if [[ "$#" -eq 2 ]]; then - benthos_tag="v$1" - benthos_version="$1" - benthos_install_path="$2" - elif [[ "$#" -eq 1 ]]; then - benthos_tag="v$1" - benthos_version=$1 - benthos_install_path="/usr/local/bin" - elif [[ "$#" -eq 0 ]]; then - benthos_tag=$(curl -s https://api.github.com/repos/benthosdev/benthos/releases/latest | grep 'tag_name' | cut -d\" -f4) - benthos_version=$(echo ${benthos_tag} | cut -c2-) - benthos_install_path="/usr/local/bin" - else - echo "Too many arguments." - exit 1 - fi - - benthos_os="unsupported" - benthos_arch="unknown" - benthos_arm="" - - header - check_tools - - if [[ -n "$PREFIX" ]]; then - benthos_install_path="$PREFIX/bin" - fi - - # Fall back to /usr/bin if necessary - if [[ ! -d $benthos_install_path ]]; then - benthos_install_path="/usr/bin" - fi - - # Not every platform has or needs sudo (https://termux.com/linux.html) - ((EUID)) && sudo_cmd="sudo" - - ######################### - # Which OS and version? # - ######################### - - benthos_bin="benthos" - benthos_dl_ext=".tar.gz" - - # NOTE: `uname -m` is more accurate and universal than `arch` - # See https://en.wikipedia.org/wiki/Uname - unamem="$(uname -m)" - if [[ $unamem == *aarch64* ]]; then - benthos_arch="arm64" - elif [[ $unamem == *arm64* ]]; then - benthos_arch="arm64" - elif [[ $unamem == *64* ]]; then - benthos_arch="amd64" - elif [[ $unamem == *armv5* ]]; then - benthos_arch="arm" - benthos_arm="v5" - elif [[ $unamem == *armv6l* ]]; then - benthos_arch="arm" - benthos_arm="v6" - elif [[ $unamem == *armv7l* ]]; then - benthos_arch="arm" - benthos_arm="v7" - else - echo "Aborted, unsupported or unknown architecture: $unamem" - return 2 - fi - - unameu="$(tr '[:lower:]' '[:upper:]' <<<$(uname))" - if [[ $unameu == *DARWIN* ]]; then - benthos_os="darwin" - version=${vers##*ProductVersion:} - elif [[ $unameu == *LINUX* ]]; then - benthos_os="linux" - elif [[ $unameu == *FREEBSD* ]]; then - benthos_os="freebsd" - elif [[ $unameu == *OPENBSD* ]]; then - benthos_os="openbsd" - elif [[ $unameu == *WIN* || $unameu == MSYS* ]]; then - # Should catch cygwin - sudo_cmd="" - benthos_os="windows" - benthos_bin=$benthos_bin.exe - else - echo "Aborted, unsupported or unknown os: $uname" - return 6 - fi - - ######################## - # Download and extract # - ######################## - - echo "Downloading Benthos for ${benthos_os}/${benthos_arch}${benthos_arm}..." - benthos_file="benthos_${benthos_os}_${benthos_arch}${benthos_arm}${benthos_dl_ext}" - - benthos_url="https://github.com/benthosdev/benthos/releases/download/${benthos_tag}/benthos_${benthos_version}_${benthos_os}_${benthos_arch}${benthos_arm}.tar.gz" - - dl="/tmp/$benthos_file" - rm -rf -- "$dl" - - curl -fsSL "$benthos_url" -o "$dl" - - echo "Extracting..." - case "$benthos_file" in - *.tar.gz) tar -xzf "$dl" -C "$PREFIX/tmp/" "$benthos_bin" ;; - esac - chmod +x "$PREFIX/tmp/$benthos_bin" - - echo "Putting benthos in $benthos_install_path (may require password)" - if [ -n "$sudo_cmd" ] && [ -n "$(find "$benthos_install_path" -prune -user "$(id -u)")" ]; then - # Skip sudo if the current user is the owner of the Benthos install path - sudo_cmd="" - fi - $sudo_cmd mv "$PREFIX/tmp/$benthos_bin" "$benthos_install_path/$benthos_bin" - $sudo_cmd rm -- "$dl" - - # check installation - $benthos_install_path/$benthos_bin -version - if ! check_cmd benthos; then - echo "Do not forget to add $benthos_install_path to your PATH!" - fi - - echo "Successfully installed" - trap ERR - return 0 -} - -install_benthos $@ diff --git a/website/tsconfig.json b/website/tsconfig.json deleted file mode 100644 index dc42f408be..0000000000 --- a/website/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@docusaurus/tsconfig", - "compilerOptions": { - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - }, -} diff --git a/website/yarn.lock b/website/yarn.lock deleted file mode 100644 index db43c31f63..0000000000 --- a/website/yarn.lock +++ /dev/null @@ -1,9721 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== - dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== - -"@algolia/cache-browser-local-storage@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.20.0.tgz#357318242fc542ffce41d6eb5b4a9b402921b0bb" - integrity sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ== - dependencies: - "@algolia/cache-common" "4.20.0" - -"@algolia/cache-common@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.17.0.tgz#bc3da15548df585b44d76c55e66b0056a2b3f917" - integrity sha512-g8mXzkrcUBIPZaulAuqE7xyHhLAYAcF2xSch7d9dABheybaU3U91LjBX6eJTEB7XVhEsgK4Smi27vWtAJRhIKQ== - -"@algolia/cache-common@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.20.0.tgz#ec52230509fce891091ffd0d890618bcdc2fa20d" - integrity sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ== - -"@algolia/cache-in-memory@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.20.0.tgz#5f18d057bd6b3b075022df085c4f83bcca4e3e67" - integrity sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg== - dependencies: - "@algolia/cache-common" "4.20.0" - -"@algolia/client-account@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.20.0.tgz#23ce0b4cffd63100fb7c1aa1c67a4494de5bd645" - integrity sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q== - dependencies: - "@algolia/client-common" "4.20.0" - "@algolia/client-search" "4.20.0" - "@algolia/transporter" "4.20.0" - -"@algolia/client-analytics@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.20.0.tgz#0aa6bef35d3a41ac3991b3f46fcd0bf00d276fa9" - integrity sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug== - dependencies: - "@algolia/client-common" "4.20.0" - "@algolia/client-search" "4.20.0" - "@algolia/requester-common" "4.20.0" - "@algolia/transporter" "4.20.0" - -"@algolia/client-common@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.17.0.tgz#67fd898006e3ac359ea3e3ed61abfc26147ffa53" - integrity sha512-jHMks0ZFicf8nRDn6ma8DNNsdwGgP/NKiAAL9z6rS7CymJ7L0+QqTJl3rYxRW7TmBhsUH40wqzmrG6aMIN/DrQ== - dependencies: - "@algolia/requester-common" "4.17.0" - "@algolia/transporter" "4.17.0" - -"@algolia/client-common@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.20.0.tgz#ca60f04466515548651c4371a742fbb8971790ef" - integrity sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ== - dependencies: - "@algolia/requester-common" "4.20.0" - "@algolia/transporter" "4.20.0" - -"@algolia/client-personalization@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.20.0.tgz#ca81308e8ad0db3b27458b78355f124f29657181" - integrity sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ== - dependencies: - "@algolia/client-common" "4.20.0" - "@algolia/requester-common" "4.20.0" - "@algolia/transporter" "4.20.0" - -"@algolia/client-search@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.20.0.tgz#3bcce817ca6caedc835e0eaf6f580e02ee7c3e15" - integrity sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg== - dependencies: - "@algolia/client-common" "4.20.0" - "@algolia/requester-common" "4.20.0" - "@algolia/transporter" "4.20.0" - -"@algolia/client-search@^4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.17.0.tgz#0053c682f5f588e006c20791c27e8bcb0aa5b53c" - integrity sha512-x4P2wKrrRIXszT8gb7eWsMHNNHAJs0wE7/uqbufm4tZenAp+hwU/hq5KVsY50v+PfwM0LcDwwn/1DroujsTFoA== - dependencies: - "@algolia/client-common" "4.17.0" - "@algolia/requester-common" "4.17.0" - "@algolia/transporter" "4.17.0" - -"@algolia/events@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" - integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== - -"@algolia/logger-common@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.17.0.tgz#0fcea39c9485554edb4cdbfd965c5748b0b837ac" - integrity sha512-DGuoZqpTmIKJFDeyAJ7M8E/LOenIjWiOsg1XJ1OqAU/eofp49JfqXxbfgctlVZVmDABIyOz8LqEoJ6ZP4DTyvw== - -"@algolia/logger-common@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.20.0.tgz#f148ddf67e5d733a06213bebf7117cb8a651ab36" - integrity sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ== - -"@algolia/logger-console@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.20.0.tgz#ac443d27c4e94357f3063e675039cef0aa2de0a7" - integrity sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA== - dependencies: - "@algolia/logger-common" "4.20.0" - -"@algolia/requester-browser-xhr@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.20.0.tgz#db16d0bdef018b93b51681d3f1e134aca4f64814" - integrity sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw== - dependencies: - "@algolia/requester-common" "4.20.0" - -"@algolia/requester-common@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.17.0.tgz#746020d2cbc829213e7cede8eef2182c7a71e32b" - integrity sha512-XJjmWFEUlHu0ijvcHBoixuXfEoiRUdyzQM6YwTuB8usJNIgShua8ouFlRWF8iCeag0vZZiUm4S2WCVBPkdxFgg== - -"@algolia/requester-common@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.20.0.tgz#65694b2263a8712b4360fef18680528ffd435b5c" - integrity sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng== - -"@algolia/requester-node-http@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.20.0.tgz#b52b182b52b0b16dec4070832267d484a6b1d5bb" - integrity sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng== - dependencies: - "@algolia/requester-common" "4.20.0" - -"@algolia/transporter@4.17.0": - version "4.17.0" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.17.0.tgz#6aabdbc20c475d72d83c8e6519f1191f1a51fb37" - integrity sha512-6xL6H6fe+Fi0AEP3ziSgC+G04RK37iRb4uUUqVAH9WPYFI8g+LYFq6iv5HS8Cbuc5TTut+Bwj6G+dh/asdb9uA== - dependencies: - "@algolia/cache-common" "4.17.0" - "@algolia/logger-common" "4.17.0" - "@algolia/requester-common" "4.17.0" - -"@algolia/transporter@4.20.0": - version "4.20.0" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.20.0.tgz#7e5b24333d7cc9a926b2f6a249f87c2889b944a9" - integrity sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg== - dependencies: - "@algolia/cache-common" "4.20.0" - "@algolia/logger-common" "4.20.0" - "@algolia/requester-common" "4.20.0" - -"@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.8.3": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" - integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/code-frame@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== - dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" - -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" - integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== - -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.3.tgz#3febd552541e62b5e883a25eb3effd7c7379db11" - integrity sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ== - -"@babel/core@^7.19.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" - integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helpers" "^7.21.0" - "@babel/parser" "^7.21.4" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.4" - "@babel/types" "^7.21.4" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" - -"@babel/core@^7.22.9": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.3.tgz#5ec09c8803b91f51cc887dedc2654a35852849c9" - integrity sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.3" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helpers" "^7.23.2" - "@babel/parser" "^7.23.3" - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.3" - "@babel/types" "^7.23.3" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" - integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== - dependencies: - "@babel/types" "^7.21.4" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.22.9", "@babel/generator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.3.tgz#86e6e83d95903fbe7613f448613b8b319f330a8e" - integrity sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg== - dependencies: - "@babel/types" "^7.23.3" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" - integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== - dependencies: - "@babel/types" "^7.22.15" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz#770cd1ce0889097ceacb99418ee6934ef0572656" - integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg== - dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-validator-option" "^7.21.0" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - -"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" - integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== - dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.15" - browserslist "^4.21.9" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" - integrity sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-class-features-plugin@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" - integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.15" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz#40411a8ab134258ad2cf3a3d987ec6aa0723cee5" - integrity sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" - integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - regexpu-core "^5.3.1" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-define-polyfill-provider@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz#a71c10f7146d809f4a256c373f462d9bba8cf6ba" - integrity sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug== - dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" - integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/types" "^7.21.0" - -"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== - dependencies: - "@babel/types" "^7.21.0" - -"@babel/helper-member-expression-to-functions@^7.22.15": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" - integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== - dependencies: - "@babel/types" "^7.23.0" - -"@babel/helper-module-imports@^7.18.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" - integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== - dependencies: - "@babel/types" "^7.21.4" - -"@babel/helper-module-imports@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" - integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== - dependencies: - "@babel/types" "^7.22.15" - -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" - integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" - -"@babel/helper-module-transforms@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" - integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.20" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-optimise-call-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" - integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-plugin-utils@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - -"@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-remap-async-to-generator@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" - integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-wrap-function" "^7.22.20" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-replace-supers@^7.22.20", "@babel/helper-replace-supers@^7.22.9": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" - integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-member-expression-to-functions" "^7.22.15" - "@babel/helper-optimise-call-expression" "^7.22.5" - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-simple-access@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" - integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" - integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - -"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" - integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== - -"@babel/helper-validator-option@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" - integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== - -"@babel/helper-wrap-function@^7.18.9": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== - dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - -"@babel/helper-wrap-function@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" - integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== - dependencies: - "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.15" - "@babel/types" "^7.22.19" - -"@babel/helpers@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" - integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" - -"@babel/helpers@^7.23.2": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" - integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== - dependencies: - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.2" - "@babel/types" "^7.23.0" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.22.13": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" - integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" - chalk "^2.4.2" - js-tokens "^4.0.0" - -"@babel/parser@^7.20.7", "@babel/parser@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" - integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== - -"@babel/parser@^7.22.15", "@babel/parser@^7.22.7", "@babel/parser@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.3.tgz#0ce0be31a4ca4f1884b5786057cadcb6c3be58f9" - integrity sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a" - integrity sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" - integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-proposal-optional-chaining" "^7.20.7" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz#f6652bb16b94f8f9c20c50941e16e9756898dc5d" - integrity sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.23.3" - -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz#20c60d4639d18f7da8602548512e9d3a4c8d7098" - integrity sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-proposal-async-generator-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-class-static-block@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d" - integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" - integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.20.7", "@babel/plugin-proposal-optional-chaining@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" - integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": - version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" - integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== - -"@babel/plugin-proposal-private-property-in-object@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc" - integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" - integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-syntax-import-assertions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz#9c05a7f592982aff1a2768260ad84bcd3f0c77fc" - integrity sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-import-attributes@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz#992aee922cf04512461d7dae3ff6951b90a2dc06" - integrity sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2" - integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" - integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.20.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8" - integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-syntax-typescript@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f" - integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-arrow-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz#94c6dcfd731af90f27a79509f9ab7fb2120fc38b" - integrity sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-async-generator-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.3.tgz#9df2627bad7f434ed13eef3e61b2b65cafd4885b" - integrity sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.20" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-transform-async-to-generator@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" - integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - -"@babel/plugin-transform-async-to-generator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz#d1f513c7a8a506d43f47df2bf25f9254b0b051fa" - integrity sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw== - dependencies: - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.20" - -"@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-block-scoped-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz#fe1177d715fb569663095e04f3598525d98e8c77" - integrity sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-block-scoping@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02" - integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-block-scoping@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.3.tgz#e99a3ff08f58edd28a8ed82481df76925a4ffca7" - integrity sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-class-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz#35c377db11ca92a785a718b6aa4e3ed1eb65dc48" - integrity sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-class-static-block@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.3.tgz#56f2371c7e5bf6ff964d84c5dc4d4db5536b5159" - integrity sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-transform-classes@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" - integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" - -"@babel/plugin-transform-classes@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz#73380c632c095b03e8503c24fd38f95ad41ffacb" - integrity sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" - "@babel/helper-split-export-declaration" "^7.22.6" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/template" "^7.20.7" - -"@babel/plugin-transform-computed-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz#652e69561fcc9d2b50ba4f7ac7f60dcf65e86474" - integrity sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.15" - -"@babel/plugin-transform-destructuring@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz#73b46d0fd11cd6ef57dea8a381b1215f4959d401" - integrity sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-destructuring@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz#8c9ee68228b12ae3dff986e56ed1ba4f3c446311" - integrity sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-dotall-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz#3f7af6054882ede89c378d0cf889b854a993da50" - integrity sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-duplicate-keys@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz#664706ca0a5dfe8d066537f99032fc1dc8b720ce" - integrity sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-dynamic-import@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.3.tgz#82625924da9ed5fb11a428efb02e43bc9a3ab13e" - integrity sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-exponentiation-operator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz#ea0d978f6b9232ba4722f3dbecdd18f450babd18" - integrity sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-export-namespace-from@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.3.tgz#dcd066d995f6ac6077e5a4ccb68322a01e23ac49" - integrity sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-transform-for-of@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-for-of@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz#afe115ff0fbce735e02868d41489093c63e15559" - integrity sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== - dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-function-name@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz#8f424fcd862bf84cb9a1a6b42bc2f47ed630f8dc" - integrity sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw== - dependencies: - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-json-strings@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.3.tgz#489724ab7d3918a4329afb4172b2fd2cf3c8d245" - integrity sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz#8214665f00506ead73de157eba233e7381f3beb4" - integrity sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-logical-assignment-operators@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.3.tgz#3a406d6083feb9487083bca6d2334a3c9b6c4808" - integrity sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-member-expression-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz#e37b3f0502289f477ac0e776b05a833d853cabcc" - integrity sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-modules-amd@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" - integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== - dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-modules-amd@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz#e19b55436a1416829df0a1afc495deedfae17f7d" - integrity sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw== - dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-modules-commonjs@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" - integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== - dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" - -"@babel/plugin-transform-modules-commonjs@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" - integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== - dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" - -"@babel/plugin-transform-modules-systemjs@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" - integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== - dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-identifier" "^7.19.1" - -"@babel/plugin-transform-modules-systemjs@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz#fa7e62248931cb15b9404f8052581c302dd9de81" - integrity sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ== - dependencies: - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" - -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-modules-umd@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz#5d4395fccd071dfefe6585a4411aa7d6b7d769e9" - integrity sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg== - dependencies: - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" - integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.20.5" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" - integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-new-target@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz#5491bb78ed6ac87e990957cea367eab781c4d980" - integrity sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-nullish-coalescing-operator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.3.tgz#8a613d514b521b640344ed7c56afeff52f9413f8" - integrity sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-transform-numeric-separator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.3.tgz#2f8da42b75ba89e5cfcd677afd0856d52c0c2e68" - integrity sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-transform-object-rest-spread@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.3.tgz#509373753b5f7202fe1940e92fd075bd7874955f" - integrity sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog== - dependencies: - "@babel/compat-data" "^7.23.3" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.23.3" - -"@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" - -"@babel/plugin-transform-object-super@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz#81fdb636dcb306dd2e4e8fd80db5b2362ed2ebcd" - integrity sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" - -"@babel/plugin-transform-optional-catch-binding@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.3.tgz#362c0b545ee9e5b0fa9d9e6fe77acf9d4c480027" - integrity sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-transform-optional-chaining@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.3.tgz#92fc83f54aa3adc34288933fa27e54c13113f4be" - integrity sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db" - integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-parameters@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz#83ef5d1baf4b1072fa6e54b2b0999a7b2527e2af" - integrity sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-private-methods@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz#b2d7a3c97e278bfe59137a978d53b2c2e038c0e4" - integrity sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-private-property-in-object@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.3.tgz#5cd34a2ce6f2d008cc8f91d8dcc29e2c41466da6" - integrity sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-property-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz#54518f14ac4755d22b92162e4a852d308a560875" - integrity sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-react-constant-elements@^7.18.12": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.21.3.tgz#b32a5556100d424b25e388dd689050d78396884d" - integrity sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-react-display-name@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" - integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-display-name@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz#70529f034dd1e561045ad3c8152a267f0d7b6200" - integrity sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-react-jsx-development@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" - integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.18.6" - -"@babel/plugin-transform-react-jsx-development@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" - integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.22.5" - -"@babel/plugin-transform-react-jsx@^7.18.6": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz#656b42c2fdea0a6d8762075d58ef9d4e3c4ab8a2" - integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.21.0" - -"@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz#7e6266d88705d7c49f11c98db8b9464531289cd6" - integrity sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - "@babel/types" "^7.22.15" - -"@babel/plugin-transform-react-pure-annotations@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844" - integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-pure-annotations@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz#fabedbdb8ee40edf5da96f3ecfc6958e3783b93c" - integrity sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-regenerator@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - regenerator-transform "^0.15.1" - -"@babel/plugin-transform-regenerator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz#141afd4a2057298602069fce7f2dc5173e6c561c" - integrity sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - regenerator-transform "^0.15.2" - -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-reserved-words@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz#4130dcee12bd3dd5705c587947eb715da12efac8" - integrity sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-runtime@^7.22.9": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.3.tgz#0aa7485862b0b5cb0559c1a5ec08b4923743ee3b" - integrity sha512-XcQ3X58CKBdBnnZpPaQjgVMePsXtSZzHoku70q9tUAQp02ggPQNM04BF3RvlW1GSM/McbSOQAzEK4MXbS7/JFg== - dependencies: - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - babel-plugin-polyfill-corejs2 "^0.4.6" - babel-plugin-polyfill-corejs3 "^0.8.5" - babel-plugin-polyfill-regenerator "^0.5.3" - semver "^6.3.1" - -"@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-shorthand-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz#97d82a39b0e0c24f8a981568a8ed851745f59210" - integrity sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-spread@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" - integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - -"@babel/plugin-transform-spread@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz#41d17aacb12bde55168403c6f2d6bdca563d362c" - integrity sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-sticky-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz#dec45588ab4a723cb579c609b294a3d1bd22ff04" - integrity sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-template-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz#5f0f028eb14e50b5d0f76be57f90045757539d07" - integrity sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz#9dfab97acc87495c0c449014eb9c547d8966bca4" - integrity sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-typescript@^7.21.3": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz#316c5be579856ea890a57ebc5116c5d064658f2b" - integrity sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-typescript" "^7.20.0" - -"@babel/plugin-transform-typescript@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.3.tgz#ce806e6cb485d468c48c4f717696719678ab0138" - integrity sha512-ogV0yWnq38CFwH20l2Afz0dfKuZBx9o/Y2Rmh5vuSS0YD1hswgEgTfyTzuSrT2q9btmHRSqYoSfwFUVaC1M1Jw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-typescript" "^7.23.3" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-escapes@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz#1f66d16cab01fab98d784867d24f70c1ca65b925" - integrity sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-unicode-property-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz#19e234129e5ffa7205010feec0d94c251083d7ad" - integrity sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-unicode-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz#26897708d8f42654ca4ce1b73e96140fbad879dc" - integrity sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-unicode-sets-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz#4fb6f0a719c2c5859d11f6b55a050cc987f3799e" - integrity sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/preset-env@^7.19.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.4.tgz#a952482e634a8dd8271a3fe5459a16eb10739c58" - integrity sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw== - dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.21.0" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.20.7" - "@babel/plugin-proposal-async-generator-functions" "^7.20.7" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.21.0" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.20.7" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.20.7" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.21.0" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.21.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.20.0" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.20.7" - "@babel/plugin-transform-async-to-generator" "^7.20.7" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.21.0" - "@babel/plugin-transform-classes" "^7.21.0" - "@babel/plugin-transform-computed-properties" "^7.20.7" - "@babel/plugin-transform-destructuring" "^7.21.3" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.21.0" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.20.11" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" - "@babel/plugin-transform-modules-systemjs" "^7.20.11" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.20.5" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.21.3" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.20.5" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.20.7" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.21.4" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - core-js-compat "^3.25.1" - semver "^6.3.0" - -"@babel/preset-env@^7.22.9": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.3.tgz#d299e0140a7650684b95c62be2db0ef8c975143e" - integrity sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q== - dependencies: - "@babel/compat-data" "^7.23.3" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.3" - "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.23.3" - "@babel/plugin-syntax-import-attributes" "^7.23.3" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.23.3" - "@babel/plugin-transform-async-generator-functions" "^7.23.3" - "@babel/plugin-transform-async-to-generator" "^7.23.3" - "@babel/plugin-transform-block-scoped-functions" "^7.23.3" - "@babel/plugin-transform-block-scoping" "^7.23.3" - "@babel/plugin-transform-class-properties" "^7.23.3" - "@babel/plugin-transform-class-static-block" "^7.23.3" - "@babel/plugin-transform-classes" "^7.23.3" - "@babel/plugin-transform-computed-properties" "^7.23.3" - "@babel/plugin-transform-destructuring" "^7.23.3" - "@babel/plugin-transform-dotall-regex" "^7.23.3" - "@babel/plugin-transform-duplicate-keys" "^7.23.3" - "@babel/plugin-transform-dynamic-import" "^7.23.3" - "@babel/plugin-transform-exponentiation-operator" "^7.23.3" - "@babel/plugin-transform-export-namespace-from" "^7.23.3" - "@babel/plugin-transform-for-of" "^7.23.3" - "@babel/plugin-transform-function-name" "^7.23.3" - "@babel/plugin-transform-json-strings" "^7.23.3" - "@babel/plugin-transform-literals" "^7.23.3" - "@babel/plugin-transform-logical-assignment-operators" "^7.23.3" - "@babel/plugin-transform-member-expression-literals" "^7.23.3" - "@babel/plugin-transform-modules-amd" "^7.23.3" - "@babel/plugin-transform-modules-commonjs" "^7.23.3" - "@babel/plugin-transform-modules-systemjs" "^7.23.3" - "@babel/plugin-transform-modules-umd" "^7.23.3" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.23.3" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.3" - "@babel/plugin-transform-numeric-separator" "^7.23.3" - "@babel/plugin-transform-object-rest-spread" "^7.23.3" - "@babel/plugin-transform-object-super" "^7.23.3" - "@babel/plugin-transform-optional-catch-binding" "^7.23.3" - "@babel/plugin-transform-optional-chaining" "^7.23.3" - "@babel/plugin-transform-parameters" "^7.23.3" - "@babel/plugin-transform-private-methods" "^7.23.3" - "@babel/plugin-transform-private-property-in-object" "^7.23.3" - "@babel/plugin-transform-property-literals" "^7.23.3" - "@babel/plugin-transform-regenerator" "^7.23.3" - "@babel/plugin-transform-reserved-words" "^7.23.3" - "@babel/plugin-transform-shorthand-properties" "^7.23.3" - "@babel/plugin-transform-spread" "^7.23.3" - "@babel/plugin-transform-sticky-regex" "^7.23.3" - "@babel/plugin-transform-template-literals" "^7.23.3" - "@babel/plugin-transform-typeof-symbol" "^7.23.3" - "@babel/plugin-transform-unicode-escapes" "^7.23.3" - "@babel/plugin-transform-unicode-property-regex" "^7.23.3" - "@babel/plugin-transform-unicode-regex" "^7.23.3" - "@babel/plugin-transform-unicode-sets-regex" "^7.23.3" - "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.6" - babel-plugin-polyfill-corejs3 "^0.8.5" - babel-plugin-polyfill-regenerator "^0.5.3" - core-js-compat "^3.31.0" - semver "^6.3.1" - -"@babel/preset-modules@0.1.6-no-external-plugins": - version "0.1.6-no-external-plugins" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" - integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-react@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" - integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-transform-react-display-name" "^7.18.6" - "@babel/plugin-transform-react-jsx" "^7.18.6" - "@babel/plugin-transform-react-jsx-development" "^7.18.6" - "@babel/plugin-transform-react-pure-annotations" "^7.18.6" - -"@babel/preset-react@^7.22.5": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.23.3.tgz#f73ca07e7590f977db07eb54dbe46538cc015709" - integrity sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-transform-react-display-name" "^7.23.3" - "@babel/plugin-transform-react-jsx" "^7.22.15" - "@babel/plugin-transform-react-jsx-development" "^7.22.5" - "@babel/plugin-transform-react-pure-annotations" "^7.23.3" - -"@babel/preset-typescript@^7.18.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz#b913ac8e6aa8932e47c21b01b4368d8aa239a529" - integrity sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.21.0" - "@babel/plugin-syntax-jsx" "^7.21.4" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" - "@babel/plugin-transform-typescript" "^7.21.3" - -"@babel/preset-typescript@^7.22.5": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz#14534b34ed5b6d435aa05f1ae1c5e7adcc01d913" - integrity sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-syntax-jsx" "^7.23.3" - "@babel/plugin-transform-modules-commonjs" "^7.23.3" - "@babel/plugin-transform-typescript" "^7.23.3" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime-corejs3@^7.22.6": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.23.2.tgz#a5cd9d8b408fb946b2f074b21ea40c04e516795c" - integrity sha512-54cIh74Z1rp4oIjsHjqN+WM4fMyCBYe+LpZ9jWm51CZ1fbH3SkAzQD/3XLoNkjbJ7YEmjobLXyvQrFypRHOrXw== - dependencies: - core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.10.2", "@babel/runtime@^7.22.6": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" - integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@^7.18.10", "@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/template@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" - -"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== - dependencies: - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.3.tgz#26ee5f252e725aa7aca3474aa5b324eaf7908b5b" - integrity sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.3" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.3" - "@babel/types" "^7.23.3" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.4.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.3.tgz#d5ea892c07f2ec371ac704420f4dcdb07b5f9598" - integrity sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@discoveryjs/json-ext@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@docsearch/css@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.5.2.tgz#610f47b48814ca94041df969d9fcc47b91fc5aac" - integrity sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA== - -"@docsearch/react@^3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.5.2.tgz#2e6bbee00eb67333b64906352734da6aef1232b9" - integrity sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng== - dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.5.2" - algoliasearch "^4.19.1" - -"@docusaurus/core@3.0.0", "@docusaurus/core@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.0.0.tgz#46bc9bf2bcd99ca98a1c8f10a70bf3afaaaf9dcb" - integrity sha512-bHWtY55tJTkd6pZhHrWz1MpWuwN4edZe0/UWgFF7PW/oJeDZvLSXKqwny3L91X1/LGGoypBGkeZn8EOuKeL4yQ== - dependencies: - "@babel/core" "^7.22.9" - "@babel/generator" "^7.22.9" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.0.0" - "@docusaurus/logger" "3.0.0" - "@docusaurus/mdx-loader" "3.0.0" - "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-common" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - "@slorber/static-site-generator-webpack-plugin" "^4.0.7" - "@svgr/webpack" "^6.5.1" - autoprefixer "^10.4.14" - babel-loader "^9.1.3" - babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" - clean-css "^5.3.2" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" - copy-webpack-plugin "^11.0.0" - core-js "^3.31.1" - css-loader "^6.8.1" - css-minimizer-webpack-plugin "^4.2.2" - cssnano "^5.1.15" - del "^6.1.1" - detect-port "^1.5.1" - escape-html "^1.0.3" - eta "^2.2.0" - file-loader "^6.2.0" - fs-extra "^11.1.1" - html-minifier-terser "^7.2.0" - html-tags "^3.3.1" - html-webpack-plugin "^5.5.3" - leven "^3.1.0" - lodash "^4.17.21" - mini-css-extract-plugin "^2.7.6" - postcss "^8.4.26" - postcss-loader "^7.3.3" - prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "^1.3.0" - react-loadable "npm:@docusaurus/react-loadable@5.5.2" - react-loadable-ssr-addon-v5-slorber "^1.0.1" - react-router "^5.3.4" - react-router-config "^5.1.1" - react-router-dom "^5.3.4" - rtl-detect "^1.0.4" - semver "^7.5.4" - serve-handler "^6.1.5" - shelljs "^0.8.5" - terser-webpack-plugin "^5.3.9" - tslib "^2.6.0" - update-notifier "^6.0.2" - url-loader "^4.1.1" - wait-on "^7.0.1" - webpack "^5.88.1" - webpack-bundle-analyzer "^4.9.0" - webpack-dev-server "^4.15.1" - webpack-merge "^5.9.0" - webpackbar "^5.0.2" - -"@docusaurus/cssnano-preset@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.0.0.tgz#87fbf9cbc7c383e207119b44c17fb1d05c73af7c" - integrity sha512-FHiRfwmVvIVdIGsHcijUOaX7hMn0mugVYB7m4GkpYI6Mi56zwQV4lH5p7DxcW5CUYNWMVxz2loWSCiWEm5ikwA== - dependencies: - cssnano-preset-advanced "^5.3.10" - postcss "^8.4.26" - postcss-sort-media-queries "^4.4.1" - tslib "^2.6.0" - -"@docusaurus/logger@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.0.0.tgz#02a4bfecec6aa3732c8bd9597ca9d5debab813a6" - integrity sha512-6eX0eOfioMQCk+qgCnHvbLLuyIAA+r2lSID6d6JusiLtDKmYMfNp3F4yyE8bnb0Abmzt2w68XwptEFYyALSAXw== - dependencies: - chalk "^4.1.2" - tslib "^2.6.0" - -"@docusaurus/mdx-loader@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.0.0.tgz#2593889e43dc4bbd8dfa074d86c8bb4206cf4171" - integrity sha512-JkGge6WYDrwjNgMxwkb6kNQHnpISt5L1tMaBWFDBKeDToFr5Kj29IL35MIQm0RfrnoOfr/29RjSH4aRtvlAR0A== - dependencies: - "@babel/parser" "^7.22.7" - "@babel/traverse" "^7.22.8" - "@docusaurus/logger" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - "@mdx-js/mdx" "^3.0.0" - "@slorber/remark-comment" "^1.0.0" - escape-html "^1.0.3" - estree-util-value-to-estree "^3.0.1" - file-loader "^6.2.0" - fs-extra "^11.1.1" - image-size "^1.0.2" - mdast-util-mdx "^3.0.0" - mdast-util-to-string "^4.0.0" - rehype-raw "^7.0.0" - remark-directive "^3.0.0" - remark-emoji "^4.0.0" - remark-frontmatter "^5.0.0" - remark-gfm "^4.0.0" - stringify-object "^3.3.0" - tslib "^2.6.0" - unified "^11.0.3" - unist-util-visit "^5.0.0" - url-loader "^4.1.1" - vfile "^6.0.1" - webpack "^5.88.1" - -"@docusaurus/module-type-aliases@3.0.0", "@docusaurus/module-type-aliases@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.0.0.tgz#9a7dd323bb87ca666eb4b0b4b90d04425f2e05d6" - integrity sha512-CfC6CgN4u/ce+2+L1JdsHNyBd8yYjl4De2B2CBj2a9F7WuJ5RjV1ciuU7KDg8uyju+NRVllRgvJvxVUjCdkPiw== - dependencies: - "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/types" "3.0.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - "@types/react-router-dom" "*" - react-helmet-async "*" - react-loadable "npm:@docusaurus/react-loadable@5.5.2" - -"@docusaurus/plugin-content-blog@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.0.0.tgz#5f3ede003b2b7103043918fbe3f436c116839ca8" - integrity sha512-iA8Wc3tIzVnROJxrbIsU/iSfixHW16YeW9RWsBw7hgEk4dyGsip9AsvEDXobnRq3lVv4mfdgoS545iGWf1Ip9w== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/logger" "3.0.0" - "@docusaurus/mdx-loader" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-common" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - cheerio "^1.0.0-rc.12" - feed "^4.2.2" - fs-extra "^11.1.1" - lodash "^4.17.21" - reading-time "^1.5.0" - srcset "^4.0.0" - tslib "^2.6.0" - unist-util-visit "^5.0.0" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@docusaurus/plugin-content-docs@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.0.0.tgz#b579c65d7386905890043bdd4a8f9da3194e90fa" - integrity sha512-MFZsOSwmeJ6rvoZMLieXxPuJsA9M9vn7/mUZmfUzSUTeHAeq+fEqvLltFOxcj4DVVDTYlQhgWYd+PISIWgamKw== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/logger" "3.0.0" - "@docusaurus/mdx-loader" "3.0.0" - "@docusaurus/module-type-aliases" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - "@types/react-router-config" "^5.0.7" - combine-promises "^1.1.0" - fs-extra "^11.1.1" - js-yaml "^4.1.0" - lodash "^4.17.21" - tslib "^2.6.0" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@docusaurus/plugin-content-pages@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.0.0.tgz#519a946a477a203989080db70dd787cb6db15fab" - integrity sha512-EXYHXK2Ea1B5BUmM0DgSwaOYt8EMSzWtYUToNo62Q/EoWxYOQFdWglYnw3n7ZEGyw5Kog4LHaRwlazAdmDomvQ== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/mdx-loader" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - fs-extra "^11.1.1" - tslib "^2.6.0" - webpack "^5.88.1" - -"@docusaurus/plugin-debug@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.0.0.tgz#9c6d4abfd5357dbebccf5b41f5aefc06116e03e3" - integrity sha512-gSV07HfQgnUboVEb3lucuVyv5pEoy33E7QXzzn++3kSc/NLEimkjXh3sSnTGOishkxCqlFV9BHfY/VMm5Lko5g== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@microlink/react-json-view" "^1.22.2" - fs-extra "^11.1.1" - tslib "^2.6.0" - -"@docusaurus/plugin-google-analytics@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.0.0.tgz#8a54f5e21b55c133b6be803ac51bf92d4a515cca" - integrity sha512-0zcLK8w+ohmSm1fjUQCqeRsjmQc0gflvXnaVA/QVVCtm2yCiBtkrSGQXqt4MdpD7Xq8mwo3qVd5nhIcvrcebqw== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - tslib "^2.6.0" - -"@docusaurus/plugin-google-gtag@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.0.0.tgz#a4c407b80cb46773bea070816ebb547c5663f0b3" - integrity sha512-asEKavw8fczUqvXu/s9kG2m1epLnHJ19W6CCCRZEmpnkZUZKiM8rlkDiEmxApwIc2JDDbIMk+Y2TMkJI8mInbQ== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - "@types/gtag.js" "^0.0.12" - tslib "^2.6.0" - -"@docusaurus/plugin-google-tag-manager@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.0.0.tgz#8befa315b4747618e9ea65add3f2f4e84df2c7ba" - integrity sha512-lytgu2eyn+7p4WklJkpMGRhwC29ezj4IjPPmVJ8vGzcSl6JkR1sADTHLG5xWOMuci420xZl9dGEiLTQ8FjCRyA== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - tslib "^2.6.0" - -"@docusaurus/plugin-sitemap@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.0.0.tgz#91f300e500d476252ea2f40449ee828766b9b9d6" - integrity sha512-cfcONdWku56Oi7Hdus2uvUw/RKRRlIGMViiHLjvQ21CEsEqnQ297MRoIgjU28kL7/CXD/+OiANSq3T1ezAiMhA== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/logger" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-common" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - fs-extra "^11.1.1" - sitemap "^7.1.1" - tslib "^2.6.0" - -"@docusaurus/preset-classic@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.0.0.tgz#b05c3960c4d0a731b2feb97e94e3757ab073c611" - integrity sha512-90aOKZGZdi0+GVQV+wt8xx4M4GiDrBRke8NO8nWwytMEXNrxrBxsQYFRD1YlISLJSCiHikKf3Z/MovMnQpnZyg== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/plugin-content-blog" "3.0.0" - "@docusaurus/plugin-content-docs" "3.0.0" - "@docusaurus/plugin-content-pages" "3.0.0" - "@docusaurus/plugin-debug" "3.0.0" - "@docusaurus/plugin-google-analytics" "3.0.0" - "@docusaurus/plugin-google-gtag" "3.0.0" - "@docusaurus/plugin-google-tag-manager" "3.0.0" - "@docusaurus/plugin-sitemap" "3.0.0" - "@docusaurus/theme-classic" "3.0.0" - "@docusaurus/theme-common" "3.0.0" - "@docusaurus/theme-search-algolia" "3.0.0" - "@docusaurus/types" "3.0.0" - -"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": - version "5.5.2" - resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" - integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== - dependencies: - "@types/react" "*" - prop-types "^15.6.2" - -"@docusaurus/theme-classic@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.0.0.tgz#a47eda40747e1a6f79190e6bb786d3a7fc4e06b2" - integrity sha512-wWOHSrKMn7L4jTtXBsb5iEJ3xvTddBye5PjYBnWiCkTAlhle2yMdc4/qRXW35Ot+OV/VXu6YFG8XVUJEl99z0A== - dependencies: - "@docusaurus/core" "3.0.0" - "@docusaurus/mdx-loader" "3.0.0" - "@docusaurus/module-type-aliases" "3.0.0" - "@docusaurus/plugin-content-blog" "3.0.0" - "@docusaurus/plugin-content-docs" "3.0.0" - "@docusaurus/plugin-content-pages" "3.0.0" - "@docusaurus/theme-common" "3.0.0" - "@docusaurus/theme-translations" "3.0.0" - "@docusaurus/types" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-common" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - "@mdx-js/react" "^3.0.0" - clsx "^1.2.1" - copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.43" - lodash "^4.17.21" - nprogress "^0.2.0" - postcss "^8.4.26" - prism-react-renderer "^2.1.0" - prismjs "^1.29.0" - react-router-dom "^5.3.4" - rtlcss "^4.1.0" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-common@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.0.0.tgz#6dc8c39a7458dd39f95a2fa6eb1c6aaf32b7e103" - integrity sha512-PahRpCLRK5owCMEqcNtUeTMOkTUCzrJlKA+HLu7f+8osYOni617YurXvHASCsSTxurjXaLz/RqZMnASnqATxIA== - dependencies: - "@docusaurus/mdx-loader" "3.0.0" - "@docusaurus/module-type-aliases" "3.0.0" - "@docusaurus/plugin-content-blog" "3.0.0" - "@docusaurus/plugin-content-docs" "3.0.0" - "@docusaurus/plugin-content-pages" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-common" "3.0.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - clsx "^1.2.1" - parse-numeric-range "^1.3.0" - prism-react-renderer "^2.1.0" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-search-algolia@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.0.0.tgz#20701c2e7945a236df401365271b511a24ff3cad" - integrity sha512-PyMUNIS9yu0dx7XffB13ti4TG47pJq3G2KE/INvOFb6M0kWh+wwCnucPg4WAOysHOPh+SD9fjlXILoLQstgEIA== - dependencies: - "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.0.0" - "@docusaurus/logger" "3.0.0" - "@docusaurus/plugin-content-docs" "3.0.0" - "@docusaurus/theme-common" "3.0.0" - "@docusaurus/theme-translations" "3.0.0" - "@docusaurus/utils" "3.0.0" - "@docusaurus/utils-validation" "3.0.0" - algoliasearch "^4.18.0" - algoliasearch-helper "^3.13.3" - clsx "^1.2.1" - eta "^2.2.0" - fs-extra "^11.1.1" - lodash "^4.17.21" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-translations@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.0.0.tgz#98590b80589f15b2064e0daa2acc3a82d126f53b" - integrity sha512-p/H3+5LdnDtbMU+csYukA6601U1ld2v9knqxGEEV96qV27HsHfP63J9Ta2RBZUrNhQAgrwFzIc9GdDO8P1Baag== - dependencies: - fs-extra "^11.1.1" - tslib "^2.6.0" - -"@docusaurus/tsconfig@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.0.0.tgz#89ce292cff8debaa03d93d651ffd6375561e7dab" - integrity sha512-yR9sng4izFudS+v1xV5yboNfc1hATMDpYp9iYfWggbBDwKSm0J1IdIgkygRnqC/AWs1ARUQUpG0gFotPCE/4Ew== - -"@docusaurus/types@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.0.0.tgz#3edabe43f70b45f81a48f3470d6a73a2eba41945" - integrity sha512-Qb+l/hmCOVemReuzvvcFdk84bUmUFyD0Zi81y651ie3VwMrXqC7C0E7yZLKMOsLj/vkqsxHbtkAuYMI89YzNzg== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - commander "^5.1.0" - joi "^17.9.2" - react-helmet-async "^1.3.0" - utility-types "^3.10.0" - webpack "^5.88.1" - webpack-merge "^5.9.0" - -"@docusaurus/utils-common@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.0.0.tgz#fb019e5228b20852a5b98f50672a02843a03ba03" - integrity sha512-7iJWAtt4AHf4PFEPlEPXko9LZD/dbYnhLe0q8e3GRK1EXZyRASah2lznpMwB3lLmVjq/FR6ZAKF+E0wlmL5j0g== - dependencies: - tslib "^2.6.0" - -"@docusaurus/utils-validation@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.0.0.tgz#56f3ba89ceba9826989408a96827897c0b724612" - integrity sha512-MlIGUspB/HBW5CYgHvRhmkZbeMiUWKbyVoCQYvbGN8S19SSzVgzyy97KRpcjCOYYeEdkhmRCUwFBJBlLg3IoNQ== - dependencies: - "@docusaurus/logger" "3.0.0" - "@docusaurus/utils" "3.0.0" - joi "^17.9.2" - js-yaml "^4.1.0" - tslib "^2.6.0" - -"@docusaurus/utils@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.0.0.tgz#2ef0c8e434036fe104dca4c694fd50022b2ba1ed" - integrity sha512-JwGjh5mtjG9XIAESyPxObL6CZ6LO/yU4OSTpq7Q0x+jN25zi/AMbvLjpSyZzWy+qm5uQiFiIhqFaOxvy+82Ekg== - dependencies: - "@docusaurus/logger" "3.0.0" - "@svgr/webpack" "^6.5.1" - escape-string-regexp "^4.0.0" - file-loader "^6.2.0" - fs-extra "^11.1.1" - github-slugger "^1.5.0" - globby "^11.1.0" - gray-matter "^4.0.3" - jiti "^1.20.0" - js-yaml "^4.1.0" - lodash "^4.17.21" - micromatch "^4.0.5" - resolve-pathname "^3.0.0" - shelljs "^0.8.5" - tslib "^2.6.0" - url-loader "^4.1.1" - webpack "^5.88.1" - -"@hapi/hoek@^9.0.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@jest/schemas@^29.4.3": - version "29.4.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" - integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== - dependencies: - "@sinclair/typebox" "^0.25.16" - -"@jest/types@^29.5.0": - version "29.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" - integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== - dependencies: - "@jest/schemas" "^29.4.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" - integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.18" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" - integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" - integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== - -"@mdx-js/mdx@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.0.0.tgz#37ef87685143fafedf1165f0a79e9fe95fbe5154" - integrity sha512-Icm0TBKBLYqroYbNW3BPnzMGn+7mwpQOK310aZ7+fkCtiU3aqv2cdcX+nd0Ydo3wI5Rx8bX2Z2QmGb/XcAClCw== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdx" "^2.0.0" - collapse-white-space "^2.0.0" - devlop "^1.0.0" - estree-util-build-jsx "^3.0.0" - estree-util-is-identifier-name "^3.0.0" - estree-util-to-js "^2.0.0" - estree-walker "^3.0.0" - hast-util-to-estree "^3.0.0" - hast-util-to-jsx-runtime "^2.0.0" - markdown-extensions "^2.0.0" - periscopic "^3.0.0" - remark-mdx "^3.0.0" - remark-parse "^11.0.0" - remark-rehype "^11.0.0" - source-map "^0.7.0" - unified "^11.0.0" - unist-util-position-from-estree "^2.0.0" - unist-util-stringify-position "^4.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - -"@mdx-js/react@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.0.tgz#eaccaa8d6a7736b19080aff5a70448a7ba692271" - integrity sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ== - dependencies: - "@types/mdx" "^2.0.0" - -"@microlink/react-json-view@^1.22.2": - version "1.23.0" - resolved "https://registry.yarnpkg.com/@microlink/react-json-view/-/react-json-view-1.23.0.tgz#641c2483b1a0014818303d4e9cce634d5dacc7e9" - integrity sha512-HYJ1nsfO4/qn8afnAMhuk7+5a1vcjEaS8Gm5Vpr1SqdHDY0yLBJGpA+9DvKyxyVKaUkXzKXt3Mif9RcmFSdtYg== - dependencies: - flux "~4.0.1" - react-base16-styling "~0.6.0" - react-lifecycles-compat "~3.0.4" - react-textarea-autosize "~8.3.2" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@pnpm/config.env-replace@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" - integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== - -"@pnpm/network.ca-file@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" - integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== - dependencies: - graceful-fs "4.2.10" - -"@pnpm/npm-conf@^2.1.0": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" - integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== - dependencies: - "@pnpm/config.env-replace" "^1.1.0" - "@pnpm/network.ca-file" "^1.0.1" - config-chain "^1.1.11" - -"@polka/url@^1.0.0-next.20": - version "1.0.0-next.21" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" - integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== - -"@sideway/address@^4.1.3": - version "4.1.4" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" - integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@sinclair/typebox@^0.25.16": - version "0.25.24" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" - integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== - -"@sindresorhus/is@^5.2.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" - integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== - -"@sindresorhus/is@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-6.1.0.tgz#71a4ca5171888fb7fc36c6d1ff3604b0a5e43555" - integrity sha512-BuvU07zq3tQ/2SIgBsEuxKYDyDjC0n7Zir52bpHy2xnBbW81+po43aLFPLbeV3HRAheFbGud1qgcqSYfhtHMAg== - -"@slorber/remark-comment@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@slorber/remark-comment/-/remark-comment-1.0.0.tgz#2a020b3f4579c89dec0361673206c28d67e08f5a" - integrity sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.1.0" - micromark-util-symbol "^1.0.1" - -"@slorber/static-site-generator-webpack-plugin@^4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz#fc1678bddefab014e2145cbe25b3ce4e1cfc36f3" - integrity sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA== - dependencies: - eval "^0.1.8" - p-map "^4.0.0" - webpack-sources "^3.2.2" - -"@svgr/babel-plugin-add-jsx-attribute@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" - integrity sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ== - -"@svgr/babel-plugin-remove-jsx-attribute@*": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-7.0.0.tgz#91da77a009dc38e8d30da45d9b62ef8736f2d90a" - integrity sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ== - -"@svgr/babel-plugin-remove-jsx-empty-expression@*": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-7.0.0.tgz#5154ff1213509e36ab315974c8c2fd48dafb827b" - integrity sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw== - -"@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz#fb9d22ea26d2bc5e0a44b763d4c46d5d3f596c60" - integrity sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg== - -"@svgr/babel-plugin-svg-dynamic-title@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz#01b2024a2b53ffaa5efceaa0bf3e1d5a4c520ce4" - integrity sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw== - -"@svgr/babel-plugin-svg-em-dimensions@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz#dd3fa9f5b24eb4f93bcf121c3d40ff5facecb217" - integrity sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA== - -"@svgr/babel-plugin-transform-react-native-svg@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz#1d8e945a03df65b601551097d8f5e34351d3d305" - integrity sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg== - -"@svgr/babel-plugin-transform-svg-component@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz#48620b9e590e25ff95a80f811544218d27f8a250" - integrity sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ== - -"@svgr/babel-preset@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.1.tgz#b90de7979c8843c5c580c7e2ec71f024b49eb828" - integrity sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "^6.5.1" - "@svgr/babel-plugin-remove-jsx-attribute" "*" - "@svgr/babel-plugin-remove-jsx-empty-expression" "*" - "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.5.1" - "@svgr/babel-plugin-svg-dynamic-title" "^6.5.1" - "@svgr/babel-plugin-svg-em-dimensions" "^6.5.1" - "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" - "@svgr/babel-plugin-transform-svg-component" "^6.5.1" - -"@svgr/core@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a" - integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw== - dependencies: - "@babel/core" "^7.19.6" - "@svgr/babel-preset" "^6.5.1" - "@svgr/plugin-jsx" "^6.5.1" - camelcase "^6.2.0" - cosmiconfig "^7.0.1" - -"@svgr/hast-util-to-babel-ast@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz#81800bd09b5bcdb968bf6ee7c863d2288fdb80d2" - integrity sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw== - dependencies: - "@babel/types" "^7.20.0" - entities "^4.4.0" - -"@svgr/plugin-jsx@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz#0e30d1878e771ca753c94e69581c7971542a7072" - integrity sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw== - dependencies: - "@babel/core" "^7.19.6" - "@svgr/babel-preset" "^6.5.1" - "@svgr/hast-util-to-babel-ast" "^6.5.1" - svg-parser "^2.0.4" - -"@svgr/plugin-svgo@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz#0f91910e988fc0b842f88e0960c2862e022abe84" - integrity sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ== - dependencies: - cosmiconfig "^7.0.1" - deepmerge "^4.2.2" - svgo "^2.8.0" - -"@svgr/webpack@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.5.1.tgz#ecf027814fc1cb2decc29dc92f39c3cf691e40e8" - integrity sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA== - dependencies: - "@babel/core" "^7.19.6" - "@babel/plugin-transform-react-constant-elements" "^7.18.12" - "@babel/preset-env" "^7.19.4" - "@babel/preset-react" "^7.18.6" - "@babel/preset-typescript" "^7.18.6" - "@svgr/core" "^6.5.1" - "@svgr/plugin-jsx" "^6.5.1" - "@svgr/plugin-svgo" "^6.5.1" - -"@szmarczak/http-timer@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" - integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== - dependencies: - defer-to-connect "^2.0.1" - -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== - -"@types/acorn@^4.0.0": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" - integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== - dependencies: - "@types/estree" "*" - -"@types/body-parser@*": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.9": - version "3.5.10" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" - integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== - dependencies: - "@types/node" "*" - -"@types/connect-history-api-fallback@^1.3.5": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - -"@types/debug@^4.0.0": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" - integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== - dependencies: - "@types/ms" "*" - -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.37.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" - integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree-jsx@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.3.tgz#f8aa833ec986d82b8271a294a92ed1565bf2c66a" - integrity sha512-pvQ+TKeRHeiUGRhvYwRrQ/ISnohKkSJR14fT2yqyZ4e9K5vqc7hrtY2Y1Dw0ZwAzQ6DQsxsaCUuSIIi8v0Cq6w== - dependencies: - "@types/estree" "*" - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.33" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" - integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@*", "@types/express@^4.17.13": - version "4.17.17" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/fs-extra@^11.0.1": - version "11.0.1" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5" - integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA== - dependencies: - "@types/jsonfile" "*" - "@types/node" "*" - -"@types/gtag.js@^0.0.12": - version "0.0.12" - resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" - integrity sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg== - -"@types/hast@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.3.tgz#7f75e6b43bc3f90316046a287d9ad3888309f7e1" - integrity sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ== - dependencies: - "@types/unist" "*" - -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - -"@types/html-minifier-terser@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" - integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== - -"@types/http-cache-semantics@^4.0.2": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" - integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== - -"@types/http-proxy@^1.17.8": - version "1.17.10" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c" - integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== - dependencies: - "@types/node" "*" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== - -"@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/jsonfile@*": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.1.tgz#ac84e9aefa74a2425a0fb3012bdea44f58970f1b" - integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png== - dependencies: - "@types/node" "*" - -"@types/mdast@^4.0.0", "@types/mdast@^4.0.2": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" - integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== - dependencies: - "@types/unist" "*" - -"@types/mdx@^2.0.0": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.10.tgz#0d7b57fb1d83e27656156e4ee0dfba96532930e4" - integrity sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg== - -"@types/mime@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== - -"@types/ms@*": - version "0.7.34" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" - integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== - -"@types/node@*", "@types/node@^18.15.13": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== - -"@types/node@^17.0.5": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/prismjs@^1.26.0": - version "1.26.3" - resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.3.tgz#47fe8e784c2dee24fe636cab82e090d3da9b7dec" - integrity sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw== - -"@types/prop-types@*": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== - -"@types/qs@*": - version "6.9.7" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/range-parser@*": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== - -"@types/react-router-config@*": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.7.tgz#36207a3fe08b271abee62b26993ee932d13cbb02" - integrity sha512-pFFVXUIydHlcJP6wJm7sDii5mD/bCmmAY0wQzq+M+uX7bqS95AQqHZWP1iNMKrWVQSuHIzj5qi9BvrtLX2/T4w== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "^5.1.0" - -"@types/react-router-config@^5.0.7": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.10.tgz#1f7537b8d23ad6bb8e7609268fdd89b8b2de1eaf" - integrity sha512-Wn6c/tXdEgi9adCMtDwx8Q2vGty6TsPTc/wCQQ9kAlye8UqFxj0vGFWWuhywNfkwqth+SOgJxQTLTZukrqDQmQ== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "^5.1.0" - -"@types/react-router-dom@*": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*", "@types/react-router@^5.1.0": - version "5.1.20" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - -"@types/react@*": - version "18.0.37" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.37.tgz#7a784e2a8b8f83abb04dc6b9ed9c9b4c0aee9be7" - integrity sha512-4yaZZtkRN3ZIQD3KSEwkfcik8s0SWV+82dlJot1AbGYHCzJkWP3ENBY6wYeDRmKZ6HkrgoGAmR2HqdwYGp6OEw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/sax@^1.2.1": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.4.tgz#8221affa7f4f3cb21abd22f244cfabfa63e6a69e" - integrity sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw== - dependencies: - "@types/node" "*" - -"@types/scheduler@*": - version "0.16.3" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== - -"@types/serve-index@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" - integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.1" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" - integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== - dependencies: - "@types/mime" "*" - "@types/node" "*" - -"@types/sockjs@^0.3.33": - version "0.3.33" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" - integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== - dependencies: - "@types/node" "*" - -"@types/unist@*", "@types/unist@^2.0.0": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" - integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== - -"@types/unist@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" - integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== - -"@types/ws@^8.5.5": - version "8.5.9" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.9.tgz#384c489f99c83225a53f01ebc3eddf3b8e202a8c" - integrity sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg== - dependencies: - "@types/node" "*" - -"@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== - -"@types/yargs@^17.0.8": - version "17.0.24" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" - integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== - dependencies: - "@types/yargs-parser" "*" - -"@ungap/structured-clone@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - -"@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c" - integrity sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - -"@webassemblyjs/floating-point-hex-parser@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz#e85dfdb01cad16b812ff166b96806c050555f1b4" - integrity sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ== - -"@webassemblyjs/helper-api-error@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz#1e82fa7958c681ddcf4eabef756ce09d49d442d1" - integrity sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA== - -"@webassemblyjs/helper-buffer@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz#91381652ea95bb38bbfd270702351c0c89d69fba" - integrity sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg== - -"@webassemblyjs/helper-numbers@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz#23380c910d56764957292839006fecbe05e135a9" - integrity sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.5" - "@webassemblyjs/helper-api-error" "1.11.5" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz#e258a25251bc69a52ef817da3001863cc1c24b9f" - integrity sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA== - -"@webassemblyjs/helper-wasm-section@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz#966e855a6fae04d5570ad4ec87fbcf29b42ba78e" - integrity sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-buffer" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/wasm-gen" "1.11.5" - -"@webassemblyjs/ieee754@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz#b2db1b33ce9c91e34236194c2b5cba9b25ca9d60" - integrity sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.5.tgz#482e44d26b6b949edf042a8525a66c649e38935a" - integrity sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.5.tgz#83bef94856e399f3740e8df9f63bc47a987eae1a" - integrity sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ== - -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz#93ee10a08037657e21c70de31c47fdad6b522b2d" - integrity sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-buffer" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/helper-wasm-section" "1.11.5" - "@webassemblyjs/wasm-gen" "1.11.5" - "@webassemblyjs/wasm-opt" "1.11.5" - "@webassemblyjs/wasm-parser" "1.11.5" - "@webassemblyjs/wast-printer" "1.11.5" - -"@webassemblyjs/wasm-gen@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz#ceb1c82b40bf0cf67a492c53381916756ef7f0b1" - integrity sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/ieee754" "1.11.5" - "@webassemblyjs/leb128" "1.11.5" - "@webassemblyjs/utf8" "1.11.5" - -"@webassemblyjs/wasm-opt@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz#b52bac29681fa62487e16d3bb7f0633d5e62ca0a" - integrity sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-buffer" "1.11.5" - "@webassemblyjs/wasm-gen" "1.11.5" - "@webassemblyjs/wasm-parser" "1.11.5" - -"@webassemblyjs/wasm-parser@1.11.5", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz#7ba0697ca74c860ea13e3ba226b29617046982e2" - integrity sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@webassemblyjs/helper-api-error" "1.11.5" - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" - "@webassemblyjs/ieee754" "1.11.5" - "@webassemblyjs/leb128" "1.11.5" - "@webassemblyjs/utf8" "1.11.5" - -"@webassemblyjs/wast-printer@1.11.5": - version "1.11.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz#7a5e9689043f3eca82d544d7be7a8e6373a6fa98" - integrity sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA== - dependencies: - "@webassemblyjs/ast" "1.11.5" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== - -acorn-jsx@^5.0.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.0.0, acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.0.0, acorn@^8.8.2: - version "8.11.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" - integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== - -acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -address@^1.0.1, address@^1.1.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" - integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@^6.12.2, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.9.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -algoliasearch-helper@^3.13.3: - version "3.15.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.15.0.tgz#d680783329920a3619a74504dccb97a4fb943443" - integrity sha512-DGUnK3TGtDQsaUE4ayF/LjSN0DGsuYThB8WBgnnDY0Wq04K6lNVruO3LfqJOgSfDiezp+Iyt8Tj4YKHi+/ivSA== - dependencies: - "@algolia/events" "^4.0.1" - -algoliasearch@^4.18.0, algoliasearch@^4.19.1: - version "4.20.0" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.20.0.tgz#700c2cb66e14f8a288460036c7b2a554d0d93cf4" - integrity sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g== - dependencies: - "@algolia/cache-browser-local-storage" "4.20.0" - "@algolia/cache-common" "4.20.0" - "@algolia/cache-in-memory" "4.20.0" - "@algolia/client-account" "4.20.0" - "@algolia/client-analytics" "4.20.0" - "@algolia/client-common" "4.20.0" - "@algolia/client-personalization" "4.20.0" - "@algolia/client-search" "4.20.0" - "@algolia/logger-common" "4.20.0" - "@algolia/logger-console" "4.20.0" - "@algolia/requester-browser-xhr" "4.20.0" - "@algolia/requester-common" "4.20.0" - "@algolia/requester-node-http" "4.20.0" - "@algolia/transporter" "4.20.0" - -ansi-align@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -arg@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - -astring@^1.8.0: - version "1.8.6" - resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" - integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@^10.4.12: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== - dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -autoprefixer@^10.4.14: - version "10.4.16" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" - integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ== - dependencies: - browserslist "^4.21.10" - caniuse-lite "^1.0.30001538" - fraction.js "^4.3.6" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -axios@^1.6.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" - integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -babel-loader@^9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== - dependencies: - find-cache-dir "^4.0.0" - schema-utils "^4.0.0" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.3" - semver "^6.1.1" - -babel-plugin-polyfill-corejs2@^0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" - integrity sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.3" - semver "^6.3.1" - -babel-plugin-polyfill-corejs3@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - core-js-compat "^3.25.1" - -babel-plugin-polyfill-corejs3@^0.8.5: - version "0.8.6" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz#25c2d20002da91fe328ff89095c85a391d6856cf" - integrity sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.3" - core-js-compat "^3.33.1" - -babel-plugin-polyfill-regenerator@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - -babel-plugin-polyfill-regenerator@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz#d4c49e4b44614607c13fb769bcd85c72bb26a4a5" - integrity sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.3" - -bail@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" - integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base16@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" - integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ== - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.0.11: - version "1.1.1" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.1.tgz#960948fa0e0153f5d26743ab15baf8e33752c135" - integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== - dependencies: - array-flatten "^2.1.2" - dns-equal "^1.0.0" - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -boxen@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d" - integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw== - dependencies: - ansi-align "^3.0.1" - camelcase "^6.2.0" - chalk "^4.1.2" - cli-boxes "^3.0.0" - string-width "^5.0.1" - type-fest "^2.5.0" - widest-line "^4.0.1" - wrap-ansi "^8.0.1" - -boxen@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.1.1.tgz#f9ba525413c2fec9cdb88987d835c4f7cad9c8f4" - integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== - dependencies: - ansi-align "^3.0.1" - camelcase "^7.0.1" - chalk "^5.2.0" - cli-boxes "^3.0.0" - string-width "^5.1.2" - type-fest "^2.13.0" - widest-line "^4.0.1" - wrap-ansi "^8.1.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== - dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - -browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.1: - version "4.22.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" - integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== - dependencies: - caniuse-lite "^1.0.30001541" - electron-to-chromium "^1.4.535" - node-releases "^2.0.13" - update-browserslist-db "^1.0.13" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacheable-lookup@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" - integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== - -cacheable-request@^10.2.8: - version "10.2.14" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" - integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== - dependencies: - "@types/http-cache-semantics" "^4.0.2" - get-stream "^6.0.1" - http-cache-semantics "^4.1.1" - keyv "^4.5.3" - mimic-response "^4.0.0" - normalize-url "^8.0.0" - responselike "^3.0.0" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -camelcase@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" - integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001481" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912" - integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ== - -caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: - version "1.0.30001563" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz#aa68a64188903e98f36eb9c56e48fba0c1fe2a32" - integrity sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw== - -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - -chalk@^2.0.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.0.1, chalk@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - -character-entities@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" - integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== - -character-reference-invalid@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" - integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== - -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== - dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - -cheerio@^1.0.0-rc.12: - version "1.0.0-rc.12" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" - integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.0.1" - htmlparser2 "^8.0.1" - parse5 "^7.0.0" - parse5-htmlparser2-tree-adapter "^7.0.0" - -chokidar@^3.4.2, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== - -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== - -classnames@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== - -clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" - integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" - integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== - -cli-table3@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -collapse-white-space@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" - integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.9.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" - integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== - -colorette@^2.0.10: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -combine-promises@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71" - integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - -commander@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commander@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -configstore@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-6.0.0.tgz#49eca2ebc80983f77e09394a1a56e0aca8235566" - integrity sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA== - dependencies: - dot-prop "^6.0.1" - graceful-fs "^4.2.6" - unique-string "^3.0.0" - write-file-atomic "^3.0.3" - xdg-basedir "^5.0.1" - -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -copy-text-to-clipboard@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz#0202b2d9bdae30a49a53f898626dcc3b49ad960b" - integrity sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q== - -copy-webpack-plugin@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -core-js-compat@^3.25.1: - version "3.30.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.1.tgz#961541e22db9c27fc48bfc13a3cafa8734171dfe" - integrity sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw== - dependencies: - browserslist "^4.21.5" - -core-js-compat@^3.31.0, core-js-compat@^3.33.1: - version "3.33.3" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.3.tgz#ec678b772c5a2d8a7c60a91c3a81869aa704ae01" - integrity sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow== - dependencies: - browserslist "^4.22.1" - -core-js-pure@^3.30.2: - version "3.33.3" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.33.3.tgz#cbf9180ac4c4653823d784862bfb5c77eac0bf98" - integrity sha512-taJ00IDOP+XYQEA2dAe4ESkmHt1fL8wzYDo3mRWQey8uO9UojlBFMneA65kMyxfYP7106c6LzWaq7/haDT6BCQ== - -core-js@^3.31.1: - version "3.33.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.3.tgz#3c644a323f0f533a0d360e9191e37f7fc059088d" - integrity sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -cosmiconfig@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -cosmiconfig@^8.2.0: - version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-random-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" - integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== - dependencies: - type-fest "^1.0.1" - -css-declaration-sorter@^6.3.1: - version "6.4.0" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz#630618adc21724484b3e9505bce812def44000ad" - integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew== - -css-loader@^6.8.1: - version "6.8.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" - integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g== - dependencies: - icss-utils "^5.1.0" - postcss "^8.4.21" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.3" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.2.0" - semver "^7.3.8" - -css-minimizer-webpack-plugin@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz#79f6199eb5adf1ff7ba57f105e3752d15211eb35" - integrity sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA== - dependencies: - cssnano "^5.1.8" - jest-worker "^29.1.2" - postcss "^8.4.17" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - source-map "^0.6.1" - -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^6.0.1, css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-advanced@^5.3.10: - version "5.3.10" - resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.10.tgz#25558a1fbf3a871fb6429ce71e41be7f5aca6eef" - integrity sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ== - dependencies: - autoprefixer "^10.4.12" - cssnano-preset-default "^5.2.14" - postcss-discard-unused "^5.1.0" - postcss-merge-idents "^5.1.1" - postcss-reduce-idents "^5.2.0" - postcss-zindex "^5.1.0" - -cssnano-preset-default@^5.2.14: - version "5.2.14" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" - integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== - dependencies: - css-declaration-sorter "^6.3.1" - cssnano-utils "^3.1.0" - postcss-calc "^8.2.3" - postcss-colormin "^5.3.1" - postcss-convert-values "^5.1.3" - postcss-discard-comments "^5.1.2" - postcss-discard-duplicates "^5.1.0" - postcss-discard-empty "^5.1.1" - postcss-discard-overridden "^5.1.0" - postcss-merge-longhand "^5.1.7" - postcss-merge-rules "^5.1.4" - postcss-minify-font-values "^5.1.0" - postcss-minify-gradients "^5.1.1" - postcss-minify-params "^5.1.4" - postcss-minify-selectors "^5.2.1" - postcss-normalize-charset "^5.1.0" - postcss-normalize-display-values "^5.1.0" - postcss-normalize-positions "^5.1.1" - postcss-normalize-repeat-style "^5.1.1" - postcss-normalize-string "^5.1.0" - postcss-normalize-timing-functions "^5.1.0" - postcss-normalize-unicode "^5.1.1" - postcss-normalize-url "^5.1.0" - postcss-normalize-whitespace "^5.1.1" - postcss-ordered-values "^5.1.3" - postcss-reduce-initial "^5.1.2" - postcss-reduce-transforms "^5.1.0" - postcss-svgo "^5.1.0" - postcss-unique-selectors "^5.1.1" - -cssnano-utils@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" - integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== - -cssnano@^5.1.15, cssnano@^5.1.8: - version "5.1.15" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" - integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== - dependencies: - cssnano-preset-default "^5.2.14" - lilconfig "^2.0.3" - yaml "^1.10.2" - -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== - -debounce@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" - integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== - -debug@2.6.9, debug@^2.6.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== - dependencies: - character-entities "^2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deepmerge@^4.0.0, deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" - -defer-to-connect@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -del@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -detect-port@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.5.1.tgz#451ca9b6eaf20451acb0799b8ab40dff7718727b" - integrity sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ== - dependencies: - address "^1.0.1" - debug "4" - -devlop@^1.0.0, devlop@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" - integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== - dependencies: - dequal "^2.0.0" - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== - -dns-packet@^5.2.2: - version "5.6.0" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.0.tgz#2202c947845c7a63c23ece58f2f70ff6ab4c2f7d" - integrity sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -dom-converter@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^2.5.2, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -domutils@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" - integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.1" - -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -dot-prop@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" - integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== - dependencies: - is-obj "^2.0.0" - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.4.284: - version "1.4.368" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.368.tgz#75901f97d3e23da2e66feb1e61fbb8e70ac96430" - integrity sha512-e2aeCAixCj9M7nJxdB/wDjO6mbYX+lJJxSJCXDzlr5YPGYVofuJwGN9nKg2o6wWInjX6XmxRinn3AeJMK81ltw== - -electron-to-chromium@^1.4.535: - version "1.4.588" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.588.tgz#d553f3c008e73488fb181fdf2601fdb0b1ffbb78" - integrity sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -emojilib@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" - integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -emoticon@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.0.1.tgz#2d2bbbf231ce3a5909e185bbb64a9da703a1e749" - integrity sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^4.2.0, entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-module-lexer@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" - integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-goat@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" - integrity sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg== - -escape-html@^1.0.3, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escape-string-regexp@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -estree-util-attach-comments@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d" - integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== - dependencies: - "@types/estree" "^1.0.0" - -estree-util-build-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz#b6d0bced1dcc4f06f25cf0ceda2b2dcaf98168f1" - integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== - dependencies: - "@types/estree-jsx" "^1.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - estree-walker "^3.0.0" - -estree-util-is-identifier-name@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" - integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== - -estree-util-to-js@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz#10a6fb924814e6abb62becf0d2bc4dea51d04f17" - integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== - dependencies: - "@types/estree-jsx" "^1.0.0" - astring "^1.8.0" - source-map "^0.7.0" - -estree-util-value-to-estree@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-3.0.1.tgz#0b7b5d6b6a4aaad5c60999ffbc265a985df98ac5" - integrity sha512-b2tdzTurEIbwRh+mKrEcaWfu1wgb8J1hVsgREg7FFiecWwK/PhO8X0kyc+0bIcKNtD4sqxIdNoRy6/p/TvECEA== - dependencies: - "@types/estree" "^1.0.0" - is-plain-obj "^4.0.0" - -estree-util-visit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz#13a9a9f40ff50ed0c022f831ddf4b58d05446feb" - integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/unist" "^3.0.0" - -estree-walker@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" - integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== - dependencies: - "@types/estree" "^1.0.0" - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eta@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eta/-/eta-2.2.0.tgz#eb8b5f8c4e8b6306561a455e62cd7492fe3a9b8a" - integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eval@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" - integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== - dependencies: - "@types/node" "*" - require-like ">= 0.1.1" - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== - dependencies: - is-extendable "^0.1.0" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.11, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - -fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== - dependencies: - reusify "^1.0.4" - -fault@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" - integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== - dependencies: - format "^0.2.0" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fbemitter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-3.0.0.tgz#00b2a1af5411254aab416cd75f9e6289bee4bff3" - integrity sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw== - dependencies: - fbjs "^3.0.0" - -fbjs-css-vars@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" - integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== - -fbjs@^3.0.0, fbjs@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.4.tgz#e1871c6bd3083bac71ff2da868ad5067d37716c6" - integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ== - dependencies: - cross-fetch "^3.1.5" - fbjs-css-vars "^1.0.0" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.30" - -feed@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" - integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== - dependencies: - xml-js "^1.6.11" - -file-loader@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-cache-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" - integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== - dependencies: - common-path-prefix "^3.0.0" - pkg-dir "^7.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" - integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== - dependencies: - locate-path "^7.1.0" - path-exists "^5.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flux@~4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/flux/-/flux-4.0.4.tgz#9661182ea81d161ee1a6a6af10d20485ef2ac572" - integrity sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw== - dependencies: - fbemitter "^3.0.0" - fbjs "^3.0.1" - -follow-redirects@^1.0.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== - -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - -form-data-encoder@^2.1.2: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" - integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -format@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" - integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - -fraction.js@^4.3.6: - version "4.3.7" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-extra@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-monkey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - -get-stream@^6.0.0, get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -github-slugger@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" - integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -globby@^13.1.1: - version "13.1.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.4.tgz#2f91c116066bcec152465ba36e5caa4a13c01317" - integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.2.11" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^4.0.0" - -got@^12.1.0: - version "12.6.1" - resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" - integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== - dependencies: - "@sindresorhus/is" "^5.2.0" - "@szmarczak/http-timer" "^5.0.1" - cacheable-lookup "^7.0.0" - cacheable-request "^10.2.8" - decompress-response "^6.0.0" - form-data-encoder "^2.1.2" - get-stream "^6.0.1" - http2-wrapper "^2.1.10" - lowercase-keys "^3.0.0" - p-cancelable "^3.0.0" - responselike "^3.0.0" - -graceful-fs@4.2.10: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -gray-matter@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" - integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== - dependencies: - js-yaml "^3.13.1" - kind-of "^6.0.2" - section-matter "^1.0.0" - strip-bom-string "^1.0.0" - -gzip-size@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" - integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== - dependencies: - duplexer "^0.1.2" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-yarn@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" - integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hast-util-from-parse5@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" - integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - hastscript "^8.0.0" - property-information "^6.0.0" - vfile "^6.0.0" - vfile-location "^5.0.0" - web-namespaces "^2.0.0" - -hast-util-parse-selector@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" - integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== - dependencies: - "@types/hast" "^3.0.0" - -hast-util-raw@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.1.tgz#2ba8510e4ed2a1e541cde2a4ebb5c38ab4c82c2d" - integrity sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - "@ungap/structured-clone" "^1.0.0" - hast-util-from-parse5 "^8.0.0" - hast-util-to-parse5 "^8.0.0" - html-void-elements "^3.0.0" - mdast-util-to-hast "^13.0.0" - parse5 "^7.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-to-estree@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz#f2afe5e869ddf0cf690c75f9fc699f3180b51b19" - integrity sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - estree-util-attach-comments "^3.0.0" - estree-util-is-identifier-name "^3.0.0" - hast-util-whitespace "^3.0.0" - mdast-util-mdx-expression "^2.0.0" - mdast-util-mdx-jsx "^3.0.0" - mdast-util-mdxjs-esm "^2.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.0" - unist-util-position "^5.0.0" - zwitch "^2.0.0" - -hast-util-to-jsx-runtime@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.2.0.tgz#ffd59bfcf0eb8321c6ed511bfc4b399ac3404bc2" - integrity sha512-wSlp23N45CMjDg/BPW8zvhEi3R+8eRE1qFbjEyAUzMCzu2l1Wzwakq+Tlia9nkCtEl5mDxa7nKHsvYJ6Gfn21A== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - comma-separated-tokens "^2.0.0" - hast-util-whitespace "^3.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.0" - unist-util-position "^5.0.0" - vfile-message "^4.0.0" - -hast-util-to-parse5@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" - integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-whitespace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" - integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== - dependencies: - "@types/hast" "^3.0.0" - -hastscript@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" - integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - hast-util-parse-selector "^4.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hoist-non-react-statics@^3.1.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-entities@^2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" - integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== - -html-escaper@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-minifier-terser@^6.0.2: - version "6.1.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" - integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== - dependencies: - camel-case "^4.1.2" - clean-css "^5.2.2" - commander "^8.3.0" - he "^1.2.0" - param-case "^3.0.4" - relateurl "^0.2.7" - terser "^5.10.0" - -html-minifier-terser@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" - integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA== - dependencies: - camel-case "^4.1.2" - clean-css "~5.3.2" - commander "^10.0.0" - entities "^4.4.0" - param-case "^3.0.4" - relateurl "^0.2.7" - terser "^5.15.1" - -html-tags@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" - integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== - -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - -html-webpack-plugin@^5.5.3: - version "5.5.3" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz#72270f4a78e222b5825b296e5e3e1328ad525a3e" - integrity sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg== - dependencies: - "@types/html-minifier-terser" "^6.0.0" - html-minifier-terser "^6.0.2" - lodash "^4.17.21" - pretty-error "^4.0.0" - tapable "^2.0.0" - -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -htmlparser2@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -http-cache-semantics@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - -http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http2-wrapper@^2.1.10: - version "2.2.1" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" - integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.2.0" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -image-size@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.2.tgz#d778b6d0ab75b2737c1556dd631652eb963bc486" - integrity sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg== - dependencies: - queue "6.0.2" - -immer@^9.0.7: - version "9.0.21" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" - integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== - -import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" - integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - -is-alphabetical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" - integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== - -is-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== - dependencies: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-ci@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-core-module@^2.11.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4" - integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== - dependencies: - has "^1.0.3" - -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" - integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== - -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-npm@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" - integrity sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-reference@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" - integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== - dependencies: - "@types/estree" "*" - -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== - -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -is-yarn-global@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.4.1.tgz#b312d902b313f81e4eaf98b6361ba2b45cd694bb" - integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jest-util@^29.5.0: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" - integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== - dependencies: - "@jest/types" "^29.5.0" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^29.1.2: - version "29.5.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d" - integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA== - dependencies: - "@types/node" "*" - jest-util "^29.5.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jiti@^1.18.2, jiti@^1.20.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== - -joi@^17.11.0, joi@^17.9.2: - version "17.11.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" - integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/topo" "^5.0.0" - "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -latest-version@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" - integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== - dependencies: - package-json "^8.1.0" - -launch-editor@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" - integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.7.3" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -lilconfig@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -load-script@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" - integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -locate-path@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" - integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== - dependencies: - p-locate "^6.0.0" - -lodash.curry@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" - integrity sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.flow@^3.3.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" - integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -lodash@^4.17.20, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -longest-streak@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" - integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lowercase-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" - integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -markdown-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" - integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== - -markdown-table@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" - integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== - -mdast-util-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz#3fb1764e705bbdf0afb0d3f889e4404c3e82561f" - integrity sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-visit-parents "^6.0.0" - -mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0" - integrity sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA== - dependencies: - "@types/mdast" "^4.0.0" - escape-string-regexp "^5.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - -mdast-util-from-markdown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" - integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - decode-named-character-reference "^1.0.0" - devlop "^1.0.0" - mdast-util-to-string "^4.0.0" - micromark "^4.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-decode-string "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-stringify-position "^4.0.0" - -mdast-util-frontmatter@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" - integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - escape-string-regexp "^5.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - micromark-extension-frontmatter "^2.0.0" - -mdast-util-gfm-autolink-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" - integrity sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg== - dependencies: - "@types/mdast" "^4.0.0" - ccount "^2.0.0" - devlop "^1.0.0" - mdast-util-find-and-replace "^3.0.0" - micromark-util-character "^2.0.0" - -mdast-util-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" - integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.1.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - -mdast-util-gfm-strikethrough@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" - integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm-table@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" - integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - markdown-table "^3.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm-task-list-item@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" - integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" - integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== - dependencies: - mdast-util-from-markdown "^2.0.0" - mdast-util-gfm-autolink-literal "^2.0.0" - mdast-util-gfm-footnote "^2.0.0" - mdast-util-gfm-strikethrough "^2.0.0" - mdast-util-gfm-table "^2.0.0" - mdast-util-gfm-task-list-item "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdx-expression@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" - integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdx-jsx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.0.0.tgz#f73631fa5bb7a36712ff1e9cedec0cafed03401c" - integrity sha512-XZuPPzQNBPAlaqsTTgRrcJnyFbSOBovSadFgbFu8SnuNgm+6Bdx1K+IWoitsmj6Lq6MNtI+ytOqwN70n//NaBA== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - ccount "^2.0.0" - devlop "^1.1.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-remove-position "^5.0.0" - unist-util-stringify-position "^4.0.0" - vfile-message "^4.0.0" - -mdast-util-mdx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz#792f9cf0361b46bee1fdf1ef36beac424a099c41" - integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== - dependencies: - mdast-util-from-markdown "^2.0.0" - mdast-util-mdx-expression "^2.0.0" - mdast-util-mdx-jsx "^3.0.0" - mdast-util-mdxjs-esm "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdxjs-esm@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" - integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-phrasing@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz#468cbbb277375523de807248b8ad969feb02a5c7" - integrity sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA== - dependencies: - "@types/mdast" "^4.0.0" - unist-util-is "^6.0.0" - -mdast-util-to-hast@^13.0.0: - version "13.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz#74c0a9f014bb2340cae6118f6fccd75467792be7" - integrity sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@ungap/structured-clone" "^1.0.0" - devlop "^1.0.0" - micromark-util-sanitize-uri "^2.0.0" - trim-lines "^3.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - -mdast-util-to-markdown@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" - integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^4.0.0" - mdast-util-to-string "^4.0.0" - micromark-util-decode-string "^2.0.0" - unist-util-visit "^5.0.0" - zwitch "^2.0.0" - -mdast-util-to-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" - integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== - dependencies: - "@types/mdast" "^4.0.0" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^3.1.2, memfs@^3.4.3: - version "3.5.1" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.1.tgz#f0cd1e2bfaef58f6fe09bfb9c2288f07fea099ec" - integrity sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA== - dependencies: - fs-monkey "^1.0.3" - -memoize-one@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromark-core-commonmark@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3" - integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== - dependencies: - decode-named-character-reference "^1.0.0" - devlop "^1.0.0" - micromark-factory-destination "^2.0.0" - micromark-factory-label "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-factory-title "^2.0.0" - micromark-factory-whitespace "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-classify-character "^2.0.0" - micromark-util-html-tag-name "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-subtokenize "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz#527869de497a6de9024138479091bc885dae076b" - integrity sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-factory-whitespace "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - parse-entities "^4.0.0" - -micromark-extension-frontmatter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" - integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== - dependencies: - fault "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-autolink-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" - integrity sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" - integrity sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg== - dependencies: - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-strikethrough@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" - integrity sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw== - dependencies: - devlop "^1.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-classify-character "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-table@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" - integrity sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-tagfilter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" - integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== - dependencies: - micromark-util-types "^2.0.0" - -micromark-extension-gfm-task-list-item@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" - integrity sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" - integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== - dependencies: - micromark-extension-gfm-autolink-literal "^2.0.0" - micromark-extension-gfm-footnote "^2.0.0" - micromark-extension-gfm-strikethrough "^2.0.0" - micromark-extension-gfm-table "^2.0.0" - micromark-extension-gfm-tagfilter "^2.0.0" - micromark-extension-gfm-task-list-item "^2.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-mdx-expression@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz#1407b9ce69916cf5e03a196ad9586889df25302a" - integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-factory-mdx-expression "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-mdx-jsx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz#4aba0797c25efb2366a3fd2d367c6b1c1159f4f5" - integrity sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - micromark-factory-mdx-expression "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - vfile-message "^4.0.0" - -micromark-extension-mdx-md@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz#1d252881ea35d74698423ab44917e1f5b197b92d" - integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== - dependencies: - micromark-util-types "^2.0.0" - -micromark-extension-mdxjs-esm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz#de21b2b045fd2059bd00d36746081de38390d54a" - integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-position-from-estree "^2.0.0" - vfile-message "^4.0.0" - -micromark-extension-mdxjs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz#b5a2e0ed449288f3f6f6c544358159557549de18" - integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== - dependencies: - acorn "^8.0.0" - acorn-jsx "^5.0.0" - micromark-extension-mdx-expression "^3.0.0" - micromark-extension-mdx-jsx "^3.0.0" - micromark-extension-mdx-md "^2.0.0" - micromark-extension-mdxjs-esm "^3.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-destination@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" - integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-label@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" - integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== - dependencies: - devlop "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-mdx-expression@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz#f2a9724ce174f1751173beb2c1f88062d3373b1b" - integrity sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-position-from-estree "^2.0.0" - vfile-message "^4.0.0" - -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-space@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" - integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-title@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" - integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== - dependencies: - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-whitespace@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" - integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== - dependencies: - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.0.1.tgz#52b824c2e2633b6fb33399d2ec78ee2a90d6b298" - integrity sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw== - dependencies: - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-chunked@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" - integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-classify-character@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" - integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-combine-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" - integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== - dependencies: - micromark-util-chunked "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-decode-numeric-character-reference@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" - integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-decode-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" - integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" - integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== - -micromark-util-events-to-acorn@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz#4275834f5453c088bd29cd72dfbf80e3327cec07" - integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - estree-util-visit "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - vfile-message "^4.0.0" - -micromark-util-html-tag-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" - integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== - -micromark-util-normalize-identifier@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" - integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-resolve-all@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" - integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== - dependencies: - micromark-util-types "^2.0.0" - -micromark-util-sanitize-uri@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" - integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-subtokenize@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz#9f412442d77e0c5789ffdf42377fa8a2bcbdf581" - integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg== - dependencies: - devlop "^1.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-symbol@^1.0.0, micromark-util-symbol@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - -micromark-util-symbol@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" - integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== - -micromark-util-types@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - -micromark-util-types@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" - integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== - -micromark@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" - integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-subtokenize "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -mimic-response@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" - integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== - -mini-css-extract-plugin@^2.7.6: - version "2.7.6" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d" - integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw== - dependencies: - schema-utils "^4.0.0" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mrmime@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - -node-emoji@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.1.tgz#188e0abb6f03335155cc3dae219f7555c5d37012" - integrity sha512-+fyi06+Z9LARCwnTmUF1sRPVQFhGlIpuye3zwlzMN8bIKou6l7k1rGV8WVOEu9EQnRLfoVOYj/p107u0CoQoKA== - dependencies: - "@sindresorhus/is" "^6.0.0" - char-regex "^1.0.2" - emojilib "^2.4.0" - skin-tone "^2.0.0" - -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-forge@^1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-releases@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" - integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== - -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - -normalize-url@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.0.tgz#593dbd284f743e8dcf6a5ddf8fadff149c82701a" - integrity sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nprogress@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" - integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^8.0.9, open@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -p-cancelable@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" - integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== - -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-locate@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" - integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== - dependencies: - p-limit "^4.0.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" - integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== - dependencies: - got "^12.1.0" - registry-auth-token "^5.0.1" - registry-url "^6.0.0" - semver "^7.3.7" - -param-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" - integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-entities@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" - integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== - dependencies: - "@types/unist" "^2.0.0" - character-entities "^2.0.0" - character-entities-legacy "^3.0.0" - character-reference-invalid "^2.0.0" - decode-named-character-reference "^1.0.0" - is-alphanumerical "^2.0.0" - is-decimal "^2.0.0" - is-hexadecimal "^2.0.0" - -parse-json@^5.0.0, parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-numeric-range@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" - integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== - -parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" - integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== - dependencies: - domhandler "^5.0.2" - parse5 "^7.0.0" - -parse5@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-exists@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" - integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-is-inside@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== - -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -periscopic@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" - integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^3.0.0" - is-reference "^3.0.0" - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pkg-dir@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" - integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== - dependencies: - find-up "^6.3.0" - -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== - dependencies: - find-up "^3.0.0" - -postcss-calc@^8.2.3: - version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" - integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== - dependencies: - postcss-selector-parser "^6.0.9" - postcss-value-parser "^4.2.0" - -postcss-colormin@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" - integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - colord "^2.9.1" - postcss-value-parser "^4.2.0" - -postcss-convert-values@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" - integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== - dependencies: - browserslist "^4.21.4" - postcss-value-parser "^4.2.0" - -postcss-discard-comments@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" - integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== - -postcss-discard-duplicates@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" - integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== - -postcss-discard-empty@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" - integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== - -postcss-discard-overridden@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" - integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== - -postcss-discard-unused@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz#8974e9b143d887677304e558c1166d3762501142" - integrity sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-loader@^7.3.3: - version "7.3.3" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd" - integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA== - dependencies: - cosmiconfig "^8.2.0" - jiti "^1.18.2" - semver "^7.3.8" - -postcss-merge-idents@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz#7753817c2e0b75d0853b56f78a89771e15ca04a1" - integrity sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw== - dependencies: - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-merge-longhand@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" - integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== - dependencies: - postcss-value-parser "^4.2.0" - stylehacks "^5.1.1" - -postcss-merge-rules@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" - integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - cssnano-utils "^3.1.0" - postcss-selector-parser "^6.0.5" - -postcss-minify-font-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" - integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-minify-gradients@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" - integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== - dependencies: - colord "^2.9.1" - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-minify-params@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" - integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== - dependencies: - browserslist "^4.21.4" - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-minify-selectors@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" - integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" - integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-normalize-charset@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" - integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== - -postcss-normalize-display-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" - integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-positions@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" - integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-repeat-style@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" - integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-string@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" - integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-timing-functions@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" - integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-unicode@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" - integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== - dependencies: - browserslist "^4.21.4" - postcss-value-parser "^4.2.0" - -postcss-normalize-url@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" - integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== - dependencies: - normalize-url "^6.0.1" - postcss-value-parser "^4.2.0" - -postcss-normalize-whitespace@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" - integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-ordered-values@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" - integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== - dependencies: - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-reduce-idents@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz#c89c11336c432ac4b28792f24778859a67dfba95" - integrity sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-reduce-initial@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" - integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" - integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-sort-media-queries@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-4.4.1.tgz#04a5a78db3921eb78f28a1a781a2e68e65258128" - integrity sha512-QDESFzDDGKgpiIh4GYXsSy6sek2yAwQx1JASl5AxBtU1Lq2JfKBljIPNdil989NcSKRQX1ToiaKphImtBuhXWw== - dependencies: - sort-css-media-queries "2.1.0" - -postcss-svgo@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" - integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== - dependencies: - postcss-value-parser "^4.2.0" - svgo "^2.7.0" - -postcss-unique-selectors@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" - integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss-zindex@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.1.0.tgz#4a5c7e5ff1050bd4c01d95b1847dfdcc58a496ff" - integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== - -postcss@^8.4.17: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.4.21, postcss@^8.4.26: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -pretty-error@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" - integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== - dependencies: - lodash "^4.17.20" - renderkid "^3.0.0" - -pretty-time@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" - integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== - -prism-react-renderer@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.2.0.tgz#f199b15716e0b8d0ccfd1986ff6fa226fb7ff2b1" - integrity sha512-j4AN0VkEr72598+47xDvpzeYyeh/wPPRNTt9nJFZqIZUxwGKwYqYgt7RVigZ3ZICJWJWN84KEuMKPNyypyhNIw== - dependencies: - "@types/prismjs" "^1.26.0" - clsx "^1.2.1" - -prismjs@^1.29.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" - integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prompts@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -property-information@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.0.tgz#6bc4c618b0c2d68b3bb8b552cbb97f8e300a0f82" - integrity sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== - -pupa@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-3.1.0.tgz#f15610274376bbcc70c9a3aa8b505ea23f41c579" - integrity sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug== - dependencies: - escape-goat "^4.0.0" - -pure-color@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" - integrity sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA== - -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-base16-styling@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c" - integrity sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ== - dependencies: - base16 "^1.0.0" - lodash.curry "^4.0.1" - lodash.flow "^3.3.0" - pure-color "^1.2.0" - -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -react-dom@18: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" - -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f" - integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg== - -react-helmet-async@*, react-helmet-async@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" - integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== - dependencies: - "@babel/runtime" "^7.12.5" - invariant "^2.2.4" - prop-types "^15.7.2" - react-fast-compare "^3.2.0" - shallowequal "^1.1.0" - -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lifecycles-compat@~3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-loadable-ssr-addon-v5-slorber@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" - integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== - dependencies: - "@babel/runtime" "^7.10.3" - -react-loadable@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/react-loadable/-/react-loadable-5.5.0.tgz#582251679d3da86c32aae2c8e689c59f1196d8c4" - integrity sha512-C8Aui0ZpMd4KokxRdVAm2bQtI03k2RMRNzOB+IipV3yxFTSVICv7WoUr5L9ALB5BmKO1iHgZtWM8EvYG83otdg== - dependencies: - prop-types "^15.5.0" - -react-player@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.12.0.tgz#2fc05dbfec234c829292fbca563b544064bd14f0" - integrity sha512-rymLRz/2GJJD+Wc01S7S+i9pGMFYnNmQibR2gVE3KmHJCBNN8BhPAlOPTGZtn1uKpJ6p4RPLlzPQ1OLreXd8gw== - dependencies: - deepmerge "^4.0.0" - load-script "^1.0.0" - memoize-one "^5.1.1" - prop-types "^15.7.2" - react-fast-compare "^3.0.1" - -react-router-config@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" - integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== - dependencies: - "@babel/runtime" "^7.1.2" - -react-router-dom@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.3.4, react-router@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-textarea-autosize@~8.3.2: - version "8.3.4" - resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz#270a343de7ad350534141b02c9cb78903e553524" - integrity sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ== - dependencies: - "@babel/runtime" "^7.10.2" - use-composed-ref "^1.3.0" - use-latest "^1.2.1" - -react@18: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" - -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== - -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== - dependencies: - "@babel/runtime" "^7.8.4" - -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== - dependencies: - "@babel/regjsgen" "^0.8.0" - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -registry-auth-token@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" - integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== - dependencies: - "@pnpm/npm-conf" "^2.1.0" - -registry-url@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" - integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== - dependencies: - rc "1.2.8" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -rehype-raw@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" - integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== - dependencies: - "@types/hast" "^3.0.0" - hast-util-raw "^9.0.0" - vfile "^6.0.0" - -relateurl@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== - -remark-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/remark-directive/-/remark-directive-3.0.0.tgz#34452d951b37e6207d2e2a4f830dc33442923268" - integrity sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-directive "^3.0.0" - micromark-extension-directive "^3.0.0" - unified "^11.0.0" - -remark-emoji@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-4.0.1.tgz#671bfda668047689e26b2078c7356540da299f04" - integrity sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg== - dependencies: - "@types/mdast" "^4.0.2" - emoticon "^4.0.1" - mdast-util-find-and-replace "^3.0.1" - node-emoji "^2.1.0" - unified "^11.0.4" - -remark-frontmatter@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz#b68d61552a421ec412c76f4f66c344627dc187a2" - integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-frontmatter "^2.0.0" - micromark-extension-frontmatter "^2.0.0" - unified "^11.0.0" - -remark-gfm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" - integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-gfm "^3.0.0" - micromark-extension-gfm "^3.0.0" - remark-parse "^11.0.0" - remark-stringify "^11.0.0" - unified "^11.0.0" - -remark-mdx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.0.0.tgz#146905a3925b078970e05fc89b0e16b9cc3bfddd" - integrity sha512-O7yfjuC6ra3NHPbRVxfflafAj3LTwx3b73aBvkEFU5z4PsD6FD4vrqJAkE5iNGLz71GdjXfgRqm3SQ0h0VuE7g== - dependencies: - mdast-util-mdx "^3.0.0" - micromark-extension-mdxjs "^3.0.0" - -remark-parse@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" - integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-from-markdown "^2.0.0" - micromark-util-types "^2.0.0" - unified "^11.0.0" - -remark-rehype@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.0.0.tgz#7f21c08738bde024be5f16e4a8b13e5d7a04cf6b" - integrity sha512-vx8x2MDMcxuE4lBmQ46zYUDfcFMmvg80WYX+UNLeG6ixjdCCLcw1lrgAukwBTuOFsS78eoAedHGn9sNM0w7TPw== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - mdast-util-to-hast "^13.0.0" - unified "^11.0.0" - vfile "^6.0.0" - -remark-stringify@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" - integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-to-markdown "^2.0.0" - unified "^11.0.0" - -renderkid@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" - integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== - dependencies: - css-select "^4.1.3" - dom-converter "^0.2.0" - htmlparser2 "^6.1.0" - lodash "^4.17.21" - strip-ansi "^6.0.1" - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -"require-like@>= 0.1.1": - version "0.1.2" - resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" - integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -resolve-alpn@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve@^1.1.6, resolve@^1.14.2: - version "1.22.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== - dependencies: - is-core-module "^2.11.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" - integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== - dependencies: - lowercase-keys "^3.0.0" - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rtl-detect@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6" - integrity sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ== - -rtlcss@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-4.1.1.tgz#f20409fcc197e47d1925996372be196fee900c0c" - integrity sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - postcss "^8.4.21" - strip-json-comments "^3.1.1" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rxjs@^7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" - -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -schema-utils@^3.0.0, schema-utils@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" - integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.1.tgz#eb2d042df8b01f4b5c276a2dfd41ba0faab72e8d" - integrity sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -section-matter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" - integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== - dependencies: - extend-shallow "^2.0.1" - kind-of "^6.0.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" - integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== - dependencies: - node-forge "^1" - -semver-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" - integrity sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA== - dependencies: - semver "^7.3.5" - -semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.2, semver@^7.3.7, semver@^7.3.8: - version "7.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.5, semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== - dependencies: - randombytes "^2.1.0" - -serve-handler@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - fast-url-parser "1.1.3" - mime-types "2.1.18" - minimatch "3.1.2" - path-is-inside "1.0.2" - path-to-regexp "2.2.1" - range-parser "1.2.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -sirv@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446" - integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA== - dependencies: - "@polka/url" "^1.0.0-next.20" - mrmime "^1.0.0" - totalist "^3.0.0" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -sitemap@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - -skin-tone@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/skin-tone/-/skin-tone-2.0.0.tgz#4e3933ab45c0d4f4f781745d64b9f4c208e41237" - integrity sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA== - dependencies: - unicode-emoji-modifier-base "^1.0.0" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -sort-css-media-queries@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce" - integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== - -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.0: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -srcset@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4" - integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -std-env@^3.0.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.2.tgz#af27343b001616015534292178327b202b9ee955" - integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-entities@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" - integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - -strip-bom-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" - integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -style-to-object@^0.4.0: - version "0.4.4" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" - integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== - dependencies: - inline-style-parser "0.1.1" - -stylehacks@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" - integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== - dependencies: - browserslist "^4.21.4" - postcss-selector-parser "^6.0.4" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svg-parser@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" - integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== - -svgo@^2.7.0, svgo@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -terser-webpack-plugin@^5.3.7: - version "5.3.7" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" - integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.17" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.16.5" - -terser-webpack-plugin@^5.3.9: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== - dependencies: - "@jridgewell/trace-mapping" "^0.3.17" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.16.8" - -terser@^5.10.0, terser@^5.16.5: - version "5.17.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69" - integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -terser@^5.15.1, terser@^5.16.8: - version "5.24.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.24.0.tgz#4ae50302977bca4831ccc7b4fef63a3c04228364" - integrity sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -tiny-invariant@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -totalist@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" - integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - -trough@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" - integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== - -ts-node@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" - integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tslib@^2.0.3, tslib@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== - -tslib@^2.6.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -type-fest@^1.0.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - -type-fest@^2.13.0, type-fest@^2.5.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typescript@^5: - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== - -ua-parser-js@^0.7.30: - version "0.7.35" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" - integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-emoji-modifier-base@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" - integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unified@^11.0.0, unified@^11.0.3, unified@^11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" - integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== - dependencies: - "@types/unist" "^3.0.0" - bail "^2.0.0" - devlop "^1.0.0" - extend "^3.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^6.0.0" - -unique-string@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" - integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== - dependencies: - crypto-random-string "^4.0.0" - -unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position-from-estree@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz#d94da4df596529d1faa3de506202f0c9a23f2200" - integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" - integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-remove-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" - integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== - dependencies: - "@types/unist" "^3.0.0" - unist-util-visit "^5.0.0" - -unist-util-stringify-position@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" - integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - -unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.0.10: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -update-browserslist-db@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" - integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -update-notifier@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" - integrity sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og== - dependencies: - boxen "^7.0.0" - chalk "^5.0.1" - configstore "^6.0.0" - has-yarn "^3.0.0" - import-lazy "^4.0.0" - is-ci "^3.0.1" - is-installed-globally "^0.4.0" - is-npm "^6.0.0" - is-yarn-global "^0.4.0" - latest-version "^7.0.0" - pupa "^3.1.0" - semver "^7.3.7" - semver-diff "^4.0.0" - xdg-basedir "^5.1.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-loader@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" - integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== - dependencies: - loader-utils "^2.0.0" - mime-types "^2.1.27" - schema-utils "^3.0.0" - -use-composed-ref@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" - integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== - -use-isomorphic-layout-effect@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" - integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== - -use-latest@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2" - integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw== - dependencies: - use-isomorphic-layout-effect "^1.1.1" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== - -utility-types@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" - integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -vfile-location@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" - integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== - dependencies: - "@types/unist" "^3.0.0" - vfile "^6.0.0" - -vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - -vfile@^6.0.0, vfile@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" - integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - vfile-message "^4.0.0" - -wait-on@^7.0.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.2.0.tgz#d76b20ed3fc1e2bebc051fae5c1ff93be7892928" - integrity sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ== - dependencies: - axios "^1.6.1" - joi "^17.11.0" - lodash "^4.17.21" - minimist "^1.2.8" - rxjs "^7.8.1" - -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -web-namespaces@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" - integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webpack-bundle-analyzer@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" - integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== - dependencies: - "@discoveryjs/json-ext" "0.5.7" - acorn "^8.0.4" - acorn-walk "^8.0.0" - commander "^7.2.0" - debounce "^1.2.1" - escape-string-regexp "^4.0.0" - gzip-size "^6.0.0" - html-escaper "^2.0.2" - is-plain-object "^5.0.0" - opener "^1.5.2" - picocolors "^1.0.0" - sirv "^2.0.3" - ws "^7.3.1" - -webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== - dependencies: - colorette "^2.0.10" - memfs "^3.4.3" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@^4.15.1: - version "4.15.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" - integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== - dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.5" - ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" - graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^5.3.1" - ws "^8.13.0" - -webpack-merge@^5.9.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" - integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== - dependencies: - clone-deep "^4.0.1" - flat "^5.0.2" - wildcard "^2.0.0" - -webpack-sources@^3.2.2, webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack@^5.88.1: - version "5.89.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" - integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" - acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.15.0" - es-module-lexer "^1.2.1" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.2.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" - watchpack "^2.4.0" - webpack-sources "^3.2.3" - -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== - dependencies: - chalk "^4.1.0" - consola "^2.15.3" - pretty-time "^1.1.0" - std-env "^3.0.1" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -widest-line@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" - integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== - dependencies: - string-width "^5.0.1" - -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== - -wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@^7.3.1: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== - -ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - -xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" - integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== - -xml-js@^1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== - -zwitch@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== From 339bfa8242d02bf97bf8196693534f3a2c133ea0 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Thu, 23 May 2024 17:17:16 +0100 Subject: [PATCH 03/17] Add main binary and kafka logger --- cmd/benthos/main.go | 14 - cmd/redpanda-connect/main.go | 41 ++ cmd/serverless/benthos-lambda/main.go | 12 - cmd/tools/benthos_docs_gen/bloblang_test.go | 142 ------- cmd/tools/benthos_docs_gen/cue_test.go | 38 -- .../benthos_docs_gen/cue_test/expected.yml | 25 -- cmd/tools/benthos_docs_gen/cue_test/test.cue | 54 --- cmd/tools/benthos_docs_gen/main.go | 123 ------ cmd/tools/benthos_docs_gen/schema_test.go | 70 ---- internal/impl/kafka/topic_logger.go | 379 ++++++++++++++++++ 10 files changed, 420 insertions(+), 478 deletions(-) delete mode 100644 cmd/benthos/main.go create mode 100644 cmd/redpanda-connect/main.go delete mode 100644 cmd/serverless/benthos-lambda/main.go delete mode 100644 cmd/tools/benthos_docs_gen/bloblang_test.go delete mode 100644 cmd/tools/benthos_docs_gen/cue_test.go delete mode 100644 cmd/tools/benthos_docs_gen/cue_test/expected.yml delete mode 100644 cmd/tools/benthos_docs_gen/cue_test/test.cue delete mode 100644 cmd/tools/benthos_docs_gen/main.go delete mode 100644 cmd/tools/benthos_docs_gen/schema_test.go create mode 100644 internal/impl/kafka/topic_logger.go diff --git a/cmd/benthos/main.go b/cmd/benthos/main.go deleted file mode 100644 index 8f1e18b734..0000000000 --- a/cmd/benthos/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "context" - - "github.com/benthosdev/benthos/v4/public/service" - - // Import all plugins defined within the repo. - _ "github.com/benthosdev/benthos/v4/public/components/all" -) - -func main() { - service.RunCLI(context.Background()) -} diff --git a/cmd/redpanda-connect/main.go b/cmd/redpanda-connect/main.go new file mode 100644 index 0000000000..d30e81fc1e --- /dev/null +++ b/cmd/redpanda-connect/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "log/slog" + + "github.com/benthosdev/benthos/v4/public/service" + + "github.com/redpanda-data/connect/v4/internal/impl/kafka" + + _ "github.com/redpanda-data/connect/v4/public/components/all" +) + +var ( + Version string + DateBuilt string +) + +func redpandaTopLevelConfigField() *service.ConfigField { + return service.NewObjectField("redpanda", kafka.TopicLoggerFields()...) +} + +func main() { + rpLogger := kafka.NewTopicLogger() + + service.RunCLI( + context.Background(), + service.CLIOptSetVersion(Version, DateBuilt), + service.CLIOptSetMainSchemaFrom(func() *service.ConfigSchema { + return service.NewEnvironment().FullConfigSchema(Version, DateBuilt). + Field(redpandaTopLevelConfigField()) + }), + service.CLIOptOnLoggerInit(func(l *service.Logger) { + rpLogger.SetFallbackLogger(l) + }), + service.CLIOptAddTeeLogger(slog.New(rpLogger)), + service.CLIOptOnConfigParse(func(fn *service.ParsedConfig) error { + return rpLogger.InitOutputFromParsed(fn.Namespace("redpanda")) + }), + ) +} diff --git a/cmd/serverless/benthos-lambda/main.go b/cmd/serverless/benthos-lambda/main.go deleted file mode 100644 index e86fe8546d..0000000000 --- a/cmd/serverless/benthos-lambda/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "github.com/benthosdev/benthos/v4/internal/serverless/lambda" - - // Import all plugins defined within the repo. - _ "github.com/benthosdev/benthos/v4/public/components/all" -) - -func main() { - lambda.Run() -} diff --git a/cmd/tools/benthos_docs_gen/bloblang_test.go b/cmd/tools/benthos_docs_gen/bloblang_test.go deleted file mode 100644 index fb03f53f67..0000000000 --- a/cmd/tools/benthos_docs_gen/bloblang_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace/noop" - - "github.com/benthosdev/benthos/v4/internal/bloblang" - "github.com/benthosdev/benthos/v4/internal/bloblang/query" - "github.com/benthosdev/benthos/v4/internal/message" - "github.com/benthosdev/benthos/v4/internal/tracing" - - _ "github.com/benthosdev/benthos/v4/public/components/all" -) - -func TestFunctionExamples(t *testing.T) { - tmpJSONFile, err := os.CreateTemp("", "benthos_bloblang_functions_test") - require.NoError(t, err) - t.Cleanup(func() { - os.Remove(tmpJSONFile.Name()) - }) - - _, err = tmpJSONFile.WriteString(`{"foo":"bar"}`) - require.NoError(t, err) - - key := "BENTHOS_TEST_BLOBLANG_FILE" - t.Setenv(key, tmpJSONFile.Name()) - - for _, spec := range query.FunctionDocs() { - spec := spec - t.Run(spec.Name, func(t *testing.T) { - t.Parallel() - for i, e := range spec.Examples { - if e.SkipTesting { - continue - } - m, err := bloblang.GlobalEnvironment().NewMapping(e.Mapping) - require.NoError(t, err) - - for j, io := range e.Results { - msg := message.Batch{message.NewPart([]byte(io[0]))} - textMap := map[string]any{ - "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - } - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) - require.NoError(t, tracing.InitSpansFromParentTextMap(noop.NewTracerProvider(), "test", textMap, msg)) - - p, err := m.MapPart(0, msg) - exp := io[1] - if strings.HasPrefix(exp, "Error(") { - exp = exp[7 : len(exp)-2] - require.EqualError(t, err, exp, fmt.Sprintf("%v-%v", i, j)) - } else { - require.NoError(t, err) - assert.Equal(t, exp, string(p.AsBytes()), fmt.Sprintf("%v-%v", i, j)) - } - } - } - }) - } -} - -func TestMethodExamples(t *testing.T) { - tmpJSONFile, err := os.CreateTemp("", "benthos_bloblang_methods_test") - require.NoError(t, err) - t.Cleanup(func() { - os.Remove(tmpJSONFile.Name()) - }) - - _, err = tmpJSONFile.WriteString(` -{ - "type":"object", - "properties":{ - "foo":{ - "type":"string" - } - } -}`) - require.NoError(t, err) - - key := "BENTHOS_TEST_BLOBLANG_SCHEMA_FILE" - t.Setenv(key, tmpJSONFile.Name()) - - for _, spec := range query.MethodDocs() { - spec := spec - t.Run(spec.Name, func(t *testing.T) { - t.Parallel() - for i, e := range spec.Examples { - if e.SkipTesting { - continue - } - m, err := bloblang.GlobalEnvironment().NewMapping(e.Mapping) - require.NoError(t, err) - - for j, io := range e.Results { - msg := message.QuickBatch([][]byte{[]byte(io[0])}) - p, err := m.MapPart(0, msg) - exp := io[1] - if strings.HasPrefix(exp, "Error(") { - exp = exp[7 : len(exp)-2] - require.EqualError(t, err, exp, fmt.Sprintf("%v-%v", i, j)) - } else if exp == "" { - require.NoError(t, err) - require.Nil(t, p) - } else { - require.NoError(t, err) - assert.Equal(t, exp, string(p.AsBytes()), fmt.Sprintf("%v-%v", i, j)) - } - } - } - for _, target := range spec.Categories { - for i, e := range target.Examples { - if e.SkipTesting { - continue - } - m, err := bloblang.GlobalEnvironment().NewMapping(e.Mapping) - require.NoError(t, err) - - for j, io := range e.Results { - msg := message.QuickBatch([][]byte{[]byte(io[0])}) - p, err := m.MapPart(0, msg) - exp := io[1] - if strings.HasPrefix(exp, "Error(") { - exp = exp[7 : len(exp)-2] - require.EqualError(t, err, exp, fmt.Sprintf("%v-%v-%v", target.Category, i, j)) - } else { - require.NoError(t, err) - assert.Equal(t, exp, string(p.AsBytes()), fmt.Sprintf("%v-%v-%v", target.Category, i, j)) - } - } - } - } - }) - } -} diff --git a/cmd/tools/benthos_docs_gen/cue_test.go b/cmd/tools/benthos_docs_gen/cue_test.go deleted file mode 100644 index 12cb6946a6..0000000000 --- a/cmd/tools/benthos_docs_gen/cue_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "testing" - - "cuelang.org/go/cue" - "cuelang.org/go/cue/cuecontext" - "cuelang.org/go/encoding/yaml" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/config/schema" - "github.com/benthosdev/benthos/v4/internal/cuegen" - - _ "embed" -) - -//go:embed cue_test/test.cue -var inputCue string - -//go:embed cue_test/expected.yml -var expectedYAML string - -func TestCUEGenerate(t *testing.T) { - source, err := cuegen.GenerateSchema(schema.New("", "")) - require.NoError(t, err) - - ctx := cuecontext.New() - schemaV := ctx.CompileBytes(source) - require.NoError(t, schemaV.Validate()) - - v := ctx.CompileString(inputCue, cue.Scope(schemaV)) - require.NoError(t, v.Validate()) - - vBytes, err := yaml.Encode(v) - require.NoError(t, err) - assert.Equal(t, expectedYAML, string(vBytes)) -} diff --git a/cmd/tools/benthos_docs_gen/cue_test/expected.yml b/cmd/tools/benthos_docs_gen/cue_test/expected.yml deleted file mode 100644 index 3657e6a571..0000000000 --- a/cmd/tools/benthos_docs_gen/cue_test/expected.yml +++ /dev/null @@ -1,25 +0,0 @@ -testCases: - simple: - input: - label: sample_input - generate: - mapping: root = 'hello' - pipeline: - processors: - - label: sample_transform - mapping: root = this.uppercase() - - switch: - - check: count("total") == 1 - processors: - - mapping: meta first = true - - processors: - - mapping: meta first = false - output: - switch: - cases: - - check: errored() - output: - reject: 'failed to process message: ${! error() }' - - output: - label: sample_output - stdout: {} diff --git a/cmd/tools/benthos_docs_gen/cue_test/test.cue b/cmd/tools/benthos_docs_gen/cue_test/test.cue deleted file mode 100644 index f1f2771438..0000000000 --- a/cmd/tools/benthos_docs_gen/cue_test/test.cue +++ /dev/null @@ -1,54 +0,0 @@ -testCases: - simple: #Config & { - input: { - label: "sample_input" - generate: mapping: "root = 'hello'" - } - - pipeline: processors: [ - { - label: "sample_transform" - mapping: "root = this.uppercase()" - }, - { - switch: [ - { - check: "count(\"total\") == 1" - processors: [ - { - mapping: "meta first = true" - }, - ] - }, - { - processors: [ - { - mapping: "meta first = false" - }, - ] - }, - ] - }, - ] - - output: #Guarded & { - _output: { - label: "sample_output" - stdout: {} - } - } - } - -#Guarded: self = { - _output: #Output - - switch: cases: [ - { - check: "errored()" - output: reject: "failed to process message: ${! error() }" - }, - { - output: self._output - }, - ] -} diff --git a/cmd/tools/benthos_docs_gen/main.go b/cmd/tools/benthos_docs_gen/main.go deleted file mode 100644 index 384afedc23..0000000000 --- a/cmd/tools/benthos_docs_gen/main.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "fmt" - "os" - "path" - "path/filepath" - - "github.com/benthosdev/benthos/v4/internal/api" - "github.com/benthosdev/benthos/v4/internal/config/test" - "github.com/benthosdev/benthos/v4/internal/docs" - "github.com/benthosdev/benthos/v4/internal/log" - "github.com/benthosdev/benthos/v4/internal/template" - "github.com/benthosdev/benthos/v4/public/service" - - _ "github.com/benthosdev/benthos/v4/public/components/all" -) - -func create(t, path string, resBytes []byte) { - if existing, err := os.ReadFile(path); err == nil { - if bytes.Equal(existing, resBytes) { - return - } - } - if err := os.WriteFile(path, resBytes, 0o644); err != nil { - panic(err) - } - fmt.Printf("Documentation for '%v' has changed, updating: %v\n", t, path) -} - -func main() { - docsDir := "./docs/modules/components/pages" - flag.StringVar(&docsDir, "dir", docsDir, "The directory to write docs to") - flag.Parse() - - service.GlobalEnvironment().WalkInputs(viewForDir(path.Join(docsDir, "./inputs"))) - service.GlobalEnvironment().WalkBuffers(viewForDir(path.Join(docsDir, "./buffers"))) - service.GlobalEnvironment().WalkCaches(viewForDir(path.Join(docsDir, "./caches"))) - service.GlobalEnvironment().WalkMetrics(viewForDir(path.Join(docsDir, "./metrics"))) - service.GlobalEnvironment().WalkOutputs(viewForDir(path.Join(docsDir, "./outputs"))) - service.GlobalEnvironment().WalkProcessors(viewForDir(path.Join(docsDir, "./processors"))) - service.GlobalEnvironment().WalkRateLimits(viewForDir(path.Join(docsDir, "./rate_limits"))) - service.GlobalEnvironment().WalkTracers(viewForDir(path.Join(docsDir, "./tracers"))) - service.GlobalEnvironment().WalkScanners(viewForDir(path.Join(docsDir, "./scanners"))) - - // Bloblang stuff - doBloblang(docsDir) - - // Unit test docs - doTestDocs(docsDir) - - // HTTP docs - doHTTP(docsDir) - - // Logger docs - doLogger(docsDir) - - // Template docs - doTemplates(docsDir) -} - -func viewForDir(docsDir string) func(name string, config *service.ConfigView) { - return func(name string, config *service.ConfigView) { - adocSpec, err := config.RenderDocs() - if err != nil { - panic(fmt.Sprintf("Failed to generate docs for '%v': %v", name, err)) - } - create(name, path.Join(docsDir, name+".adoc"), adocSpec) - } -} - -func doBloblang(dir string) { - adocSpec, err := docs.BloblangFunctionsMarkdown() - if err != nil { - panic(fmt.Sprintf("Failed to generate docs for bloblang functions: %v", err)) - } - - create("bloblang functions", filepath.Join(dir, "../..", "guides", "pages", "bloblang", "functions.adoc"), adocSpec) - - if adocSpec, err = docs.BloblangMethodsMarkdown(); err != nil { - panic(fmt.Sprintf("Failed to generate docs for bloblang methods: %v", err)) - } - - create("bloblang methods", filepath.Join(dir, "../..", "guides", "pages", "bloblang", "methods.adoc"), adocSpec) -} - -func doTestDocs(dir string) { - adocSpec, err := test.DocsMarkdown() - if err != nil { - panic(fmt.Sprintf("Failed to generate docs for unit tests: %v", err)) - } - - create("test docs", filepath.Join(dir, "../..", "configuration", "pages", "unit_testing.adoc"), adocSpec) -} - -func doHTTP(dir string) { - adocSpec, err := api.DocsMarkdown() - if err != nil { - panic(fmt.Sprintf("Failed to generate docs for http: %v", err)) - } - - create("http docs", filepath.Join(dir, "http", "about.adoc"), adocSpec) -} - -func doLogger(dir string) { - adocSpec, err := log.DocsMarkdown() - if err != nil { - panic(fmt.Sprintf("Failed to generate docs for logger: %v", err)) - } - - create("logger docs", filepath.Join(dir, "logger", "about.adoc"), adocSpec) -} - -func doTemplates(dir string) { - adocSpec, err := template.DocsMarkdown() - if err != nil { - panic(fmt.Sprintf("Failed to generate docs for templates: %v", err)) - } - - create("template docs", filepath.Join(dir, "../..", "configuration", "pages", "templating.adoc"), adocSpec) -} diff --git a/cmd/tools/benthos_docs_gen/schema_test.go b/cmd/tools/benthos_docs_gen/schema_test.go deleted file mode 100644 index f7e3257209..0000000000 --- a/cmd/tools/benthos_docs_gen/schema_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/benthosdev/benthos/v4/internal/bundle" - "github.com/benthosdev/benthos/v4/internal/config" - "github.com/benthosdev/benthos/v4/internal/docs" - - _ "github.com/benthosdev/benthos/v4/public/components/all" -) - -func TestComponentExamples(t *testing.T) { - confSpec := config.Spec() - testComponent := func(componentType, typeName, title, conf string, deprecated bool) { - node, err := docs.UnmarshalYAML([]byte(conf)) - require.NoError(t, err, "%v:%v:%v", componentType, typeName, title) - - pConf, err := confSpec.ParsedConfigFromAny(node) - require.NoError(t, err, "%v:%v:%v", componentType, typeName, title) - - _, err = config.FromParsed(bundle.GlobalEnvironment, pConf, nil) - require.NoError(t, err, "%v:%v:%v", componentType, typeName, title) - - lConf := docs.NewLintConfig(bundle.GlobalEnvironment) - lConf.RejectDeprecated = !deprecated - lints := confSpec.LintYAML(docs.NewLintContext(lConf), node) - for _, lint := range lints { - t.Errorf("%v %v:%v:%v", lint, componentType, typeName, title) - } - } - - for _, spec := range bundle.AllInputs.Docs() { - for _, example := range spec.Examples { - testComponent("input", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } - for _, spec := range bundle.AllBuffers.Docs() { - for _, example := range spec.Examples { - testComponent("buffer", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } - for _, spec := range bundle.AllProcessors.Docs() { - for _, example := range spec.Examples { - testComponent("processor", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } - for _, spec := range bundle.AllOutputs.Docs() { - for _, example := range spec.Examples { - testComponent("output", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } - for _, spec := range bundle.AllCaches.Docs() { - for _, example := range spec.Examples { - testComponent("cache", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } - for _, spec := range bundle.AllRateLimits.Docs() { - for _, example := range spec.Examples { - testComponent("ratelimit", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } - for _, spec := range bundle.AllScanners.Docs() { - for _, example := range spec.Examples { - testComponent("scanner", spec.Name, example.Title, example.Config, spec.Status == docs.StatusDeprecated) - } - } -} diff --git a/internal/impl/kafka/topic_logger.go b/internal/impl/kafka/topic_logger.go new file mode 100644 index 0000000000..f3688269d5 --- /dev/null +++ b/internal/impl/kafka/topic_logger.go @@ -0,0 +1,379 @@ +package kafka + +import ( + "context" + "crypto/tls" + "fmt" + "log/slog" + "math" + "strings" + "sync/atomic" + "time" + + "github.com/dustin/go-humanize" + "github.com/twmb/franz-go/pkg/kgo" + "github.com/twmb/franz-go/pkg/sasl" + + "github.com/benthosdev/benthos/v4/public/service" +) + +func TopicLoggerFields() []*service.ConfigField { + return []*service.ConfigField{ + service.NewStringListField("seed_brokers"). + Description("A list of broker addresses to connect to in order to establish connections. If an item of the list contains commas it will be expanded into multiple addresses."). + Optional(). + Example([]string{"localhost:9092"}). + Example([]string{"foo:9092", "bar:9092"}). + Example([]string{"foo:9092,bar:9092"}), + service.NewStringField("logs_topic"). + Default("__redpanda.connect.logs"), + service.NewStringEnumField("logs_level", "debug", "info", "warn", "error"). + Default("info"), + service.NewStringField("client_id"). + Description("An identifier for the client connection."). + Default("benthos"). + Advanced(), + service.NewStringField("rack_id"). + Description("A rack identifier for this client."). + Default(""). + Advanced(), + service.NewDurationField("timeout"). + Description("The maximum period of time to wait for message sends before abandoning the request and retrying"). + Default("10s"). + Advanced(), + service.NewStringField("max_message_bytes"). + Description("The maximum space in bytes than an individual message may take, messages larger than this value will be rejected. This field corresponds to Kafka's `max.message.bytes`."). + Advanced(). + Default("1MB"). + Example("100MB"). + Example("50mib"), + service.NewTLSToggledField("tls"), + saslField(), + } +} + +// TopicLogger provides a mechanism for sending service-wide logs into a kafka +// topic. The writing is done by a regular output, but this type is necessary in +// order to allow hot swapping of log components during start up. +type TopicLogger struct { + fallbackLogger *atomic.Pointer[service.Logger] + o *atomic.Pointer[service.OwnedOutput] + level *atomic.Pointer[slog.Level] + attrs []slog.Attr +} + +func NewTopicLogger() *TopicLogger { + return &TopicLogger{ + fallbackLogger: &atomic.Pointer[service.Logger]{}, + o: &atomic.Pointer[service.OwnedOutput]{}, + level: &atomic.Pointer[slog.Level]{}, + } +} + +func (l *TopicLogger) SetFallbackLogger(fLogger *service.Logger) { + l.fallbackLogger.Store(fLogger) +} + +func (l *TopicLogger) InitOutputFromParsed(pConf *service.ParsedConfig) error { + w, err := newTopicLoggerWriterFromConfig(pConf, l.fallbackLogger.Load()) + if err != nil { + return err + } + if w == nil { + return nil + } + + lvlStr, err := pConf.FieldString("logs_level") + if err != nil { + return err + } + + var lvl slog.Level + switch strings.ToLower(lvlStr) { + case "debug": + lvl = slog.LevelDebug + case "info": + lvl = slog.LevelInfo + case "warn": + lvl = slog.LevelWarn + case "error": + lvl = slog.LevelError + default: + return fmt.Errorf("log level not recognized: %v", lvlStr) + } + l.level.Store(&lvl) + + res := service.MockResources(service.MockResourcesOptUseLogger(l.fallbackLogger.Load())) + tmpO, err := res.ManagedBatchOutput("redpanda_logger", 24, w) + if err != nil { + return err + } + + batchPol, err := (service.BatchPolicy{ + Count: 50, + Period: "1s", + }).NewBatcher(service.MockResources()) + if err != nil { + return err + } + + tmpO = tmpO.BatchedWith(batchPol) + if err := tmpO.PrimeBuffered(100); err == nil { + l.o.Store(tmpO) + } else { + l.fallbackLogger.Load().With("error", err.Error()).Warn("failed to initialise topic logs writer") + } + return nil +} + +func (l *TopicLogger) Enabled(ctx context.Context, atLevel slog.Level) bool { + lvl := l.level.Load() + if lvl == nil { + return true + } + return atLevel >= *lvl +} + +func (l *TopicLogger) Handle(ctx context.Context, r slog.Record) error { + tmpO := l.o.Load() + if tmpO == nil { + return nil + } + + lvl := l.level.Load() + if lvl == nil || r.Level < *lvl { + return nil + } + + msg := service.NewMessage(nil) + + v := map[string]any{ + "message": r.Message, + "level": r.Level.String(), + "time": r.Time.Format(time.RFC3339Nano), + } + for _, a := range l.attrs { + v[a.Key] = a.Value.String() + } + r.Attrs(func(a slog.Attr) bool { + v[a.Key] = a.Value.String() + return true + }) + msg.SetStructured(v) + _ = tmpO.WriteBatchNonBlocking(service.MessageBatch{msg}, func(ctx context.Context, err error) error { + return nil // TODO: Log nacks + }) // TODO: Log errors (occasionally) + return nil +} + +func (l *TopicLogger) WithAttrs(attrs []slog.Attr) slog.Handler { + newL := *l + newAttributes := make([]slog.Attr, 0, len(attrs)+len(l.attrs)) + newAttributes = append(newAttributes, l.attrs...) + newAttributes = append(newAttributes, attrs...) + newL.attrs = newAttributes + return &newL +} + +func (l *TopicLogger) WithGroup(name string) slog.Handler { + return l // TODO +} + +//------------------------------------------------------------------------------ + +type franzTopicLoggerWriter struct { + topic string + + seedBrokers []string + clientID string + rackID string + tlsConf *tls.Config + saslConfs []sasl.Mechanism + partitioner kgo.Partitioner + timeout time.Duration + produceMaxBytes int32 + compressionPrefs []kgo.CompressionCodec + + client *kgo.Client + + log *service.Logger +} + +func newTopicLoggerWriterFromConfig(conf *service.ParsedConfig, log *service.Logger) (*franzTopicLoggerWriter, error) { + f := franzTopicLoggerWriter{ + log: log, + } + + if !conf.Contains("seed_brokers") { + return nil, nil + } + + brokerList, err := conf.FieldStringList("seed_brokers") + if err != nil { + return nil, err + } + for _, b := range brokerList { + f.seedBrokers = append(f.seedBrokers, strings.Split(b, ",")...) + } + if len(brokerList) == 0 { + return nil, nil + } + + if f.topic, err = conf.FieldString("logs_topic"); err != nil { + return nil, err + } + if f.topic == "" { + return nil, nil + } + + if f.timeout, err = conf.FieldDuration("timeout"); err != nil { + return nil, err + } + + maxBytesStr, err := conf.FieldString("max_message_bytes") + if err != nil { + return nil, err + } + maxBytes, err := humanize.ParseBytes(maxBytesStr) + if err != nil { + return nil, fmt.Errorf("failed to parse max_message_bytes: %w", err) + } + if maxBytes > uint64(math.MaxInt32) { + return nil, fmt.Errorf("invalid max_message_bytes, must not exceed %v", math.MaxInt32) + } + f.produceMaxBytes = int32(maxBytes) + + if conf.Contains("compression") { + cStr, err := conf.FieldString("compression") + if err != nil { + return nil, err + } + + var c kgo.CompressionCodec + switch cStr { + case "lz4": + c = kgo.Lz4Compression() + case "gzip": + c = kgo.GzipCompression() + case "snappy": + c = kgo.SnappyCompression() + case "zstd": + c = kgo.ZstdCompression() + case "none": + c = kgo.NoCompression() + default: + return nil, fmt.Errorf("compression codec %v not recognised", cStr) + } + f.compressionPrefs = append(f.compressionPrefs, c) + } + + f.partitioner = kgo.StickyKeyPartitioner(nil) + if conf.Contains("partitioner") { + partStr, err := conf.FieldString("partitioner") + if err != nil { + return nil, err + } + switch partStr { + case "murmur2_hash": + f.partitioner = kgo.StickyKeyPartitioner(nil) + case "round_robin": + f.partitioner = kgo.RoundRobinPartitioner() + case "least_backup": + f.partitioner = kgo.LeastBackupPartitioner() + case "manual": + f.partitioner = kgo.ManualPartitioner() + default: + return nil, fmt.Errorf("unknown partitioner: %v", partStr) + } + } + + if f.clientID, err = conf.FieldString("client_id"); err != nil { + return nil, err + } + + if f.rackID, err = conf.FieldString("rack_id"); err != nil { + return nil, err + } + + tlsConf, tlsEnabled, err := conf.FieldTLSToggled("tls") + if err != nil { + return nil, err + } + if tlsEnabled { + f.tlsConf = tlsConf + } + if f.saslConfs, err = saslMechanismsFromConfig(conf); err != nil { + return nil, err + } + + return &f, nil +} + +//------------------------------------------------------------------------------ + +func (f *franzTopicLoggerWriter) Connect(ctx context.Context) error { + if f.client != nil { + return nil + } + + clientOpts := []kgo.Opt{ + kgo.SeedBrokers(f.seedBrokers...), + kgo.SASL(f.saslConfs...), + kgo.AllowAutoTopicCreation(), // TODO: Configure this + kgo.ProducerBatchMaxBytes(f.produceMaxBytes), + kgo.ProduceRequestTimeout(f.timeout), + kgo.ClientID(f.clientID), + kgo.Rack(f.rackID), + kgo.WithLogger(&kgoLogger{f.log}), + } + if f.tlsConf != nil { + clientOpts = append(clientOpts, kgo.DialTLSConfig(f.tlsConf)) + } + if f.partitioner != nil { + clientOpts = append(clientOpts, kgo.RecordPartitioner(f.partitioner)) + } + if len(f.compressionPrefs) > 0 { + clientOpts = append(clientOpts, kgo.ProducerBatchCompression(f.compressionPrefs...)) + } + + cl, err := kgo.NewClient(clientOpts...) + if err != nil { + return err + } + + f.client = cl + return nil +} + +func (f *franzTopicLoggerWriter) WriteBatch(ctx context.Context, b service.MessageBatch) (err error) { + if f.client == nil { + return service.ErrNotConnected + } + + records := make([]*kgo.Record, 0, len(b)) + for _, msg := range b { + record := &kgo.Record{Topic: f.topic} + if record.Value, err = msg.AsBytes(); err != nil { + return + } + records = append(records, record) + } + + // TODO: This is very cool and allows us to easily return granular errors, + // so we should honor travis by doing it. + err = f.client.ProduceSync(ctx, records...).FirstErr() + return +} + +func (f *franzTopicLoggerWriter) disconnect() { + if f.client == nil { + return + } + f.client.Close() + f.client = nil +} + +func (f *franzTopicLoggerWriter) Close(ctx context.Context) error { + f.disconnect() + return nil +} From a6cafcf248c368b1a250f2c7ccb377f6e622d75d Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Fri, 24 May 2024 11:47:12 +0100 Subject: [PATCH 04/17] Renames and rework gh actions --- .dockerignore | 2 +- .github/CODEOWNERS | 1 - .github/workflows/cross_build.yml | 3 +- .github/workflows/docker_edge.yml | 59 ------------ .github/workflows/integration_test.yml | 28 ------ .github/workflows/release.yml | 8 +- .github/workflows/test.yml | 11 +-- .golangci.yml | 4 - .goreleaser.yml | 62 ++++++------- CONTRIBUTING.md | 54 ----------- LICENSE | 19 ---- SECURITY.md | 15 ++-- config/README.md | 4 +- resources/docker/Dockerfile | 24 ++--- resources/docker/Dockerfile.cgo | 20 ++--- resources/docker/README.md | 6 +- resources/docker/profiling/README.md | 2 +- resources/docker/schema_registry/README.md | 6 +- .../schema_registry/docker-compose.yaml | 18 ++-- resources/docker/streams_mode/benthos.yaml | 6 -- .../docker/streams_mode/docker-compose.yaml | 8 -- .../docker/streams_mode/streams/bars.yaml | 9 -- .../docker/streams_mode/streams/foos.yaml | 9 -- resources/docker/tracing/a.yaml | 33 ------- resources/docker/tracing/b.yaml | 27 ------ resources/docker/tracing/c.yaml | 25 ------ resources/docker/tracing/docker-compose.yaml | 19 ---- resources/k8s/nats-and-studio/README.md | 90 ------------------- .../nats-and-studio/benthos-generator.yaml | 47 ---------- .../k8s/nats-and-studio/benthos-server.yaml | 63 ------------- .../nats-and-studio/benthos-studio-node.yaml | 26 ------ resources/k8s/nats-and-studio/nats.yaml | 31 ------- resources/scripts/field_alignment.sh | 8 -- resources/scripts/package_weight.sh | 4 - resources/scripts/release_notes.sh | 6 +- .../lambda/benthos-lambda-al2-sam.yaml | 30 ------- .../serverless/lambda/benthos-lambda-al2.tf | 16 ---- .../serverless/lambda/benthos-lambda-sam.yaml | 29 ------ resources/serverless/lambda/benthos-lambda.tf | 15 ---- 39 files changed, 90 insertions(+), 757 deletions(-) delete mode 100644 .github/CODEOWNERS delete mode 100644 .github/workflows/docker_edge.yml delete mode 100644 .github/workflows/integration_test.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 resources/docker/streams_mode/benthos.yaml delete mode 100644 resources/docker/streams_mode/docker-compose.yaml delete mode 100644 resources/docker/streams_mode/streams/bars.yaml delete mode 100644 resources/docker/streams_mode/streams/foos.yaml delete mode 100644 resources/docker/tracing/a.yaml delete mode 100644 resources/docker/tracing/b.yaml delete mode 100644 resources/docker/tracing/c.yaml delete mode 100644 resources/docker/tracing/docker-compose.yaml delete mode 100644 resources/k8s/nats-and-studio/README.md delete mode 100644 resources/k8s/nats-and-studio/benthos-generator.yaml delete mode 100644 resources/k8s/nats-and-studio/benthos-server.yaml delete mode 100644 resources/k8s/nats-and-studio/benthos-studio-node.yaml delete mode 100644 resources/k8s/nats-and-studio/nats.yaml delete mode 100755 resources/scripts/field_alignment.sh delete mode 100755 resources/scripts/package_weight.sh delete mode 100644 resources/serverless/lambda/benthos-lambda-al2-sam.yaml delete mode 100644 resources/serverless/lambda/benthos-lambda-al2.tf delete mode 100644 resources/serverless/lambda/benthos-lambda-sam.yaml delete mode 100644 resources/serverless/lambda/benthos-lambda.tf diff --git a/.dockerignore b/.dockerignore index c344dcd1dc..85c5d783ac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,5 +2,5 @@ resources icon.png LICENSE README.md -target/bin/benthos +target/bin target/dist diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 1ace0eecca..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @jeffail \ No newline at end of file diff --git a/.github/workflows/cross_build.yml b/.github/workflows/cross_build.yml index 24d475e7b1..e5cd59f5c2 100644 --- a/.github/workflows/cross_build.yml +++ b/.github/workflows/cross_build.yml @@ -7,11 +7,10 @@ on: jobs: cross-build: - if: ${{ github.repository == 'benthosdev/benthos' }} strategy: fail-fast: false matrix: - go-version: [1.20.x, 1.21.x] + go-version: [1.21.x, 1.22.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} env: diff --git a/.github/workflows/docker_edge.yml b/.github/workflows/docker_edge.yml deleted file mode 100644 index f39a9ab970..0000000000 --- a/.github/workflows/docker_edge.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Docker Edge - -on: - workflow_dispatch: {} - schedule: - - cron: '0 2 * * *' # run at 2 AM UTC - -jobs: - build: - if: ${{ github.repository == 'benthosdev/benthos' || github.event_name != 'schedule' }} - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - - name: Check Out Repo - uses: actions/checkout@v4 - - - name: Free up some disk space on ubuntu - if: ${{ runner.os == 'Linux' }} - run: | - # Workaround to provide additional free space for testing. - # https://github.com/actions/virtual-environments/issues/2840 - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Log in to registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin - - - name: Install Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: ./ - file: ./resources/docker/Dockerfile - builder: ${{ steps.buildx.outputs.name }} - platforms: linux/amd64,linux/arm64 - push: true - tags: jeffail/benthos:edge,ghcr.io/${{ github.repository_owner }}/benthos:edge - - - name: Build and push CGO - uses: docker/build-push-action@v5 - with: - context: ./ - file: ./resources/docker/Dockerfile.cgo - push: true - tags: jeffail/benthos:edge-cgo,ghcr.io/${{ github.repository_owner }}/benthos:edge-cgo diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml deleted file mode 100644 index 3eced7a075..0000000000 --- a/.github/workflows/integration_test.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Integration Test - -on: - schedule: - - cron: '0 1 * * *' # run at 1 AM UTC - -jobs: - integration-test: - if: ${{ github.repository == 'benthosdev/benthos' || github.event_name != 'schedule' }} - runs-on: ubuntu-latest - env: - CGO_ENABLED: 0 - steps: - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: 1.21.x - check-latest: true - - - name: Deps - run: make deps && git diff-index --quiet HEAD || { >&2 echo "Stale go.{mod,sum} detected. This can be fixed with 'make deps'."; exit 1; } - - - name: Integration Test - run: go test -run "^Test.*Integration$" -timeout 60m ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9041c4699d..53a4f62e9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.21.x + go-version: 1.22.x check-latest: true - name: Release Notes @@ -78,8 +78,7 @@ jobs: uses: docker/metadata-action@v5 with: images: | - jeffail/benthos - ghcr.io/${{ github.repository_owner }}/benthos + ghcr.io/${{ github.repository_owner }}/connect flavor: | latest=auto suffix=-cgo @@ -101,8 +100,7 @@ jobs: uses: docker/metadata-action@v5 with: images: | - jeffail/benthos - ghcr.io/${{ github.repository_owner }}/benthos + ghcr.io/${{ github.repository_owner }}/connect tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37135bcac9..fae98ca072 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ on: jobs: test: - if: ${{ github.repository == 'benthosdev/benthos' || github.event_name != 'schedule' }} + if: ${{ github.repository == 'redpanda-data/connect' || github.event_name != 'schedule' }} runs-on: ubuntu-latest env: CGO_ENABLED: 0 @@ -22,20 +22,21 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.21.x + go-version: 1.22.x check-latest: true - name: Deps run: make deps && git diff-index --quiet HEAD || { >&2 echo "Stale go.{mod,sum} detected. This can be fixed with 'make deps'."; exit 1; } - - name: Docs - run: make docs && git diff-index --quiet HEAD || { >&2 echo "Stale docs detected. This can be fixed with 'make docs'."; exit 1; } + # TODO + # - name: Docs + # run: make docs && git diff-index --quiet HEAD || { >&2 echo "Stale docs detected. This can be fixed with 'make docs'."; exit 1; } - name: Test run: make test golangci-lint: - if: ${{ github.repository == 'benthosdev/benthos' || github.event_name != 'schedule' }} + if: ${{ github.repository == 'redpanda-data/connect' || github.event_name != 'schedule' }} runs-on: ubuntu-latest env: CGO_ENABLED: 0 diff --git a/.golangci.yml b/.golangci.yml index 9a596084e0..80ff1c7006 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,10 +18,6 @@ linters-settings: enable-all-rules: false rules: - name: superfluous-else - errcheck: - exclude-functions: - - (*github.com/benthosdev/benthos/v4/internal/batch.Error).Failed - - (*github.com/benthosdev/benthos/v4/public/service.BatchError).Failed govet: enable-all: true disable: diff --git a/.goreleaser.yml b/.goreleaser.yml index 2ff638b3bf..9dc55f24dc 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,7 +1,7 @@ builds: - - id: benthos - main: cmd/benthos/main.go - binary: benthos + - id: connect + main: cmd/redpanda-connect/main.go + binary: redpanda-connect goos: [ windows, darwin, linux, freebsd, openbsd ] goarch: [ amd64, arm, arm64 ] goarm: [ 6, 7 ] @@ -14,42 +14,42 @@ builds: - CGO_ENABLED=0 ldflags: > -s -w - -X github.com/benthosdev/benthos/v4/internal/cli.Version={{.Version}} - -X github.com/benthosdev/benthos/v4/internal/cli.DateBuilt={{.Date}} - - id: benthos-lambda - main: cmd/serverless/benthos-lambda/main.go - binary: benthos-lambda - env: - - CGO_ENABLED=0 - goos: [ linux ] - goarch: [ amd64 ] - - id: benthos-lambda-al2 - main: cmd/serverless/benthos-lambda/main.go - binary: bootstrap - env: - - CGO_ENABLED=0 - goos: [ linux ] - goarch: [ amd64, arm64 ] + -X main.Version={{.Version}} + -X main.DateBuilt={{.Date}} + # - id: connect-lambda + # main: cmd/serverless/connect-lambda/main.go + # binary: redpanda-connect-lambda + # env: + # - CGO_ENABLED=0 + # goos: [ linux ] + # goarch: [ amd64 ] + # - id: connect-lambda-al2 + # main: cmd/serverless/connect-lambda/main.go + # binary: bootstrap + # env: + # - CGO_ENABLED=0 + # goos: [ linux ] + # goarch: [ amd64, arm64 ] archives: - - id: benthos - builds: [ benthos ] + - id: connect + builds: [ connect ] format: tar.gz files: - README.md - CHANGELOG.md - LICENSE - - id: benthos-lambda - builds: [ benthos-lambda ] - format: zip - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" - - id: benthos-lambda-al2 - builds: [ benthos-lambda-al2 ] - format: zip - name_template: "benthos-lambda-al2_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + # - id: connect-lambda + # builds: [ connect-lambda ] + # format: zip + # name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + # - id: connect-lambda-al2 + # builds: [ connect-lambda-al2 ] + # format: zip + # name_template: "connect-lambda-al2_{{ .Version }}_{{ .Os }}_{{ .Arch }}" dist: target/dist release: github: - owner: benthosdev - name: benthos + owner: redpanda-data + name: connect prerelease: auto disable: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 700196e0ba..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,54 +0,0 @@ -Contributing to Benthos -======================= - -Joining Club Blob by contributing to the Benthos project is a selfless, boring and occasionally painful act. As such any contributors to this project will be treated with the respect and compassion that they deserve. - -Please be dull, please be respecting of others and their efforts, please do not take criticism or rejection of your ideas personally. - -## Reporting Bugs - -If you find a bug then please let the project know by opening an issue after doing the following: - -- Do a quick search of the existing issues to make sure the bug isn't already reported -- Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing -- Collect as much information as you can to help identify what the issue is (project version, configuration files, etc) - -## Suggesting Enhancements - -Having even the most casual interest in Benthos gives you honorary membership of Club Blob, entitling you to give a reserved (and hypothetical) tickle of the projects' toes in order to steer it in the direction of your whim. - -Please don't abuse this entitlement, the poor blobfish can only gobble so many features before it starts to droop beyond repair. Enhancements should roughly follow the general goals of Benthos and be: - -- Common use cases -- Simple to understand -- Simple to monitor - -You can help us out by doing the following before raising a new issue: - -- Check that the feature hasn't been requested already by searching existing issues -- Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea -- Explain your own use cases as the basis of the request - -## Adding Features - -Pull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating an issue. This allows us to discuss the changes and make sure they are a good fit for the project. - -Please always make sure a pull request has been: - -- Unit tested with `make test` -- Linted with `make lint` -- Formatted with `make fmt` - -If your change impacts inputs, outputs or other connectors then try to test them with `make test-integration`. If the integration tests aren't working on your machine then don't panic, just mention it in your PR. - -If your change has an impact on documentation then make sure it is generated with `make docs`. You can test out the documentation site locally by running `yarn && yarn start` in the `./website` directory. - -### Adding New Components - -The APIs for adding new components (inputs, outputs, processors, caches, etc) has recently been simplified. If you are planning to create a new component you should use the latest implementations within `./internal/impl` as inspiration. - -### Plugins - -The core components within Benthos (inputs, processors, conditions and outputs) are all easily pluggable. If you are interested in adding new components please raise a ticket and we can discuss whether it's a good fit for the project. - -If not then it's still easy to build your own version of Benthos with custom components. For guidance take a look at [this example repo](https://github.com/benthosdev/benthos-plugin-example). diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 008fb0cf67..0000000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2020 Ashley Jeffs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/SECURITY.md b/SECURITY.md index a598f3d835..c09a12348e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,11 @@ # Security Policy -## Supported Versions - -The current major version of Benthos continually receives security updates. We recommend that users maintain a reasonable upgrade plan that follows current releases of Benthos. - -| Version | Supported | -| ------- | ------------------ | -| 3.x | :white_check_mark: | -| < 3.0 | :x: | +Official Redpanda Security Policy can be found on [redpanda.com/security](https://redpanda.com/security) ## Reporting a Vulnerability -If you have found or suspect to have found a vulnerability in Benthos then please proceed to report it to security@benthos.dev. +As with any complex system, it is certain that bugs will be found, some of them security-relevant. If you find a security bug please report it privately via email to [security@redpanda.com](mailto:security@redpanda.com). We will fix the issue as soon as possible and coordinate a release date with you. You will be able to choose if you want public acknowledgement of your effort and if you want to be mentioned by name. + +## Public Disclosure Timing -Please note, we do not operate a bug bounty program. However, we deeply appreciate any contributions that guide us towards building a better and safer project. +The public disclosure date is agreed between the Redpanda Team and the bug submitter. We prefer to fully disclose the bug as soon as possible, but only after a mitigation or fix is available. We will ask for delay if the bug or the fix is not yet fully understood or the solution is not tested to our standards yet. While there is no fixed time frame for fix & disclosure, we will try our best to be quick and do not expect to need the usual 90 days most companies ask or. For a vulnerability with a straightforward mitigation, we expect report date to disclosure date to be on the order of 7 days. diff --git a/config/README.md b/config/README.md index 29b0e1d2e3..ec42baf1ff 100644 --- a/config/README.md +++ b/config/README.md @@ -3,10 +3,10 @@ Config This directory shows some config examples. Some are real world applications, some are examples of [config unit tests][unit-tests]. -If you're looking for specific config examples for a use case you have then try generating one with the `benthos create` subcommand. For example, to create a config that reads Kafka messages, decodes them with a schema registry service, and writes them to NATS JetStream you could use the following command: +If you're looking for specific config examples for a use case you have then try generating one with the `redpanda-connect create` subcommand. For example, to create a config that reads Kafka messages, decodes them with a schema registry service, and writes them to NATS JetStream you could use the following command: ```sh benthos create kafka/schema_registry_decode/nats_jetstream > example.yaml ``` -[unit-tests]: https://www.benthos.dev/docs/configuration/unit_testing +[unit-tests]: https://www.docs.redpanda.com/redpanda-connect/docs/configuration/unit_testing diff --git a/resources/docker/Dockerfile b/resources/docker/Dockerfile index f0fde55f1e..8b449dfaa0 100644 --- a/resources/docker/Dockerfile +++ b/resources/docker/Dockerfile @@ -1,16 +1,16 @@ -FROM golang:1.21 AS build +FROM golang:1.22 AS build ENV CGO_ENABLED=0 ENV GOOS=linux -RUN useradd -u 10001 benthos +RUN useradd -u 10001 connect -WORKDIR /go/src/github.com/benthosdev/benthos/ +WORKDIR /go/src/github.com/redpanda-data/connect/ # Update dependencies: On unchanged dependencies, cached layer will be reused -COPY go.* /go/src/github.com/benthosdev/benthos/ +COPY go.* /go/src/github.com/redpanda-data/connect/ RUN go mod download # Build -COPY . /go/src/github.com/benthosdev/benthos/ +COPY . /go/src/github.com/redpanda-data/connect/ # Tag timetzdata required for busybox base image: # https://github.com/benthosdev/benthos/issues/897 RUN make TAGS="timetzdata" @@ -18,20 +18,20 @@ RUN make TAGS="timetzdata" # Pack FROM busybox AS package -LABEL maintainer="Ashley Jeffs " -LABEL org.opencontainers.image.source="https://github.com/benthosdev/benthos" +LABEL maintainer="Ashley Jeffs " +LABEL org.opencontainers.image.source="https://github.com/redpanda-data/connect" WORKDIR / COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /etc/passwd /etc/passwd -COPY --from=build /go/src/github.com/benthosdev/benthos/target/bin/benthos . -COPY ./config/docker.yaml /benthos.yaml +COPY --from=build /go/src/github.com/redpanda-data/connect/target/bin/redpanda-connect . +COPY ./config/docker.yaml /connect.yaml -USER benthos +USER connect EXPOSE 4195 -ENTRYPOINT ["/benthos"] +ENTRYPOINT ["/redpanda-connect"] -CMD ["-c", "/benthos.yaml"] +CMD ["-c", "/connect.yaml"] diff --git a/resources/docker/Dockerfile.cgo b/resources/docker/Dockerfile.cgo index dc8c7ff3fa..6698bd9a2d 100644 --- a/resources/docker/Dockerfile.cgo +++ b/resources/docker/Dockerfile.cgo @@ -1,36 +1,36 @@ -FROM golang:1.21 AS build +FROM golang:1.22 AS build ENV CGO_ENABLED=1 ENV GOOS=linux -WORKDIR /go/src/github.com/benthosdev/benthos/ +WORKDIR /go/src/github.com/redpanda-data/connect/ # Update dependencies: On unchanged dependencies, cached layer will be reused -COPY go.* /go/src/github.com/benthosdev/benthos/ +COPY go.* /go/src/github.com/redpanda-data/connect/ RUN go mod download RUN apt-get update && apt-get install -y --no-install-recommends libzmq3-dev # Build -COPY . /go/src/github.com/benthosdev/benthos/ +COPY . /go/src/github.com/redpanda-data/connect/ RUN make TAGS=x_benthos_extra # Pack FROM debian:latest -LABEL maintainer="Ashley Jeffs " -LABEL org.opencontainers.image.source="https://github.com/benthosdev/benthos" +LABEL maintainer="Ashley Jeffs " +LABEL org.opencontainers.image.source="https://github.com/redpanda-data/connect" WORKDIR /root/ RUN apt-get update && apt-get install -y --no-install-recommends libzmq3-dev COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=build /go/src/github.com/benthosdev/benthos/target/bin/benthos . -COPY ./config/docker.yaml /benthos.yaml +COPY --from=build /go/src/github.com/redpanda-data/connect/target/bin/redpanda-connect . +COPY ./config/docker.yaml /connect.yaml EXPOSE 4195 -ENTRYPOINT ["./benthos"] +ENTRYPOINT ["./redpanda-connect"] -CMD ["-c", "/benthos.yaml"] +CMD ["-c", "/connect.yaml"] diff --git a/resources/docker/README.md b/resources/docker/README.md index 52a9355ed3..23dc11a3df 100644 --- a/resources/docker/README.md +++ b/resources/docker/README.md @@ -3,14 +3,14 @@ Benthos Docker This directory contains two Dockerfile definitions, one is a pure Go image based on [`busybox`][docker.busybox] (`Dockerfile`), the other (`Dockerfile.cgo`) is a CGO enabled build based on [`debian`][docker.debian]. -The image has a [default config][default.config] but it's not particularly useful, so you'll either want to use the `-s` cli flag to define config values or copy a config into the path `/benthos.yaml` as a volume. +The image has a [default config][default.config] but it's not particularly useful, so you'll either want to use the `-s` cli flag to define config values or copy a config into the path `/connect.yaml` as a volume. ```shell # Using a config file -docker run --rm -v /path/to/your/config.yaml:/benthos.yaml ghcr.io/benthosdev/benthos +docker run --rm -v /path/to/your/config.yaml:/connect.yaml ghcr.io/redpanda-data/connect # Using a series of -s flags -docker run --rm -p 4195:4195 ghcr.io/benthosdev/benthos \ +docker run --rm -p 4195:4195 ghcr.io/redpanda-data/connect \ -s "input.type=http_server" \ -s "output.type=kafka" \ -s "output.kafka.addresses=kafka-server:9092" \ diff --git a/resources/docker/profiling/README.md b/resources/docker/profiling/README.md index 8d47d4e885..edc9bdb9b3 100644 --- a/resources/docker/profiling/README.md +++ b/resources/docker/profiling/README.md @@ -7,7 +7,7 @@ This docker compose sets a Benthos instance up with a custom config, [Prometheus - Run Grafana and Prometheus with `docker-compose up`. - Edit `config.yaml` and add whatever components you want to profile with. -- Run Benthos with `benthos -c ./config.yaml`. +- Run Redpanda Connect with `redpanda-connect -c ./config.yaml`. - Open up Grafana at [http://localhost:3000/d/PHrVlmniz/benthos-dash](http://localhost:3000/d/PHrVlmniz/benthos-dash) - Go to [http://localhost:16686](http://localhost:16686) in order to observe opentracing events with Jaeger. - Use `go tool pprof http://localhost:4195/debug/pprof/profile` and similar endpoints to get profiling data. diff --git a/resources/docker/schema_registry/README.md b/resources/docker/schema_registry/README.md index 7c83d77f1b..7a20e80746 100644 --- a/resources/docker/schema_registry/README.md +++ b/resources/docker/schema_registry/README.md @@ -1,11 +1,11 @@ Schema Registry =============== -This is a neat little example of using a schema registry service with Benthos. Both the Kafka implementation and the schema registry service are being handled with [Redpanda](https://vectorized.io/redpanda/). +This is a neat little example of using a schema registry service with Benthos. Both the Kafka implementation and the schema registry service are being handled with [Redpanda](https://redpanda.com/). Video run through of this demo: [https://youtu.be/HzuqbNw-vMo](https://youtu.be/HzuqbNw-vMo) More information about schema registry service: [https://docs.confluent.io/platform/current/schema-registry/index.html](https://docs.confluent.io/platform/current/schema-registry/index.html) -How to set up a schema registry with Redpanda: [https://vectorized.io/blog/schema_registry/](https://vectorized.io/blog/schema_registry/) +How to set up a schema registry with Redpanda: [https://docs.redpanda.com/current/manage/schema-reg/](https://docs.redpanda.com/current/manage/schema-reg/) ## Run @@ -22,5 +22,5 @@ docker-compose up -d ## See generated messages ```sh -docker-compose logs -f benthos-out +docker-compose logs -f connect-out ``` diff --git a/resources/docker/schema_registry/docker-compose.yaml b/resources/docker/schema_registry/docker-compose.yaml index 916d236898..43eff8ca5d 100644 --- a/resources/docker/schema_registry/docker-compose.yaml +++ b/resources/docker/schema_registry/docker-compose.yaml @@ -1,7 +1,7 @@ version: '3.3' services: redpanda: - image: docker.vectorized.io/vectorized/redpanda + image: docker.redpanda.com/redpandadata/redpanda ports: - 8081:8081 command: @@ -13,14 +13,14 @@ services: - '--pandaproxy-addr 0.0.0.0:8082' - '--advertise-pandaproxy-addr redpanda:8082' - benthos-in: - image: ghcr.io/benthosdev/benthos - command: [ '-w', '-c', '/benthos.yaml' ] + connect-in: + image: ghcr.io/redpanda-data/connect + command: [ '-w', '-c', '/connect.yaml' ] volumes: - - ./in.yaml:/benthos.yaml + - ./in.yaml:/connect.yaml - benthos-out: - image: ghcr.io/benthosdev/benthos - command: [ '-w', '-c', '/benthos.yaml' ] + connect-out: + image: ghcr.io/redpanda-data/connect + command: [ '-w', '-c', '/connect.yaml' ] volumes: - - ./out.yaml:/benthos.yaml + - ./out.yaml:/connect.yaml diff --git a/resources/docker/streams_mode/benthos.yaml b/resources/docker/streams_mode/benthos.yaml deleted file mode 100644 index ca6d20d5d9..0000000000 --- a/resources/docker/streams_mode/benthos.yaml +++ /dev/null @@ -1,6 +0,0 @@ -http: - enabled: true - -output_resources: - - label: to_stdout - stdout: {} diff --git a/resources/docker/streams_mode/docker-compose.yaml b/resources/docker/streams_mode/docker-compose.yaml deleted file mode 100644 index 07d97438d0..0000000000 --- a/resources/docker/streams_mode/docker-compose.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3.3' -services: - benthos: - image: ghcr.io/benthosdev/benthos - command: [ '-c', '/benthos.yaml', 'streams', '/streams/*.yaml' ] - volumes: - - ./benthos.yaml:/benthos.yaml - - ./streams:/streams diff --git a/resources/docker/streams_mode/streams/bars.yaml b/resources/docker/streams_mode/streams/bars.yaml deleted file mode 100644 index e55b7ae64e..0000000000 --- a/resources/docker/streams_mode/streams/bars.yaml +++ /dev/null @@ -1,9 +0,0 @@ -input: - generate: - interval: 100ms - mapping: | - root.id = uuid_v4() - root.purpose = "too bar" - -output: - resource: to_stdout \ No newline at end of file diff --git a/resources/docker/streams_mode/streams/foos.yaml b/resources/docker/streams_mode/streams/foos.yaml deleted file mode 100644 index b09256f1cb..0000000000 --- a/resources/docker/streams_mode/streams/foos.yaml +++ /dev/null @@ -1,9 +0,0 @@ -input: - generate: - interval: 100ms - mapping: | - root.id = uuid_v4() - root.purpose = "too foo" - -output: - resource: to_stdout \ No newline at end of file diff --git a/resources/docker/tracing/a.yaml b/resources/docker/tracing/a.yaml deleted file mode 100644 index 4026cb57c5..0000000000 --- a/resources/docker/tracing/a.yaml +++ /dev/null @@ -1,33 +0,0 @@ -http: - enabled: false - -input: - generate: - interval: "1s" - mapping: | - root = { - "locations": [ - {"name": "Seattle", "state": "WA"}, - {"name": "New York", "state": "NY"}, - {"name": "Bellevue", "state": "WA"}, - {"name": "Olympia", "state": "WA"} - ] - } - -pipeline: - processors: - - bloblang: '{"Cities":this.locations.filter(this.state == "WA").map_each(this.name).sort().join(", ")}' - -output: - kafka: - addresses: [ localhost:9092 ] - topic: a_results - client_id: a_client - max_in_flight: 100 - inject_tracing_map: 'meta = this' - -tracer: - jaeger: - agent_address: 'localhost:6831' - tags: - pipeline: my_tracing_example diff --git a/resources/docker/tracing/b.yaml b/resources/docker/tracing/b.yaml deleted file mode 100644 index 4fe7fd8c4e..0000000000 --- a/resources/docker/tracing/b.yaml +++ /dev/null @@ -1,27 +0,0 @@ -http: - enabled: false - -input: - kafka: - addresses: [ localhost:9092 ] - topics: [ a_results ] - consumer_group: benthos-tracing-test - client_id: b_client - extract_tracing_map: 'root = meta()' - -pipeline: - processors: - - bloblang: 'root = content().uppercase()' - -output: - kafka: - addresses: [ localhost:9092 ] - topic: b_results - client_id: b_client - max_in_flight: 100 - -tracer: - jaeger: - agent_address: 'localhost:6831' - tags: - pipeline: my_tracing_example diff --git a/resources/docker/tracing/c.yaml b/resources/docker/tracing/c.yaml deleted file mode 100644 index 724d5835bd..0000000000 --- a/resources/docker/tracing/c.yaml +++ /dev/null @@ -1,25 +0,0 @@ -http: - enabled: false - -input: - kafka: - addresses: [ localhost:9092 ] - topics: [ b_results ] - consumer_group: benthos-tracing-test - client_id: c_client - extract_tracing_map: 'root = meta()' - -pipeline: - processors: - - bloblang: | - root = this - root.cities = this.CITIES.lowercase() - -output: - stdout: {} - -tracer: - jaeger: - agent_address: 'localhost:6831' - tags: - pipeline: my_tracing_example diff --git a/resources/docker/tracing/docker-compose.yaml b/resources/docker/tracing/docker-compose.yaml deleted file mode 100644 index 3ecd53e8c0..0000000000 --- a/resources/docker/tracing/docker-compose.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3.3' - -services: - jaeger: - image: jaegertracing/all-in-one - ports: - - 6831:6831/udp - - 16686:16686 - - redpanda: - image: vectorized/redpanda - ports: - - 9092:9092 - command: - - 'redpanda start' - - '--smp 1' - - '--overprovisioned' - - '--kafka-addr 0.0.0.0:9092' - - '--advertise-kafka-addr localhost:9092' diff --git a/resources/k8s/nats-and-studio/README.md b/resources/k8s/nats-and-studio/README.md deleted file mode 100644 index 3c6d1a6167..0000000000 --- a/resources/k8s/nats-and-studio/README.md +++ /dev/null @@ -1,90 +0,0 @@ -Studio Demo -=========== - -This directory contains some fun toys for messing around with Benthos in Kubernetes, both with ConfigMap based deployments as well as instances using [Benthos Studio](https://studio.benthos.dev/) for their config management. - -## Preparation - -Firstly, make sure you have Kubernetes set up and the `kubectl` command configured to access the context you're interested in. - -Next, run any queue systems you might want to experiment with along with Benthos, this directory contains a very bare-bones [NATS](https://nats.io/) set up that you can run with: - -```sh -kubectl apply -f ./nats.yaml -``` - -> Hint: Expose the NATS server to your local environment with `kubectl port-forward service/nats 4222:4222`. - -## Benthos with ConfigMap - -There are two Benthos deployments that utilise a ConfigMap you can run, the first `./benthos-generator.yaml` generates data and dumps it into a NATS subject "demo-a", you can run it with: - -```sh -kubectl apply -f ./benthos-generator.yaml -``` - -And similarly you can run the other ConfigMap example `./benthos-server.yaml`, which creates an HTTP server via Benthos, with: - -```sh -kubectl apply -f ./benthos-server.yaml -``` - -> Hint: Expose the Benthos server to your local environment with `kubectl port-forward service/benthos-server 4195:4195`. - -## Benthos with Studio - -A major advantage to using [Benthos Studio](https://studio.benthos.dev/) is that you can deploy an arbitrary (and dynamic) number of Benthos replicas to k8s and they will be automatically distributed across your configs. No need for using a ConfigMap and no need to have a separate deployment for each config. - -Once you've created a session, some configs and at least one deployment add a secret to k8s containing the access token and secret combination, these are all found in the Benthos Studio session page: - -```bash -kubectl create secret generic benthos-studio \ - --from-literal=token='TODO' \ - --from-literal=secret='TODO' -``` - -And edit the `benthos-studio-node.yaml` deployment template to swap out `` within the container args to match the session you're targetting. - -Then deploy your Benthos instances with: - -```sh -kubectl apply -f ./benthos-studio-node.yaml -``` - -> Hint: Now in your Studio Session try adding a config that joins all these components together: - -```yaml -input: - nats: - urls: - - nats://nats:4222 - subject: demo-a - queue: studio-nodes -pipeline: - processors: - - http: - url: http://benthos-server:4195/test/a - verb: POST - - http: - url: http://benthos-server:4195/test/b - verb: POST -output: - nats: - urls: - - nats://nats:4222 - subject: demo-b -``` - -## Probing the Data - -In order to see data at different subjects from your local machine ensure that you have the NATS server port 4222 forwarded onto your machine: - -```sh -kubectl port-forward service/nats 4222:4222 -``` - -And then run a simple Benthos server to consume data at a given subject: - -```sh -benthos -s 'input.nats.urls=nats://localhost:4222' -s 'input.nats.subject=demo-b' -``` diff --git a/resources/k8s/nats-and-studio/benthos-generator.yaml b/resources/k8s/nats-and-studio/benthos-generator.yaml deleted file mode 100644 index 4798c4dd8a..0000000000 --- a/resources/k8s/nats-and-studio/benthos-generator.yaml +++ /dev/null @@ -1,47 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: benthos-generator-config -data: - config.yaml: | - input: - generate: - interval: 1s - mapping: | - root.id = uuid_v4() - output: - nats: - urls: - - nats://nats:4222 - subject: demo-a ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: benthos-generator -spec: - replicas: 1 - strategy: {} - selector: - matchLabels: - app: benthos-generator - template: - metadata: - labels: - app: benthos-generator - spec: - volumes: - - name: config-volume - configMap: - name: benthos-generator-config - containers: - - name: benthos - image: ghcr.io/benthosdev/benthos:latest - args: - - "-c" - - "/etc/benthos/config.yaml" - volumeMounts: - - name: config-volume - mountPath: "/etc/benthos" - readOnly: true diff --git a/resources/k8s/nats-and-studio/benthos-server.yaml b/resources/k8s/nats-and-studio/benthos-server.yaml deleted file mode 100644 index 0552a069c3..0000000000 --- a/resources/k8s/nats-and-studio/benthos-server.yaml +++ /dev/null @@ -1,63 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: benthos-server-config -data: - config.yaml: | - input: - http_server: - path: /test - pipeline: - processors: - - switch: - - check: '@.http_server_request_path.or("").has_prefix("/test/a")' - processors: - - mapping: 'root = content().uppercase()' - - check: '@.http_server_request_path.or("").has_prefix("/test/b")' - processors: - - mapping: 'root = content() + " AND THIS"' - output: - sync_response: {} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: benthos-server -spec: - replicas: 1 - strategy: {} - selector: - matchLabels: - app: benthos-server - template: - metadata: - labels: - app: benthos-server - spec: - volumes: - - name: config-volume - configMap: - name: benthos-server-config - containers: - - name: benthos - image: ghcr.io/benthosdev/benthos:latest - args: - - "-c" - - "/etc/benthos/config.yaml" - ports: - - containerPort: 4195 - volumeMounts: - - name: config-volume - mountPath: "/etc/benthos" - readOnly: true ---- -apiVersion: v1 -kind: Service -metadata: - name: benthos-server -spec: - selector: - app: benthos-server - ports: - - port: 4195 diff --git a/resources/k8s/nats-and-studio/benthos-studio-node.yaml b/resources/k8s/nats-and-studio/benthos-studio-node.yaml deleted file mode 100644 index 031c9afbbf..0000000000 --- a/resources/k8s/nats-and-studio/benthos-studio-node.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: benthos-studio-node -spec: - replicas: 10 - strategy: {} - selector: - matchLabels: - app: benthos-studio-node - template: - metadata: - labels: - app: benthos-studio-node - spec: - containers: - - name: benthos - image: ghcr.io/benthosdev/benthos:latest - args: [ "studio", "pull", "-s", "" ] - env: - - name: BSTDIO_NODE_TOKEN - valueFrom: - secretKeyRef: { name: benthos-studio, key: token } - - name: BSTDIO_NODE_SECRET - valueFrom: - secretKeyRef: { name: benthos-studio, key: secret } diff --git a/resources/k8s/nats-and-studio/nats.yaml b/resources/k8s/nats-and-studio/nats.yaml deleted file mode 100644 index 027b376c90..0000000000 --- a/resources/k8s/nats-and-studio/nats.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nats -spec: - replicas: 1 - strategy: {} - selector: - matchLabels: - app: nats - template: - metadata: - labels: - app: nats - spec: - containers: - - name: nats - image: nats:alpine - ports: - - containerPort: 4222 ---- -apiVersion: v1 -kind: Service -metadata: - name: nats -spec: - selector: - app: nats - ports: - - port: 4222 diff --git a/resources/scripts/field_alignment.sh b/resources/scripts/field_alignment.sh deleted file mode 100755 index d7681d4c67..0000000000 --- a/resources/scripts/field_alignment.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -# Check for anonymous field stuct constructors: -# grep -e '{\( \+\)\?[.a-zA-Z]\+,' -RIn ./internal ./cmd ./public -# pcregrep -Mnr '{([\n \t]+)?([.a-zA-Z]+,( )?)+$' ./internal ./cmd ./public - -go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest -fieldalignment -fix ./internal/... ./cmd/... ./public/... diff --git a/resources/scripts/package_weight.sh b/resources/scripts/package_weight.sh deleted file mode 100755 index f687b1b65b..0000000000 --- a/resources/scripts/package_weight.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -go install github.com/jondot/goweight@latest -goweight ./cmd/benthos diff --git a/resources/scripts/release_notes.sh b/resources/scripts/release_notes.sh index a5af17cbca..10f5a72eaa 100755 --- a/resources/scripts/release_notes.sh +++ b/resources/scripts/release_notes.sh @@ -1,15 +1,15 @@ #!/bin/sh -echo "For installation instructions check out the [getting started guide](https://www.benthos.dev/docs/guides/getting_started)." +echo "For installation instructions check out the [getting started guide](https://www.docs.redpanda.com/redpanda-connect/guides/getting_started)." cat CHANGELOG.md | awk ' /^## [0-9]/ { release++; } /TBD$/ { print ""; - print "NOTE: This is a release candidate, you can download a binary from this page or pull a docker image from https://github.com/benthosdev/benthos/pkgs/container/benthos with the specific tag of the release candidate."; + print "NOTE: This is a release candidate, you can download a binary from this page or pull a docker image from https://github.com/redpanda-data/connect/pkgs/container/connect with the specific tag of the release candidate."; } !/^## [0-9]/ { if ( release == 1 ) print; if ( release > 1 ) exit; }' -echo "The full change log can be [found here](https://github.com/benthosdev/benthos/blob/main/CHANGELOG.md)." +echo "The full change log can be [found here](https://github.com/redpanda-data/connect/blob/main/CHANGELOG.md)." diff --git a/resources/serverless/lambda/benthos-lambda-al2-sam.yaml b/resources/serverless/lambda/benthos-lambda-al2-sam.yaml deleted file mode 100644 index f1aa72661f..0000000000 --- a/resources/serverless/lambda/benthos-lambda-al2-sam.yaml +++ /dev/null @@ -1,30 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: 'AWS::Serverless-2016-10-31' - -Parameters: - BenthosConfig: - Type: String - Description: > - A YAML configuration for the Benthos pipeline, can include any traditional - sections except for input or buffer. - Default: | - pipeline: - processors: - - type: metadata - metadata: - operator: set - key: AWS_LAMBDA_FUNCTION_VERSION - value: "${AWS_LAMBDA_FUNCTION_VERSION}" - -Resources: - MyFunction: - Type: 'AWS::Serverless::Function' - Properties: - Handler: not.used.for.provided.al2.runtime - Runtime: provided.al2 - Architectures: [ arm64 ] - CodeUri: 'target/serverless/benthos-lambda-al2.zip' - Environment: - Variables: - BENTHOS_CONFIG: - Ref: BenthosConfig diff --git a/resources/serverless/lambda/benthos-lambda-al2.tf b/resources/serverless/lambda/benthos-lambda-al2.tf deleted file mode 100644 index f26e79d476..0000000000 --- a/resources/serverless/lambda/benthos-lambda-al2.tf +++ /dev/null @@ -1,16 +0,0 @@ -resource "aws_lambda_function" "benthos-lambda" { - function_name = "benthos-lambda" - role = "${aws_iam_role.lambda-role.arn}" - handler = "not.used.for.provided.al2.runtime" - runtime = "provided.al2" - architectures = ["arm64"] - - s3_bucket = "${var.bucket_name}" - s3_key = "benthos-lambda-${var.version}.zip" - - environment { - variables = { - LAMBDA_ENV = "${data.template_file.conf.rendered}" - } - } -} diff --git a/resources/serverless/lambda/benthos-lambda-sam.yaml b/resources/serverless/lambda/benthos-lambda-sam.yaml deleted file mode 100644 index 7d222c1ae6..0000000000 --- a/resources/serverless/lambda/benthos-lambda-sam.yaml +++ /dev/null @@ -1,29 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: 'AWS::Serverless-2016-10-31' - -Parameters: - BenthosConfig: - Type: String - Description: > - A YAML configuration for the Benthos pipeline, can include any traditional - sections except for input or buffer. - Default: | - pipeline: - processors: - - type: metadata - metadata: - operator: set - key: AWS_LAMBDA_FUNCTION_VERSION - value: "${AWS_LAMBDA_FUNCTION_VERSION}" - -Resources: - MyFunction: - Type: 'AWS::Serverless::Function' - Properties: - Handler: benthos-lambda - Runtime: go1.x - CodeUri: 'target/serverless/benthos-lambda.zip' - Environment: - Variables: - BENTHOS_CONFIG: - Ref: BenthosConfig \ No newline at end of file diff --git a/resources/serverless/lambda/benthos-lambda.tf b/resources/serverless/lambda/benthos-lambda.tf deleted file mode 100644 index aee6b9c17d..0000000000 --- a/resources/serverless/lambda/benthos-lambda.tf +++ /dev/null @@ -1,15 +0,0 @@ -resource "aws_lambda_function" "benthos-lambda" { - function_name = "benthos-lambda" - role = "${aws_iam_role.lambda-role.arn}" - handler = "benthos-lambda" - runtime = "go1.x" - - s3_bucket = "${var.bucket_name}" - s3_key = "benthos-lambda-${var.version}.zip" - - environment { - variables = { - LAMBDA_ENV = "${data.template_file.conf.rendered}" - } - } -} \ No newline at end of file From 2183627814b385d796de462d87ad3abe791cac67 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Fri, 24 May 2024 12:17:53 +0100 Subject: [PATCH 05/17] Fix goreleaser builds --- .goreleaser.yml | 2 +- licenses/keep | 0 resources/scripts/install | 166 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 licenses/keep create mode 100755 resources/scripts/install diff --git a/.goreleaser.yml b/.goreleaser.yml index 9dc55f24dc..d42de4cda6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -37,7 +37,7 @@ archives: files: - README.md - CHANGELOG.md - - LICENSE + - licenses # - id: connect-lambda # builds: [ connect-lambda ] # format: zip diff --git a/licenses/keep b/licenses/keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/scripts/install b/resources/scripts/install new file mode 100755 index 0000000000..5f58e9b704 --- /dev/null +++ b/resources/scripts/install @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# +# Installs Redpanda Connect the quick way, for adventurers that want to spend +# more time grooming their cats. +# +# Requires curl, grep, cut, tar, uname, chmod, mv, rm. + +[[ $- = *i* ]] && echo "Don't source this script!" && return 10 + +header() { + cat 1>&2 < /dev/null 2>&1 +} + +check_tools() { + Tools=("curl" "grep" "cut" "tar" "uname" "chmod" "mv" "rm") + + for tool in ${Tools[*]}; do + if ! check_cmd $tool; then + echo "Aborted, missing $tool, sorry!" + exit 6 + fi + done +} + +install_redpanda_connect() +{ + trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; exit 1' ERR + + # Process the command line + if [[ "$#" -eq 2 ]]; then + connect_tag="v$1" + connect_version="$1" + connect_install_path="$2" + elif [[ "$#" -eq 1 ]]; then + connect_tag="v$1" + connect_version=$1 + connect_install_path="/usr/local/bin" + elif [[ "$#" -eq 0 ]]; then + connect_tag=$(curl -s https://api.github.com/repos/redpanda-data/connect/releases/latest | grep 'tag_name' | cut -d\" -f4) + connect_version=$(echo ${connect_tag} | cut -c2-) + connect_install_path="/usr/local/bin" + else + echo "Too many arguments." + exit 1 + fi + + connect_os="unsupported" + connect_arch="unknown" + connect_arm="" + + header + check_tools + + if [[ -n "$PREFIX" ]]; then + connect_install_path="$PREFIX/bin" + fi + + # Fall back to /usr/bin if necessary + if [[ ! -d $connect_install_path ]]; then + connect_install_path="/usr/bin" + fi + + # Not every platform has or needs sudo (https://termux.com/linux.html) + ((EUID)) && sudo_cmd="sudo" + + ######################### + # Which OS and version? # + ######################### + + connect_bin="redpanda-connect" + connect_dl_ext=".tar.gz" + + # NOTE: `uname -m` is more accurate and universal than `arch` + # See https://en.wikipedia.org/wiki/Uname + unamem="$(uname -m)" + if [[ $unamem == *aarch64* ]]; then + connect_arch="arm64" + elif [[ $unamem == *arm64* ]]; then + connect_arch="arm64" + elif [[ $unamem == *64* ]]; then + connect_arch="amd64" + elif [[ $unamem == *armv5* ]]; then + connect_arch="arm" + connect_arm="v5" + elif [[ $unamem == *armv6l* ]]; then + connect_arch="arm" + connect_arm="v6" + elif [[ $unamem == *armv7l* ]]; then + connect_arch="arm" + connect_arm="v7" + else + echo "Aborted, unsupported or unknown architecture: $unamem" + return 2 + fi + + unameu="$(tr '[:lower:]' '[:upper:]' <<<$(uname))" + if [[ $unameu == *DARWIN* ]]; then + connect_os="darwin" + version=${vers##*ProductVersion:} + elif [[ $unameu == *LINUX* ]]; then + connect_os="linux" + elif [[ $unameu == *FREEBSD* ]]; then + connect_os="freebsd" + elif [[ $unameu == *OPENBSD* ]]; then + connect_os="openbsd" + elif [[ $unameu == *WIN* || $unameu == MSYS* ]]; then + # Should catch cygwin + sudo_cmd="" + connect_os="windows" + connect_bin=$connect_bin.exe + else + echo "Aborted, unsupported or unknown os: $uname" + return 6 + fi + + ######################## + # Download and extract # + ######################## + + echo "Downloading Redpanda Connect for ${connect_os}/${connect_arch}${connect_arm}..." + connect_file="connect_${connect_os}_${connect_arch}${connect_arm}${connect_dl_ext}" + + connect_url="https://github.com/redpanda-data/connect/releases/download/${connect_tag}/connect_${connect_version}_${connect_os}_${connect_arch}${connect_arm}.tar.gz" + + dl="/tmp/$connect_file" + rm -rf -- "$dl" + + curl -fsSL "$connect_url" -o "$dl" + + echo "Extracting..." + case "$connect_file" in + *.tar.gz) tar -xzf "$dl" -C "$PREFIX/tmp/" "$connect_bin" ;; + esac + chmod +x "$PREFIX/tmp/$connect_bin" + + echo "Putting redpanda-connect in $connect_install_path (may require password)" + if [ -n "$sudo_cmd" ] && [ -n "$(find "$connect_install_path" -prune -user "$(id -u)")" ]; then + # Skip sudo if the current user is the owner of the Benthos install path + sudo_cmd="" + fi + $sudo_cmd mv "$PREFIX/tmp/$connect_bin" "$connect_install_path/$connect_bin" + $sudo_cmd rm -- "$dl" + + # check installation + $connect_install_path/$connect_bin -version + if ! check_cmd redpanda-connect; then + echo "Do not forget to add $connect_install_path to your PATH!" + fi + + echo "Successfully installed" + trap ERR + return 0 +} + +install_redpanda_connect $@ From 7c9da2c65255470def0c12cbef9603d43db65a27 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Fri, 24 May 2024 12:23:42 +0100 Subject: [PATCH 06/17] Remove docker login from release --- .github/workflows/release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53a4f62e9d..64b5ad3064 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,12 +60,6 @@ jobs: sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin From 08fbe7489c0a58622b3b76a99342e2c156b93af1 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Fri, 24 May 2024 12:46:19 +0100 Subject: [PATCH 07/17] Add errcheck lints back in --- .golangci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 80ff1c7006..9a596084e0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,6 +18,10 @@ linters-settings: enable-all-rules: false rules: - name: superfluous-else + errcheck: + exclude-functions: + - (*github.com/benthosdev/benthos/v4/internal/batch.Error).Failed + - (*github.com/benthosdev/benthos/v4/public/service.BatchError).Failed govet: enable-all: true disable: From 5765d0270068b2cef57fc26501805114e265ae75 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Fri, 24 May 2024 13:12:58 +0100 Subject: [PATCH 08/17] Update archive path --- .goreleaser.yml | 1 + resources/scripts/install | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index d42de4cda6..fce9f1ab2e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,4 @@ +project_name: redpanda-connect builds: - id: connect main: cmd/redpanda-connect/main.go diff --git a/resources/scripts/install b/resources/scripts/install index 5f58e9b704..98017671cf 100755 --- a/resources/scripts/install +++ b/resources/scripts/install @@ -129,9 +129,9 @@ install_redpanda_connect() ######################## echo "Downloading Redpanda Connect for ${connect_os}/${connect_arch}${connect_arm}..." - connect_file="connect_${connect_os}_${connect_arch}${connect_arm}${connect_dl_ext}" + connect_file="redpanda-connect_${connect_os}_${connect_arch}${connect_arm}${connect_dl_ext}" - connect_url="https://github.com/redpanda-data/connect/releases/download/${connect_tag}/connect_${connect_version}_${connect_os}_${connect_arch}${connect_arm}.tar.gz" + connect_url="https://github.com/redpanda-data/connect/releases/download/${connect_tag}/redpanda-connect_${connect_version}_${connect_os}_${connect_arch}${connect_arm}.tar.gz" dl="/tmp/$connect_file" rm -rf -- "$dl" From e404d31f9aab7367a67a13f3a1737e09e6cd2d91 Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Sun, 26 May 2024 08:55:20 +0100 Subject: [PATCH 09/17] doc updates --- .../components/pages/caches/aws_dynamodb.adoc | 5 +- .../components/pages/caches/aws_s3.adoc | 5 +- .../components/pages/caches/couchbase.adoc | 3 +- docs/modules/components/pages/caches/lru.adoc | 2 +- .../components/pages/caches/mongodb.adoc | 3 +- .../components/pages/caches/nats_kv.adoc | 34 +++---- .../components/pages/caches/redis.adoc | 12 +-- .../components/pages/caches/ristretto.adoc | 2 +- docs/modules/components/pages/caches/sql.adoc | 4 +- .../components/pages/caches/ttlru.adoc | 2 +- .../components/pages/inputs/amqp_0_9.adoc | 12 +-- .../components/pages/inputs/amqp_1.adoc | 15 ++- .../components/pages/inputs/aws_kinesis.adoc | 5 +- .../components/pages/inputs/aws_s3.adoc | 5 +- .../components/pages/inputs/aws_sqs.adoc | 5 +- .../pages/inputs/azure_blob_storage.adoc | 4 +- .../pages/inputs/azure_cosmosdb.adoc | 16 ++- .../components/pages/inputs/cassandra.adoc | 15 ++- .../pages/inputs/cockroachdb_changefeed.adoc | 21 ++-- .../components/pages/inputs/gcp_pubsub.adoc | 4 +- .../components/pages/inputs/http_client.adoc | 24 ++--- .../components/pages/inputs/http_server.adoc | 2 +- .../components/pages/inputs/kafka.adoc | 15 ++- .../components/pages/inputs/kafka_franz.adoc | 22 ++--- .../components/pages/inputs/mongodb.adoc | 3 +- .../modules/components/pages/inputs/mqtt.adoc | 15 ++- .../modules/components/pages/inputs/nats.adoc | 34 +++---- .../pages/inputs/nats_jetstream.adoc | 34 +++---- .../components/pages/inputs/nats_kv.adoc | 34 +++---- .../components/pages/inputs/nats_stream.adoc | 36 ++++--- docs/modules/components/pages/inputs/nsq.adoc | 12 +-- .../components/pages/inputs/parquet.adoc | 4 +- .../components/pages/inputs/pulsar.adoc | 2 +- .../components/pages/inputs/redis_list.adoc | 12 +-- .../components/pages/inputs/redis_pubsub.adoc | 12 +-- .../components/pages/inputs/redis_scan.adoc | 12 +-- .../pages/inputs/redis_streams.adoc | 12 +-- .../modules/components/pages/inputs/sftp.adoc | 6 +- .../components/pages/inputs/sql_raw.adoc | 4 +- .../components/pages/inputs/sql_select.adoc | 4 +- .../pages/inputs/twitter_search.adoc | 14 +-- .../components/pages/inputs/websocket.adoc | 21 ++-- .../components/pages/logger/about.adoc | 2 +- .../pages/metrics/aws_cloudwatch.adoc | 5 +- .../components/pages/metrics/influxdb.adoc | 15 ++- .../components/pages/metrics/prometheus.adoc | 5 +- .../components/pages/metrics/statsd.adoc | 2 +- .../components/pages/outputs/amqp_0_9.adoc | 12 +-- .../components/pages/outputs/amqp_1.adoc | 15 ++- .../pages/outputs/aws_dynamodb.adoc | 5 +- .../components/pages/outputs/aws_kinesis.adoc | 5 +- .../pages/outputs/aws_kinesis_firehose.adoc | 5 +- .../components/pages/outputs/aws_s3.adoc | 5 +- .../components/pages/outputs/aws_sns.adoc | 5 +- .../components/pages/outputs/aws_sqs.adoc | 5 +- .../pages/outputs/azure_blob_storage.adoc | 2 +- .../pages/outputs/azure_cosmosdb.adoc | 18 ++-- .../components/pages/outputs/cassandra.adoc | 15 ++- .../components/pages/outputs/discord.adoc | 2 +- .../pages/outputs/elasticsearch.adoc | 20 ++-- .../pages/outputs/gcp_bigquery.adoc | 4 +- .../components/pages/outputs/gcp_pubsub.adoc | 4 +- .../components/pages/outputs/http_client.adoc | 28 +++--- .../components/pages/outputs/http_server.adoc | 2 +- .../components/pages/outputs/kafka.adoc | 15 ++- .../components/pages/outputs/kafka_franz.adoc | 22 ++--- .../components/pages/outputs/mongodb.adoc | 9 +- .../components/pages/outputs/mqtt.adoc | 15 ++- .../components/pages/outputs/nats.adoc | 34 +++---- .../pages/outputs/nats_jetstream.adoc | 34 +++---- .../components/pages/outputs/nats_kv.adoc | 34 +++---- .../components/pages/outputs/nats_stream.adoc | 36 ++++--- .../modules/components/pages/outputs/nsq.adoc | 12 +-- .../components/pages/outputs/opensearch.adoc | 22 ++--- .../components/pages/outputs/redis_hash.adoc | 12 +-- .../components/pages/outputs/redis_list.adoc | 12 +-- .../pages/outputs/redis_pubsub.adoc | 12 +-- .../pages/outputs/redis_streams.adoc | 12 +-- .../components/pages/outputs/sftp.adoc | 6 +- .../pages/outputs/snowflake_put.adoc | 42 ++++---- .../components/pages/outputs/splunk_hec.adoc | 2 +- .../components/pages/outputs/sql_insert.adoc | 4 +- .../components/pages/outputs/sql_raw.adoc | 4 +- .../components/pages/outputs/websocket.adoc | 21 ++-- .../components/pages/processors/archive.adoc | 2 +- .../components/pages/processors/awk.adoc | 2 +- .../processors/aws_dynamodb_partiql.adoc | 5 +- .../pages/processors/aws_lambda.adoc | 5 +- .../pages/processors/azure_cosmosdb.adoc | 18 ++-- .../pages/processors/couchbase.adoc | 3 +- .../components/pages/processors/grok.adoc | 2 +- .../components/pages/processors/http.adoc | 26 +++-- .../pages/processors/javascript.adoc | 6 +- .../components/pages/processors/jq.adoc | 4 +- .../pages/processors/json_schema.adoc | 2 +- .../components/pages/processors/mongodb.adoc | 9 +- .../components/pages/processors/msgpack.adoc | 2 +- .../components/pages/processors/nats_kv.adoc | 36 ++++--- .../pages/processors/nats_request_reply.adoc | 34 +++---- .../components/pages/processors/parquet.adoc | 2 +- .../pages/processors/parquet_decode.adoc | 4 +- .../pages/processors/parquet_encode.adoc | 4 +- .../pages/processors/parse_log.adoc | 6 +- .../components/pages/processors/protobuf.adoc | 4 +- .../components/pages/processors/redis.adoc | 12 +-- .../pages/processors/redis_script.adoc | 14 +-- .../processors/schema_registry_decode.adoc | 29 +++--- .../processors/schema_registry_encode.adoc | 33 +++---- .../pages/processors/sentry_capture.adoc | 2 +- .../pages/processors/sql_insert.adoc | 4 +- .../components/pages/processors/sql_raw.adoc | 4 +- .../pages/processors/sql_select.adoc | 4 +- .../components/pages/processors/switch.adoc | 6 +- .../pages/processors/unarchive.adoc | 2 +- .../components/pages/processors/wasm.adoc | 4 +- .../components/pages/processors/workflow.adoc | 2 +- .../components/pages/rate_limits/redis.adoc | 12 +-- .../components/pages/scanners/avro.adoc | 6 +- .../pages/tracers/gcp_cloudtrace.adoc | 2 +- .../components/pages/tracers/jaeger.adoc | 2 +- .../tracers/open_telemetry_collector.adoc | 2 +- .../configuration/pages/templating.adoc | 3 +- .../guides/pages/bloblang/functions.adoc | 9 +- .../guides/pages/bloblang/methods.adoc | 97 ++++++------------- internal/impl/avro/scanner.go | 6 +- internal/impl/awk/processor.go | 2 +- internal/impl/aws/config/config.go | 2 +- internal/impl/azure/cosmosdb/docs.go | 8 +- internal/impl/azure/input_blob_storage.go | 4 +- internal/impl/azure/input_cosmosdb.go | 4 +- internal/impl/azure/output_blob_storage.go | 2 +- internal/impl/azure/output_cosmosdb.go | 4 +- internal/impl/azure/processor_cosmosdb.go | 4 +- internal/impl/changelog/bloblang.go | 4 +- internal/impl/cockroachdb/input_changefeed.go | 8 +- .../processor_schema_registry_decode.go | 8 +- .../processor_schema_registry_encode.go | 12 +-- internal/impl/dgraph/cache_ristretto.go | 2 +- internal/impl/discord/output.go | 2 +- internal/impl/gcp/input_pubsub.go | 4 +- internal/impl/gcp/output_bigquery.go | 4 +- internal/impl/gcp/output_pubsub.go | 4 +- internal/impl/gcp/tracer_cloudtrace.go | 2 +- internal/impl/jaeger/tracer_jaeger.go | 2 +- internal/impl/javascript/processor.go | 6 +- internal/impl/kafka/input_kafka_franz.go | 2 +- internal/impl/kafka/output_kafka_franz.go | 2 +- internal/impl/lang/bloblang.go | 6 +- internal/impl/maxmind/bloblang_geoip.go | 2 +- internal/impl/mongodb/common.go | 6 +- internal/impl/msgpack/bloblang.go | 4 +- internal/impl/msgpack/processor.go | 2 +- internal/impl/nats/auth.go | 16 +-- internal/impl/nats/input_stream.go | 2 +- internal/impl/nats/output_stream.go | 2 +- internal/impl/nats/processor_kv.go | 2 +- internal/impl/nsq/integration_test.go | 2 +- internal/impl/opensearch/output.go | 2 +- internal/impl/otlp/tracer_otlp.go | 2 +- internal/impl/parquet/bloblang.go | 2 +- internal/impl/parquet/input_parquet.go | 4 +- internal/impl/parquet/processor.go | 2 +- internal/impl/parquet/processor_decode.go | 4 +- internal/impl/parquet/processor_encode.go | 4 +- .../impl/prometheus/metrics_prometheus.go | 2 +- internal/impl/protobuf/processor_protobuf.go | 4 +- internal/impl/pulsar/input.go | 2 +- internal/impl/redis/script_processor.go | 2 +- internal/impl/sentry/processor_capture.go | 2 +- .../impl/snowflake/output_snowflake_put.go | 36 +++---- internal/impl/splunk/template_output.yaml | 2 +- internal/impl/sql/conn_fields.go | 10 +- internal/impl/statsd/metrics_statsd.go | 2 +- .../impl/twitter/template_search_input.yaml | 14 +-- internal/impl/wasm/processor_wazero.go | 4 +- 175 files changed, 791 insertions(+), 925 deletions(-) diff --git a/docs/modules/components/pages/caches/aws_dynamodb.adoc b/docs/modules/components/pages/caches/aws_dynamodb.adoc index 4b1abb2adf..a8b1fbe0dc 100644 --- a/docs/modules/components/pages/caches/aws_dynamodb.adoc +++ b/docs/modules/components/pages/caches/aws_dynamodb.adoc @@ -231,8 +231,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -254,7 +253,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/caches/aws_s3.adoc b/docs/modules/components/pages/caches/aws_s3.adoc index 1b7428cc6f..5f8f4aa84b 100644 --- a/docs/modules/components/pages/caches/aws_s3.adoc +++ b/docs/modules/components/pages/caches/aws_s3.adoc @@ -200,8 +200,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -223,7 +222,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/caches/couchbase.adoc b/docs/modules/components/pages/caches/couchbase.adoc index feaefb9431..05ba5435c7 100644 --- a/docs/modules/components/pages/caches/couchbase.adoc +++ b/docs/modules/components/pages/caches/couchbase.adoc @@ -84,8 +84,7 @@ Username to connect to the cluster. === `password` Password to connect to the cluster. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/caches/lru.adoc b/docs/modules/components/pages/caches/lru.adoc index 20b325e373..f84f5aa896 100644 --- a/docs/modules/components/pages/caches/lru.adoc +++ b/docs/modules/components/pages/caches/lru.adoc @@ -53,7 +53,7 @@ lru: This provides the lru package which implements a fixed-size thread safe LRU cache. -It uses the package https://github.com/hashicorp/golang-lru/v2[`lru`] +It uses the package https://github.com/hashicorp/golang-lru/v2[`lru`^] The field init_values can be used to pre-populate the memory cache with any number of key/value pairs: diff --git a/docs/modules/components/pages/caches/mongodb.adoc b/docs/modules/components/pages/caches/mongodb.adoc index 586da9ca67..ef2b317d80 100644 --- a/docs/modules/components/pages/caches/mongodb.adoc +++ b/docs/modules/components/pages/caches/mongodb.adoc @@ -67,8 +67,7 @@ The username to connect to the database. === `password` The password to connect to the database. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/caches/nats_kv.adoc b/docs/modules/components/pages/caches/nats_kv.adoc index ee9e4a7812..0591e91f1b 100644 --- a/docs/modules/components/pages/caches/nats_kv.adoc +++ b/docs/modules/components/pages/caches/nats_kv.adoc @@ -75,10 +75,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -86,21 +86,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -175,8 +175,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -244,8 +243,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -276,9 +274,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -336,8 +336,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -350,8 +349,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/caches/redis.adoc b/docs/modules/components/pages/caches/redis.adoc index 5920f63e59..8097b09654 100644 --- a/docs/modules/components/pages/caches/redis.adoc +++ b/docs/modules/components/pages/caches/redis.adoc @@ -160,8 +160,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -229,8 +228,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -261,9 +259,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/caches/ristretto.adoc b/docs/modules/components/pages/caches/ristretto.adoc index 4a81b380b0..af6ec02172 100644 --- a/docs/modules/components/pages/caches/ristretto.adoc +++ b/docs/modules/components/pages/caches/ristretto.adoc @@ -14,7 +14,7 @@ component_type_dropdown::[] -Stores key/value pairs in a map held in the memory-bound https://github.com/dgraph-io/ristretto[Ristretto cache]. +Stores key/value pairs in a map held in the memory-bound https://github.com/dgraph-io/ristretto[Ristretto cache^]. [tabs] diff --git a/docs/modules/components/pages/caches/sql.adoc b/docs/modules/components/pages/caches/sql.adoc index 12a7555d03..a8e8042beb 100644 --- a/docs/modules/components/pages/caches/sql.adoc +++ b/docs/modules/components/pages/caches/sql.adoc @@ -156,9 +156,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/caches/ttlru.adoc b/docs/modules/components/pages/caches/ttlru.adoc index 2f9ef4df39..e400c743ee 100644 --- a/docs/modules/components/pages/caches/ttlru.adoc +++ b/docs/modules/components/pages/caches/ttlru.adoc @@ -55,7 +55,7 @@ The cache ttlru provides a simple, goroutine safe, cache with a fixed number of This TTL is reset on both modification and access of the value. As a result, if the cache is full, and no items have expired, when adding a new item, the item with the soonest expiration will be evicted. -It uses the package https://github.com/hashicorp/golang-lru/v2/expirable[`expirable`] +It uses the package https://github.com/hashicorp/golang-lru/v2/expirable[`expirable`^] The field init_values can be used to pre-populate the memory cache with any number of key/value pairs: diff --git a/docs/modules/components/pages/inputs/amqp_0_9.adoc b/docs/modules/components/pages/inputs/amqp_0_9.adoc index ae9f624a5d..4415b141f6 100644 --- a/docs/modules/components/pages/inputs/amqp_0_9.adoc +++ b/docs/modules/components/pages/inputs/amqp_0_9.adoc @@ -292,8 +292,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -361,8 +360,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -393,9 +391,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/amqp_1.adoc b/docs/modules/components/pages/inputs/amqp_1.adoc index dd50a091b5..c9f4d96596 100644 --- a/docs/modules/components/pages/inputs/amqp_1.adoc +++ b/docs/modules/components/pages/inputs/amqp_1.adoc @@ -205,8 +205,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -274,8 +273,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -306,9 +304,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -374,8 +374,7 @@ user: ${USER} === `sasl.password` A SASL plain text password. It is recommended that you use environment variables to populate this field. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/aws_kinesis.adoc b/docs/modules/components/pages/inputs/aws_kinesis.adoc index 26ab42a668..a857770020 100644 --- a/docs/modules/components/pages/inputs/aws_kinesis.adoc +++ b/docs/modules/components/pages/inputs/aws_kinesis.adoc @@ -284,8 +284,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -307,7 +306,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/inputs/aws_s3.adoc b/docs/modules/components/pages/inputs/aws_s3.adoc index 02dfd2e679..b14ecd6b55 100644 --- a/docs/modules/components/pages/inputs/aws_s3.adoc +++ b/docs/modules/components/pages/inputs/aws_s3.adoc @@ -182,8 +182,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -205,7 +204,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/inputs/aws_sqs.adoc b/docs/modules/components/pages/inputs/aws_sqs.adoc index ddf193969b..49b162af95 100644 --- a/docs/modules/components/pages/inputs/aws_sqs.adoc +++ b/docs/modules/components/pages/inputs/aws_sqs.adoc @@ -177,8 +177,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -200,7 +199,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/inputs/azure_blob_storage.adoc b/docs/modules/components/pages/inputs/azure_blob_storage.adoc index e9bafabd28..fd8894dab1 100644 --- a/docs/modules/components/pages/inputs/azure_blob_storage.adoc +++ b/docs/modules/components/pages/inputs/azure_blob_storage.adoc @@ -71,7 +71,7 @@ Supports multiple authentication methods but only one of the following is requir - `storage_connection_string` - `storage_account` and `storage_access_key` - `storage_account` and `storage_sas_token` -- `storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- `storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^] If multiple are set then the `storage_connection_string` is given priority. @@ -84,7 +84,7 @@ When downloading large files it's often necessary to process it in streamed part == Stream new files -By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage[an Event Grid source] and then use this as a <>, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. +By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage[an Event Grid source^] and then use this as a <>, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. == Metadata diff --git a/docs/modules/components/pages/inputs/azure_cosmosdb.adoc b/docs/modules/components/pages/inputs/azure_cosmosdb.adoc index cf9b8c112d..41c13f98ae 100644 --- a/docs/modules/components/pages/inputs/azure_cosmosdb.adoc +++ b/docs/modules/components/pages/inputs/azure_cosmosdb.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Executes a SQL query against https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB] and creates a batch of messages from each page of items. +Executes a SQL query against https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB^] and creates a batch of messages from each page of items. Introduced in version v4.25.0. @@ -75,7 +75,7 @@ input: == Cross-partition queries -Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey values must be known in advance and specified in the config. https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989[See details]. +Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey values must be known in advance and specified in the config. https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989[See details^]. == Credentials @@ -83,7 +83,7 @@ Cross-partition queries are currently not supported by the underlying driver. Fo You can use one of the following authentication mechanisms: - Set the `endpoint` field and the `account_key` field -- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^] - Set the `connection_string` field @@ -145,8 +145,7 @@ endpoint: https://localhost:8081 === `account_key` Account key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -165,8 +164,7 @@ account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZ === `connection_string` Connection string. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -284,7 +282,7 @@ Whether messages that are rejected (nacked) at the output level should be automa == CosmosDB emulator -If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here^], the following Docker command should do the trick: ```bash > docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator @@ -292,7 +290,7 @@ If you wish to run the CosmosDB emulator that is referenced in the documentation Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. -Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy] like so: +Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy^] like so: ```bash > mitmproxy -k --mode "reverse:https://localhost:8081" diff --git a/docs/modules/components/pages/inputs/cassandra.adoc b/docs/modules/components/pages/inputs/cassandra.adoc index b3f229f2ee..24c9b58749 100644 --- a/docs/modules/components/pages/inputs/cassandra.adoc +++ b/docs/modules/components/pages/inputs/cassandra.adoc @@ -168,8 +168,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -237,8 +236,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -269,9 +267,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -319,8 +319,7 @@ The username to authenticate as. === `password_authenticator.password` The password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc b/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc index 73d1a3ce60..e2c7d11f1a 100644 --- a/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc +++ b/docs/modules/components/pages/inputs/cockroachdb_changefeed.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Listens to a https://www.cockroachlabs.com/docs/stable/changefeed-examples[CockroachDB Core Changefeed] and creates a message for each row received. Each message is a json object looking like: +Listens to a https://www.cockroachlabs.com/docs/stable/changefeed-examples[CockroachDB Core Changefeed^] and creates a message for each row received. Each message is a json object looking like: ```json { "primary_key": "[\"1a7ff641-3e3b-47ee-94fe-a0cadb56cd8f\", 2]", // stringifed JSON array @@ -70,7 +70,7 @@ input: This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered. -Note: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core[the official CockroachDB documentation]. +Note: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core[the official CockroachDB documentation^]. == Fields @@ -118,8 +118,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -187,8 +186,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -219,9 +217,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -258,7 +258,7 @@ tables: === `cursor_cache` -A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache resource] to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table. +A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache resource^] to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table. *Type*: `string` @@ -267,7 +267,8 @@ A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache r === `options` A list of options to be included in the changefeed (WITH X, Y...). -**NOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case.** + +NOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case. *Type*: `array` diff --git a/docs/modules/components/pages/inputs/gcp_pubsub.adoc b/docs/modules/components/pages/inputs/gcp_pubsub.adoc index e61dfd59ba..9a779359c4 100644 --- a/docs/modules/components/pages/inputs/gcp_pubsub.adoc +++ b/docs/modules/components/pages/inputs/gcp_pubsub.adoc @@ -61,7 +61,7 @@ input: -- ====== -For information on how to set up credentials see https://cloud.google.com/docs/authentication/production[this guide]. +For information on how to set up credentials see https://cloud.google.com/docs/authentication/production[this guide^]. == Metadata @@ -96,7 +96,7 @@ The target subscription ID. === `endpoint` -An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]. +An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document^]. *Type*: `string` diff --git a/docs/modules/components/pages/inputs/http_client.adoc b/docs/modules/components/pages/inputs/http_client.adoc index 053e35687c..889ff1e9aa 100644 --- a/docs/modules/components/pages/inputs/http_client.adoc +++ b/docs/modules/components/pages/inputs/http_client.adoc @@ -308,8 +308,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -332,8 +331,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -373,8 +371,7 @@ A value used to identify the client to the token provider. === `oauth2.client_secret` A secret used to establish ownership of the client key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -454,8 +451,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -558,8 +554,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -627,8 +622,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -659,9 +653,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/http_server.adoc b/docs/modules/components/pages/inputs/http_server.adoc index 0a3262b65f..82d4d3c687 100644 --- a/docs/modules/components/pages/inputs/http_server.adoc +++ b/docs/modules/components/pages/inputs/http_server.adoc @@ -92,7 +92,7 @@ The following fields specify endpoints that are registered for sending messages, This endpoint expects POST requests where the entire request body is consumed as a single message. -If the request contains a multipart `content-type` header as per https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[rfc1341] then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. +If the request contains a multipart `content-type` header as per https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[rfc1341^] then the multiple parts are consumed as a batch of messages, where each body part is a message of the batch. === `ws_path` (defaults to `/post/ws`) diff --git a/docs/modules/components/pages/inputs/kafka.adoc b/docs/modules/components/pages/inputs/kafka.adoc index 61b2a36e06..ec8fb50eb1 100644 --- a/docs/modules/components/pages/inputs/kafka.adoc +++ b/docs/modules/components/pages/inputs/kafka.adoc @@ -239,8 +239,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -308,8 +307,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -340,9 +338,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -412,8 +412,7 @@ user: ${USER} === `sasl.password` A PLAIN password. It is recommended that you use environment variables to populate this field. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/kafka_franz.adoc b/docs/modules/components/pages/inputs/kafka_franz.adoc index 2f6f9721d9..755ffce749 100644 --- a/docs/modules/components/pages/inputs/kafka_franz.adoc +++ b/docs/modules/components/pages/inputs/kafka_franz.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -A Kafka input using the https://github.com/twmb/franz-go[Franz Kafka client library]. +A Kafka input using the https://github.com/twmb/franz-go[Franz Kafka client library^]. Introduced in version 3.61.0. @@ -268,8 +268,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -337,8 +336,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -369,9 +367,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -445,8 +445,7 @@ A username to provide for PLAIN or SCRAM-* authentication. === `sasl[].password` A password to provide for PLAIN or SCRAM-* authentication. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -529,8 +528,7 @@ The ID of credentials to use. === `sasl[].aws.credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -552,7 +550,7 @@ The token for the credentials being used, required when using short term credent === `sasl[].aws.credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/inputs/mongodb.adoc b/docs/modules/components/pages/inputs/mongodb.adoc index e1a1b30bd8..21b2543535 100644 --- a/docs/modules/components/pages/inputs/mongodb.adoc +++ b/docs/modules/components/pages/inputs/mongodb.adoc @@ -112,8 +112,7 @@ The username to connect to the database. === `password` The password to connect to the database. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/mqtt.adoc b/docs/modules/components/pages/inputs/mqtt.adoc index a81c657027..10f4e59e1f 100644 --- a/docs/modules/components/pages/inputs/mqtt.adoc +++ b/docs/modules/components/pages/inputs/mqtt.adoc @@ -214,8 +214,7 @@ A username to connect with. === `password` A password to connect with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -274,8 +273,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -343,8 +341,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -375,9 +372,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/nats.adoc b/docs/modules/components/pages/inputs/nats.adoc index 6a933f81af..937faa5cbd 100644 --- a/docs/modules/components/pages/inputs/nats.adoc +++ b/docs/modules/components/pages/inputs/nats.adoc @@ -94,10 +94,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -105,21 +105,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -240,8 +240,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -309,8 +308,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -341,9 +339,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -401,8 +401,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -415,8 +414,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/nats_jetstream.adoc b/docs/modules/components/pages/inputs/nats_jetstream.adoc index e830aa6d8e..df59d5866d 100644 --- a/docs/modules/components/pages/inputs/nats_jetstream.adoc +++ b/docs/modules/components/pages/inputs/nats_jetstream.adoc @@ -111,10 +111,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -122,21 +122,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -298,8 +298,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -367,8 +366,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -399,9 +397,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -459,8 +459,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -473,8 +472,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/nats_kv.adoc b/docs/modules/components/pages/inputs/nats_kv.adoc index 5912d7350a..ce0587b187 100644 --- a/docs/modules/components/pages/inputs/nats_kv.adoc +++ b/docs/modules/components/pages/inputs/nats_kv.adoc @@ -97,10 +97,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -108,21 +108,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -254,8 +254,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -323,8 +322,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -355,9 +353,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -415,8 +415,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -429,8 +428,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/nats_stream.adoc b/docs/modules/components/pages/inputs/nats_stream.adoc index 9f08cee74b..9a2c3a3253 100644 --- a/docs/modules/components/pages/inputs/nats_stream.adoc +++ b/docs/modules/components/pages/inputs/nats_stream.adoc @@ -79,7 +79,7 @@ input: [CAUTION] .Deprecation notice ==== -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream^]. ==== Tracking and persisting offsets through a durable name is also optional and works with or without a queue. If a durable name is not provided then subjects are consumed from the most recently published message. @@ -102,10 +102,10 @@ You can access these metadata fields using xref:configuration:interpolation.adoc == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -113,21 +113,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -268,8 +268,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -337,8 +336,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -369,9 +367,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -429,8 +429,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -443,8 +442,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/nsq.adoc b/docs/modules/components/pages/inputs/nsq.adoc index 61f371bdd6..ca74ae64ef 100644 --- a/docs/modules/components/pages/inputs/nsq.adoc +++ b/docs/modules/components/pages/inputs/nsq.adoc @@ -138,8 +138,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -207,8 +206,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -239,9 +237,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/parquet.adoc b/docs/modules/components/pages/inputs/parquet.adoc index b890e519df..730f8ea03e 100644 --- a/docs/modules/components/pages/inputs/parquet.adoc +++ b/docs/modules/components/pages/inputs/parquet.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Reads and decodes https://parquet.apache.org/docs/[Parquet files] into a stream of structured messages. +Reads and decodes https://parquet.apache.org/docs/[Parquet files^] into a stream of structured messages. Introduced in version 4.8.0. @@ -53,7 +53,7 @@ input: -- ====== -This input uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This input uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go^], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. By default any BYTE_ARRAY or FIXED_LEN_BYTE_ARRAY value will be extracted as a byte slice (`[]byte`) unless the logical type is UTF8, in which case they are extracted as a string (`string`). diff --git a/docs/modules/components/pages/inputs/pulsar.adoc b/docs/modules/components/pages/inputs/pulsar.adoc index 983fae4f9c..4993f812ae 100644 --- a/docs/modules/components/pages/inputs/pulsar.adoc +++ b/docs/modules/components/pages/inputs/pulsar.adoc @@ -139,7 +139,7 @@ Specify the subscription name for this consumer. Specify the subscription type for this consumer. -> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement[Pulsar documentation] and https://github.com/apache/pulsar/issues/12208[this Github issue] for more details. +> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement[Pulsar documentation^] and https://github.com/apache/pulsar/issues/12208[this Github issue^] for more details. *Type*: `string` diff --git a/docs/modules/components/pages/inputs/redis_list.adoc b/docs/modules/components/pages/inputs/redis_list.adoc index 95a98319fa..1646f67c2b 100644 --- a/docs/modules/components/pages/inputs/redis_list.adoc +++ b/docs/modules/components/pages/inputs/redis_list.adoc @@ -163,8 +163,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -232,8 +231,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -264,9 +262,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/redis_pubsub.adoc b/docs/modules/components/pages/inputs/redis_pubsub.adoc index 586093fca5..7fa4123bdd 100644 --- a/docs/modules/components/pages/inputs/redis_pubsub.adoc +++ b/docs/modules/components/pages/inputs/redis_pubsub.adoc @@ -170,8 +170,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -239,8 +238,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -271,9 +269,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/redis_scan.adoc b/docs/modules/components/pages/inputs/redis_scan.adoc index 7190bbcd3d..fc997d6dfc 100644 --- a/docs/modules/components/pages/inputs/redis_scan.adoc +++ b/docs/modules/components/pages/inputs/redis_scan.adoc @@ -173,8 +173,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -242,8 +241,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -274,9 +272,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/redis_streams.adoc b/docs/modules/components/pages/inputs/redis_streams.adoc index 581f56edfc..408787b7f2 100644 --- a/docs/modules/components/pages/inputs/redis_streams.adoc +++ b/docs/modules/components/pages/inputs/redis_streams.adoc @@ -174,8 +174,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -243,8 +242,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -275,9 +273,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/sftp.adoc b/docs/modules/components/pages/inputs/sftp.adoc index eb5d13152f..345d0d3af6 100644 --- a/docs/modules/components/pages/inputs/sftp.adoc +++ b/docs/modules/components/pages/inputs/sftp.adoc @@ -119,8 +119,7 @@ The username to connect to the SFTP server. === `credentials.password` The password for the username to connect to the SFTP server. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -143,8 +142,7 @@ The private key for the username to connect to the SFTP server. === `credentials.private_key_pass` Optional passphrase for private key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/inputs/sql_raw.adoc b/docs/modules/components/pages/inputs/sql_raw.adoc index b3c40785a9..1fada44254 100644 --- a/docs/modules/components/pages/inputs/sql_raw.adoc +++ b/docs/modules/components/pages/inputs/sql_raw.adoc @@ -161,9 +161,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/inputs/sql_select.adoc b/docs/modules/components/pages/inputs/sql_select.adoc index d9dc1e7738..fb2be22256 100644 --- a/docs/modules/components/pages/inputs/sql_select.adoc +++ b/docs/modules/components/pages/inputs/sql_select.adoc @@ -169,9 +169,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/inputs/twitter_search.adoc b/docs/modules/components/pages/inputs/twitter_search.adoc index 9c885ba304..b23d1afa41 100644 --- a/docs/modules/components/pages/inputs/twitter_search.adoc +++ b/docs/modules/components/pages/inputs/twitter_search.adoc @@ -62,13 +62,13 @@ input: -- ====== -Continuously polls the [Twitter recent search V2 API](https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent) for tweets that match a given search query. +Continuously polls the https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent[Twitter recent search V2 API^] for tweets that match a given search query. -Each tweet received is emitted as a JSON object message, with a field `id` and `text` by default. Extra fields [can be obtained from the search API](https://developer.twitter.com/en/docs/twitter-api/fields) when listed with the `tweet_fields` field. +Each tweet received is emitted as a JSON object message, with a field `id` and `text` by default. Extra fields https://developer.twitter.com/en/docs/twitter-api/fields[can be obtained from the search API^] when listed with the `tweet_fields` field. -In order to paginate requests that are made the ID of the latest received tweet is stored in a [cache resource](/docs/components/caches/about), which is then used by subsequent requests to ensure only tweets after it are consumed. It is recommended that the cache you use is persistent so that Benthos can resume searches at the correct place on a restart. +In order to paginate requests that are made the ID of the latest received tweet is stored in a xref:components:caches/about.adoc[cache resource], which is then used by subsequent requests to ensure only tweets after it are consumed. It is recommended that the cache you use is persistent so that Benthos can resume searches at the correct place on a restart. -Authentication is done using OAuth 2.0 credentials which can be generated within the [Twitter developer portal](https://developer.twitter.com). +Authentication is done using OAuth 2.0 credentials which can be generated within the https://developer.twitter.com[Twitter developer portal^]. == Fields @@ -83,7 +83,7 @@ A search expression to use. === `tweet_fields` -An optional list of additional fields to obtain for each tweet, by default only the fields `id` and `text` are returned. For more info refer to the [twitter API docs.](https://developer.twitter.com/en/docs/twitter-api/fields) +An optional list of additional fields to obtain for each tweet, by default only the fields `id` and `text` are returned. For more info refer to the https://developer.twitter.com/en/docs/twitter-api/fields[twitter API docs^]. *Type*: `array` @@ -136,7 +136,7 @@ An optional rate limit resource to restrict API requests with. === `api_key` -An API key for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). +An API key for OAuth 2.0 authentication. It is recommended that you populate this field using xref:configuration:interpolation.adoc[environment variables]. *Type*: `string` @@ -144,7 +144,7 @@ An API key for OAuth 2.0 authentication. It is recommended that you populate thi === `api_secret` -An API secret for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). +An API secret for OAuth 2.0 authentication. It is recommended that you populate this field using xref:configuration:interpolation.adoc[environment variables]. *Type*: `string` diff --git a/docs/modules/components/pages/inputs/websocket.adoc b/docs/modules/components/pages/inputs/websocket.adoc index 1021e0f975..542cc16767 100644 --- a/docs/modules/components/pages/inputs/websocket.adoc +++ b/docs/modules/components/pages/inputs/websocket.adoc @@ -170,8 +170,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -239,8 +238,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -271,9 +269,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -345,8 +345,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -369,8 +368,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -410,8 +408,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/logger/about.adoc b/docs/modules/components/pages/logger/about.adoc index 2645340a66..6a5bd48109 100644 --- a/docs/modules/components/pages/logger/about.adoc +++ b/docs/modules/components/pages/logger/about.adoc @@ -8,7 +8,7 @@ internal/log/docs.adoc //// -{page-component-title} logging prints to stdout (or stderr if your output is stdout) and is formatted as https://brandur.org/logfmt[logfmt] by default. Use these configuration options to change both the logging formats as well as the destination of logs. +{page-component-title} logging prints to stdout (or stderr if your output is stdout) and is formatted as https://brandur.org/logfmt[logfmt^] by default. Use these configuration options to change both the logging formats as well as the destination of logs. [tabs] ====== diff --git a/docs/modules/components/pages/metrics/aws_cloudwatch.adoc b/docs/modules/components/pages/metrics/aws_cloudwatch.adoc index 63682805c5..9ade92dcbf 100644 --- a/docs/modules/components/pages/metrics/aws_cloudwatch.adoc +++ b/docs/modules/components/pages/metrics/aws_cloudwatch.adoc @@ -147,8 +147,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -170,7 +169,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/metrics/influxdb.adoc b/docs/modules/components/pages/metrics/influxdb.adoc index 1c9f0f0c16..93fd96f705 100644 --- a/docs/modules/components/pages/metrics/influxdb.adoc +++ b/docs/modules/components/pages/metrics/influxdb.adoc @@ -129,8 +129,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -198,8 +197,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -230,9 +228,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -263,8 +263,7 @@ A username (when applicable). === `password` A password (when applicable). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/metrics/prometheus.adoc b/docs/modules/components/pages/metrics/prometheus.adoc index cb41b91404..7716f69a3f 100644 --- a/docs/modules/components/pages/metrics/prometheus.adoc +++ b/docs/modules/components/pages/metrics/prometheus.adoc @@ -188,8 +188,7 @@ The Basic Authentication username. === `push_basic_auth.password` The Basic Authentication password. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -211,7 +210,7 @@ An optional file path to write all prometheus metrics on service shutdown. == Push gateway -The field `push_url` is optional and when set will trigger a push of metrics to a https://prometheus.io/docs/instrumenting/pushing/[Prometheus Push Gateway] once Benthos shuts down. It is also possible to specify a `push_interval` which results in periodic pushes. +The field `push_url` is optional and when set will trigger a push of metrics to a https://prometheus.io/docs/instrumenting/pushing/[Prometheus Push Gateway^] once Benthos shuts down. It is also possible to specify a `push_interval` which results in periodic pushes. The Push Gateway is useful for when Benthos instances are short lived. Do not include the "/metrics/jobs/..." path in the push URL. diff --git a/docs/modules/components/pages/metrics/statsd.adoc b/docs/modules/components/pages/metrics/statsd.adoc index d7634172bb..660a2341a8 100644 --- a/docs/modules/components/pages/metrics/statsd.adoc +++ b/docs/modules/components/pages/metrics/statsd.adoc @@ -14,7 +14,7 @@ component_type_dropdown::[] -Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. Supported tagging formats are 'none', 'datadog' and 'influxdb'. +Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol^]. Supported tagging formats are 'none', 'datadog' and 'influxdb'. ```yml # Config fields, showing default values diff --git a/docs/modules/components/pages/outputs/amqp_0_9.adoc b/docs/modules/components/pages/outputs/amqp_0_9.adoc index df08fb135e..33857cc4be 100644 --- a/docs/modules/components/pages/outputs/amqp_0_9.adoc +++ b/docs/modules/components/pages/outputs/amqp_0_9.adoc @@ -389,8 +389,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -458,8 +457,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -490,9 +488,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/amqp_1.adoc b/docs/modules/components/pages/outputs/amqp_1.adoc index 65c8fa77a9..69e9033f43 100644 --- a/docs/modules/components/pages/outputs/amqp_1.adoc +++ b/docs/modules/components/pages/outputs/amqp_1.adoc @@ -167,8 +167,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -236,8 +235,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -268,9 +266,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -344,8 +344,7 @@ user: ${USER} === `sasl.password` A SASL plain text password. It is recommended that you use environment variables to populate this field. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/aws_dynamodb.adoc b/docs/modules/components/pages/outputs/aws_dynamodb.adoc index 48d6366690..11d5dd1d6a 100644 --- a/docs/modules/components/pages/outputs/aws_dynamodb.adoc +++ b/docs/modules/components/pages/outputs/aws_dynamodb.adoc @@ -348,8 +348,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -371,7 +370,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/aws_kinesis.adoc b/docs/modules/components/pages/outputs/aws_kinesis.adoc index e1b5b8e068..d7627aa95c 100644 --- a/docs/modules/components/pages/outputs/aws_kinesis.adoc +++ b/docs/modules/components/pages/outputs/aws_kinesis.adoc @@ -287,8 +287,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -310,7 +309,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc b/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc index 65e718a3a5..b1f4ef08bc 100644 --- a/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc +++ b/docs/modules/components/pages/outputs/aws_kinesis_firehose.adoc @@ -257,8 +257,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -280,7 +279,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/aws_s3.adoc b/docs/modules/components/pages/outputs/aws_s3.adoc index f5b6d1b0e8..9fc015ab31 100644 --- a/docs/modules/components/pages/outputs/aws_s3.adoc +++ b/docs/modules/components/pages/outputs/aws_s3.adoc @@ -496,8 +496,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -519,7 +518,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/aws_sns.adoc b/docs/modules/components/pages/outputs/aws_sns.adoc index 6e92068453..0a166b4342 100644 --- a/docs/modules/components/pages/outputs/aws_sns.adoc +++ b/docs/modules/components/pages/outputs/aws_sns.adoc @@ -192,8 +192,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -215,7 +214,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/aws_sqs.adoc b/docs/modules/components/pages/outputs/aws_sqs.adoc index b88b8d5235..325135bc30 100644 --- a/docs/modules/components/pages/outputs/aws_sqs.adoc +++ b/docs/modules/components/pages/outputs/aws_sqs.adoc @@ -314,8 +314,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -337,7 +336,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/azure_blob_storage.adoc b/docs/modules/components/pages/outputs/azure_blob_storage.adoc index 045343382b..1c2189b48e 100644 --- a/docs/modules/components/pages/outputs/azure_blob_storage.adoc +++ b/docs/modules/components/pages/outputs/azure_blob_storage.adoc @@ -72,7 +72,7 @@ Supports multiple authentication methods but only one of the following is requir - `storage_connection_string` - `storage_account` and `storage_access_key` - `storage_account` and `storage_sas_token` -- `storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- `storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^] If multiple are set then the `storage_connection_string` is given priority. diff --git a/docs/modules/components/pages/outputs/azure_cosmosdb.adoc b/docs/modules/components/pages/outputs/azure_cosmosdb.adoc index 45f47c1c79..57194e3959 100644 --- a/docs/modules/components/pages/outputs/azure_cosmosdb.adoc +++ b/docs/modules/components/pages/outputs/azure_cosmosdb.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]. +Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB^]. Introduced in version v4.25.0. @@ -80,7 +80,7 @@ output: -- ====== -When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. +When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details^]. The `partition_keys` field must resolve to the same value(s) across the entire message batch. @@ -90,13 +90,13 @@ The `partition_keys` field must resolve to the same value(s) across the entire m You can use one of the following authentication mechanisms: - Set the `endpoint` field and the `account_key` field -- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^] - Set the `connection_string` field == Batching -CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here]). +CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here^]). == Performance @@ -179,8 +179,7 @@ endpoint: https://localhost:8081 === `account_key` Account key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -199,8 +198,7 @@ account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZ === `connection_string` Connection string. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -513,7 +511,7 @@ The maximum number of messages to have in flight at a given time. Increase this == CosmosDB emulator -If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here^], the following Docker command should do the trick: ```bash > docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator @@ -521,7 +519,7 @@ If you wish to run the CosmosDB emulator that is referenced in the documentation Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. -Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy] like so: +Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy^] like so: ```bash > mitmproxy -k --mode "reverse:https://localhost:8081" diff --git a/docs/modules/components/pages/outputs/cassandra.adoc b/docs/modules/components/pages/outputs/cassandra.adoc index b926013fb3..a37a781dc9 100644 --- a/docs/modules/components/pages/outputs/cassandra.adoc +++ b/docs/modules/components/pages/outputs/cassandra.adoc @@ -206,8 +206,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -275,8 +274,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -307,9 +305,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -357,8 +357,7 @@ The username to authenticate as. === `password_authenticator.password` The password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/discord.adoc b/docs/modules/components/pages/outputs/discord.adoc index ba7cb2bb1d..8719d7b428 100644 --- a/docs/modules/components/pages/outputs/discord.adoc +++ b/docs/modules/components/pages/outputs/discord.adoc @@ -28,7 +28,7 @@ output: This output POSTs messages to the `/channels/\{channel_id}/messages` Discord API endpoint authenticated as a bot using token based authentication. -If the format of a message is a JSON object matching the https://discord.com/developers/docs/resources/channel#message-object[Discord API message type] then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. +If the format of a message is a JSON object matching the https://discord.com/developers/docs/resources/channel#message-object[Discord API message type^] then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. == Fields diff --git a/docs/modules/components/pages/outputs/elasticsearch.adoc b/docs/modules/components/pages/outputs/elasticsearch.adoc index 4ca54b7213..9a97015480 100644 --- a/docs/modules/components/pages/outputs/elasticsearch.adoc +++ b/docs/modules/components/pages/outputs/elasticsearch.adoc @@ -256,8 +256,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -325,8 +324,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -357,9 +355,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -460,8 +460,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -638,8 +637,7 @@ The ID of credentials to use. === `aws.credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -661,7 +659,7 @@ The token for the credentials being used, required when using short term credent === `aws.credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/gcp_bigquery.adoc b/docs/modules/components/pages/outputs/gcp_bigquery.adoc index 1551013e65..787ab7c257 100644 --- a/docs/modules/components/pages/outputs/gcp_bigquery.adoc +++ b/docs/modules/components/pages/outputs/gcp_bigquery.adoc @@ -93,8 +93,8 @@ By default Benthos will use a shared credentials file when connecting to GCP ser == Format This output currently supports only CSV and NEWLINE_DELIMITED_JSON formats. Learn more about how to use GCP BigQuery with them here: -- https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json[`NEWLINE_DELIMITED_JSON`] -- https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv[`CSV`] +- https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json[`NEWLINE_DELIMITED_JSON`^] +- https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv[`CSV`^] Each message may contain multiple elements separated by newlines. For example a single message containing: diff --git a/docs/modules/components/pages/outputs/gcp_pubsub.adoc b/docs/modules/components/pages/outputs/gcp_pubsub.adoc index 8d426778ce..605883608b 100644 --- a/docs/modules/components/pages/outputs/gcp_pubsub.adoc +++ b/docs/modules/components/pages/outputs/gcp_pubsub.adoc @@ -81,7 +81,7 @@ output: -- ====== -For information on how to set up credentials, see https://cloud.google.com/docs/authentication/production[this guide]. +For information on how to set up credentials, see https://cloud.google.com/docs/authentication/production[this guide^]. == Troubleshooting @@ -125,7 +125,7 @@ This field supports xref:configuration:interpolation.adoc#bloblang-queries[inter === `endpoint` -An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]. +An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document^]. *Type*: `string` diff --git a/docs/modules/components/pages/outputs/http_client.adoc b/docs/modules/components/pages/outputs/http_client.adoc index 633f70f79e..4f5a0cfa8e 100644 --- a/docs/modules/components/pages/outputs/http_client.adoc +++ b/docs/modules/components/pages/outputs/http_client.adoc @@ -121,7 +121,7 @@ When the number of retries expires the output will reject the message, the behav The URL and header values of this type can be dynamically set using function interpolations described in xref:configuration:interpolation.adoc#bloblang-queries[Bloblang queries]. -The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be disabled by setting the field <> to `false`. +The body of the HTTP request is the raw contents of the message payload. If the message has multiple parts (is a batch) the request will be sent according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341^]. This behavior can be disabled by setting the field <> to `false`. == Propagate responses @@ -280,8 +280,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -304,8 +303,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -345,8 +343,7 @@ A value used to identify the client to the token provider. === `oauth2.client_secret` A secret used to establish ownership of the client key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -426,8 +423,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -530,8 +526,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -599,8 +594,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -631,9 +625,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -783,7 +779,7 @@ An optional HTTP proxy URL. === `batch_as_multipart` -Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. If disabled messages in batches will be sent as individual requests. +Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341^]. If disabled messages in batches will be sent as individual requests. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/http_server.adoc b/docs/modules/components/pages/outputs/http_server.adoc index 2315fc40e2..1ccc2bb32e 100644 --- a/docs/modules/components/pages/outputs/http_server.adoc +++ b/docs/modules/components/pages/outputs/http_server.adoc @@ -68,7 +68,7 @@ Sets up an HTTP server that will send messages over HTTP(S) GET requests. If the Three endpoints will be registered at the paths specified by the fields `path`, `stream_path` and `ws_path`. Which allow you to consume a single message batch, a continuous stream of line delimited messages, or a websocket of messages for each request respectively. -When messages are batched the `path` endpoint encodes the batch according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. This behavior can be overridden by xref:configuration:batching.adoc#post-batch-processing[archiving your batches]. +When messages are batched the `path` endpoint encodes the batch according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341^]. This behavior can be overridden by xref:configuration:batching.adoc#post-batch-processing[archiving your batches]. Please note, messages are considered delivered as soon as the data is written to the client. There is no concept of at least once delivery on this output. diff --git a/docs/modules/components/pages/outputs/kafka.adoc b/docs/modules/components/pages/outputs/kafka.adoc index c4311586d2..7296857ed0 100644 --- a/docs/modules/components/pages/outputs/kafka.adoc +++ b/docs/modules/components/pages/outputs/kafka.adoc @@ -200,8 +200,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -269,8 +268,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -301,9 +299,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -373,8 +373,7 @@ user: ${USER} === `sasl.password` A PLAIN password. It is recommended that you use environment variables to populate this field. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/kafka_franz.adoc b/docs/modules/components/pages/outputs/kafka_franz.adoc index 76c975d643..958e72e480 100644 --- a/docs/modules/components/pages/outputs/kafka_franz.adoc +++ b/docs/modules/components/pages/outputs/kafka_franz.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -A Kafka output using the https://github.com/twmb/franz-go[Franz Kafka client library]. +A Kafka output using the https://github.com/twmb/franz-go[Franz Kafka client library^]. Introduced in version 3.61.0. @@ -443,8 +443,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -512,8 +511,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -544,9 +542,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -620,8 +620,7 @@ A username to provide for PLAIN or SCRAM-* authentication. === `sasl[].password` A password to provide for PLAIN or SCRAM-* authentication. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -704,8 +703,7 @@ The ID of credentials to use. === `sasl[].aws.credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -727,7 +725,7 @@ The token for the credentials being used, required when using short term credent === `sasl[].aws.credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/mongodb.adoc b/docs/modules/components/pages/outputs/mongodb.adoc index e1cbbe1192..fa4d74208a 100644 --- a/docs/modules/components/pages/outputs/mongodb.adoc +++ b/docs/modules/components/pages/outputs/mongodb.adoc @@ -132,8 +132,7 @@ The username to connect to the database. === `password` The password to connect to the database. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -206,7 +205,7 @@ The write concern timeout. === `document_map` -A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The document map is required for the operations insert-one, replace-one and update-one. +A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. The document map is required for the operations insert-one, replace-one and update-one. *Type*: `string` @@ -223,7 +222,7 @@ document_map: |- === `filter_map` -A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. +A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. *Type*: `string` @@ -240,7 +239,7 @@ filter_map: |- === `hint_map` -A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. +A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. *Type*: `string` diff --git a/docs/modules/components/pages/outputs/mqtt.adoc b/docs/modules/components/pages/outputs/mqtt.adoc index 773dde6d36..99adb0324d 100644 --- a/docs/modules/components/pages/outputs/mqtt.adoc +++ b/docs/modules/components/pages/outputs/mqtt.adoc @@ -211,8 +211,7 @@ A username to connect with. === `password` A password to connect with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -271,8 +270,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -340,8 +338,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -372,9 +369,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/nats.adoc b/docs/modules/components/pages/outputs/nats.adoc index c18e9e9021..4c15d7ed72 100644 --- a/docs/modules/components/pages/outputs/nats.adoc +++ b/docs/modules/components/pages/outputs/nats.adoc @@ -88,10 +88,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -99,21 +99,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -266,8 +266,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -335,8 +334,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -367,9 +365,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -427,8 +427,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -441,8 +440,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/nats_jetstream.adoc b/docs/modules/components/pages/outputs/nats_jetstream.adoc index a4dbdf87ee..31ad711d11 100644 --- a/docs/modules/components/pages/outputs/nats_jetstream.adoc +++ b/docs/modules/components/pages/outputs/nats_jetstream.adoc @@ -88,10 +88,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -99,21 +99,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -271,8 +271,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -340,8 +339,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -372,9 +370,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -432,8 +432,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -446,8 +445,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/nats_kv.adoc b/docs/modules/components/pages/outputs/nats_kv.adoc index 67715fef2c..62d2f4945a 100644 --- a/docs/modules/components/pages/outputs/nats_kv.adoc +++ b/docs/modules/components/pages/outputs/nats_kv.adoc @@ -85,10 +85,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -96,21 +96,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -213,8 +213,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -282,8 +281,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -314,9 +312,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -374,8 +374,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -388,8 +387,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/nats_stream.adoc b/docs/modules/components/pages/outputs/nats_stream.adoc index ca6435dd88..78c032e946 100644 --- a/docs/modules/components/pages/outputs/nats_stream.adoc +++ b/docs/modules/components/pages/outputs/nats_stream.adoc @@ -72,7 +72,7 @@ output: [CAUTION] .Deprecation notice ==== -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream^]. ==== @@ -80,10 +80,10 @@ The NATS Streaming Server is being deprecated. Critical bug fixes and security f == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -91,21 +91,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Performance @@ -204,8 +204,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -273,8 +272,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -305,9 +303,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -365,8 +365,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -379,8 +378,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/nsq.adoc b/docs/modules/components/pages/outputs/nsq.adoc index 1f259f81cc..a25047b32f 100644 --- a/docs/modules/components/pages/outputs/nsq.adoc +++ b/docs/modules/components/pages/outputs/nsq.adoc @@ -133,8 +133,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -202,8 +201,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -234,9 +232,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/opensearch.adoc b/docs/modules/components/pages/outputs/opensearch.adoc index bf3f5e540b..67ef72482f 100644 --- a/docs/modules/components/pages/outputs/opensearch.adoc +++ b/docs/modules/components/pages/outputs/opensearch.adoc @@ -108,7 +108,7 @@ Updating Documents:: + -- -When https://opensearch.org/docs/latest/api-reference/document-apis/update-document/[updating documents] the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors. +When https://opensearch.org/docs/latest/api-reference/document-apis/update-document/[updating documents^] the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors. ```yaml output: @@ -235,8 +235,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -304,8 +303,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -336,9 +334,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -395,8 +395,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -573,8 +572,7 @@ The ID of credentials to use. === `aws.credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -596,7 +594,7 @@ The token for the credentials being used, required when using short term credent === `aws.credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/outputs/redis_hash.adoc b/docs/modules/components/pages/outputs/redis_hash.adoc index 0b17908873..70c82fcf54 100644 --- a/docs/modules/components/pages/outputs/redis_hash.adoc +++ b/docs/modules/components/pages/outputs/redis_hash.adoc @@ -197,8 +197,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -266,8 +265,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -298,9 +296,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/redis_list.adoc b/docs/modules/components/pages/outputs/redis_list.adoc index 3cf474597a..812f215a5c 100644 --- a/docs/modules/components/pages/outputs/redis_list.adoc +++ b/docs/modules/components/pages/outputs/redis_list.adoc @@ -180,8 +180,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -249,8 +248,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -281,9 +279,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/redis_pubsub.adoc b/docs/modules/components/pages/outputs/redis_pubsub.adoc index 7ef89ae265..4b80a59950 100644 --- a/docs/modules/components/pages/outputs/redis_pubsub.adoc +++ b/docs/modules/components/pages/outputs/redis_pubsub.adoc @@ -179,8 +179,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -248,8 +247,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -280,9 +278,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/redis_streams.adoc b/docs/modules/components/pages/outputs/redis_streams.adoc index 46d5470258..961397dafd 100644 --- a/docs/modules/components/pages/outputs/redis_streams.adoc +++ b/docs/modules/components/pages/outputs/redis_streams.adoc @@ -189,8 +189,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -258,8 +257,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -290,9 +288,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/sftp.adoc b/docs/modules/components/pages/outputs/sftp.adoc index d2166dba98..52c7e0ee18 100644 --- a/docs/modules/components/pages/outputs/sftp.adoc +++ b/docs/modules/components/pages/outputs/sftp.adoc @@ -113,8 +113,7 @@ The username to connect to the SFTP server. === `credentials.password` The password for the username to connect to the SFTP server. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -137,8 +136,7 @@ The private key for the username to connect to the SFTP server. === `credentials.private_key_pass` Optional passphrase for private key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/outputs/snowflake_put.adoc b/docs/modules/components/pages/outputs/snowflake_put.adoc index 7c41e8dfff..3b0566fa9b 100644 --- a/docs/modules/components/pages/outputs/snowflake_put.adoc +++ b/docs/modules/components/pages/outputs/snowflake_put.adoc @@ -118,10 +118,10 @@ Snowpipe. === Key pair authentication This authentication mechanism allows Snowpipe functionality, but it does require configuring an SSH Private Key -beforehand. Please consult the https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[documentation] +beforehand. Please consult the https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[documentation^] for details on how to set it up and assign the Public Key to your user. -Note that the Snowflake documentation https://twitter.com/felipehoffa/status/1560811785606684672[used to suggest] +Note that the Snowflake documentation https://twitter.com/felipehoffa/status/1560811785606684672[used to suggest^] using this command: ```bash @@ -143,7 +143,7 @@ If you have an existing key encrypted with PKCS#5 v1.5, you can re-encrypt it wi openssl pkcs8 -in rsa_key_original.p8 -topk8 -v2 des3 -out rsa_key.p8 ``` -Please consult the https://linux.die.net/man/1/pkcs8[pkcs8 command documentation] for details on PKCS#5 algorithms. +Please consult the https://linux.die.net/man/1/pkcs8[pkcs8 command documentation^] for details on PKCS#5 algorithms. == Batching @@ -152,7 +152,7 @@ messages at the output level and join the batch of messages with an xref:components:processors/archive.adoc[`archive`] and/or xref:components:processors/compress.adoc[`compress`] processor. -For the optimal batch size, please consult the Snowflake https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html[documentation]. +For the optimal batch size, please consult the Snowflake https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html[documentation^]. == Snowpipe @@ -172,7 +172,7 @@ you can configure Benthos to use the implicit table stage `@%BENTHOS_TBL` as the `BENTHOS_PIPE` as the `snowpipe`. In this case, you must set `compression` to `AUTO` and, if using message batching, you'll need to configure an xref:components:processors/archive.adoc[`archive`] processor with the `concatenate` format. Since the `compression` is set to `AUTO`, the -https://github.com/snowflakedb/gosnowflake[gosnowflake] client library will compress the messages automatically so you +https://github.com/snowflakedb/gosnowflake[gosnowflake^] client library will compress the messages automatically so you don't need to add a xref:components:processors/compress.adoc[`compress`] processor for message batches. If you add `STRIP_OUTER_ARRAY = TRUE` in your Snowpipe `FILE_FORMAT` @@ -182,17 +182,17 @@ NOTE: Only Snowpipes with `FILE_FORMAT` `TYPE` `JSON` are currently supported. == Snowpipe troubleshooting -Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html[provides] the `insertReport` +Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html[provides^] the `insertReport` and `loadHistoryScan` REST API endpoints which can be used to get information about recent Snowpipe calls. In order to query them, you'll first need to generate a valid JWT token for your Snowflake account. There are two methods for doing so: -- Using the `snowsql` https://docs.snowflake.com/en/user-guide/snowsql.html[utility]: +- Using the `snowsql` https://docs.snowflake.com/en/user-guide/snowsql.html[utility^]: ```bash snowsql --private-key-path rsa_key.p8 --generate-jwt -a -u ``` -- Using the Python `sql-api-generate-jwt` https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python[utility]: +- Using the Python `sql-api-generate-jwt` https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python[utility^]: ```bash python3 sql-api-generate-jwt.py --private_key_file_path=rsa_key.p8 --account= --user= @@ -213,12 +213,12 @@ then configure `request_id: ${ @request_id }` ). Alternatively, you can xref:com == General troubleshooting -The underlying https://github.com/snowflakedb/gosnowflake[`gosnowflake` driver] requires write access to -the default directory to use for temporary files. Please consult the https://pkg.go.dev/os#TempDir[`os.TempDir`] +The underlying https://github.com/snowflakedb/gosnowflake[`gosnowflake` driver^] requires write access to +the default directory to use for temporary files. Please consult the https://pkg.go.dev/os#TempDir[`os.TempDir`^] docs for details on how to change this directory via environment variables. -A silent failure can occur due to https://github.com/snowflakedb/gosnowflake/issues/701[this issue], where the -underlying https://github.com/snowflakedb/gosnowflake[`gosnowflake` driver] doesn't return an error and doesn't +A silent failure can occur due to https://github.com/snowflakedb/gosnowflake/issues/701[this issue^], where the +underlying https://github.com/snowflakedb/gosnowflake[`gosnowflake` driver^] doesn't return an error and doesn't log a failure if it can't figure out the current username. One way to trigger this behavior is by running Benthos in a Docker container with a non-existent user ID (such as `--user 1000:1000`). @@ -237,7 +237,7 @@ Kafka / realtime brokers:: + -- -Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table[Kafka Connector scheme] and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition. +Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table[Kafka Connector scheme^] and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition. ```yaml input: @@ -439,8 +439,8 @@ output: === `account` -Account name, which is the same as the https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used[Account Identifier]. -However, when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator], +Account name, which is the same as the https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used[Account Identifier^]. +However, when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator^], the Account Identifier is formatted as `..` and this field needs to be populated using the `` part. @@ -451,7 +451,7 @@ populated using the `` part. === `region` Optional region field which needs to be populated when using -an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] +an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator^] and it must be set to the `` part of the Account Identifier (`..`). @@ -468,7 +468,7 @@ region: us-west-2 === `cloud` Optional cloud platform field which needs to be populated -when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] +when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator^] and it must be set to the `` part of the Account Identifier (`..`). @@ -497,8 +497,7 @@ Username. === `password` An optional password. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -519,8 +518,7 @@ The path to a file containing the private SSH key. === `private_key_pass` An optional private SSH key passphrase. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -565,7 +563,7 @@ Schema. === `stage` Stage name. Use either one of the - https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html[supported] stage types. + https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html[supported^] stage types. This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. diff --git a/docs/modules/components/pages/outputs/splunk_hec.adoc b/docs/modules/components/pages/outputs/splunk_hec.adoc index 548b67dabe..2b118ed4f0 100644 --- a/docs/modules/components/pages/outputs/splunk_hec.adoc +++ b/docs/modules/components/pages/outputs/splunk_hec.adoc @@ -69,7 +69,7 @@ output: -- ====== -This output POSTs messages to a Splunk HTTP Endpoint Collector (HEC) using token based authentication. The format of the message must be a [valid event JSON](https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/FormateventsforHTTPEventCollector). Raw is not supported. +This output POSTs messages to a Splunk HTTP Endpoint Collector (HEC) using token based authentication. The format of the message must be a https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/FormateventsforHTTPEventCollector[valid event JSON^]. Raw is not supported. == Fields diff --git a/docs/modules/components/pages/outputs/sql_insert.adoc b/docs/modules/components/pages/outputs/sql_insert.adoc index c6e14103aa..32537b1fb6 100644 --- a/docs/modules/components/pages/outputs/sql_insert.adoc +++ b/docs/modules/components/pages/outputs/sql_insert.adoc @@ -177,9 +177,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/outputs/sql_raw.adoc b/docs/modules/components/pages/outputs/sql_raw.adoc index 2fb6a0ef0b..0b5c391806 100644 --- a/docs/modules/components/pages/outputs/sql_raw.adoc +++ b/docs/modules/components/pages/outputs/sql_raw.adoc @@ -173,9 +173,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/outputs/websocket.adoc b/docs/modules/components/pages/outputs/websocket.adoc index 6da8e243a2..f6ab752d86 100644 --- a/docs/modules/components/pages/outputs/websocket.adoc +++ b/docs/modules/components/pages/outputs/websocket.adoc @@ -120,8 +120,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -189,8 +188,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -221,9 +219,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -271,8 +271,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -295,8 +294,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -336,8 +334,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/archive.adoc b/docs/modules/components/pages/processors/archive.adoc index 3d99683ab4..ff314c755a 100644 --- a/docs/modules/components/pages/processors/archive.adoc +++ b/docs/modules/components/pages/processors/archive.adoc @@ -45,7 +45,7 @@ The archiving format to apply. | Option | Summary | `binary` -| Archive messages to a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format]. +| Archive messages to a https://github.com/{project-github}/blob/main/internal/message/message.go#L96[binary blob format^]. | `concatenate` | Join the raw contents of each message into a single binary message. | `json_array` diff --git a/docs/modules/components/pages/processors/awk.adoc b/docs/modules/components/pages/processors/awk.adoc index a9b6f3b129..30110cc079 100644 --- a/docs/modules/components/pages/processors/awk.adoc +++ b/docs/modules/components/pages/processors/awk.adoc @@ -31,7 +31,7 @@ Comes with a wide range of <> for accessing mess Check out the <> in order to see how this processor can be used. -This processor uses https://github.com/benhoyt/goawk[GoAWK], in order to understand the differences in how the program works you can read more about it in https://github.com/benhoyt/goawk#differences-from-awk[goawk.differences]. +This processor uses https://github.com/benhoyt/goawk[GoAWK^], in order to understand the differences in how the program works you can read more about it in https://github.com/benhoyt/goawk#differences-from-awk[goawk.differences^]. == Fields diff --git a/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc b/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc index ef9dccec62..eb7aad9cc0 100644 --- a/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc +++ b/docs/modules/components/pages/processors/aws_dynamodb_partiql.adoc @@ -164,8 +164,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -187,7 +186,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/processors/aws_lambda.adoc b/docs/modules/components/pages/processors/aws_lambda.adoc index 3f4c330b83..4022fbca94 100644 --- a/docs/modules/components/pages/processors/aws_lambda.adoc +++ b/docs/modules/components/pages/processors/aws_lambda.adoc @@ -200,8 +200,7 @@ The ID of credentials to use. === `credentials.secret` The secret for the credentials being used. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -223,7 +222,7 @@ The token for the credentials being used, required when using short term credent === `credentials.from_ec2_role` -Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]. +Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]. *Type*: `bool` diff --git a/docs/modules/components/pages/processors/azure_cosmosdb.adoc b/docs/modules/components/pages/processors/azure_cosmosdb.adoc index d5b6cb5899..42b3034400 100644 --- a/docs/modules/components/pages/processors/azure_cosmosdb.adoc +++ b/docs/modules/components/pages/processors/azure_cosmosdb.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]. +Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB^]. Introduced in version v4.25.0. @@ -66,7 +66,7 @@ azure_cosmosdb: -- ====== -When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. +When creating documents, each message must have the `id` property (case-sensitive) set (or use `auto_id: true`). It is the unique name that identifies the document, that is, no two documents share the same `id` within a logical partition. The `id` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details^]. The `partition_keys` field must resolve to the same value(s) across the entire message batch. @@ -76,7 +76,7 @@ The `partition_keys` field must resolve to the same value(s) across the entire m You can use one of the following authentication mechanisms: - Set the `endpoint` field and the `account_key` field -- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set only the `endpoint` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^] - Set the `connection_string` field @@ -93,7 +93,7 @@ You can access these metadata fields using xref:configuration:interpolation.adoc == Batching -CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here]). +CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here^]). == Examples @@ -167,8 +167,7 @@ endpoint: https://localhost:8081 === `account_key` Account key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -187,8 +186,7 @@ account_key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZ === `connection_string` Connection string. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -401,7 +399,7 @@ Enable content response on write operations. To save some bandwidth, set this to == CosmosDB emulator -If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here^], the following Docker command should do the trick: ```bash > docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator @@ -409,7 +407,7 @@ If you wish to run the CosmosDB emulator that is referenced in the documentation Note: `AZURE_COSMOS_EMULATOR_PARTITION_COUNT` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. -Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy] like so: +Additionally, instead of installing the container self-signed certificate which is exposed via `https://localhost:8081/_explorer/emulator.pem`, you can run https://mitmproxy.org/[mitmproxy^] like so: ```bash > mitmproxy -k --mode "reverse:https://localhost:8081" diff --git a/docs/modules/components/pages/processors/couchbase.adoc b/docs/modules/components/pages/processors/couchbase.adoc index 7ff26ea327..aaffacc575 100644 --- a/docs/modules/components/pages/processors/couchbase.adoc +++ b/docs/modules/components/pages/processors/couchbase.adoc @@ -92,8 +92,7 @@ Username to connect to the cluster. === `password` Password to connect to the cluster. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/grok.adoc b/docs/modules/components/pages/processors/grok.adoc index b502b8ea8a..c22e54734d 100644 --- a/docs/modules/components/pages/processors/grok.adoc +++ b/docs/modules/components/pages/processors/grok.adoc @@ -57,7 +57,7 @@ Type hints within patterns are respected, therefore with the pattern `%\{WORD:fi == Performance -This processor currently uses the https://golang.org/s/re2syntax[Go RE2] regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information, see https://swtch.com/~rsc/regexp/regexp1.html. +This processor currently uses the https://golang.org/s/re2syntax[Go RE2^] regular expression engine, which is guaranteed to run in time linear to the size of the input. However, this property often makes it less performant than PCRE based implementations of grok. For more information, see https://swtch.com/~rsc/regexp/regexp1.html. == Examples diff --git a/docs/modules/components/pages/processors/http.adoc b/docs/modules/components/pages/processors/http.adoc index 53060f5707..8fc5c92de3 100644 --- a/docs/modules/components/pages/processors/http.adoc +++ b/docs/modules/components/pages/processors/http.adoc @@ -300,8 +300,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -324,8 +323,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -365,8 +363,7 @@ A value used to identify the client to the token provider. === `oauth2.client_secret` A secret used to establish ownership of the client key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -446,8 +443,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -550,8 +546,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -619,8 +614,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -651,9 +645,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -803,7 +799,7 @@ An optional HTTP proxy URL. === `batch_as_multipart` -Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341]. +Send message batches as a single request using https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html[RFC1341^]. *Type*: `bool` diff --git a/docs/modules/components/pages/processors/javascript.adoc b/docs/modules/components/pages/processors/javascript.adoc index 0165e334b9..79ebf3559b 100644 --- a/docs/modules/components/pages/processors/javascript.adoc +++ b/docs/modules/components/pages/processors/javascript.adoc @@ -28,11 +28,11 @@ javascript: global_folders: [] ``` -The https://github.com/dop251/goja[execution engine] behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. +The https://github.com/dop251/goja[execution engine^] behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. -Imports via `require` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be found on https://github.com/dop251/goja#known-incompatibilities-and-caveats[GitHub]. +Imports via `require` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be found on https://github.com/dop251/goja#known-incompatibilities-and-caveats[GitHub^]. -This processor is implemented using the https://github.com/dop251/goja[github.com/dop251/goja] library. +This processor is implemented using the https://github.com/dop251/goja[github.com/dop251/goja^] library. == Fields diff --git a/docs/modules/components/pages/processors/jq.adoc b/docs/modules/components/pages/processors/jq.adoc index ae0f3fb642..fc4658bfa6 100644 --- a/docs/modules/components/pages/processors/jq.adoc +++ b/docs/modules/components/pages/processors/jq.adoc @@ -58,11 +58,11 @@ The provided query is executed on each message, targeting either the contents as Message metadata is also accessible within the query from the variable `$metadata`. -This processor uses the https://github.com/itchyny/gojq[gojq library], and therefore does not require jq to be installed as a dependency. However, this also means there are some https://github.com/itchyny/gojq#difference-to-jq[differences in how these queries are executed] versus the jq cli. +This processor uses the https://github.com/itchyny/gojq[gojq library^], and therefore does not require jq to be installed as a dependency. However, this also means there are some https://github.com/itchyny/gojq#difference-to-jq[differences in how these queries are executed^] versus the jq cli. If the query does not emit any value then the message is filtered, if the query returns multiple values then the resulting message will be an array containing all values. -The full query syntax is described in https://stedolan.github.io/jq/manual/[jq's documentation]. +The full query syntax is described in https://stedolan.github.io/jq/manual/[jq's documentation^]. == Error handling diff --git a/docs/modules/components/pages/processors/json_schema.adoc b/docs/modules/components/pages/processors/json_schema.adoc index be5db5afe1..38685dcad4 100644 --- a/docs/modules/components/pages/processors/json_schema.adoc +++ b/docs/modules/components/pages/processors/json_schema.adoc @@ -25,7 +25,7 @@ json_schema: schema_path: "" # No default (optional) ``` -Please refer to the https://json-schema.org/[JSON Schema website] for information and tutorials regarding the syntax of the schema. +Please refer to the https://json-schema.org/[JSON Schema website^] for information and tutorials regarding the syntax of the schema. == Fields diff --git a/docs/modules/components/pages/processors/mongodb.adoc b/docs/modules/components/pages/processors/mongodb.adoc index bf7a471dc7..9cd81b5691 100644 --- a/docs/modules/components/pages/processors/mongodb.adoc +++ b/docs/modules/components/pages/processors/mongodb.adoc @@ -111,8 +111,7 @@ The username to connect to the database. === `password` The password to connect to the database. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -186,7 +185,7 @@ The write concern timeout. === `document_map` -A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The document map is required for the operations insert-one, replace-one and update-one. +A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. The document map is required for the operations insert-one, replace-one and update-one. *Type*: `string` @@ -203,7 +202,7 @@ document_map: |- === `filter_map` -A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. +A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. The filter map is required for all operations except insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should have the fields required to locate the document to delete. *Type*: `string` @@ -220,7 +219,7 @@ filter_map: |- === `hint_map` -A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. +A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. This map is optional and is used with all operations except insert-one. It is used to improve performance of finding the documents in the mongodb. *Type*: `string` diff --git a/docs/modules/components/pages/processors/msgpack.adoc b/docs/modules/components/pages/processors/msgpack.adoc index b30c7431b4..833d3da9e7 100644 --- a/docs/modules/components/pages/processors/msgpack.adoc +++ b/docs/modules/components/pages/processors/msgpack.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Converts messages to or from the https://msgpack.org/[MessagePack] format. +Converts messages to or from the https://msgpack.org/[MessagePack^] format. Introduced in version 3.59.0. diff --git a/docs/modules/components/pages/processors/nats_kv.adoc b/docs/modules/components/pages/processors/nats_kv.adoc index 717ce3370b..264f35df3c 100644 --- a/docs/modules/components/pages/processors/nats_kv.adoc +++ b/docs/modules/components/pages/processors/nats_kv.adoc @@ -112,10 +112,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -123,21 +123,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -207,7 +207,7 @@ The operation to perform on the KV bucket. === `key` -The key for each message. Supports https://docs.nats.io/nats-concepts/subjects#wildcards[wildcards] for the `history` and `keys` operations. +The key for each message. Supports https://docs.nats.io/nats-concepts/subjects#wildcards[wildcards^] for the `history` and `keys` operations. This field supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions]. @@ -293,8 +293,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -362,8 +361,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -394,9 +392,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -454,8 +454,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -468,8 +467,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/nats_request_reply.adoc b/docs/modules/components/pages/processors/nats_request_reply.adoc index ffea8cc3f4..7decbd65a4 100644 --- a/docs/modules/components/pages/processors/nats_request_reply.adoc +++ b/docs/modules/components/pages/processors/nats_request_reply.adoc @@ -102,10 +102,10 @@ NATS component, so that monitoring tools between NATS and benthos can stay in sy == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/developing-with-nats/security/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -113,21 +113,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the `nkey_file` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/developing-with-nats/security/nkey[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The `user_credentials_file` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the `user_jwt` field can contain a plain text JWT and the `user_nkey_seed`can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details]. +https://docs.nats.io/developing-with-nats/security/creds[More details^]. == Fields @@ -298,8 +298,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -367,8 +366,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -399,9 +397,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -459,8 +459,7 @@ user_credentials_file: ./user.creds === `auth.user_jwt` An optional plain text user JWT (given along with the corresponding user NKey Seed). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -473,8 +472,7 @@ This field contains sensitive information that usually shouldn't be added to a c === `auth.user_nkey_seed` An optional plain text user NKey Seed (given along with the corresponding user JWT). -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/parquet.adoc b/docs/modules/components/pages/processors/parquet.adoc index cb6645f435..39d700a40f 100644 --- a/docs/modules/components/pages/processors/parquet.adoc +++ b/docs/modules/components/pages/processors/parquet.adoc @@ -20,7 +20,7 @@ component_type_dropdown::[] ==== This component is deprecated and will be removed in the next major version release. Please consider moving onto <>. ==== -Converts batches of documents to or from https://parquet.apache.org/docs/[Parquet files]. +Converts batches of documents to or from https://parquet.apache.org/docs/[Parquet files^]. Introduced in version 3.62.0. diff --git a/docs/modules/components/pages/processors/parquet_decode.adoc b/docs/modules/components/pages/processors/parquet_decode.adoc index f7f5ab745a..94a1382cc4 100644 --- a/docs/modules/components/pages/processors/parquet_decode.adoc +++ b/docs/modules/components/pages/processors/parquet_decode.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Decodes https://parquet.apache.org/docs/[Parquet files] into a batch of structured messages. +Decodes https://parquet.apache.org/docs/[Parquet files^] into a batch of structured messages. Introduced in version 4.4.0. @@ -25,7 +25,7 @@ label: "" parquet_decode: {} ``` -This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go^], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. == Examples diff --git a/docs/modules/components/pages/processors/parquet_encode.adoc b/docs/modules/components/pages/processors/parquet_encode.adoc index 1ebcddf83d..aeb3404dde 100644 --- a/docs/modules/components/pages/processors/parquet_encode.adoc +++ b/docs/modules/components/pages/processors/parquet_encode.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Encodes https://parquet.apache.org/docs/[Parquet files] from a batch of structured messages. +Encodes https://parquet.apache.org/docs/[Parquet files^] from a batch of structured messages. Introduced in version 4.4.0. @@ -51,7 +51,7 @@ parquet_encode: -- ====== -This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go^], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. == Examples diff --git a/docs/modules/components/pages/processors/parse_log.adoc b/docs/modules/components/pages/processors/parse_log.adoc index 4babc61756..eb59132314 100644 --- a/docs/modules/components/pages/processors/parse_log.adoc +++ b/docs/modules/components/pages/processors/parse_log.adoc @@ -94,7 +94,7 @@ Sets the strategy used to set the year for rfc3164 timestamps. Applicable to for === `default_timezone` -Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the https://golang.org/pkg/time/#LoadLocation[time.LoadLocation] format. +Sets the strategy to decide the timezone for rfc3164 timestamps. Applicable to format `syslog_rfc3164`. This value should follow the https://golang.org/pkg/time/#LoadLocation[time.LoadLocation^] format. *Type*: `string` @@ -109,7 +109,7 @@ Currently the only supported structured data codec is `json`. === `syslog_rfc5424` -Attempts to parse a log following the https://tools.ietf.org/html/rfc5424[Syslog rfc5424] spec. The resulting structured document may contain any of the following fields: +Attempts to parse a log following the https://tools.ietf.org/html/rfc5424[Syslog rfc5424^] spec. The resulting structured document may contain any of the following fields: - `message` (string) - `timestamp` (string, RFC3339) @@ -125,7 +125,7 @@ Attempts to parse a log following the https://tools.ietf.org/html/rfc5424[Syslog === `syslog_rfc3164` -Attempts to parse a log following the https://tools.ietf.org/html/rfc3164[Syslog rfc3164] spec. The resulting structured document may contain any of the following fields: +Attempts to parse a log following the https://tools.ietf.org/html/rfc3164[Syslog rfc3164^] spec. The resulting structured document may contain any of the following fields: - `message` (string) - `timestamp` (string, RFC3339) diff --git a/docs/modules/components/pages/processors/protobuf.adoc b/docs/modules/components/pages/processors/protobuf.adoc index a300be16e0..81fe08dc31 100644 --- a/docs/modules/components/pages/processors/protobuf.adoc +++ b/docs/modules/components/pages/processors/protobuf.adoc @@ -30,9 +30,9 @@ protobuf: import_paths: [] ``` -The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json[https://developers.google.com/protocol-buffers/docs/proto3#json] +The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json[https://developers.google.com/protocol-buffers/docs/proto3#json^] -Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at https://github.com/benthosdev/benthos-plugin-example[https://github.com/benthosdev/benthos-plugin-example] +Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at https://github.com/benthosdev/benthos-plugin-example[https://github.com/benthosdev/benthos-plugin-example^] == Operators diff --git a/docs/modules/components/pages/processors/redis.adoc b/docs/modules/components/pages/processors/redis.adoc index 1be4134e70..be30cb7b73 100644 --- a/docs/modules/components/pages/processors/redis.adoc +++ b/docs/modules/components/pages/processors/redis.adoc @@ -223,8 +223,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -292,8 +291,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -324,9 +322,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/redis_script.adoc b/docs/modules/components/pages/processors/redis_script.adoc index f25b2f6783..4d09b1407f 100644 --- a/docs/modules/components/pages/processors/redis_script.adoc +++ b/docs/modules/components/pages/processors/redis_script.adoc @@ -15,7 +15,7 @@ component_type_dropdown::[] -Performs actions against Redis using https://redis.io/docs/manual/programmability/eval-intro/[LUA scripts]. +Performs actions against Redis using https://redis.io/docs/manual/programmability/eval-intro/[LUA scripts^]. Introduced in version 4.11.0. @@ -200,8 +200,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -269,8 +268,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -301,9 +299,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/schema_registry_decode.adoc b/docs/modules/components/pages/processors/schema_registry_decode.adoc index fa376a6222..4e5d20a0d1 100644 --- a/docs/modules/components/pages/processors/schema_registry_decode.adoc +++ b/docs/modules/components/pages/processors/schema_registry_decode.adoc @@ -69,13 +69,13 @@ schema_registry_decode: -- ====== -Decodes messages automatically from a schema stored within a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. +Decodes messages automatically from a schema stored within a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service^] by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. == Avro JSON format -This processor creates documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +This processor creates documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is `null`, then it is encoded as a JSON `null`; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -86,7 +86,7 @@ For example, the union schema `["null","string","Foo"]`, where `Foo` is a record - the string `"a"` as `\{"string": "a"}`; and - a `Foo` instance as `\{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. -However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field <> to `true`. +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format^] by setting the field <> to `true`. == Protobuf format @@ -97,7 +97,7 @@ This processor decodes protobuf messages to JSON documents, you can read more ab === `avro_raw_json` -Whether Avro messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json. +Whether Avro messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json^] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json^]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro^], the https://github.com/linkedin/goavro[underlining library used for avro serialization^], that explains in more detail the difference between the standard json and avro json. *Type*: `bool` @@ -142,8 +142,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -166,8 +165,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -208,8 +206,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -304,8 +301,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -373,8 +369,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -405,9 +400,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/schema_registry_encode.adoc b/docs/modules/components/pages/processors/schema_registry_encode.adoc index 10ff2149f9..eaf98d483c 100644 --- a/docs/modules/components/pages/processors/schema_registry_encode.adoc +++ b/docs/modules/components/pages/processors/schema_registry_encode.adoc @@ -75,7 +75,7 @@ schema_registry_encode: -- ====== -Encodes messages automatically from schemas obtains from a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by polling the service for the latest schema version for target subjects. +Encodes messages automatically from schemas obtains from a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service^] by polling the service for the latest schema version for target subjects. If a message fails to encode under the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. @@ -83,7 +83,7 @@ Avro, Protobuf and Json schemas are supported, all are capable of expanding from == Avro JSON format -By default this processor expects documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +By default this processor expects documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^] when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is `null`, then it is encoded as a JSON `null`; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -94,11 +94,11 @@ For example, the union schema `["null","string","Foo"]`, where `Foo` is a record - the string `"a"` as `\{"string": "a"}`; and - a `Foo` instance as `\{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. -However, it is possible to instead consume documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field <> to `true`. +However, it is possible to instead consume documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format^] by setting the field <> to `true`. === Known issues -Important! There is an outstanding issue in the https://github.com/linkedin/goavro[avro serializing library] that benthos uses which means it https://github.com/linkedin/goavro/issues/252[doesn't encode logical types correctly]. It's still possible to encode logical types that are in-line with the spec if `avro_raw_json` is set to true, though now of course non-logical types will not be in-line with the spec. +Important! There is an outstanding issue in the https://github.com/linkedin/goavro[avro serializing library^] that benthos uses which means it https://github.com/linkedin/goavro/issues/252[doesn't encode logical types correctly^]. It's still possible to encode logical types that are in-line with the spec if `avro_raw_json` is set to true, though now of course non-logical types will not be in-line with the spec. == Protobuf format @@ -108,7 +108,7 @@ This processor encodes protobuf messages either from any format parsed within Be When a target subject presents a protobuf schema that contains multiple messages it becomes ambiguous which message definition a given input data should be encoded against. In such scenarios Benthos will attempt to encode the data against each of them and select the first to successfully match against the data, this process currently *ignores all nested message definitions*. In order to speed up this exhaustive search the last known successful message will be attempted first for each subsequent input. -We will be considering alternative approaches in future so please https://redpanda.com/slack[get in touch] with thoughts and feedback. +We will be considering alternative approaches in future so please https://redpanda.com/slack[get in touch^] with thoughts and feedback. == Fields @@ -157,7 +157,7 @@ refresh_period: 1h === `avro_raw_json` -Whether messages encoded in Avro format should be parsed as normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be parsed as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between standard json and avro json. +Whether messages encoded in Avro format should be parsed as normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^]. If `true` the schema returned from the subject should be parsed as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json^] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json^]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro^], the https://github.com/linkedin/goavro[underlining library used for avro serialization^], that explains in more detail the difference between standard json and avro json. *Type*: `bool` @@ -195,8 +195,7 @@ A value used to identify the client to the service provider. === `oauth.consumer_secret` A secret used to establish ownership of the consumer key. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -219,8 +218,7 @@ A value used to gain access to the protected resources on behalf of the user. === `oauth.access_token_secret` A secret provided in order to establish ownership of a given access token. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -261,8 +259,7 @@ A username to authenticate as. === `basic_auth.password` A password to authenticate with. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -357,8 +354,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -426,8 +422,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -458,9 +453,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/processors/sentry_capture.adoc b/docs/modules/components/pages/processors/sentry_capture.adoc index 26a3eb989e..35673b3235 100644 --- a/docs/modules/components/pages/processors/sentry_capture.adoc +++ b/docs/modules/components/pages/processors/sentry_capture.adoc @@ -14,7 +14,7 @@ component_type_dropdown::[] -Captures log events from messages and submits them to https://sentry.io/[Sentry]. +Captures log events from messages and submits them to https://sentry.io/[Sentry^]. Introduced in version 4.16.0. diff --git a/docs/modules/components/pages/processors/sql_insert.adoc b/docs/modules/components/pages/processors/sql_insert.adoc index 76d3b987b9..58f5cb0612 100644 --- a/docs/modules/components/pages/processors/sql_insert.adoc +++ b/docs/modules/components/pages/processors/sql_insert.adoc @@ -165,9 +165,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/processors/sql_raw.adoc b/docs/modules/components/pages/processors/sql_raw.adoc index 7a4248c1b7..4f7ff49e33 100644 --- a/docs/modules/components/pages/processors/sql_raw.adoc +++ b/docs/modules/components/pages/processors/sql_raw.adoc @@ -178,9 +178,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/processors/sql_select.adoc b/docs/modules/components/pages/processors/sql_select.adoc index e1a7f657a1..e2db1fd592 100644 --- a/docs/modules/components/pages/processors/sql_select.adoc +++ b/docs/modules/components/pages/processors/sql_select.adoc @@ -169,9 +169,9 @@ The following is a list of supported drivers, their placeholder style, and their Please note that the `postgres` driver enforces SSL by default, you can override this with the parameter `sslmode=disable` if required. -The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. +The `snowflake` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: `@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`, where the value for the `privateKey` parameter can be constructed from an unencrypted RSA private key file `rsa_key.p8` using `openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0` (you can use `gbasenc` insted of `basenc` on OSX if you install `coreutils` via Homebrew). If you have a password-encrypted private key, you can decrypt it using `openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`. Also, make sure fields such as the username are URL-encoded. -The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details. +The https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^] driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details. *Type*: `string` diff --git a/docs/modules/components/pages/processors/switch.adoc b/docs/modules/components/pages/processors/switch.adoc index 4a7e99ed63..f3b457d29b 100644 --- a/docs/modules/components/pages/processors/switch.adoc +++ b/docs/modules/components/pages/processors/switch.adoc @@ -66,14 +66,14 @@ Indicates whether, if this case passes for a message, the next case should also [tabs] ====== -I Hate George:: +Ignore George:: + -- -We have a system where we're counting a metric for all messages that pass through our system. However, occasionally we get messages from George where he's rambling about dumb stuff we don't care about. +We have a system where we're counting a metric for all messages that pass through our system. However, occasionally we get messages from George that we don't care about. -For Georges messages we want to instead emit a metric that gauges how angry he is about being ignored and then we drop it. +For George's messages we want to instead emit a metric that gauges how angry he is about being ignored and then we drop it. ```yaml pipeline: diff --git a/docs/modules/components/pages/processors/unarchive.adoc b/docs/modules/components/pages/processors/unarchive.adoc index db68f7dcb5..d88a85f3bf 100644 --- a/docs/modules/components/pages/processors/unarchive.adoc +++ b/docs/modules/components/pages/processors/unarchive.adoc @@ -45,7 +45,7 @@ The unarchiving format to apply. | Option | Summary | `binary` -| Extract messages from a https://github.com/benthosdev/benthos/blob/main/internal/message/message.go#L96[binary blob format]. +| Extract messages from a https://github.com/{project-github}/blob/main/internal/message/message.go#L96[binary blob format^]. | `csv` | Attempt to parse the message as a csv file (header required) and for each row in the file expands its contents into a json object in a new message. | `csv:x` diff --git a/docs/modules/components/pages/processors/wasm.adoc b/docs/modules/components/pages/processors/wasm.adoc index 7265634f71..22b071de7f 100644 --- a/docs/modules/components/pages/processors/wasm.adoc +++ b/docs/modules/components/pages/processors/wasm.adoc @@ -27,9 +27,9 @@ wasm: function: process ``` -This processor uses https://github.com/tetratelabs/wazero[Wazero] to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. +This processor uses https://github.com/tetratelabs/wazero[Wazero^] to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. -This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in https://github.com/benthosdev/benthos/tree/main/public/wasm/README.md[the codebase]. +This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in https://github.com/{project-github}/tree/main/public/wasm/README.md[the codebase^]. These examples, as well as the processor itself, is a work in progress. diff --git a/docs/modules/components/pages/processors/workflow.adoc b/docs/modules/components/pages/processors/workflow.adoc index af6f597255..0cd02e85a9 100644 --- a/docs/modules/components/pages/processors/workflow.adoc +++ b/docs/modules/components/pages/processors/workflow.adoc @@ -63,7 +63,7 @@ When a processing pipeline contains multiple network processors that aren't depe === Simplifying processor topology -A workflow is often expressed as a https://en.wikipedia.org/wiki/Directed_acyclic_graph[DAG] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. +A workflow is often expressed as a https://en.wikipedia.org/wiki/Directed_acyclic_graph[DAG^] of processing stages, where each stage can result in N possible next stages, until finally the flow ends at an exit node. For example, if we had processing stages A, B, C and D, where stage A could result in either stage B or C being next, always followed by D, it might look something like this: diff --git a/docs/modules/components/pages/rate_limits/redis.adoc b/docs/modules/components/pages/rate_limits/redis.adoc index 3a218155d4..ad6929df46 100644 --- a/docs/modules/components/pages/rate_limits/redis.adoc +++ b/docs/modules/components/pages/rate_limits/redis.adoc @@ -161,8 +161,7 @@ Requires version 3.45.0 or newer === `tls.root_cas` An optional root certificate authority to use. This is a string, representing a certificate chain from the parent trusted root certificate, to possible intermediate signing certificates, to the host certificate. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -230,8 +229,7 @@ A plain text certificate to use. === `tls.client_certs[].key` A plain text certificate key to use. -[WARNING] -.Secret +[CAUTION] ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== @@ -262,9 +260,11 @@ The path of a certificate key to use. === `tls.client_certs[].password` -A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. Warning: Since it does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. +A plain text password for when the private key is password encrypted in PKCS#1 or PKCS#8 format. The obsolete `pbeWithMD5AndDES-CBC` algorithm is not supported for the PKCS#8 format. + +Because the obsolete pbeWithMD5AndDES-CBC algorithm does not authenticate the ciphertext, it is vulnerable to padding oracle attacks that can let an attacker recover the plaintext. + [WARNING] -.Secret ==== This field contains sensitive information that usually shouldn't be added to a config directly, read our xref:configuration:secrets.adoc[secrets page for more info]. ==== diff --git a/docs/modules/components/pages/scanners/avro.adoc b/docs/modules/components/pages/scanners/avro.adoc index f0c2a5732b..28366b2848 100644 --- a/docs/modules/components/pages/scanners/avro.adoc +++ b/docs/modules/components/pages/scanners/avro.adoc @@ -44,7 +44,7 @@ avro: == Avro JSON format -This scanner yields documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +This scanner yields documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is `null`, then it is encoded as a JSON `null`; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -55,14 +55,14 @@ For example, the union schema `["null","string","Foo"]`, where `Foo` is a record - the string `"a"` as `{"string": "a"}`; and - a `Foo` instance as `{"Foo": {...}}`, where `{...}` indicates the JSON encoding of a `Foo` instance. -However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field <> to `true`. +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format^] by setting the field <> to `true`. == Fields === `raw_json` -Whether messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json. +Whether messages should be decoded into normal JSON ("json that meets the expectations of regular internet json") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json^] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json^]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro^], the https://github.com/linkedin/goavro[underlining library used for avro serialization^], that explains in more detail the difference between the standard json and avro json. *Type*: `bool` diff --git a/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc b/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc index 12957d01fc..4d2c17d416 100644 --- a/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc +++ b/docs/modules/components/pages/tracers/gcp_cloudtrace.adoc @@ -14,7 +14,7 @@ component_type_dropdown::[] -Send tracing events to a https://cloud.google.com/trace[Google Cloud Trace]. +Send tracing events to a https://cloud.google.com/trace[Google Cloud Trace^]. Introduced in version 4.2.0. diff --git a/docs/modules/components/pages/tracers/jaeger.adoc b/docs/modules/components/pages/tracers/jaeger.adoc index 8e237c88d4..2ff9a82e09 100644 --- a/docs/modules/components/pages/tracers/jaeger.adoc +++ b/docs/modules/components/pages/tracers/jaeger.adoc @@ -14,7 +14,7 @@ component_type_dropdown::[] -Send tracing events to a https://www.jaegertracing.io/[Jaeger] agent or collector. +Send tracing events to a https://www.jaegertracing.io/[Jaeger^] agent or collector. [tabs] diff --git a/docs/modules/components/pages/tracers/open_telemetry_collector.adoc b/docs/modules/components/pages/tracers/open_telemetry_collector.adoc index cf6736edcc..74fe4b2039 100644 --- a/docs/modules/components/pages/tracers/open_telemetry_collector.adoc +++ b/docs/modules/components/pages/tracers/open_telemetry_collector.adoc @@ -14,7 +14,7 @@ component_type_dropdown::[] -Send tracing events to an https://opentelemetry.io/docs/collector/[Open Telemetry collector]. +Send tracing events to an https://opentelemetry.io/docs/collector/[Open Telemetry collector^]. [tabs] diff --git a/docs/modules/configuration/pages/templating.adoc b/docs/modules/configuration/pages/templating.adoc index 34b3dabfba..0b1646c025 100644 --- a/docs/modules/configuration/pages/templating.adoc +++ b/docs/modules/configuration/pages/templating.adoc @@ -9,8 +9,7 @@ internal/template/docs.adoc //// -[WARNING] -.Experimental +[CAUTION] ==== Templates are an experimental feature and therefore subject to change outside of major version releases. ==== diff --git a/docs/modules/guides/pages/bloblang/functions.adoc b/docs/modules/guides/pages/bloblang/functions.adoc index d2cb9878a0..c79624459d 100644 --- a/docs/modules/guides/pages/bloblang/functions.adoc +++ b/docs/modules/guides/pages/bloblang/functions.adoc @@ -31,7 +31,6 @@ root.values_two = range(0, this.max, 2) === `counter` [CAUTION] -.Experimental ==== This function is experimental and therefore breaking changes could be made to it outside of major version releases. ==== @@ -278,7 +277,6 @@ root.doc.contents = (this.body.content | this.thing.body) === `ulid` [CAUTION] -.Experimental ==== This function is experimental and therefore breaking changes could be made to it outside of major version releases. ==== @@ -432,7 +430,6 @@ root.all_metadata = metadata() === `tracing_id` [CAUTION] -.Experimental ==== This function is experimental and therefore breaking changes could be made to it outside of major version releases. ==== @@ -448,7 +445,6 @@ meta trace_id = tracing_id() === `tracing_span` [CAUTION] -.Experimental ==== This function is experimental and therefore breaking changes could be made to it outside of major version releases. ==== @@ -622,12 +618,11 @@ root.received_at = timestamp_unix_nano() === `fake` -[NOTE] -.Beta +[CAUTION] ==== This function is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== -Takes in a string that maps to a https://github.com/go-faker/faker[faker] function and returns the result from that faker function. Returns an error if the given string doesn't match a supported faker function. Supported functions: `latitude`, `longitude`, `unix_time`, `date`, `time_string`, `month_name`, `year_string`, `day_of_week`, `day_of_month`, `timestamp`, `century`, `timezone`, `time_period`, `email`, `mac_address`, `domain_name`, `url`, `username`, `ipv4`, `ipv6`, `password`, `jwt`, `word`, `sentence`, `paragraph`, `cc_type`, `cc_number`, `currency`, `amount_with_currency`, `title_male`, `title_female`, `first_name`, `first_name_male`, `first_name_female`, `last_name`, `name`, `gender`, `chinese_first_name`, `chinese_last_name`, `chinese_name`, `phone_number`, `toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the https://github.com/go-faker/faker[faker] docs for details on these functions. +Takes in a string that maps to a https://github.com/go-faker/faker[faker^] function and returns the result from that faker function. Returns an error if the given string doesn't match a supported faker function. Supported functions: `latitude`, `longitude`, `unix_time`, `date`, `time_string`, `month_name`, `year_string`, `day_of_week`, `day_of_month`, `timestamp`, `century`, `timezone`, `time_period`, `email`, `mac_address`, `domain_name`, `url`, `username`, `ipv4`, `ipv6`, `password`, `jwt`, `word`, `sentence`, `paragraph`, `cc_type`, `cc_number`, `currency`, `amount_with_currency`, `title_male`, `title_female`, `first_name`, `first_name_male`, `first_name_female`, `last_name`, `name`, `gender`, `chinese_first_name`, `chinese_last_name`, `chinese_name`, `phone_number`, `toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the https://github.com/go-faker/faker[faker^] docs for details on these functions. ==== Parameters diff --git a/docs/modules/guides/pages/bloblang/methods.adoc b/docs/modules/guides/pages/bloblang/methods.adoc index 008217c034..3d37255ef9 100644 --- a/docs/modules/guides/pages/bloblang/methods.adoc +++ b/docs/modules/guides/pages/bloblang/methods.adoc @@ -321,7 +321,7 @@ root.path_sep = this.path.filepath_split() === `format` -Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go https://pkg.go.dev/fmt[`fmt` package documentation] for the list of valid format verbs. +Use a value string as a format specifier in order to produce a new string, using any number of provided arguments. Please refer to the Go https://pkg.go.dev/fmt[`fmt` package documentation^] for the list of valid format verbs. ==== Examples @@ -534,11 +534,10 @@ root.the_rest = this.value.slice(0, -4) === `slug` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== -Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its https://pkg.go.dev/github.com/gosimple/slug[docs] for more information. +Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its https://pkg.go.dev/github.com/gosimple/slug[docs^] for more information. Introduced in version 4.2.0. @@ -900,7 +899,7 @@ root.new_value = this.value.ceil() Converts a numerical type into a 32-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`strconv.ParseFloat` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 32-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`strconv.ParseFloat` documentation^] for details regarding the supported formats. ==== Examples @@ -919,7 +918,7 @@ root.out = this.in.float32() Converts a numerical type into a 64-bit floating point number, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`strconv.ParseFloat` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 64-bit floating point number. Please refer to the https://pkg.go.dev/strconv#ParseFloat[`strconv.ParseFloat` documentation^] for details regarding the supported formats. ==== Examples @@ -952,7 +951,7 @@ root.new_value = this.value.floor() Converts a numerical type into a 16-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 16-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 16-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -983,7 +982,7 @@ root = this.int16() Converts a numerical type into a 32-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 32-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 32-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1014,7 +1013,7 @@ root = this.int32() Converts a numerical type into a 64-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 64-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 64-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1045,7 +1044,7 @@ root = this.int64() Converts a numerical type into a 8-bit signed integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 8-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 8-bit signed integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1175,7 +1174,7 @@ root.new_value = this.value.round() Converts a numerical type into a 16-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 16-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 16-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1206,7 +1205,7 @@ root = this.uint16() Converts a numerical type into a 32-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 32-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 32-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1237,7 +1236,7 @@ root = this.uint32() Converts a numerical type into a 64-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 64-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 64-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1268,7 +1267,7 @@ root = this.uint64() Converts a numerical type into a 8-bit unsigned integer, this is for advanced use cases where a specific data type is needed for a given component (such as the ClickHouse SQL driver). -If the value is a string then an attempt will be made to parse it as a 8-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation] for details regarding the supported formats. +If the value is a string then an attempt will be made to parse it as a 8-bit unsigned integer. If the target value exceeds the capacity of an integer or contains decimal values then this method will throw an error. In order to convert a floating point number containing decimals first use <> on the value. Please refer to the https://pkg.go.dev/strconv#ParseInt[`strconv.ParseInt` documentation^] for details regarding the supported formats. ==== Examples @@ -1320,7 +1319,6 @@ root.delay_for_s = this.delay_for.parse_duration() / 1000000000 === `parse_duration_iso8601` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1359,7 +1357,6 @@ root.delay_for_s = this.delay_for.parse_duration_iso8601() / 1000000000 === `ts_add_iso8601` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1372,7 +1369,6 @@ Parse parameter string as ISO 8601 period and add it to value with high precisio === `ts_format` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1425,7 +1421,6 @@ root.something_at = this.created_at.ts_format("2006-Jan-02 15:04:05.999999", "UT === `ts_parse` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1450,7 +1445,6 @@ root.doc.timestamp = this.doc.timestamp.ts_parse("2006-Jan-02") === `ts_round` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1478,7 +1472,6 @@ root.created_at_hour = this.created_at.ts_round("1h".parse_duration()) === `ts_strftime` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1492,7 +1485,7 @@ Attempts to format a timestamp value as a string according to a specified strfti ==== Examples -The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strftime[man 3 strftime] for the list of format specifiers. +The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strftime[man 3 strftime^] for the list of format specifiers. ```coffeescript root.something_at = (this.created_at + 300).ts_strftime("%Y-%b-%d %H:%M:%S") @@ -1510,7 +1503,7 @@ root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S", "UTC") # Out: {"something_at":"2020-Aug-14 11:50:26"} ``` -As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. +As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go^], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. ```coffeescript root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S.%f", "UTC") @@ -1525,7 +1518,6 @@ root.something_at = this.created_at.ts_strftime("%Y-%b-%d %H:%M:%S.%f", "UTC") === `ts_strptime` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1538,7 +1530,7 @@ Attempts to parse a string as a timestamp following a specified strptime-compati ==== Examples -The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strptime[man 3 strptime] for the list of format specifiers. +The format consists of zero or more conversion specifiers and ordinary characters (except `%`). All ordinary characters are copied to the output string without modification. Each conversion specification begins with a `%` character followed by the character that determines the behavior of the specifier. Please refer to https://linux.die.net/man/3/strptime[man 3 strptime^] for the list of format specifiers. ```coffeescript root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d") @@ -1547,7 +1539,7 @@ root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d") # Out: {"doc":{"timestamp":"2020-08-14T00:00:00Z"}} ``` -As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. +As an extension provided by the underlying formatting library, https://github.com/itchyny/timefmt-go[itchyny/timefmt-go^], the `%f` directive is supported for zero-padded microseconds, which originates from Python. Note that E and O modifier characters are not supported. ```coffeescript root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d %H:%M:%S.%f") @@ -1559,7 +1551,6 @@ root.doc.timestamp = this.doc.timestamp.ts_strptime("%Y-%b-%d %H:%M:%S.%f") === `ts_sub` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1587,7 +1578,6 @@ root.between = this.started_at.ts_sub("2020-08-14T05:54:23Z").abs() === `ts_sub_iso8601` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1600,7 +1590,6 @@ Parse parameter string as ISO 8601 period and subtract it from value with high p === `ts_tz` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1626,7 +1615,6 @@ root.created_at_utc = this.created_at.ts_tz("UTC") === `ts_unix` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1645,7 +1633,6 @@ root.created_at_unix = this.created_at.ts_unix() === `ts_unix_micro` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1664,7 +1651,6 @@ root.created_at_unix = this.created_at.ts_unix_micro() === `ts_unix_milli` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -1683,7 +1669,6 @@ root.created_at_unix = this.created_at.ts_unix_milli() === `ts_unix_nano` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2016,11 +2001,10 @@ root.has_bar = this.thing.contains(20) === `diff` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== -Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information. +Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs^] for more information. Introduced in version 4.25.0. @@ -2108,7 +2092,6 @@ root.new_dict = this.dict.filter(item -> item.value.contains("foo")) === `find` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2138,7 +2121,6 @@ root.index = this.things.find(this.goal) === `find_all` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2168,7 +2150,6 @@ root.indexes = this.things.find_all(this.goal) === `find_all_by` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2191,7 +2172,6 @@ root.index = this.find_all_by(v -> v != "bar") === `find_by` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2332,7 +2312,6 @@ root.joined_numbers = this.numbers.map_each(this.string()).join(",") === `json_path` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== @@ -2365,11 +2344,10 @@ root.text_objects = this.json_path("$.body[?(@.type=='text')]") === `json_schema` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== -Checks a https://json-schema.org/[JSON schema] against a value and returns the value if it matches or throws and error if it does not. +Checks a https://json-schema.org/[JSON schema^] against a value and returns the value if it matches or throws and error if it does not. ==== Parameters @@ -2529,11 +2507,10 @@ root = this.foo.merge(this.bar) === `patch` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== -Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information. +Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs^] for more information. Introduced in version 4.25.0. @@ -2728,7 +2705,6 @@ root.foo = this.foo.zip(this.bar, this.baz) === `bloblang` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2754,7 +2730,6 @@ root.body = this.body.bloblang(this.mapping) === `format_json` [CAUTION] -.Beta ==== This method is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with it is found. ==== @@ -2810,7 +2785,7 @@ root = this.doc.format_json(no_indent: true) === `format_msgpack` -Formats data as a https://msgpack.org/[MessagePack] message in bytes format. +Formats data as a https://msgpack.org/[MessagePack^] message in bytes format. ==== Examples @@ -3000,7 +2975,7 @@ root.doc = this.doc.parse_json(use_number: true) === `parse_msgpack` -Parses a https://msgpack.org/[MessagePack] message into a structured document. +Parses a https://msgpack.org/[MessagePack^] message into a structured document. ==== Examples @@ -3021,7 +2996,7 @@ root = this.encoded.decode("base64").parse_msgpack() === `parse_parquet` -Decodes a https://parquet.apache.org/docs/[Parquet file] into an array of objects, one for each row within the file. +Decodes a https://parquet.apache.org/docs/[Parquet file^] into an array of objects, one for each row within the file. ==== Parameters @@ -3147,7 +3122,7 @@ root.compressed = content().compress("lz4").encode("base64") Decodes an encoded string target according to a chosen scheme and returns the result as a byte array. When mapping the result to a JSON field the value should be cast to a string using the method `string`, or encoded using the method `encode`, otherwise it will be base64 encoded by default. -Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`. +Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)^], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)^], `hex`, `ascii85`. ==== Parameters @@ -3221,7 +3196,7 @@ root.decrypted = this.value.decode("hex").decrypt_aes("ctr", $key, $vector).stri === `encode` -Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)], `hex`, `ascii85`. +Encodes a string or byte array target according to a chosen scheme and returns a string result. Available schemes are: `base64`, `base64url` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 with padding characters)^], `base64rawurl` https://rfc-editor.org/rfc/rfc4648.html[(RFC 4648 without padding characters)^], `hex`, `ascii85`. ==== Parameters @@ -3734,11 +3709,10 @@ root.signed = this.claims.sign_jwt_rs512("""-----BEGIN RSA PRIVATE KEY----- === `geoip_anonymous_ip` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the anonymous IP associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the anonymous IP associated with it. ==== Parameters @@ -3747,11 +3721,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_asn` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the ASN associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the ASN associated with it. ==== Parameters @@ -3760,11 +3733,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_city` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the city associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the city associated with it. ==== Parameters @@ -3773,11 +3745,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_connection_type` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the connection type associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the connection type associated with it. ==== Parameters @@ -3786,11 +3757,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_country` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the country associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the country associated with it. ==== Parameters @@ -3799,11 +3769,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_domain` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the domain associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the domain associated with it. ==== Parameters @@ -3812,11 +3781,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_enterprise` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the enterprise associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the enterprise associated with it. ==== Parameters @@ -3825,11 +3793,10 @@ Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind databas === `geoip_isp` [CAUTION] -.Experimental ==== This method is experimental and therefore breaking changes could be made to it outside of major version releases. ==== -Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the ISP associated with it. +Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the ISP associated with it. ==== Parameters diff --git a/internal/impl/avro/scanner.go b/internal/impl/avro/scanner.go index 4200dc7fbe..5462433a3b 100644 --- a/internal/impl/avro/scanner.go +++ b/internal/impl/avro/scanner.go @@ -21,7 +21,7 @@ func avroScannerSpec() *service.ConfigSpec { Description(` == Avro JSON format -This scanner yields documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +This scanner yields documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is ` + "`null`, then it is encoded as a JSON `null`" + `; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -32,11 +32,11 @@ For example, the union schema ` + "`[\"null\",\"string\",\"Foo\"]`, where `Foo`" - the string ` + "`\"a\"` as `{\"string\": \"a\"}`" + `; and - a ` + "`Foo` instance as `{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. -However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field ` + "<> to `true`" + `. +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format^] by setting the field ` + "<> to `true`" + `. `). Fields( service.NewBoolField(sFieldRawJSON). - Description("Whether messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json."). + Description("Whether messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json^] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json^]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro^], the https://github.com/linkedin/goavro[underlining library used for avro serialization^], that explains in more detail the difference between the standard json and avro json."). Advanced(). Default(false), ) diff --git a/internal/impl/awk/processor.go b/internal/impl/awk/processor.go index 8850a979e7..9372e6979b 100644 --- a/internal/impl/awk/processor.go +++ b/internal/impl/awk/processor.go @@ -32,7 +32,7 @@ Comes with a wide range of <> for accessing mess Check out the <> in order to see how this processor can be used. -This processor uses https://github.com/benhoyt/goawk[GoAWK], in order to understand the differences in how the program works you can read more about it in https://github.com/benhoyt/goawk#differences-from-awk[goawk.differences].`). +This processor uses https://github.com/benhoyt/goawk[GoAWK^], in order to understand the differences in how the program works you can read more about it in https://github.com/benhoyt/goawk#differences-from-awk[goawk.differences^].`). Footnotes(` == Codecs diff --git a/internal/impl/aws/config/config.go b/internal/impl/aws/config/config.go index c07c1ad48f..f0cdf3f598 100644 --- a/internal/impl/aws/config/config.go +++ b/internal/impl/aws/config/config.go @@ -29,7 +29,7 @@ func SessionFields() []*service.ConfigField { Description("The token for the credentials being used, required when using short term credentials."). Default("").Advanced(), service.NewBoolField("from_ec2_role"). - Description("Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance]."). + Description("Use the credentials of a host EC2 machine configured to assume https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html[an IAM role associated with the instance^]."). Default(false).Version("4.2.0"), service.NewStringField("role"). Description("A role ARN to assume."). diff --git a/internal/impl/azure/cosmosdb/docs.go b/internal/impl/azure/cosmosdb/docs.go index 1444bb85fd..da437d9727 100644 --- a/internal/impl/azure/cosmosdb/docs.go +++ b/internal/impl/azure/cosmosdb/docs.go @@ -79,7 +79,7 @@ var CredentialsDocs = ` You can use one of the following authentication mechanisms: - Set the ` + "`endpoint`" + ` field and the ` + "`account_key`" + ` field -- Set only the ` + "`endpoint`" + ` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential] +- Set only the ` + "`endpoint`" + ` field to use https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^] - Set the ` + "`connection_string`" + ` field ` @@ -102,7 +102,7 @@ var BatchingDocs = ` == Batching -CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here]). +CosmosDB limits the maximum batch size to 100 messages and the payload must not exceed 2MB (https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits[details here^]). ` // EmulatorDocs emulator docs @@ -110,7 +110,7 @@ var EmulatorDocs = ` == CosmosDB emulator -If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here], the following Docker command should do the trick: +If you wish to run the CosmosDB emulator that is referenced in the documentation https://learn.microsoft.com/en-us/azure/cosmos-db/linux-emulator[here^], the following Docker command should do the trick: ` + "```bash" + ` > docker run --rm -it -p 8081:8081 --name=cosmosdb -e AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 -e AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator @@ -118,7 +118,7 @@ If you wish to run the CosmosDB emulator that is referenced in the documentation Note: ` + "`AZURE_COSMOS_EMULATOR_PARTITION_COUNT`" + ` controls the number of partitions that will be supported by the emulator. The bigger the value, the longer it takes for the container to start up. -Additionally, instead of installing the container self-signed certificate which is exposed via ` + "`https://localhost:8081/_explorer/emulator.pem`" + `, you can run https://mitmproxy.org/[mitmproxy] like so: +Additionally, instead of installing the container self-signed certificate which is exposed via ` + "`https://localhost:8081/_explorer/emulator.pem`" + `, you can run https://mitmproxy.org/[mitmproxy^] like so: ` + "```bash" + ` > mitmproxy -k --mode "reverse:https://localhost:8081" diff --git a/internal/impl/azure/input_blob_storage.go b/internal/impl/azure/input_blob_storage.go index 5b0f28be20..cfbb8ea378 100644 --- a/internal/impl/azure/input_blob_storage.go +++ b/internal/impl/azure/input_blob_storage.go @@ -75,7 +75,7 @@ Supports multiple authentication methods but only one of the following is requir - `+"`storage_connection_string`"+` - `+"`storage_account` and `storage_access_key`"+` - `+"`storage_account` and `storage_sas_token`"+` -- `+"`storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential]"+` +- `+"`storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^]"+` If multiple are set then the `+"`storage_connection_string`"+` is given priority. @@ -88,7 +88,7 @@ When downloading large files it's often necessary to process it in streamed part == Stream new files -By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage[an Event Grid source] and then use this as a `+"<>"+`, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. +By default this input will consume all files found within the target container and will then gracefully terminate. This is referred to as a "batch" mode of operation. However, it's possible to instead configure a container as https://learn.microsoft.com/en-gb/azure/event-grid/event-schema-blob-storage[an Event Grid source^] and then use this as a `+"<>"+`, in which case new files are consumed as they're uploaded and Benthos will continue listening for and downloading files as they arrive. This is referred to as a "streamed" mode of operation. == Metadata diff --git a/internal/impl/azure/input_cosmosdb.go b/internal/impl/azure/input_cosmosdb.go index a645764d11..773fd1312d 100644 --- a/internal/impl/azure/input_cosmosdb.go +++ b/internal/impl/azure/input_cosmosdb.go @@ -27,11 +27,11 @@ func cosmosDBInputSpec() *service.ConfigSpec { // Beta(). Categories("Azure"). Version("v4.25.0"). - Summary(`Executes a SQL query against https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB] and creates a batch of messages from each page of items.`). + Summary(`Executes a SQL query against https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB^] and creates a batch of messages from each page of items.`). Description(` == Cross-partition queries -Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey values must be known in advance and specified in the config. https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989[See details]. +Cross-partition queries are currently not supported by the underlying driver. For every query, the PartitionKey values must be known in advance and specified in the config. https://github.com/Azure/azure-sdk-for-go/issues/18578#issuecomment-1222510989[See details^]. `+cosmosdb.CredentialsDocs+cosmosdb.MetadataDocs). Footnotes(cosmosdb.EmulatorDocs). Fields(cosmosdb.ContainerClientConfigFields()...). diff --git a/internal/impl/azure/output_blob_storage.go b/internal/impl/azure/output_blob_storage.go index abece7ee06..04f89d7c04 100644 --- a/internal/impl/azure/output_blob_storage.go +++ b/internal/impl/azure/output_blob_storage.go @@ -72,7 +72,7 @@ Supports multiple authentication methods but only one of the following is requir - `+"`storage_connection_string`"+` - `+"`storage_account` and `storage_access_key`"+` - `+"`storage_account` and `storage_sas_token`"+` -- `+"`storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential]"+` +- `+"`storage_account` to access via https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential[DefaultAzureCredential^]"+` If multiple are set then the `+"`storage_connection_string`"+` is given priority. diff --git a/internal/impl/azure/output_cosmosdb.go b/internal/impl/azure/output_cosmosdb.go index 57b1cba78c..c53824ca8e 100644 --- a/internal/impl/azure/output_cosmosdb.go +++ b/internal/impl/azure/output_cosmosdb.go @@ -21,9 +21,9 @@ func cosmosDBOutputConfig() *service.ConfigSpec { // Stable(). TODO Categories("Azure"). Version("v4.25.0"). - Summary("Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]."). + Summary("Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB^]."). Description(` -When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. +When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details^]. The `+"`partition_keys`"+` field must resolve to the same value(s) across the entire message batch. `+cosmosdb.CredentialsDocs+cosmosdb.BatchingDocs+service.OutputPerformanceDocs(true, true)). diff --git a/internal/impl/azure/processor_cosmosdb.go b/internal/impl/azure/processor_cosmosdb.go index 2f83dadbc0..e1a0d25a6e 100644 --- a/internal/impl/azure/processor_cosmosdb.go +++ b/internal/impl/azure/processor_cosmosdb.go @@ -20,9 +20,9 @@ func cosmosDBProcessorConfig() *service.ConfigSpec { // Stable(). TODO Categories("Azure"). Version("v4.25.0"). - Summary("Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB]."). + Summary("Creates or updates messages as JSON documents in https://learn.microsoft.com/en-us/azure/cosmos-db/introduction[Azure CosmosDB^]."). Description(` -When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details]. +When creating documents, each message must have the `+"`id`"+` property (case-sensitive) set (or use `+"`auto_id: true`"+`). It is the unique name that identifies the document, that is, no two documents share the same `+"`id`"+` within a logical partition. The `+"`id`"+` field must not exceed 255 characters. https://learn.microsoft.com/en-us/rest/api/cosmos-db/documents[See details^]. The `+"`partition_keys`"+` field must resolve to the same value(s) across the entire message batch. `+cosmosdb.CredentialsDocs+cosmosdb.MetadataDocs+cosmosdb.BatchingDocs). diff --git a/internal/impl/changelog/bloblang.go b/internal/impl/changelog/bloblang.go index ac45e03c75..23277b293c 100644 --- a/internal/impl/changelog/bloblang.go +++ b/internal/impl/changelog/bloblang.go @@ -12,7 +12,7 @@ func init() { diffSpec := bloblang.NewPluginSpec(). Beta(). Category("Object & Array Manipulation"). - Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information.`). + Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs^] for more information.`). Version("4.25.0"). Param(bloblang.NewAnyParam("other").Description("The value to compare against.")) @@ -45,7 +45,7 @@ func init() { patchSpec := bloblang.NewPluginSpec(). Beta(). Category("Object & Array Manipulation"). - Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs] for more information.`). + Description(`Create a diff by comparing the current value with the given one. Wraps the github.com/r3labs/diff/v3 package. See its https://pkg.go.dev/github.com/r3labs/diff/v3[docs^] for more information.`). Version("4.25.0"). Param(bloblang.NewAnyParam("changelog").Description("The changelog to apply.")) diff --git a/internal/impl/cockroachdb/input_changefeed.go b/internal/impl/cockroachdb/input_changefeed.go index dfa609ea0a..4b8b9d4ca5 100644 --- a/internal/impl/cockroachdb/input_changefeed.go +++ b/internal/impl/cockroachdb/input_changefeed.go @@ -30,8 +30,8 @@ var sampleString = `{ func crdbChangefeedInputConfig() *service.ConfigSpec { return service.NewConfigSpec(). Categories("Integration"). - Summary(fmt.Sprintf("Listens to a https://www.cockroachlabs.com/docs/stable/changefeed-examples[CockroachDB Core Changefeed] and creates a message for each row received. Each message is a json object looking like: \n```json\n%s\n```", sampleString)). - Description("This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered.\n\nNote: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core[the official CockroachDB documentation]."). + Summary(fmt.Sprintf("Listens to a https://www.cockroachlabs.com/docs/stable/changefeed-examples[CockroachDB Core Changefeed^] and creates a message for each row received. Each message is a json object looking like: \n```json\n%s\n```", sampleString)). + Description("This input will continue to listen to the changefeed until shutdown. A backfill of the full current state of the table will be delivered upon each run unless a cache is configured for storing cursor timestamps, as this is how Benthos keeps track as to which changes have been successfully delivered.\n\nNote: You must have `SET CLUSTER SETTING kv.rangefeed.enabled = true;` on your CRDB cluster, for more information refer to https://www.cockroachlabs.com/docs/stable/changefeed-examples?filters=core[the official CockroachDB documentation^]."). Fields( service.NewStringField("dsn"). Description(`A Data Source Name to identify the target database.`). @@ -41,10 +41,10 @@ func crdbChangefeedInputConfig() *service.ConfigSpec { Description("CSV of tables to be included in the changefeed"). Example([]string{"table1", "table2"}), service.NewStringField("cursor_cache"). - Description("A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache resource] to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table."). + Description("A https://www.docs.redpanda.com/redpanda-connect/components/caches/about[cache resource^] to use for storing the current latest cursor that has been successfully delivered, this allows Benthos to continue from that cursor upon restart, rather than consume the entire state of the table."). Optional(), service.NewStringListField("options"). - Description("A list of options to be included in the changefeed (WITH X, Y...).\n**NOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case.**"). + Description("A list of options to be included in the changefeed (WITH X, Y...).\n\nNOTE: Both the CURSOR option and UPDATED will be ignored from these options when a `cursor_cache` is specified, as they are set explicitly by Benthos in this case."). Example([]string{`virtual_columns="omitted"`}). Advanced(). Optional(), diff --git a/internal/impl/confluent/processor_schema_registry_decode.go b/internal/impl/confluent/processor_schema_registry_decode.go index 5987aad617..38dd0ede01 100644 --- a/internal/impl/confluent/processor_schema_registry_decode.go +++ b/internal/impl/confluent/processor_schema_registry_decode.go @@ -23,13 +23,13 @@ func schemaRegistryDecoderConfig() *service.ConfigSpec { Categories("Parsing", "Integration"). Summary("Automatically decodes and validates messages with schemas from a Confluent Schema Registry service."). Description(` -Decodes messages automatically from a schema stored within a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. +Decodes messages automatically from a schema stored within a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service^] by extracting a schema ID from the message and obtaining the associated schema from the registry. If a message fails to match against the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. Avro, Protobuf and Json schemas are supported, all are capable of expanding from schema references as of v4.22.0. == Avro JSON format -This processor creates documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +This processor creates documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^] when decoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is ` + "`null`, then it is encoded as a JSON `null`" + `; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -40,14 +40,14 @@ For example, the union schema ` + "`[\"null\",\"string\",\"Foo\"]`, where `Foo`" - the string ` + "`\"a\"` as `\\{\"string\": \"a\"}`" + `; and - a ` + "`Foo` instance as `\\{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. -However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field ` + "<> to `true`" + `. +However, it is possible to instead create documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format^] by setting the field ` + "<> to `true`" + `. == Protobuf format This processor decodes protobuf messages to JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json `). Field(service.NewBoolField("avro_raw_json"). - Description("Whether Avro messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between the standard json and avro json."). + Description("Whether Avro messages should be decoded into normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^]. If `true` the schema returned from the subject should be decoded as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json^] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json^]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro^], the https://github.com/linkedin/goavro[underlining library used for avro serialization^], that explains in more detail the difference between the standard json and avro json."). Advanced().Default(false)). Field(service.NewURLField("url").Description("The base URL of the schema registry service.")) diff --git a/internal/impl/confluent/processor_schema_registry_encode.go b/internal/impl/confluent/processor_schema_registry_encode.go index 46fe3f3e0b..6a8f42ea54 100644 --- a/internal/impl/confluent/processor_schema_registry_encode.go +++ b/internal/impl/confluent/processor_schema_registry_encode.go @@ -24,7 +24,7 @@ func schemaRegistryEncoderConfig() *service.ConfigSpec { Categories("Parsing", "Integration"). Summary("Automatically encodes and validates messages with schemas from a Confluent Schema Registry service."). Description(` -Encodes messages automatically from schemas obtains from a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service] by polling the service for the latest schema version for target subjects. +Encodes messages automatically from schemas obtains from a https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry service^] by polling the service for the latest schema version for target subjects. If a message fails to encode under the schema then it will remain unchanged and the error can be caught using xref:configuration:error_handling.adoc[error handling methods]. @@ -32,7 +32,7 @@ Avro, Protobuf and Json schemas are supported, all are capable of expanding from == Avro JSON format -By default this processor expects documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON] when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: +By default this processor expects documents formatted as https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^] when encoding with Avro schemas. In this format the value of a union is encoded in JSON as follows: - if its type is ` + "`null`, then it is encoded as a JSON `null`" + `; - otherwise it is encoded as a JSON object with one name/value pair whose name is the type's name and whose value is the recursively encoded value. For Avro's named types (record, fixed or enum) the user-specified name is used, for other types the type name is used. @@ -43,11 +43,11 @@ For example, the union schema ` + "`[\"null\",\"string\",\"Foo\"]`, where `Foo`" - the string ` + "`\"a\"` as `\\{\"string\": \"a\"}`" + `; and - a ` + "`Foo` instance as `\\{\"Foo\": {...}}`, where `{...}` indicates the JSON encoding of a `Foo`" + ` instance. -However, it is possible to instead consume documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format] by setting the field ` + "<> to `true`" + `. +However, it is possible to instead consume documents in https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard/raw JSON format^] by setting the field ` + "<> to `true`" + `. === Known issues -Important! There is an outstanding issue in the https://github.com/linkedin/goavro[avro serializing library] that benthos uses which means it https://github.com/linkedin/goavro/issues/252[doesn't encode logical types correctly]. It's still possible to encode logical types that are in-line with the spec if ` + "`avro_raw_json` is set to true" + `, though now of course non-logical types will not be in-line with the spec. +Important! There is an outstanding issue in the https://github.com/linkedin/goavro[avro serializing library^] that benthos uses which means it https://github.com/linkedin/goavro/issues/252[doesn't encode logical types correctly^]. It's still possible to encode logical types that are in-line with the spec if ` + "`avro_raw_json` is set to true" + `, though now of course non-logical types will not be in-line with the spec. == Protobuf format @@ -57,7 +57,7 @@ This processor encodes protobuf messages either from any format parsed within Be When a target subject presents a protobuf schema that contains multiple messages it becomes ambiguous which message definition a given input data should be encoded against. In such scenarios Benthos will attempt to encode the data against each of them and select the first to successfully match against the data, this process currently *ignores all nested message definitions*. In order to speed up this exhaustive search the last known successful message will be attempted first for each subsequent input. -We will be considering alternative approaches in future so please https://redpanda.com/slack[get in touch] with thoughts and feedback. +We will be considering alternative approaches in future so please https://redpanda.com/slack[get in touch^] with thoughts and feedback. `). Field(service.NewURLField("url").Description("The base URL of the schema registry service.")). Field(service.NewInterpolatedStringField("subject").Description("The schema subject to derive schemas from."). @@ -69,7 +69,7 @@ We will be considering alternative approaches in future so please https://redpan Example("60s"). Example("1h")). Field(service.NewBoolField("avro_raw_json"). - Description("Whether messages encoded in Avro format should be parsed as normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON]. If `true` the schema returned from the subject should be parsed as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro], the https://github.com/linkedin/goavro[underlining library used for avro serialization], that explains in more detail the difference between standard json and avro json."). + Description("Whether messages encoded in Avro format should be parsed as normal JSON (\"json that meets the expectations of regular internet json\") rather than https://avro.apache.org/docs/current/specification/_print/#json-encoding[Avro JSON^]. If `true` the schema returned from the subject should be parsed as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodecForStandardJSONFull[standard json^] instead of as https://pkg.go.dev/github.com/linkedin/goavro/v2#NewCodec[avro json^]. There is a https://github.com/linkedin/goavro/blob/5ec5a5ee7ec82e16e6e2b438d610e1cab2588393/union.go#L224-L249[comment in goavro^], the https://github.com/linkedin/goavro[underlining library used for avro serialization^], that explains in more detail the difference between standard json and avro json."). Advanced().Default(false).Version("3.59.0")) for _, f := range service.NewHTTPRequestAuthSignerFields() { diff --git a/internal/impl/dgraph/cache_ristretto.go b/internal/impl/dgraph/cache_ristretto.go index 36388f1c20..8e874f3c5c 100644 --- a/internal/impl/dgraph/cache_ristretto.go +++ b/internal/impl/dgraph/cache_ristretto.go @@ -20,7 +20,7 @@ func ristrettoCacheConfig() *service.ConfigSpec { spec := service.NewConfigSpec(). Stable(). - Summary(`Stores key/value pairs in a map held in the memory-bound https://github.com/dgraph-io/ristretto[Ristretto cache].`). + Summary(`Stores key/value pairs in a map held in the memory-bound https://github.com/dgraph-io/ristretto[Ristretto cache^].`). Description(`This cache is more efficient and appropriate for high-volume use cases than the standard memory cache. However, the add command is non-atomic, and therefore this cache is not suitable for deduplication.`). Field(service.NewDurationField("default_ttl"). Description("A default TTL to set for items, calculated from the moment the item is cached. Set to an empty string or zero duration to disable TTLs."). diff --git a/internal/impl/discord/output.go b/internal/impl/discord/output.go index 43085b053f..a5a4fe19ec 100644 --- a/internal/impl/discord/output.go +++ b/internal/impl/discord/output.go @@ -17,7 +17,7 @@ func outputConfig() *service.ConfigSpec { Description(` This output POSTs messages to the `+"`/channels/\\{channel_id}/messages`"+` Discord API endpoint authenticated as a bot using token based authentication. -If the format of a message is a JSON object matching the https://discord.com/developers/docs/resources/channel#message-object[Discord API message type] then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. +If the format of a message is a JSON object matching the https://discord.com/developers/docs/resources/channel#message-object[Discord API message type^] then it is sent directly, otherwise an object matching the API type is created with the content of the message added as a string. `). Fields( service.NewStringField("channel_id"). diff --git a/internal/impl/gcp/input_pubsub.go b/internal/impl/gcp/input_pubsub.go index 8b901fb2af..5d25194f79 100644 --- a/internal/impl/gcp/input_pubsub.go +++ b/internal/impl/gcp/input_pubsub.go @@ -73,7 +73,7 @@ func pbiSpec() *service.ConfigSpec { Categories("Services", "GCP"). Summary(`Consumes messages from a GCP Cloud Pub/Sub subscription.`). Description(` -For information on how to set up credentials see https://cloud.google.com/docs/authentication/production[this guide]. +For information on how to set up credentials see https://cloud.google.com/docs/authentication/production[this guide^]. == Metadata @@ -93,7 +93,7 @@ You can access these metadata fields using xref:configuration:interpolation.adoc service.NewStringField(pbiFieldSubscriptionID). Description("The target subscription ID."), service.NewStringField(pbiFieldEndpoint). - Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]."). + Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document^]."). Example("us-central1-pubsub.googleapis.com:443"). Example("us-west3-pubsub.googleapis.com:443"). Default(""), diff --git a/internal/impl/gcp/output_bigquery.go b/internal/impl/gcp/output_bigquery.go index dc64b01415..7be9bdd168 100644 --- a/internal/impl/gcp/output_bigquery.go +++ b/internal/impl/gcp/output_bigquery.go @@ -126,8 +126,8 @@ By default Benthos will use a shared credentials file when connecting to GCP ser == Format This output currently supports only CSV and NEWLINE_DELIMITED_JSON formats. Learn more about how to use GCP BigQuery with them here: -- ` + "https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json[`NEWLINE_DELIMITED_JSON`]" + ` -- ` + "https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv[`CSV`]" + ` +- ` + "https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json[`NEWLINE_DELIMITED_JSON`^]" + ` +- ` + "https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv[`CSV`^]" + ` Each message may contain multiple elements separated by newlines. For example a single message containing: diff --git a/internal/impl/gcp/output_pubsub.go b/internal/impl/gcp/output_pubsub.go index 409a8a73fd..0403c8e75d 100644 --- a/internal/impl/gcp/output_pubsub.go +++ b/internal/impl/gcp/output_pubsub.go @@ -20,7 +20,7 @@ func newPubSubOutputConfig() *service.ConfigSpec { Categories("Services", "GCP"). Summary("Sends messages to a GCP Cloud Pub/Sub topic. xref:configuration:metadata.adoc[Metadata] from messages are sent as attributes."). Description(` -For information on how to set up credentials, see https://cloud.google.com/docs/authentication/production[this guide]. +For information on how to set up credentials, see https://cloud.google.com/docs/authentication/production[this guide^]. == Troubleshooting @@ -49,7 +49,7 @@ pipeline: Default(""). Example("us-central1-pubsub.googleapis.com:443"). Example("us-west3-pubsub.googleapis.com:443"). - Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document]."), + Description("An optional endpoint to override the default of `pubsub.googleapis.com:443`. This can be used to connect to a region specific pubsub endpoint. For a list of valid values, see https://cloud.google.com/pubsub/docs/reference/service_apis_overview#list_of_regional_endpoints[this document^]."), service.NewInterpolatedStringField("ordering_key"). Optional(). Description("The ordering key to use for publishing messages."). diff --git a/internal/impl/gcp/tracer_cloudtrace.go b/internal/impl/gcp/tracer_cloudtrace.go index ff3d35ad99..baccf99931 100644 --- a/internal/impl/gcp/tracer_cloudtrace.go +++ b/internal/impl/gcp/tracer_cloudtrace.go @@ -24,7 +24,7 @@ const ( func cloudTraceSpec() *service.ConfigSpec { return service.NewConfigSpec(). Version("4.2.0"). - Summary(`Send tracing events to a https://cloud.google.com/trace[Google Cloud Trace].`). + Summary(`Send tracing events to a https://cloud.google.com/trace[Google Cloud Trace^].`). Fields( service.NewStringField(ctFieldProject). Description("The google project with Cloud Trace API enabled. If this is omitted then the Google Cloud SDK will attempt auto-detect it from the environment."), diff --git a/internal/impl/jaeger/tracer_jaeger.go b/internal/impl/jaeger/tracer_jaeger.go index 01dd3a79d3..3b9601ccf0 100644 --- a/internal/impl/jaeger/tracer_jaeger.go +++ b/internal/impl/jaeger/tracer_jaeger.go @@ -39,7 +39,7 @@ type jaegerConfig struct { func jaegerConfigSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). - Summary("Send tracing events to a https://www.jaegertracing.io/[Jaeger] agent or collector."). + Summary("Send tracing events to a https://www.jaegertracing.io/[Jaeger^] agent or collector."). Fields( service.NewStringField(jtFieldAgentAddress). Description("The address of a Jaeger agent to send tracing events to."). diff --git a/internal/impl/javascript/processor.go b/internal/impl/javascript/processor.go index c7c9778ce1..67ad0c2312 100644 --- a/internal/impl/javascript/processor.go +++ b/internal/impl/javascript/processor.go @@ -44,11 +44,11 @@ func javascriptProcessorConfig() *service.ConfigSpec { Version("4.14.0"). Summary("Executes a provided JavaScript code block or file for each message."). Description(` -The https://github.com/dop251/goja[execution engine] behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. +The https://github.com/dop251/goja[execution engine^] behind this processor provides full ECMAScript 5.1 support (including regex and strict mode). Most of the ECMAScript 6 spec is implemented but this is a work in progress. -Imports via `+"`require`"+` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be found on https://github.com/dop251/goja#known-incompatibilities-and-caveats[GitHub]. +Imports via `+"`require`"+` should work similarly to NodeJS, and access to the console is supported which will print via the Benthos logger. More caveats can be found on https://github.com/dop251/goja#known-incompatibilities-and-caveats[GitHub^]. -This processor is implemented using the https://github.com/dop251/goja[github.com/dop251/goja] library.`). +This processor is implemented using the https://github.com/dop251/goja[github.com/dop251/goja^] library.`). Footnotes(` == Runtime diff --git a/internal/impl/kafka/input_kafka_franz.go b/internal/impl/kafka/input_kafka_franz.go index 902bf22903..d555c63533 100644 --- a/internal/impl/kafka/input_kafka_franz.go +++ b/internal/impl/kafka/input_kafka_franz.go @@ -24,7 +24,7 @@ func franzKafkaInputConfig() *service.ConfigSpec { Beta(). Categories("Services"). Version("3.61.0"). - Summary(`A Kafka input using the https://github.com/twmb/franz-go[Franz Kafka client library].`). + Summary(`A Kafka input using the https://github.com/twmb/franz-go[Franz Kafka client library^].`). Description(` When a consumer group is specified this input consumes one or more topics where partitions will automatically balance across any other connected clients with the same consumer group. When a consumer group is not specified topics can either be consumed in their entirety or with explicit partitions. diff --git a/internal/impl/kafka/output_kafka_franz.go b/internal/impl/kafka/output_kafka_franz.go index da168f6eaa..3649a5fdae 100644 --- a/internal/impl/kafka/output_kafka_franz.go +++ b/internal/impl/kafka/output_kafka_franz.go @@ -21,7 +21,7 @@ func franzKafkaOutputConfig() *service.ConfigSpec { Beta(). Categories("Services"). Version("3.61.0"). - Summary("A Kafka output using the https://github.com/twmb/franz-go[Franz Kafka client library]."). + Summary("A Kafka output using the https://github.com/twmb/franz-go[Franz Kafka client library^]."). Description(` Writes a batch of messages to Kafka brokers and waits for acknowledgement before propagating it back to the input. diff --git a/internal/impl/lang/bloblang.go b/internal/impl/lang/bloblang.go index ac959de5f1..75eddd0b92 100644 --- a/internal/impl/lang/bloblang.go +++ b/internal/impl/lang/bloblang.go @@ -24,7 +24,7 @@ func init() { slugSpec := bloblang.NewPluginSpec(). Beta(). Category("String Manipulation"). - Description(`Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its https://pkg.go.dev/github.com/gosimple/slug[docs] for more information.`). + Description(`Creates a "slug" from a given string. Wraps the github.com/gosimple/slug package. See its https://pkg.go.dev/github.com/gosimple/slug[docs^] for more information.`). Version("4.2.0"). Example("Creates a slug from an English string", `root.slug = this.value.slug()`, @@ -57,13 +57,13 @@ func init() { fakerSpec := bloblang.NewPluginSpec(). Beta(). Category("Fake Data Generation"). - Description("Takes in a string that maps to a https://github.com/go-faker/faker[faker] function and returns the result from that faker function. "+ + Description("Takes in a string that maps to a https://github.com/go-faker/faker[faker^] function and returns the result from that faker function. "+ "Returns an error if the given string doesn't match a supported faker function. Supported functions: `latitude`, `longitude`, `unix_time`, "+ "`date`, `time_string`, `month_name`, `year_string`, `day_of_week`, `day_of_month`, `timestamp`, `century`, `timezone`, `time_period`, "+ "`email`, `mac_address`, `domain_name`, `url`, `username`, `ipv4`, `ipv6`, `password`, `jwt`, `word`, `sentence`, `paragraph`, "+ "`cc_type`, `cc_number`, `currency`, `amount_with_currency`, `title_male`, `title_female`, `first_name`, `first_name_male`, "+ "`first_name_female`, `last_name`, `name`, `gender`, `chinese_first_name`, `chinese_last_name`, `chinese_name`, `phone_number`, "+ - "`toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the https://github.com/go-faker/faker[faker] docs "+ + "`toll_free_phone_number`, `e164_phone_number`, `uuid_hyphenated`, `uuid_digit`. Refer to the https://github.com/go-faker/faker[faker^] docs "+ "for details on these functions."). Param(bloblang.NewStringParam("function").Description("The name of the function to use to generate the value.").Default("")). Example("Use `time_string` to generate a time in the format `00:00:00`:", diff --git a/internal/impl/maxmind/bloblang_geoip.go b/internal/impl/maxmind/bloblang_geoip.go index 673471afa8..e515998613 100644 --- a/internal/impl/maxmind/bloblang_geoip.go +++ b/internal/impl/maxmind/bloblang_geoip.go @@ -16,7 +16,7 @@ func registerMaxmindMethodSpec(name, entity string, fn func(*geoip2.Reader, net. bloblang.NewPluginSpec(). Experimental(). Category("GeoIP"). - Description(fmt.Sprintf("Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file] and, if found, returns an object describing the %v associated with it.", entity)). + Description(fmt.Sprintf("Looks up an IP address against a https://www.maxmind.com/en/home[MaxMind database file^] and, if found, returns an object describing the %v associated with it.", entity)). Param(bloblang.NewStringParam("path").Description("A path to an mmdb (maxmind) file.")), func(args *bloblang.ParsedParams) (bloblang.Method, error) { path, err := args.GetString("path") diff --git a/internal/impl/mongodb/common.go b/internal/impl/mongodb/common.go index 69c614404f..f78e87a7c2 100644 --- a/internal/impl/mongodb/common.go +++ b/internal/impl/mongodb/common.go @@ -296,18 +296,18 @@ const ( func writeMapsFields() []*service.ConfigField { return []*service.ConfigField{ service.NewBloblangField(commonFieldDocumentMap). - Description("A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The document map is required for the operations " + + Description("A bloblang map representing a document to store within MongoDB, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. The document map is required for the operations " + "insert-one, replace-one and update-one."). Examples(mapExamples()...). Default(""), service.NewBloblangField(commonFieldFilterMap). - Description("A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. The filter map is required for all operations except " + + Description("A bloblang map representing a filter for a MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. The filter map is required for all operations except " + "insert-one. It is used to find the document(s) for the operation. For example in a delete-one case, the filter map should " + "have the fields required to locate the document to delete."). Examples(mapExamples()...). Default(""), service.NewBloblangField(commonFieldHintMap). - Description("A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form]. This map is optional and is used with all operations " + + Description("A bloblang map representing the hint for the MongoDB command, expressed as https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/[extended JSON in canonical form^]. This map is optional and is used with all operations " + "except insert-one. It is used to improve performance of finding the documents in the mongodb."). Examples(mapExamples()...). Default(""), diff --git a/internal/impl/msgpack/bloblang.go b/internal/impl/msgpack/bloblang.go index 55891f1262..156ae5ef61 100644 --- a/internal/impl/msgpack/bloblang.go +++ b/internal/impl/msgpack/bloblang.go @@ -12,7 +12,7 @@ func init() { msgpackParseSpec := bloblang.NewPluginSpec(). Category("Parsing"). - Description("Parses a https://msgpack.org/[MessagePack] message into a structured document."). + Description("Parses a https://msgpack.org/[MessagePack^] message into a structured document."). Example("", `root = content().decode("hex").parse_msgpack()`, [2]string{ @@ -47,7 +47,7 @@ func init() { msgpackFormatSpec := bloblang.NewPluginSpec(). Category("Parsing"). - Description("Formats data as a https://msgpack.org/[MessagePack] message in bytes format."). + Description("Formats data as a https://msgpack.org/[MessagePack^] message in bytes format."). Example("", `root = this.format_msgpack().encode("hex")`, [2]string{ diff --git a/internal/impl/msgpack/processor.go b/internal/impl/msgpack/processor.go index 52ed350d9a..24a4425eb3 100644 --- a/internal/impl/msgpack/processor.go +++ b/internal/impl/msgpack/processor.go @@ -13,7 +13,7 @@ func processorConfig() *service.ConfigSpec { return service.NewConfigSpec(). Beta(). Categories("Parsing"). - Summary("Converts messages to or from the https://msgpack.org/[MessagePack] format."). + Summary("Converts messages to or from the https://msgpack.org/[MessagePack^] format."). Field(service.NewStringAnnotatedEnumField("operator", map[string]string{ "to_json": "Convert MessagePack messages to JSON format", "from_json": "Convert JSON messages to MessagePack format", diff --git a/internal/impl/nats/auth.go b/internal/impl/nats/auth.go index af87233e06..58d58a8c06 100644 --- a/internal/impl/nats/auth.go +++ b/internal/impl/nats/auth.go @@ -21,10 +21,10 @@ func authDescription() string { == Authentication There are several components within Benthos which uses NATS services. You will find that each of these components -support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys] -and https://docs.nats.io/developing-with-nats/security/creds[User Credentials]. +support optional advanced authentication parameters for https://docs.nats.io/nats-server/configuration/securing_nats/auth_intro/nkey_auth[NKeys^] +and https://docs.nats.io/using-nats/developer/connecting/creds[User Credentials^]. -See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial]. +See an https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt[in-depth tutorial^]. === NKey file @@ -32,21 +32,21 @@ The NATS server can use these NKeys in several ways for authentication. The simp with a list of known public keys and for the clients to respond to the challenge by signing it with its private NKey configured in the ` + "`nkey_file`" + ` field. -https://docs.nats.io/developing-with-nats/security/nkey[More details]. +https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/nkey_auth[More details^]. === User credentials -NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT] -and a corresponding https://docs.nats.io/developing-with-nats/security/nkey[NKey secret] when connecting to a server +NATS server supports decentralized authentication based on JSON Web Tokens (JWT). Clients need an https://docs.nats.io/nats-server/configuration/securing_nats/jwt#json-web-tokens[user JWT^] +and a corresponding https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/nkey_auth[NKey secret^] when connecting to a server which is configured to use this authentication scheme. The ` + "`user_credentials_file`" + ` field should point to a file containing both the private key and the JWT and can be -generated with the https://docs.nats.io/nats-tools/nsc[nsc tool]. +generated with the https://docs.nats.io/nats-tools/nsc[nsc tool^]. Alternatively, the ` + "`user_jwt`" + ` field can contain a plain text JWT and the ` + "`user_nkey_seed`" + `can contain the plain text NKey Seed. -https://docs.nats.io/developing-with-nats/security/creds[More details].` +https://docs.nats.io/using-nats/developer/connecting/creds[More details^].` } func authFieldSpec() *service.ConfigField { diff --git a/internal/impl/nats/input_stream.go b/internal/impl/nats/input_stream.go index 7dcc626066..c5a82983ea 100644 --- a/internal/impl/nats/input_stream.go +++ b/internal/impl/nats/input_stream.go @@ -85,7 +85,7 @@ func siSpec() *service.ConfigSpec { [CAUTION] .Deprecation notice ==== -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream^]. ==== Tracking and persisting offsets through a durable name is also optional and works with or without a queue. If a durable name is not provided then subjects are consumed from the most recently published message. diff --git a/internal/impl/nats/output_stream.go b/internal/impl/nats/output_stream.go index f11237c641..8e7a1345cc 100644 --- a/internal/impl/nats/output_stream.go +++ b/internal/impl/nats/output_stream.go @@ -56,7 +56,7 @@ func soSpec() *service.ConfigSpec { [CAUTION] .Deprecation notice ==== -The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream]. +The NATS Streaming Server is being deprecated. Critical bug fixes and security fixes will be applied until June of 2023. NATS-enabled applications requiring persistence should use https://docs.nats.io/nats-concepts/jetstream[JetStream^]. ==== `+authDescription()+service.OutputPerformanceDocs(true, false)). diff --git a/internal/impl/nats/processor_kv.go b/internal/impl/nats/processor_kv.go index af8ac3f901..4b000a3e92 100644 --- a/internal/impl/nats/processor_kv.go +++ b/internal/impl/nats/processor_kv.go @@ -91,7 +91,7 @@ This processor adds the following metadata fields to each message, depending on service.NewStringAnnotatedEnumField(kvpFieldOperation, kvpOperations). Description("The operation to perform on the KV bucket."), service.NewInterpolatedStringField(kvpFieldKey). - Description("The key for each message. Supports https://docs.nats.io/nats-concepts/subjects#wildcards[wildcards] for the `history` and `keys` operations."). + Description("The key for each message. Supports https://docs.nats.io/nats-concepts/subjects#wildcards[wildcards^] for the `history` and `keys` operations."). Example("foo"). Example("foo.bar.baz"). Example("foo.*"). diff --git a/internal/impl/nsq/integration_test.go b/internal/impl/nsq/integration_test.go index 5ef149a1e4..8456fa120e 100644 --- a/internal/impl/nsq/integration_test.go +++ b/internal/impl/nsq/integration_test.go @@ -31,7 +31,7 @@ output: input: nsq: nsqd_tcp_addresses: [ localhost:4150 ] - lookupd_http_addresses: [ localhost:4160 ] + lookupd_http_addresses: [ localhost:4160 ^] topic: topic-$ID channel: channel-$ID # user_agent: "" diff --git a/internal/impl/opensearch/output.go b/internal/impl/opensearch/output.go index c3be2e68ed..bbd7353a01 100644 --- a/internal/impl/opensearch/output.go +++ b/internal/impl/opensearch/output.go @@ -177,7 +177,7 @@ Both the `+"`id` and `index`"+` fields can be dynamically set using function int service.NewBatchPolicyField(esoFieldBatching), AWSField(), ). - Example("Updating Documents", "When https://opensearch.org/docs/latest/api-reference/document-apis/update-document/[updating documents] the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors.", ` + Example("Updating Documents", "When https://opensearch.org/docs/latest/api-reference/document-apis/update-document/[updating documents^] the request body should contain a combination of a `doc`, `upsert`, and/or `script` fields at the top level, this should be done via mapping processors.", ` output: processors: - mapping: | diff --git a/internal/impl/otlp/tracer_otlp.go b/internal/impl/otlp/tracer_otlp.go index 85dab447e0..9c7aade1a1 100644 --- a/internal/impl/otlp/tracer_otlp.go +++ b/internal/impl/otlp/tracer_otlp.go @@ -20,7 +20,7 @@ import ( func oltpSpec() *service.ConfigSpec { return service.NewConfigSpec(). - Summary("Send tracing events to an https://opentelemetry.io/docs/collector/[Open Telemetry collector]."). + Summary("Send tracing events to an https://opentelemetry.io/docs/collector/[Open Telemetry collector^]."). Field(service.NewObjectListField("http", service.NewStringField("address"). Description("The endpoint of a collector to send tracing events to."). diff --git a/internal/impl/parquet/bloblang.go b/internal/impl/parquet/bloblang.go index e8889573e0..aa9d2ecb07 100644 --- a/internal/impl/parquet/bloblang.go +++ b/internal/impl/parquet/bloblang.go @@ -14,7 +14,7 @@ func init() { parquetParseSpec := bloblang.NewPluginSpec(). Category("Parsing"). - Description("Decodes a https://parquet.apache.org/docs/[Parquet file] into an array of objects, one for each row within the file."). + Description("Decodes a https://parquet.apache.org/docs/[Parquet file^] into an array of objects, one for each row within the file."). Param(bloblang.NewBoolParam("byte_array_as_string"). Description("Deprecated: This parameter is no longer used.").Default(false)). Example("", `root = content().parse_parquet()`) diff --git a/internal/impl/parquet/input_parquet.go b/internal/impl/parquet/input_parquet.go index 1fbc44c67e..f3e9118474 100644 --- a/internal/impl/parquet/input_parquet.go +++ b/internal/impl/parquet/input_parquet.go @@ -18,7 +18,7 @@ func parquetInputConfig() *service.ConfigSpec { return service.NewConfigSpec(). // Stable(). TODO Categories("Local"). - Summary("Reads and decodes https://parquet.apache.org/docs/[Parquet files] into a stream of structured messages."). + Summary("Reads and decodes https://parquet.apache.org/docs/[Parquet files^] into a stream of structured messages."). Field(service.NewStringListField("paths"). Description("A list of file paths to read from. Each file will be read sequentially until the list is exhausted, at which point the input will close. Glob patterns are supported, including super globs (double star)."). Example("/tmp/foo.parquet"). @@ -30,7 +30,7 @@ func parquetInputConfig() *service.ConfigSpec { Advanced()). Field(service.NewAutoRetryNacksToggleField()). Description(` -This input uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This input uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go^], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. By default any BYTE_ARRAY or FIXED_LEN_BYTE_ARRAY value will be extracted as a byte slice (` + "`[]byte`" + `) unless the logical type is UTF8, in which case they are extracted as a string (` + "`string`" + `). diff --git a/internal/impl/parquet/processor.go b/internal/impl/parquet/processor.go index b8fc5ac810..eab6616490 100644 --- a/internal/impl/parquet/processor.go +++ b/internal/impl/parquet/processor.go @@ -18,7 +18,7 @@ func parquetProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). Deprecated(). Categories("Parsing"). - Summary("Converts batches of documents to or from https://parquet.apache.org/docs/[Parquet files]."). + Summary("Converts batches of documents to or from https://parquet.apache.org/docs/[Parquet files^]."). Description(` == Alternatives diff --git a/internal/impl/parquet/processor_decode.go b/internal/impl/parquet/processor_decode.go index 9ad92344af..ae25901ee4 100644 --- a/internal/impl/parquet/processor_decode.go +++ b/internal/impl/parquet/processor_decode.go @@ -16,12 +16,12 @@ func parquetDecodeProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). // Stable(). TODO Categories("Parsing"). - Summary("Decodes https://parquet.apache.org/docs/[Parquet files] into a batch of structured messages."). + Summary("Decodes https://parquet.apache.org/docs/[Parquet files^] into a batch of structured messages."). Field(service.NewBoolField("byte_array_as_string"). Description("Whether to extract BYTE_ARRAY and FIXED_LEN_BYTE_ARRAY values as strings rather than byte slices in all cases. Values with a logical type of UTF8 will automatically be extracted as strings irrespective of this field. Enabling this field makes serializing the data as JSON more intuitive as `[]byte` values are serialized as base64 encoded strings by default."). Default(false).Deprecated()). Description(` -This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases.`). +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go^], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases.`). Version("4.4.0"). Example("Reading Parquet Files from AWS S3", "In this example we consume files from AWS S3 as they're written by listening onto an SQS queue for upload events. We make sure to use the `to_the_end` scanner which means files are read into memory in full, which then allows us to use a `parquet_decode` processor to expand each file into a batch of messages. Finally, we write the data out to local files as newline delimited JSON.", diff --git a/internal/impl/parquet/processor_encode.go b/internal/impl/parquet/processor_encode.go index c126e30dcd..35f6590378 100644 --- a/internal/impl/parquet/processor_encode.go +++ b/internal/impl/parquet/processor_encode.go @@ -15,7 +15,7 @@ func parquetEncodeProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). // Stable(). TODO Categories("Parsing"). - Summary("Encodes https://parquet.apache.org/docs/[Parquet files] from a batch of structured messages."). + Summary("Encodes https://parquet.apache.org/docs/[Parquet files^] from a batch of structured messages."). Field(parquetSchemaConfig()). Field(service.NewStringEnumField("default_compression", "uncompressed", "snappy", "gzip", "brotli", "zstd", "lz4raw", @@ -30,7 +30,7 @@ func parquetEncodeProcessorConfig() *service.ConfigSpec { Advanced(). Version("4.11.0")). Description(` -This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. +This processor uses https://github.com/parquet-go/parquet-go[https://github.com/parquet-go/parquet-go^], which is itself experimental. Therefore changes could be made into how this processor functions outside of major version releases. `). Version("4.4.0"). // TODO: Add an example that demonstrates error handling diff --git a/internal/impl/prometheus/metrics_prometheus.go b/internal/impl/prometheus/metrics_prometheus.go index 8d2fc15b3a..160e62f21f 100644 --- a/internal/impl/prometheus/metrics_prometheus.go +++ b/internal/impl/prometheus/metrics_prometheus.go @@ -41,7 +41,7 @@ func ConfigSpec() *service.ConfigSpec { Footnotes(` == Push gateway -The field `+"`push_url`"+` is optional and when set will trigger a push of metrics to a https://prometheus.io/docs/instrumenting/pushing/[Prometheus Push Gateway] once Benthos shuts down. It is also possible to specify a `+"`push_interval`"+` which results in periodic pushes. +The field `+"`push_url`"+` is optional and when set will trigger a push of metrics to a https://prometheus.io/docs/instrumenting/pushing/[Prometheus Push Gateway^] once Benthos shuts down. It is also possible to specify a `+"`push_interval`"+` which results in periodic pushes. The Push Gateway is useful for when Benthos instances are short lived. Do not include the "/metrics/jobs/..." path in the push URL. diff --git a/internal/impl/protobuf/processor_protobuf.go b/internal/impl/protobuf/processor_protobuf.go index ff74734b5e..2df8bfb69e 100644 --- a/internal/impl/protobuf/processor_protobuf.go +++ b/internal/impl/protobuf/processor_protobuf.go @@ -32,9 +32,9 @@ func protobufProcessorSpec() *service.ConfigSpec { Summary(` Performs conversions to or from a protobuf message. This processor uses reflection, meaning conversions can be made directly from the target .proto files. `).Description(` -The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json[https://developers.google.com/protocol-buffers/docs/proto3#json] +The main functionality of this processor is to map to and from JSON documents, you can read more about JSON mapping of protobuf messages here: https://developers.google.com/protocol-buffers/docs/proto3#json[https://developers.google.com/protocol-buffers/docs/proto3#json^] -Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at https://github.com/benthosdev/benthos-plugin-example[https://github.com/benthosdev/benthos-plugin-example] +Using reflection for processing protobuf messages in this way is less performant than generating and using native code. Therefore when performance is critical it is recommended that you use Benthos plugins instead for processing protobuf messages natively, you can find an example of Benthos plugins at https://github.com/benthosdev/benthos-plugin-example[https://github.com/benthosdev/benthos-plugin-example^] == Operators diff --git a/internal/impl/pulsar/input.go b/internal/impl/pulsar/input.go index 7bf72086da..0d2c2f931c 100644 --- a/internal/impl/pulsar/input.go +++ b/internal/impl/pulsar/input.go @@ -68,7 +68,7 @@ xref:configuration:interpolation.adoc#bloblang-queries[function interpolation]. Field(service.NewStringField("subscription_name"). Description("Specify the subscription name for this consumer.")). Field(service.NewStringEnumField("subscription_type", "shared", "key_shared", "failover", "exclusive"). - Description("Specify the subscription type for this consumer.\n\n> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement[Pulsar documentation] and https://github.com/apache/pulsar/issues/12208[this Github issue] for more details."). + Description("Specify the subscription type for this consumer.\n\n> NOTE: Using a `key_shared` subscription type will __allow out-of-order delivery__ since nack-ing messages sets non-zero nack delivery delay - this can potentially cause consumers to stall. See https://pulsar.apache.org/docs/en/2.8.1/concepts-messaging/#negative-acknowledgement[Pulsar documentation^] and https://github.com/apache/pulsar/issues/12208[this Github issue^] for more details."). Default(defaultSubscriptionType)). Field(service.NewObjectField("tls", service.NewStringField("root_cas_file"). diff --git a/internal/impl/redis/script_processor.go b/internal/impl/redis/script_processor.go index 19d8b33cf9..8b283f7271 100644 --- a/internal/impl/redis/script_processor.go +++ b/internal/impl/redis/script_processor.go @@ -15,7 +15,7 @@ func redisScriptProcConfig() *service.ConfigSpec { spec := service.NewConfigSpec(). Beta(). Version("4.11.0"). - Summary(`Performs actions against Redis using https://redis.io/docs/manual/programmability/eval-intro/[LUA scripts].`). + Summary(`Performs actions against Redis using https://redis.io/docs/manual/programmability/eval-intro/[LUA scripts^].`). Description(`Actions are performed for each message and the message contents are replaced with the result. In order to merge the result into the original message compose this processor within a ` + "xref:components:processors/branch.adoc[`branch` processor]" + `.`). diff --git a/internal/impl/sentry/processor_capture.go b/internal/impl/sentry/processor_capture.go index bb02adc364..3eff5886fb 100644 --- a/internal/impl/sentry/processor_capture.go +++ b/internal/impl/sentry/processor_capture.go @@ -20,7 +20,7 @@ const ( func newCaptureProcessorConfig() *service.ConfigSpec { return service.NewConfigSpec(). Version("4.16.0"). - Summary("Captures log events from messages and submits them to https://sentry.io/[Sentry]."). + Summary("Captures log events from messages and submits them to https://sentry.io/[Sentry^]."). Fields( service.NewStringField("dsn"). Default(""). diff --git a/internal/impl/snowflake/output_snowflake_put.go b/internal/impl/snowflake/output_snowflake_put.go index 29c7b363e8..4f4675db63 100644 --- a/internal/impl/snowflake/output_snowflake_put.go +++ b/internal/impl/snowflake/output_snowflake_put.go @@ -77,10 +77,10 @@ Snowpipe. === Key pair authentication This authentication mechanism allows Snowpipe functionality, but it does require configuring an SSH Private Key -beforehand. Please consult the https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[documentation] +beforehand. Please consult the https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[documentation^] for details on how to set it up and assign the Public Key to your user. -Note that the Snowflake documentation https://twitter.com/felipehoffa/status/1560811785606684672[used to suggest] +Note that the Snowflake documentation https://twitter.com/felipehoffa/status/1560811785606684672[used to suggest^] using this command: `+"```bash"+` @@ -102,7 +102,7 @@ If you have an existing key encrypted with PKCS#5 v1.5, you can re-encrypt it wi openssl pkcs8 -in rsa_key_original.p8 -topk8 -v2 des3 -out rsa_key.p8 `+"```"+` -Please consult the https://linux.die.net/man/1/pkcs8[pkcs8 command documentation] for details on PKCS#5 algorithms. +Please consult the https://linux.die.net/man/1/pkcs8[pkcs8 command documentation^] for details on PKCS#5 algorithms. == Batching @@ -111,7 +111,7 @@ messages at the output level and join the batch of messages with an `+"xref:components:processors/archive.adoc[`archive`]"+` and/or `+"xref:components:processors/compress.adoc[`compress`]"+` processor. -For the optimal batch size, please consult the Snowflake https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html[documentation]. +For the optimal batch size, please consult the Snowflake https://docs.snowflake.com/en/user-guide/data-load-considerations-prepare.html[documentation^]. == Snowpipe @@ -131,7 +131,7 @@ you can configure Benthos to use the implicit table stage `+"`@%BENTHOS_TBL`"+` `+"`BENTHOS_PIPE`"+` as the `+"`snowpipe`"+`. In this case, you must set `+"`compression`"+` to `+"`AUTO`"+` and, if using message batching, you'll need to configure an xref:components:processors/archive.adoc[`+"`archive`"+`] processor with the `+"`concatenate`"+` format. Since the `+"`compression`"+` is set to `+"`AUTO`"+`, the -https://github.com/snowflakedb/gosnowflake[gosnowflake] client library will compress the messages automatically so you +https://github.com/snowflakedb/gosnowflake[gosnowflake^] client library will compress the messages automatically so you don't need to add a `+"xref:components:processors/compress.adoc[`compress`]"+` processor for message batches. If you add `+"`STRIP_OUTER_ARRAY = TRUE`"+` in your Snowpipe `+"`FILE_FORMAT`"+` @@ -141,17 +141,17 @@ NOTE: Only Snowpipes with `+"`FILE_FORMAT`"+` `+"`TYPE`"+` `+"`JSON`"+` are curr == Snowpipe troubleshooting -Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html[provides] the `+"`insertReport`"+` +Snowpipe https://docs.snowflake.com/en/user-guide/data-load-snowpipe-rest-apis.html[provides^] the `+"`insertReport`"+` and `+"`loadHistoryScan`"+` REST API endpoints which can be used to get information about recent Snowpipe calls. In order to query them, you'll first need to generate a valid JWT token for your Snowflake account. There are two methods for doing so: -- Using the `+"`snowsql`"+` https://docs.snowflake.com/en/user-guide/snowsql.html[utility]: +- Using the `+"`snowsql`"+` https://docs.snowflake.com/en/user-guide/snowsql.html[utility^]: `+"```bash"+` snowsql --private-key-path rsa_key.p8 --generate-jwt -a -u `+"```"+` -- Using the Python `+"`sql-api-generate-jwt`"+` https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python[utility]: +- Using the Python `+"`sql-api-generate-jwt`"+` https://docs.snowflake.com/en/developer-guide/sql-api/authenticating.html#generating-a-jwt-in-python[utility^]: `+"```bash"+` python3 sql-api-generate-jwt.py --private_key_file_path=rsa_key.p8 --account= --user= @@ -172,27 +172,27 @@ then configure `+"`request_id: ${ @request_id }`"+` ). Alternatively, you can xr == General troubleshooting -The underlying https://github.com/snowflakedb/gosnowflake[`+"`gosnowflake`"+` driver] requires write access to -the default directory to use for temporary files. Please consult the https://pkg.go.dev/os#TempDir[`+"`os.TempDir`"+`] +The underlying https://github.com/snowflakedb/gosnowflake[`+"`gosnowflake`"+` driver^] requires write access to +the default directory to use for temporary files. Please consult the https://pkg.go.dev/os#TempDir[`+"`os.TempDir`"+`^] docs for details on how to change this directory via environment variables. -A silent failure can occur due to https://github.com/snowflakedb/gosnowflake/issues/701[this issue], where the -underlying https://github.com/snowflakedb/gosnowflake[`+"`gosnowflake`"+` driver] doesn't return an error and doesn't +A silent failure can occur due to https://github.com/snowflakedb/gosnowflake/issues/701[this issue^], where the +underlying https://github.com/snowflakedb/gosnowflake[`+"`gosnowflake`"+` driver^] doesn't return an error and doesn't log a failure if it can't figure out the current username. One way to trigger this behavior is by running Benthos in a Docker container with a non-existent user ID (such as `+"`--user 1000:1000`"+`). `+service.OutputPerformanceDocs(true, true)). - Field(service.NewStringField("account").Description(`Account name, which is the same as the https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used[Account Identifier]. -However, when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator], + Field(service.NewStringField("account").Description(`Account name, which is the same as the https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#where-are-account-identifiers-used[Account Identifier^]. +However, when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator^], the Account Identifier is formatted as `+"`..`"+` and this field needs to be populated using the `+"``"+` part. `)). Field(service.NewStringField("region").Description(`Optional region field which needs to be populated when using -an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] +an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator^] and it must be set to the `+"``"+` part of the Account Identifier (`+"`..`"+`). `).Example("us-west-2").Optional()). Field(service.NewStringField("cloud").Description(`Optional cloud platform field which needs to be populated -when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator] +when using an https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#using-an-account-locator-as-an-identifier[Account Locator^] and it must be set to the `+"``"+` part of the Account Identifier (`+"`..`"+`). `).Example("aws").Example("gcp").Example("azure").Optional()). @@ -205,7 +205,7 @@ and it must be set to the `+"``"+` part of the Account Identifier Field(service.NewStringField("warehouse").Description("Warehouse.")). Field(service.NewStringField("schema").Description("Schema.")). Field(service.NewInterpolatedStringField("stage").Description(`Stage name. Use either one of the - https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html[supported] stage types.`)). + https://docs.snowflake.com/en/user-guide/data-load-local-file-system-create-stage.html[supported^] stage types.`)). Field(service.NewInterpolatedStringField("path").Description("Stage path.").Default("")). Field(service.NewInterpolatedStringField("file_name").Description("Stage file name. Will be equal to the Request ID if not set or empty.").Optional().Default("").Version("v4.12.0")). Field(service.NewInterpolatedStringField("file_extension").Description("Stage file extension. Will be derived from the configured `compression` if not set or empty.").Optional().Default("").Example("csv").Example("parquet").Version("v4.12.0")). @@ -227,7 +227,7 @@ and it must be set to the `+"``"+` part of the Account Identifier this.exists("password") && this.password != "" && this.exists("private_key_file") && this.private_key_file != "" => [ "both `+"`password`"+` and `+"`private_key_file`"+` can't be set simultaneously" ], this.exists("snowpipe") && this.snowpipe != "" && (!this.exists("private_key_file") || this.private_key_file == "") => [ "`+"`private_key_file`"+` is required when setting `+"`snowpipe`"+`" ], }`). - Example("Kafka / realtime brokers", "Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table[Kafka Connector scheme] and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition.", ` + Example("Kafka / realtime brokers", "Upload message batches from realtime brokers such as Kafka persisting the batch partition and offsets in the stage path and filename similarly to the https://docs.snowflake.com/en/user-guide/kafka-connector-ts.html#step-1-view-the-copy-history-for-the-table[Kafka Connector scheme^] and call Snowpipe to load them into a table. When batching is configured at the input level, it is done per-partition.", ` input: kafka: addresses: diff --git a/internal/impl/splunk/template_output.yaml b/internal/impl/splunk/template_output.yaml index 9be37781df..d8ffe310d7 100644 --- a/internal/impl/splunk/template_output.yaml +++ b/internal/impl/splunk/template_output.yaml @@ -4,7 +4,7 @@ status: experimental categories: [ Services ] summary: Writes messages to a Splunk HTTP Endpoint Collector. description: | - This output POSTs messages to a Splunk HTTP Endpoint Collector (HEC) using token based authentication. The format of the message must be a [valid event JSON](https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/FormateventsforHTTPEventCollector). Raw is not supported. + This output POSTs messages to a Splunk HTTP Endpoint Collector (HEC) using token based authentication. The format of the message must be a https://docs.splunk.com/Documentation/SplunkCloud/latest/Data/FormateventsforHTTPEventCollector[valid event JSON^]. Raw is not supported. fields: - name: url description: Full HTTP Endpoint Collector (HEC) URL, ie. https://foobar.splunkcloud.com/services/collector/event diff --git a/internal/impl/sql/conn_fields.go b/internal/impl/sql/conn_fields.go index a24b1c0a3d..79eccdcf01 100644 --- a/internal/impl/sql/conn_fields.go +++ b/internal/impl/sql/conn_fields.go @@ -26,7 +26,7 @@ The following is a list of supported drivers, their placeholder style, and their | Driver | Data Source Name Format ` + "| `clickhouse` " + ` -` + "| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password]@][netloc][:port]/dbname[?param1=value1&...¶mN=valueN]`] " + ` +` + "| https://github.com/ClickHouse/clickhouse-go#dsn[`clickhouse://[username[:password\\]@\\][netloc\\][:port\\]/dbname[?param1=value1&...¶mN=valueN\\]`^] " + ` ` + "| `mysql` " + ` ` + "| `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` " + ` @@ -47,17 +47,17 @@ The following is a list of supported drivers, their placeholder style, and their ` + "| `username[:password]@account_identifier/dbname/schemaname[?param1=value&...¶mN=valueN]` " + ` ` + "| `trino` " + ` -` + "| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s]://user[:pass]@host[:port][?parameters]`] " + ` +` + "| https://github.com/trinodb/trino-go-client#dsn-data-source-name[`http[s\\]://user[:pass\\]@host[:port\\][?parameters\\]`^] " + ` ` + "| `gocosmos` " + ` -` + "| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=][;Version=][;DefaultDb/Db=][;AutoId=][;InsecureSkipVerify=]`] " + ` +` + "| https://pkg.go.dev/github.com/microsoft/gocosmos#readme-example-usage[`AccountEndpoint=;AccountKey=[;TimeoutMs=\\][;Version=\\][;DefaultDb/Db=\\][;AutoId=\\][;InsecureSkipVerify=\\]`^] " + ` |=== Please note that the ` + "`postgres`" + ` driver enforces SSL by default, you can override this with the parameter ` + "`sslmode=disable`" + ` if required. -The ` + "`snowflake`" + ` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication], the DSN has the following format: ` + "`@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`" + `, where the value for the ` + "`privateKey`" + ` parameter can be constructed from an unencrypted RSA private key file ` + "`rsa_key.p8`" + ` using ` + "`openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0`" + ` (you can use ` + "`gbasenc`" + ` insted of ` + "`basenc`" + ` on OSX if you install ` + "`coreutils`" + ` via Homebrew). If you have a password-encrypted private key, you can decrypt it using ` + "`openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`" + `. Also, make sure fields such as the username are URL-encoded. +The ` + "`snowflake`" + ` driver supports multiple DSN formats. Please consult https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String[the docs^] for more details. For https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication[key pair authentication^], the DSN has the following format: ` + "`@//?warehouse=&role=&authenticator=snowflake_jwt&privateKey=`" + `, where the value for the ` + "`privateKey`" + ` parameter can be constructed from an unencrypted RSA private key file ` + "`rsa_key.p8`" + ` using ` + "`openssl enc -d -base64 -in rsa_key.p8 | basenc --base64url -w0`" + ` (you can use ` + "`gbasenc`" + ` insted of ` + "`basenc`" + ` on OSX if you install ` + "`coreutils`" + ` via Homebrew). If you have a password-encrypted private key, you can decrypt it using ` + "`openssl pkcs8 -in rsa_key_encrypted.p8 -out rsa_key.p8`" + `. Also, make sure fields such as the username are URL-encoded. -The ` + "https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`]" + ` driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes] for details.`). +The ` + "https://pkg.go.dev/github.com/microsoft/gocosmos[`gocosmos`^]" + ` driver is still experimental, but it has support for https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys[hierarchical partition keys^] as well as https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-query-container#cross-partition-query[cross-partition queries^]. Please refer to the https://github.com/microsoft/gocosmos/blob/main/SQL.md[SQL notes^] for details.`). Example("clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60"). Example("foouser:foopassword@tcp(localhost:3306)/foodb"). Example("postgres://foouser:foopass@localhost:5432/foodb?sslmode=disable"). diff --git a/internal/impl/statsd/metrics_statsd.go b/internal/impl/statsd/metrics_statsd.go index 61ec7e7a21..ac7fd8eefd 100644 --- a/internal/impl/statsd/metrics_statsd.go +++ b/internal/impl/statsd/metrics_statsd.go @@ -20,7 +20,7 @@ const ( func statsdSpec() *service.ConfigSpec { return service.NewConfigSpec(). Stable(). - Summary("Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol]. Supported tagging formats are 'none', 'datadog' and 'influxdb'."). + Summary("Pushes metrics using the https://github.com/statsd/statsd[StatsD protocol^]. Supported tagging formats are 'none', 'datadog' and 'influxdb'."). Fields( service.NewStringField(smFieldAddress). Description("The address to send metrics to."), diff --git a/internal/impl/twitter/template_search_input.yaml b/internal/impl/twitter/template_search_input.yaml index 1f9b80ee8f..f7154b2f57 100644 --- a/internal/impl/twitter/template_search_input.yaml +++ b/internal/impl/twitter/template_search_input.yaml @@ -4,13 +4,13 @@ status: experimental categories: [ Services, Social ] summary: Consumes tweets matching a given search using the Twitter recent search V2 API. description: | - Continuously polls the [Twitter recent search V2 API](https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent) for tweets that match a given search query. + Continuously polls the https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent[Twitter recent search V2 API^] for tweets that match a given search query. - Each tweet received is emitted as a JSON object message, with a field `id` and `text` by default. Extra fields [can be obtained from the search API](https://developer.twitter.com/en/docs/twitter-api/fields) when listed with the `tweet_fields` field. + Each tweet received is emitted as a JSON object message, with a field `id` and `text` by default. Extra fields https://developer.twitter.com/en/docs/twitter-api/fields[can be obtained from the search API^] when listed with the `tweet_fields` field. - In order to paginate requests that are made the ID of the latest received tweet is stored in a [cache resource](/docs/components/caches/about), which is then used by subsequent requests to ensure only tweets after it are consumed. It is recommended that the cache you use is persistent so that Benthos can resume searches at the correct place on a restart. + In order to paginate requests that are made the ID of the latest received tweet is stored in a xref:components:caches/about.adoc[cache resource], which is then used by subsequent requests to ensure only tweets after it are consumed. It is recommended that the cache you use is persistent so that Benthos can resume searches at the correct place on a restart. - Authentication is done using OAuth 2.0 credentials which can be generated within the [Twitter developer portal](https://developer.twitter.com). + Authentication is done using OAuth 2.0 credentials which can be generated within the https://developer.twitter.com[Twitter developer portal^]. fields: - name: query @@ -18,7 +18,7 @@ fields: type: string - name: tweet_fields - description: An optional list of additional fields to obtain for each tweet, by default only the fields `id` and `text` are returned. For more info refer to the [twitter API docs.](https://developer.twitter.com/en/docs/twitter-api/fields) + description: An optional list of additional fields to obtain for each tweet, by default only the fields `id` and `text` are returned. For more info refer to the https://developer.twitter.com/en/docs/twitter-api/fields[twitter API docs^]. type: string kind: list default: [] @@ -50,11 +50,11 @@ fields: advanced: true - name: api_key - description: An API key for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). + description: An API key for OAuth 2.0 authentication. It is recommended that you populate this field using xref:configuration:interpolation.adoc[environment variables]. type: string - name: api_secret - description: An API secret for OAuth 2.0 authentication. It is recommended that you populate this field using [environment variables](/docs/configuration/interpolation). + description: An API secret for OAuth 2.0 authentication. It is recommended that you populate this field using xref:configuration:interpolation.adoc[environment variables]. type: string mapping: | diff --git a/internal/impl/wasm/processor_wazero.go b/internal/impl/wasm/processor_wazero.go index 0d88010854..a17fa99ae2 100644 --- a/internal/impl/wasm/processor_wazero.go +++ b/internal/impl/wasm/processor_wazero.go @@ -20,9 +20,9 @@ func wazeroAllocProcessorConfig() *service.ConfigSpec { Categories("Utility"). Summary("Executes a function exported by a WASM module for each message."). Description(` -This processor uses https://github.com/tetratelabs/wazero[Wazero] to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. +This processor uses https://github.com/tetratelabs/wazero[Wazero^] to execute a WASM module (with support for WASI), calling a specific function for each message being processed. From within the WASM module it is possible to query and mutate the message being processed via a suite of functions exported to the module. -This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in https://github.com/benthosdev/benthos/tree/main/public/wasm/README.md[the codebase]. +This ecosystem is delicate as WASM doesn't have a single clearly defined way to pass strings back and forth between the host and the module. In order to remedy this we're gradually working on introducing libraries and examples for multiple languages which can be found in https://github.com/{project-github}/tree/main/public/wasm/README.md[the codebase^]. These examples, as well as the processor itself, is a work in progress. From e653dc3f8a6eee072b391d5b3faa6a9a46ba00c9 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Tue, 28 May 2024 12:13:10 +0100 Subject: [PATCH 10/17] Add product and docs site cli overrides --- cmd/redpanda-connect/main.go | 2 ++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/redpanda-connect/main.go b/cmd/redpanda-connect/main.go index d30e81fc1e..79d9262b57 100644 --- a/cmd/redpanda-connect/main.go +++ b/cmd/redpanda-connect/main.go @@ -26,6 +26,8 @@ func main() { service.RunCLI( context.Background(), service.CLIOptSetVersion(Version, DateBuilt), + service.CLIOptSetProductName("Redpanda Connect"), + service.CLIOptSetDocumentationURL("https://docs.redpanda.com/redpanda-connect"), service.CLIOptSetMainSchemaFrom(func() *service.ConfigSchema { return service.NewEnvironment().FullConfigSchema(Version, DateBuilt). Field(redpandaTopLevelConfigField()) diff --git a/go.mod b/go.mod index 3ac2a26713..d9ce9d7b05 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 github.com/beanstalkd/go-beanstalk v0.2.0 github.com/benhoyt/goawk v1.25.0 - github.com/benthosdev/benthos/v4 v4.27.1-0.20240522131344-c11a4d51792f + github.com/benthosdev/benthos/v4 v4.27.1-0.20240528110950-b2a748722d18 github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/snowflake v0.3.0 diff --git a/go.sum b/go.sum index 1337aefc69..9654ef5145 100644 --- a/go.sum +++ b/go.sum @@ -241,8 +241,8 @@ github.com/beanstalkd/go-beanstalk v0.2.0/go.mod h1:/G8YTyChOtpOArwLTQPY1CHB+i21 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benhoyt/goawk v1.25.0 h1:DW4DCn2IrVp6FUar2W404G1YyQDXseWAVDwb11PUL+I= github.com/benhoyt/goawk v1.25.0/go.mod h1:FjIAicXvrv3wbqAhSTo5bn4mIM5y1iy3lcnIynlJvoI= -github.com/benthosdev/benthos/v4 v4.27.1-0.20240522131344-c11a4d51792f h1:1nuINiKbTpJUhKSTBTzTrbDHElAwXDa1mr9K/7qldWg= -github.com/benthosdev/benthos/v4 v4.27.1-0.20240522131344-c11a4d51792f/go.mod h1:KbKrzlHGhf67eIdUqk4pjT5yaD8nTPb7vczP5/twTOc= +github.com/benthosdev/benthos/v4 v4.27.1-0.20240528110950-b2a748722d18 h1:W1lcLEREKp4dSiNaTuH9M4U9q9dAf8UWCBpas5UfVPo= +github.com/benthosdev/benthos/v4 v4.27.1-0.20240528110950-b2a748722d18/go.mod h1:KbKrzlHGhf67eIdUqk4pjT5yaD8nTPb7vczP5/twTOc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= From b719fe16157e15398dbad216a9281c9d0ea0ee64 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Tue, 28 May 2024 16:43:38 +0100 Subject: [PATCH 11/17] Add rcl license --- internal/impl/kafka/topic_logger.go | 8 + .../impl/snowflake/output_snowflake_put.go | 8 + internal/impl/splunk/template_output.yaml | 8 + licenses/Apache-2.0.txt | 202 +++++++++ licenses/README.md | 9 + licenses/cla.md | 118 +++++ licenses/keep | 0 licenses/rcl.md | 408 ++++++++++++++++++ licenses/rcl_header.go.txt | 7 + 9 files changed, 768 insertions(+) create mode 100644 licenses/Apache-2.0.txt create mode 100644 licenses/README.md create mode 100644 licenses/cla.md delete mode 100644 licenses/keep create mode 100644 licenses/rcl.md create mode 100644 licenses/rcl_header.go.txt diff --git a/internal/impl/kafka/topic_logger.go b/internal/impl/kafka/topic_logger.go index f3688269d5..d954855118 100644 --- a/internal/impl/kafka/topic_logger.go +++ b/internal/impl/kafka/topic_logger.go @@ -1,3 +1,11 @@ +// Copyright 2024 Redpanda Data, Inc. +// +// Licensed as a Redpanda Enterprise file under the Redpanda Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/redpanda-data/connect/blob/main/licenses/rcl.md + package kafka import ( diff --git a/internal/impl/snowflake/output_snowflake_put.go b/internal/impl/snowflake/output_snowflake_put.go index 4f4675db63..3b32688ac9 100644 --- a/internal/impl/snowflake/output_snowflake_put.go +++ b/internal/impl/snowflake/output_snowflake_put.go @@ -1,3 +1,11 @@ +// Copyright 2024 Redpanda Data, Inc. +// +// Licensed as a Redpanda Enterprise file under the Redpanda Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/redpanda-data/connect/blob/main/licenses/rcl.md + package snowflake import ( diff --git a/internal/impl/splunk/template_output.yaml b/internal/impl/splunk/template_output.yaml index d8ffe310d7..1fc3f2bc68 100644 --- a/internal/impl/splunk/template_output.yaml +++ b/internal/impl/splunk/template_output.yaml @@ -1,3 +1,11 @@ +# Copyright 2024 Redpanda Data, Inc. +# +# Licensed as a Redpanda Enterprise file under the Redpanda Community +# License (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://github.com/redpanda-data/connect/blob/main/licenses/rcl.md + name: splunk_hec type: output status: experimental diff --git a/licenses/Apache-2.0.txt b/licenses/Apache-2.0.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/licenses/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/README.md b/licenses/README.md new file mode 100644 index 0000000000..00717bdea3 --- /dev/null +++ b/licenses/README.md @@ -0,0 +1,9 @@ +# FAQ + +There are 2 licenses for Redpanda Connect. Apache-2.0 covers the majority of connectors and functionality, and RCL (Redpanda Community License) +which covers enterprise features. + +1. [Apache-2.0](Apache-2.0.txt): Covers the majority of connectors and functionality. + +2. [RCL](rcl.md): Redpanda Community License - is intended to allow you to use enterprise features +that you pay for. diff --git a/licenses/cla.md b/licenses/cla.md new file mode 100644 index 0000000000..e9798dca7a --- /dev/null +++ b/licenses/cla.md @@ -0,0 +1,118 @@ +**Redpanda Data, Inc.** + +**Redpanda Contributor License Agreement** + +Thank you for your interest in the open source project(s) managed by +Redpanda Data, Inc. (“Redpanda Data”). In order to clarify the intellectual +property license granted with Contributions from any person or entity, +Redpanda Data must have a Contributor License Agreement (“CLA”) on file +that has been entered into by each contributor, indicating agreement to +the license terms below. This license is for your protection as a +contributor as well as the protection of Redpanda Data and its other +contributors and users; it does not change your rights to use your own +Contributions for any other purpose. + +By clicking “Accept” You accept and agree to these terms and conditions +for Your present and future Contributions submitted to Redpanda Data. In +return, Redpanda Data shall consider Your Contributions for addition to the +official Redpanda Data open source project(s) for which they were +submitted. Except for the license granted herein to Redpanda Data and +recipients of software distributed by Redpanda Data, You reserve all right, +title, and interest in and to Your Contributions. + +1\. Definitions. + +“You” (or “Your”) shall mean the copyright owner or legal entity +authorized by the copyright owner that is entering into this CLA with +Redpanda Data. For legal entities, the entity making a Contribution and all +other entities that control, are controlled by, or are under common +control with that entity are considered to be a single Contributor. For +the purposes of this definition, “control” means (i) the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or (ii) ownership of fifty percent +(50%) or more of the outstanding shares, or (iii) beneficial ownership +of such entity. + +“Contribution” shall mean any code, documentation or other original +works of authorship, including any modifications or additions to an +existing work, that are intentionally submitted by You to Redpanda Data for +inclusion in, or documentation of, any of the products owned or managed +by Redpanda Data (the “Work”). For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written +communication sent to Redpanda Data or its representatives, including but +not limited to communication on electronic mailing lists, source code +control systems, and issue tracking systems that are managed by, or on +behalf of, Redpanda Data for the purpose of discussing and improving the +Work, but excluding communication that is conspicuously marked or +otherwise designated in writing by You as “Not a Contribution.” + +2\. Grant of Copyright License. Subject to the terms and conditions of +this CLA, You hereby grant to Redpanda Data and to recipients of software +distributed by Redpanda Data a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable copyright license to reproduce, +prepare derivative works of, publicly display, publicly perform, +sublicense, and distribute Your Contributions and such derivative works. + +3\. Grant of Patent License. Subject to the terms and conditions of this +CLA, You hereby grant to Redpanda Data and to recipients of software +distributed by Redpanda Data a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, and +otherwise transfer the Work, where such license applies only to those +patent claims licensable by You that are necessarily infringed by Your +Contribution(s) alone or by combination of Your Contribution(s) with the +Work to which such Contribution(s) were submitted. If any entity +institutes patent litigation against You or any other entity (including +a cross-claim or counterclaim in a lawsuit) alleging that Your +Contribution, or the Work to which You have contributed, constitutes +direct or contributory patent infringement, then any patent licenses +granted to that entity under this CLA for that Contribution or Work +shall terminate as of the date such litigation is filed. + +4\. Authority. You represent and warrant that You are legally entitled +to grant the above license. If You are an individual and Your +employer(s) has rights to intellectual property that You create that +includes Your Contributions, You represent that You have received +permission to make Contributions on behalf of that employer, that Your +employer has waived such rights for Your Contributions to Redpanda Data, or +that Your employer has entered into a separate CLA with Redpanda Data +covering Your Contributions. If You are a Company, You represent further +that each employee making a Contribution to Redpanda Data under the +Company’s name is authorized to submit Contributions on behalf of the +Company. + +5\. Original Works. You represent and warrant that each of Your +Contributions is Your original creation (see section 7 for submissions +on behalf of others). You represent and warrant that, to Your knowledge, +none of Your Contributions infringe, violate, or misappropriate any +third party intellectual property or other proprietary rights. + +6\. Disclaimer. You are not expected to provide support for Your +Contributions, except to the extent You desire to provide support. You +may provide support for free, for a fee, or not at all. UNLESS REQUIRED +BY APPLICABLE LAW OR AGREED TO IN WRITING, EXCEPT FOR THE WARRANTIES SET +FORTH ABOVE, YOU PROVIDE YOUR CONTRIBUTIONS ON AN “AS IS” BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + +7\. Submissions on Behalf of Others. Should You wish to submit work that +is not Your original creation, You may submit it to Redpanda Data +separately from any Contribution, identifying the complete details of +its source and of any license or other restriction (including, but not +limited to, related patents, trademarks, and license agreements) of +which You are personally aware, and conspicuously marking the work as +“Submitted on behalf of a third-party: \[name here\]”. + +8\. Additional Facts/Circumstances. You agree to notify Redpanda Data of +any facts or circumstances of which You become aware that would make the +above representations and warranties inaccurate in any respect. + +9\. Authorization. If You are entering into this CLA as a Company, You +represent and warrant that the individual accepting this CLA is duly +authorized to enter into this CLA on the Company’s behalf. + +\[Field for Copyright Notice from Contributor, Inc. Name & (if +applicable) Company\] + +\[ACCEPT\] diff --git a/licenses/keep b/licenses/keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/licenses/rcl.md b/licenses/rcl.md new file mode 100644 index 0000000000..d4ccad7a0f --- /dev/null +++ b/licenses/rcl.md @@ -0,0 +1,408 @@ +**Redpanda Community License Agreement** + +Please read this Redpanda Community License Agreement (the “Agreement”) +carefully before using Redpanda (as defined below), which is offered by +Redpanda Data, Inc. or its affiliated Legal Entities (“Redpanda Data”). + +By downloading Redpanda or using it in any manner, You agree that You +have read and agree to be bound by the terms of this Agreement. If You +are accessing Redpanda on behalf of a Legal Entity, You represent and +warrant that You have the authority to agree to these terms on its +behalf and the right to bind that Legal Entity to this Agreement. Use of +Redpanda is expressly conditioned upon Your assent to all the terms of +this Agreement, to the exclusion of all other terms. + +1. **Definitions.** In addition to other + terms defined elsewhere in this Agreement, the terms below have the + following meanings. + +(a) “Redpanda” shall mean the event streaming platform provided by Redpanda Data, including both Redpanda Core and Redpanda Enterprise Edition, as defined below. + +(b) “Redpanda Core” shall mean the version of Redpanda, available free of charge at https://github.com/redpanda-data/redpanda. + +(c) “Redpanda Enterprise Edition” shall mean the additional features made available by Redpanda Data, the use of which is subject to additional terms set out below. + +(d) “Contribution” shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted Redpanda Data for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to Redpanda Data or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Redpanda Data for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as “Not a Contribution.” + +(e) “Contributor” shall mean any copyright owner or individual or Legal Entity authorized by the copyright owner, other than Redpanda Data, from whom Redpanda Data receives a Contribution that Redpanda Data subsequently incorporates within the Work. + +(f) “Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work, such as a translation, abridgement, condensation, or any other recasting, transformation, or adaptation for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +(g) “Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +(h) “License” shall mean the terms and conditions for use, reproduction, and distribution of a Work as defined by this Agreement. + +(i) “Licensor” shall mean Redpanda Data or a Contributor, as applicable. + +(j) “Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +(k) “Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +(l) “Third Party Works” shall mean Works, including Contributions, and other technology owned by a person or Legal Entity other than Redpanda Data, as indicated by a copyright notice that is included in or attached to such Works or technology. + +(m) “Work” shall mean the work of authorship, whether in Source or Object form, made available under a License, as indicated by a copyright notice that is included in or attached to the work. + +(n) “You” (or “Your”) shall mean an individual or Legal Entity exercising permissions granted by this License. + +2. **Licenses**. + + 1. **License to Redpanda Core.** The License for Redpanda Core is + the Business Source License v.1.1 ("BSL License"). Please see + the text of the Redpanda [BSL License](bsl.md) for full terms. + Redpanda Core is a no-cost, entry-level license and as such, + contains the following disclaimers: TO THE EXTENT PERMITTED BY + APPLICABLE LAW, REDPANDA CORE IS PROVIDED ON AN “AS IS” BASIS. + LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS + OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, + NON-INFRINGEMENT, AND TITLE. For clarity, the terms of this + Agreement, other than the relevant definitions in Section 1 and + this Section 2(a) do not apply to Redpanda Core. + + 2. **License to Redpanda Enterprise Edition.** + + 1. ***Grant of Copyright License:*** Subject to the terms of + this Agreement, Licensor hereby grants to You a worldwide, + non-exclusive, non-transferable limited license to + reproduce, prepare Enterprise Derivative Works (as defined + below) of, publicly display, publicly perform, sublicense, + and distribute Redpanda Enterprise Edition for Your business + purposes, for so long as You are not in violation of this + Section 2(b) and are current on all payments required by + Section 4 below. + + 2. ***Grant of Patent License:*** Subject to the terms of this + Agreement, Licensor hereby grants to You a worldwide, + non-exclusive, non-transferable limited patent license to + make, have made, use, offer to sell, sell, import, and + otherwise transfer Redpanda Enterprise Edition, where such + license applies only to those patent claims licensable by + Licensor that are necessarily infringed by their + Contribution(s) alone or by combination of their + Contribution(s) with the Work to which such Contribution(s) + was submitted. If You institute patent litigation against + any entity (including a cross-claim or counterclaim in a + lawsuit) alleging that the Work or a Contribution + incorporated within the Work constitutes direct or + contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall + terminate as of the date such litigation is filed. + + 3. ***License to Third Party Works:*** From time to time + Redpanda Data may use, or provide You access to, Third Party + Works in connection Redpanda Enterprise Edition. You + acknowledge and agree that in addition to this Agreement, + Your use of Third Party Works is subject to all other terms + and conditions set forth in the License provided with or + contained in such Third Party Works. Some Third Party Works + may be licensed to You solely for use with Redpanda + Enterprise Edition under the terms of a third party License, + or as otherwise notified by Redpanda Data, and not under the + terms of this Agreement. You agree that the owners and third + party licensors of Third Party Works are intended third + party beneficiaries to this Agreement. + + 4. ***Use Restriction:*** You may make use of Redpanda + Enterprise Edition, provided that you may not use Redpanda + Enterprise Edition for a Streaming or Queuing Service. A + “Streaming or Queueing Service” is a commercial offering + that allows third parties (other than your employees and + individual contractors) to access the functionality of + Redpanda Enterprise Edition by performing an action directly + or indirectly that causes the creation of a topic in the + Work. For clarity, a Streaming or Queuing Service would + include providers of infrastructure services, such as cloud + services, hosting services, data center services and + similarly situated third parties (including affiliates of + such entities) that would offer Redpanda Enterprise Edition + in connection with a broader service offering to customers + or subscribers of such of such third party’s core services. + +3. **Support.** From time to time, in + its sole discretion, Redpanda Data may offer professional services or + support for Redpanda, which may now or in the future be subject to + additional fees. + +4. **Fees for Redpanda Enterprise Edition or + Redpanda Support.** + + 1. **Fees.** The License to Redpanda Enterprise Edition is + conditioned upon Your payment of the fees specified on + [pricing](https://redpanda.com/contact) which You agree to pay to Redpanda Data in accordance + with the payment terms set out on that page. Any professional + services or support for Redpanda may also be subject to Your + payment of fees, which will be specified by Redpanda Data when you + sign up to receive such professional services or support. + Redpanda Data reserves the right to change the fees at any time + with prior written notice; for recurring fees, any such + adjustments will take effect as of the next payment period. + + 2. **Overdue Payments and Taxes.** Overdue payments are subject to + a service charge equal to the lesser of 1.5% per month or the + maximum legal interest rate allowed by law, and You shall pay + all Redpanda Data’s reasonable costs of collection, including court + costs and attorneys’ fees. Fees are stated and payable in U.S. + dollars and are exclusive of all sales, use, value added and + similar taxes, duties, withholdings and other governmental + assessments (but excluding taxes based on Redpanda Data’s income) + that may be levied on the transactions contemplated by this + Agreement in any jurisdiction, all of which are Your + responsibility unless you have provided Redpanda Data with a valid + tax-exempt certificate. + + 3. **Record-keeping and Audit.** If fees for Redpanda Enterprise + Edition are based on the number of cores or servers running on + Redpanda Enterprise Edition or another use-based unit of + measurement, You must maintain complete and accurate records + with respect Your use of Redpanda Enterprise Edition and will + provide such records to Redpanda Data for inspection or audit upon + Redpanda Data’s reasonable request. If an inspection or audit + uncovers additional usage by You for which fees are owed under + this Agreement, then You shall pay for such additional usage at + Redpanda Data’s then-current rates. + +5. **Trial License.** If You have signed + up for a trial or evaluation of Redpanda Enterprise Edition, Your + License to Redpanda Enterprise Edition is granted without charge for + the trial or evaluation period specified when You signed up, or if + no term was specified, for thirty (30) calendar days, provided that + Your License is granted solely for purposes of Your internal + evaluation of Redpanda Enterprise Edition during the trial or + evaluation period (a “Trial License”). You may not use Redpanda + Enterprise Edition under a Trial License more than once in any + twelve (12) month period. Redpanda Data may revoke a Trial License at + any time and for any reason. Sections 3, 4, 9 and 11 of this + Agreement do not apply to Trial Licenses. + +6. **Redistribution.** You may reproduce + and distribute copies of the Work or Derivative Works thereof in any + medium, with or without modifications, and in Source or Object form, + provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices + stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that + You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, excluding + those notices that do not pertain to any part of the Derivative + Works; and + + 4. If the Work includes a “NOTICE” text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one of + the following places: within a NOTICE text file distributed as + part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and do + not modify the License. You may add Your own attribution notices + within Derivative Works that You distribute, alongside or as an + addendum to the NOTICE text from the Work, provided that such + additional attribution notices cannot be construed as modifying + the License. + + 5. You may add Your own copyright statement to Your modifications + and may provide additional or different license terms and + conditions for use, reproduction, or distribution of Your + modifications, or for any such Derivative Works as a whole, + provided Your use, reproduction, and distribution of the Work + otherwise complies with the conditions stated in this License. + + 6. **Enterprise Derivative Works.** Derivative Works of Redpanda + Enterprise Edition (“Enterprise Derivative Works”) may be made, + reproduced and distributed in any medium, with or without + modifications, in Source or Object form, provided that each + Enterprise Derivative Work will be considered to include a + License to Redpanda Enterprise Edition and thus will be subject + to the payment of fees to Redpanda Data by any user of the + Enterprise Derivative Work. + +7. **Submission of Contributions.** + Unless You explicitly state otherwise, any Contribution + intentionally submitted for inclusion in Redpanda by You to + Redpanda Data shall be under the terms and conditions of + [https://cla-assistant.io/redpanda-data/redpanda] (which is based off of the + Apache License), without any additional terms or conditions, + payments of royalties or otherwise to Your benefit. Notwithstanding + the above, nothing herein shall supersede or modify the terms of any + separate license agreement You may have executed with Redpanda Data + regarding such Contributions. + +8. **Trademarks.** This License does not + grant permission to use the trade names, trademarks, service marks, + or product names of Licensor, except as required for reasonable and + customary use in describing the origin of the Work and reproducing + the content of the NOTICE file. + +9. **Limited Warranty.** + + 1. **Warranties.** Redpanda Data warrants to You that: (i) Redpanda + Enterprise Edition will materially perform in accordance with + the applicable documentation for ninety (90) days after initial + delivery to You; and (ii) any professional services performed by + Redpanda Data under this Agreement will be performed in a + workmanlike manner, in accordance with general industry + standards. + + 2. **Exclusions.** Redpanda Data’s warranties in this Section 9 do not + extend to problems that result from: (i) Your failure to + implement updates issued by Redpanda Data during the warranty + period; (ii) any alterations or additions (including Enterprise + Derivative Works and Contributions) to Redpanda not performed by + or at the direction of Redpanda Data; (iii) failures that are not + reproducible by Redpanda Data; (iv) operation of Redpanda + Enterprise Edition in violation of this Agreement or not in + accordance with its documentation; (v) failures caused by + software, hardware or products not licensed or provided by + Redpanda Data hereunder; or (vi) Third Party Works. + + 3. **Remedies.** In the event of a breach of a warranty under this + Section 9, Redpanda Data will, at its discretion and cost, either + repair, replace or re-perform the applicable Works or services + or refund a portion of fees previously paid to Redpanda Data that + are associated with the defective Works or services. This is + Your exclusive remedy, and Redpanda Data’s sole liability, arising + in connection with the limited warranties herein. + +10. **Disclaimer of Warranty.** EXCEPT AS + SET OUT IN SECTION 9, UNLESS REQUIRED BY APPLICABLE LAW, LICENSOR + PROVIDES THE WORK (AND EACH CONTRIBUTOR PROVIDES ITS CONTRIBUTIONS) + ON AN “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + EITHER EXPRESS OR IMPLIED, ARISING OUT OF COURSE OF DEALING, COURSE + OF PERFORMANCE, OR USAGE IN TRADE, INCLUDING, WITHOUT LIMITATION, + ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, + MERCHANTABILITY, CORRECTNESS, RELIABILITY, OR FITNESS FOR A + PARTICULAR PURPOSE, ALL OF WHICH ARE HEREBY DISCLAIMED. YOU ARE + SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING OR + REDISTRIBUTING WORKS AND ASSUME ANY RISKS ASSOCIATED WITH YOUR + EXERCISE OF PERMISSIONS UNDER THE APPLICABLE LICENSE FOR SUCH WORKS. + +11. **Limited Indemnity.** + + 1. **Indemnity.** Redpanda Data will defend, indemnify and hold You + harmless against any third party claims, liabilities or expenses + incurred (including reasonable attorneys’ fees), as well as + amounts finally awarded in a settlement or a non-appealable + judgement by a court (“Losses”), to the extent arising from any + claim or allegation by a third party that Redpanda Enterprise + Edition infringes or misappropriates a valid United States + patent, copyright or trade secret right of a third party; + provided that You give Redpanda Data: (i) prompt written notice of + any such claim or allegation; (ii) sole control of the defense + and settlement thereof; and (iii) reasonable cooperation and + assistance in such defense or settlement. If any Work within + Redpanda Enterprise Edition becomes or, in Redpanda Data’s opinion, + is likely to become, the subject of an injunction, Redpanda Data + may, at its option, (A) procure for You the right to continue + using such Work, (B) replace or modify such Work so that it + becomes non-infringing without substantially compromising its + functionality, or, if (A) and (B) are not commercially + practicable, then (C) terminate Your license to the allegedly + infringing Work and refund to You a prorated portion of the + prepaid and unearned fees for such infringing Work. The + foregoing states the entire liability of Redpanda Data with respect + to infringement of patents, copyrights, trade secrets or other + intellectual property rights. + + 2. **Exclusions.** The foregoing obligations shall not apply + to: (i) Works modified by any party other than Redpanda Data + (including Enterprise Derivative Works and Contributions), if + the alleged infringement relates to such modification, (ii) + Works combined or bundled with any products, processes or + materials not provided by Redpanda Data where the alleged + infringement relates to such combination, (iii) use of a version + of Redpanda Enterprise Edition other than the version that was + current at the time of such use, as long as a non-infringing + version had been released, (iv) any Works created to Your + specifications, (v) infringement or misappropriation of any + proprietary right in which You have an interest, or (vi) Third + Party Works. You will defend, indemnify and hold Redpanda Data + harmless against any Losses arising from any such claim or + allegation, subject to conditions reciprocal to those in Section + 11(a). + +12. **Limitation of Liability.** In no + event and under no legal or equitable theory, whether in tort + (including negligence), contract, or otherwise, unless required by + applicable law (such as deliberate and grossly negligent acts), and + notwithstanding anything in this Agreement to the contrary, shall + Licensor or any Contributor be liable to You for (i) any amounts in + excess, in the aggregate, of the fees paid by You to Redpanda Data + under this Agreement in the twelve (12) months preceding the date + the first cause of liability arose), or (ii) any indirect, special, + incidental, punitive, exemplary, reliance, or consequential damages + of any character arising as a result of this Agreement or out of the + use or inability to use the Work (including but not limited to + damages for loss of goodwill, profits, data or data use, work + stoppage, computer failure or malfunction, cost of procurement of + substitute goods, technology or services, or any and all other + commercial damages or losses), even if such Licensor or Contributor + has been advised of the possibility of such damages. THESE + LIMITATIONS SHALL APPLY NOTWITHSTANDING THE FAILURE OF THE ESSENTIAL + PURPOSE OF ANY LIMITED REMEDY. + +13. **Accepting Warranty or Additional + Liability.** While redistributing Works or Derivative Works + thereof, and without limiting your obligations under Section 6, You + may choose to offer, and charge a fee for, acceptance of support, + warranty, indemnity, or other liability obligations and/or rights + consistent with this License. However, in accepting such + obligations, You may act only on Your own behalf and on Your sole + responsibility, not on behalf of any other Contributor, and only if + You agree to indemnify, defend, and hold Redpanda Data and each other + Contributor harmless for any liability incurred by, or claims + asserted against, such Contributor by reason of your accepting any + such warranty or additional liability. + +14. **General.** + + 1. **Relationship of Parties.** You and Redpanda Data are independent + contractors, and nothing herein shall be deemed to constitute + either party as the agent or representative of the other or both + parties as joint venturers or partners for any purpose. + + 2. **Export Control.** You shall comply with the U.S. Foreign + Corrupt Practices Act and all applicable export laws, + restrictions and regulations of the U.S. Department of Commerce, + and any other applicable U.S. and foreign authority. + + 3. **Assignment.** This Agreement and the rights and obligations + herein may not be assigned or transferred, in whole or in part, + by You without the prior written consent of Redpanda Data. Any + assignment in violation of this provision is void. This + Agreement shall be binding upon, and inure to the benefit of, + the successors and permitted assigns of the parties. + + 4. **Governing Law.** This Agreement shall be governed by and + construed under the laws of the State of California and the + United States without regard to conflicts of laws provisions + thereof, and without regard to the Uniform Computer Information + Transactions Act. + + 5. **Attorneys’ Fees.** In any action or proceeding to enforce + rights under this Agreement, the prevailing party shall be + entitled to recover its costs, expenses and attorneys’ fees. + + 6. **Severability.** If any provision of this Agreement is held to + be invalid, illegal or unenforceable in any respect, that + provision shall be limited or eliminated to the minimum extent + necessary so that this Agreement otherwise remains in full force + and effect and enforceable. + + 7. **Entire Agreement; Waivers; Modification.** This Agreement + constitutes the entire agreement between the parties relating to + the subject matter hereof and supersedes all proposals, + understandings, or discussions, whether written or oral, + relating to the subject matter of this Agreement and all past + dealing or industry custom. The failure of either party to + enforce its rights under this Agreement at any time for any + period shall not be construed as a waiver of such rights. No + changes, modifications or waivers to this Agreement will be + effective unless in writing and signed by both parties. diff --git a/licenses/rcl_header.go.txt b/licenses/rcl_header.go.txt new file mode 100644 index 0000000000..d0facaae1f --- /dev/null +++ b/licenses/rcl_header.go.txt @@ -0,0 +1,7 @@ +// Copyright 2024 Redpanda Data, Inc. +// +// Licensed as a Redpanda Enterprise file under the Redpanda Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/redpanda-data/connect/blob/main/licenses/rcl.md From 630eeb26dda8ce596bd5d217978a9587268acbb1 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Tue, 28 May 2024 17:07:37 +0100 Subject: [PATCH 12/17] Add darwin signing to goreleaser --- .goreleaser.yml | 10 ++++++++++ resources/scripts/sign_for_darwin.sh | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100755 resources/scripts/sign_for_darwin.sh diff --git a/.goreleaser.yml b/.goreleaser.yml index fce9f1ab2e..fa1d94136f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -6,6 +6,16 @@ builds: goos: [ windows, darwin, linux, freebsd, openbsd ] goarch: [ amd64, arm, arm64 ] goarm: [ 6, 7 ] + hooks: + post: + # The binary is signed and notarized when running a production release, but for snapshot builds notarization is + # skipped and only ad-hoc signing is performed (not cryptographic material is needed). + # + # note: environment variables required for signing and notarization (set in CI) but are not needed for snapshot builds + # QUILL_SIGN_P12, QUILL_SIGN_PASSWORD, QUILL_NOTARY_KEY, QUILL_NOTARY_KEY_ID, QUILL_NOTARY_ISSUER + - cmd: ./resources/scripts/sign_for_darwin.sh "{{ .Os }}" "{{ .Path }}" "{{ .IsSnapshot }}" + env: + - QUILL_LOG_FILE=target/dist/quill-{{ .Target }}.log ignore: - goos: windows goarch: arm diff --git a/resources/scripts/sign_for_darwin.sh b/resources/scripts/sign_for_darwin.sh new file mode 100755 index 0000000000..9e236c151c --- /dev/null +++ b/resources/scripts/sign_for_darwin.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -eux + +_OS=$1 +_PATH_TO_SIGN=$2 +_IS_SNAPSHOT=$3 + + +if [ "$_OS" = "darwin" ]; then + quill sign-and-notarize "$_PATH_TO_SIGN" --dry-run="$_IS_SNAPSHOT" --ad-hoc="$_IS_SNAPSHOT" -vv +else + echo "No need to sign binaries for ${_OS}" +fi From 46322e032908c4a9690a0b84599999ad6b042e54 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Tue, 28 May 2024 17:10:55 +0100 Subject: [PATCH 13/17] Pin the latest benthos commit --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d9ce9d7b05..d21df7ca1d 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 github.com/beanstalkd/go-beanstalk v0.2.0 github.com/benhoyt/goawk v1.25.0 - github.com/benthosdev/benthos/v4 v4.27.1-0.20240528110950-b2a748722d18 + github.com/benthosdev/benthos/v4 v4.27.1-0.20240528154316-e55f48c9d190 github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/snowflake v0.3.0 diff --git a/go.sum b/go.sum index 9654ef5145..21368b9b4a 100644 --- a/go.sum +++ b/go.sum @@ -241,8 +241,8 @@ github.com/beanstalkd/go-beanstalk v0.2.0/go.mod h1:/G8YTyChOtpOArwLTQPY1CHB+i21 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benhoyt/goawk v1.25.0 h1:DW4DCn2IrVp6FUar2W404G1YyQDXseWAVDwb11PUL+I= github.com/benhoyt/goawk v1.25.0/go.mod h1:FjIAicXvrv3wbqAhSTo5bn4mIM5y1iy3lcnIynlJvoI= -github.com/benthosdev/benthos/v4 v4.27.1-0.20240528110950-b2a748722d18 h1:W1lcLEREKp4dSiNaTuH9M4U9q9dAf8UWCBpas5UfVPo= -github.com/benthosdev/benthos/v4 v4.27.1-0.20240528110950-b2a748722d18/go.mod h1:KbKrzlHGhf67eIdUqk4pjT5yaD8nTPb7vczP5/twTOc= +github.com/benthosdev/benthos/v4 v4.27.1-0.20240528154316-e55f48c9d190 h1:IJn9xZWz0MZ/icyb1rJLPoIN1wamlhNU5Y+LbDPRAEw= +github.com/benthosdev/benthos/v4 v4.27.1-0.20240528154316-e55f48c9d190/go.mod h1:KbKrzlHGhf67eIdUqk4pjT5yaD8nTPb7vczP5/twTOc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= From 9a212eeae1edeabfd8c66224b8a60ce1a4bb5f4d Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Wed, 29 May 2024 13:01:40 +0100 Subject: [PATCH 14/17] Use new benthos path --- .golangci.yml | 4 ++-- Makefile | 2 +- cmd/redpanda-connect/main.go | 2 +- internal/impl/amqp09/input.go | 2 +- internal/impl/amqp09/integration_test.go | 2 +- internal/impl/amqp09/output.go | 2 +- internal/impl/amqp1/config.go | 2 +- internal/impl/amqp1/input.go | 2 +- internal/impl/amqp1/integration_service_bus_test.go | 4 ++-- internal/impl/amqp1/integration_test.go | 2 +- internal/impl/amqp1/output.go | 4 ++-- internal/impl/avro/processor.go | 2 +- internal/impl/avro/processor_test.go | 2 +- internal/impl/avro/scanner.go | 2 +- internal/impl/awk/processor.go | 2 +- internal/impl/awk/processor_test.go | 2 +- internal/impl/aws/cache_dynamodb.go | 2 +- internal/impl/aws/cache_dynamodb_integration_test.go | 2 +- internal/impl/aws/cache_s3.go | 2 +- internal/impl/aws/config/config.go | 2 +- internal/impl/aws/input_kinesis.go | 2 +- internal/impl/aws/input_kinesis_checkpointer.go | 2 +- internal/impl/aws/input_kinesis_record_batcher.go | 2 +- internal/impl/aws/input_s3.go | 4 ++-- internal/impl/aws/input_sqs.go | 2 +- internal/impl/aws/input_sqs_test.go | 2 +- internal/impl/aws/integration_kinesis_test.go | 2 +- internal/impl/aws/integration_s3_test.go | 4 ++-- internal/impl/aws/integration_sqs_test.go | 2 +- internal/impl/aws/integration_test.go | 2 +- internal/impl/aws/metrics_cloudwatch.go | 2 +- internal/impl/aws/output_dynamodb.go | 2 +- internal/impl/aws/output_dynamodb_test.go | 2 +- internal/impl/aws/output_kinesis.go | 2 +- internal/impl/aws/output_kinesis_firehose.go | 2 +- internal/impl/aws/output_kinesis_firehose_test.go | 2 +- internal/impl/aws/output_kinesis_integration_test.go | 4 ++-- internal/impl/aws/output_kinesis_test.go | 2 +- internal/impl/aws/output_s3.go | 4 ++-- internal/impl/aws/output_sns.go | 4 ++-- internal/impl/aws/output_sqs.go | 4 ++-- internal/impl/aws/output_sqs_test.go | 2 +- internal/impl/aws/processor_dynamodb_partiql.go | 4 ++-- internal/impl/aws/processor_dynamodb_partiql_test.go | 4 ++-- internal/impl/aws/processor_lambda.go | 2 +- internal/impl/aws/processor_lambda_test.go | 2 +- internal/impl/aws/session.go | 2 +- internal/impl/azure/auth.go | 2 +- internal/impl/azure/cosmosdb/docs.go | 4 ++-- internal/impl/azure/cosmosdb/executor.go | 2 +- internal/impl/azure/input_blob_storage.go | 4 ++-- internal/impl/azure/input_cosmosdb.go | 2 +- internal/impl/azure/input_queue_storage.go | 2 +- internal/impl/azure/input_table_storage.go | 2 +- internal/impl/azure/integration_test.go | 8 ++++---- internal/impl/azure/output_blob_storage.go | 2 +- internal/impl/azure/output_cosmosdb.go | 2 +- internal/impl/azure/output_queue_storage.go | 2 +- internal/impl/azure/output_table_storage.go | 2 +- internal/impl/azure/processor_cosmosdb.go | 2 +- internal/impl/beanstalkd/input.go | 2 +- internal/impl/beanstalkd/integration_test.go | 2 +- internal/impl/beanstalkd/output.go | 2 +- internal/impl/cassandra/input.go | 2 +- internal/impl/cassandra/integration_test.go | 2 +- internal/impl/cassandra/output.go | 4 ++-- internal/impl/cassandra/shared.go | 2 +- internal/impl/changelog/bloblang.go | 2 +- internal/impl/changelog/bloblang_test.go | 2 +- internal/impl/cockroachdb/config_test.go | 2 +- internal/impl/cockroachdb/exploration_test.go | 6 +++--- internal/impl/cockroachdb/input_changefeed.go | 2 +- internal/impl/cockroachdb/integration_test.go | 8 ++++---- internal/impl/confluent/client.go | 2 +- .../impl/confluent/processor_schema_registry_decode.go | 2 +- .../confluent/processor_schema_registry_decode_test.go | 2 +- .../impl/confluent/processor_schema_registry_encode.go | 2 +- .../confluent/processor_schema_registry_encode_test.go | 2 +- internal/impl/confluent/serde_avro.go | 2 +- internal/impl/confluent/serde_avro_test.go | 2 +- internal/impl/confluent/serde_json.go | 2 +- internal/impl/confluent/serde_json_test.go | 2 +- internal/impl/confluent/serde_protobuf.go | 2 +- internal/impl/confluent/serde_protobuf_test.go | 2 +- internal/impl/couchbase/cache.go | 2 +- internal/impl/couchbase/cache_test.go | 2 +- internal/impl/couchbase/client.go | 2 +- internal/impl/couchbase/client/docs.go | 2 +- internal/impl/couchbase/processor.go | 4 ++-- internal/impl/couchbase/processor_test.go | 4 ++-- internal/impl/crypto/argon2.go | 2 +- internal/impl/crypto/argon2_test.go | 2 +- internal/impl/crypto/bcrypt.go | 2 +- internal/impl/crypto/bcrypt_test.go | 2 +- internal/impl/crypto/jwt_parse.go | 2 +- internal/impl/crypto/jwt_parse_test.go | 2 +- internal/impl/crypto/jwt_sign.go | 2 +- internal/impl/crypto/jwt_sign_test.go | 2 +- internal/impl/dgraph/cache_ristretto.go | 2 +- internal/impl/dgraph/cache_ristretto_test.go | 2 +- internal/impl/discord/input.go | 2 +- internal/impl/discord/output.go | 2 +- internal/impl/elasticsearch/aws/aws.go | 2 +- internal/impl/elasticsearch/aws/integration_test.go | 4 ++-- internal/impl/elasticsearch/integration_test.go | 2 +- internal/impl/elasticsearch/output.go | 2 +- internal/impl/elasticsearch/writer_integration_test.go | 4 ++-- internal/impl/gcp/bigquery.go | 2 +- internal/impl/gcp/bigquery_test.go | 2 +- internal/impl/gcp/cache_cloud_storage.go | 2 +- internal/impl/gcp/input_bigquery_select.go | 4 ++-- internal/impl/gcp/input_bigquery_select_test.go | 2 +- internal/impl/gcp/input_cloud_storage.go | 4 ++-- internal/impl/gcp/input_pubsub.go | 2 +- internal/impl/gcp/integration_pubsub_test.go | 2 +- internal/impl/gcp/integration_test.go | 4 ++-- internal/impl/gcp/output_bigquery.go | 2 +- internal/impl/gcp/output_bigquery_test.go | 2 +- internal/impl/gcp/output_cloud_storage.go | 2 +- internal/impl/gcp/output_pubsub.go | 2 +- internal/impl/gcp/output_pubsub_test.go | 2 +- internal/impl/gcp/processor_bigquery_select.go | 4 ++-- internal/impl/gcp/processor_bigquery_select_test.go | 2 +- internal/impl/gcp/tracer_cloudtrace.go | 2 +- internal/impl/hdfs/input.go | 2 +- internal/impl/hdfs/integration_test.go | 2 +- internal/impl/hdfs/output.go | 2 +- internal/impl/influxdb/metrics_influxdb.go | 2 +- .../impl/influxdb/metrics_influxdb_integration_test.go | 2 +- internal/impl/jaeger/tracer_jaeger.go | 2 +- internal/impl/javascript/benchmark_test.go | 2 +- internal/impl/javascript/functions.go | 2 +- internal/impl/javascript/logger.go | 2 +- internal/impl/javascript/processor.go | 2 +- internal/impl/javascript/processor_test.go | 2 +- internal/impl/javascript/vm.go | 2 +- internal/impl/jsonpath/bloblang_jsonpath.go | 2 +- internal/impl/kafka/aws/aws.go | 2 +- internal/impl/kafka/input_kafka_franz.go | 2 +- internal/impl/kafka/input_sarama_kafka.go | 2 +- internal/impl/kafka/input_sarama_kafka_cg.go | 2 +- internal/impl/kafka/input_sarama_kafka_parts.go | 2 +- internal/impl/kafka/input_sarama_kafka_test.go | 2 +- internal/impl/kafka/integration_sarama_test.go | 4 ++-- internal/impl/kafka/integration_test.go | 2 +- internal/impl/kafka/logger.go | 2 +- internal/impl/kafka/output_kafka_franz.go | 2 +- internal/impl/kafka/output_kafka_franz_test.go | 2 +- internal/impl/kafka/output_sarama_kafka.go | 4 ++-- internal/impl/kafka/sasl.go | 2 +- internal/impl/kafka/sasl_test.go | 4 ++-- internal/impl/kafka/topic_logger.go | 2 +- internal/impl/lang/bloblang.go | 2 +- internal/impl/lang/bloblang_test.go | 2 +- internal/impl/maxmind/bloblang_geoip.go | 2 +- internal/impl/maxmind/bloblang_geoip_test.go | 2 +- internal/impl/memcached/cache.go | 2 +- internal/impl/memcached/cache_integration_test.go | 2 +- internal/impl/mongodb/cache.go | 2 +- internal/impl/mongodb/common.go | 4 ++-- internal/impl/mongodb/input.go | 2 +- internal/impl/mongodb/input_test.go | 4 ++-- internal/impl/mongodb/integration_test.go | 2 +- internal/impl/mongodb/output.go | 2 +- internal/impl/mongodb/processor.go | 2 +- internal/impl/mongodb/processor_test.go | 4 ++-- internal/impl/mqtt/client.go | 2 +- internal/impl/mqtt/input.go | 2 +- internal/impl/mqtt/integration_test.go | 2 +- internal/impl/mqtt/output.go | 2 +- internal/impl/msgpack/bloblang.go | 2 +- internal/impl/msgpack/processor.go | 2 +- internal/impl/msgpack/processor_test.go | 2 +- internal/impl/nanomsg/input.go | 2 +- internal/impl/nanomsg/integration_test.go | 2 +- internal/impl/nanomsg/output.go | 2 +- internal/impl/nats/auth.go | 2 +- internal/impl/nats/auth_test.go | 2 +- internal/impl/nats/cache_kv.go | 2 +- internal/impl/nats/connection.go | 2 +- internal/impl/nats/docs.go | 2 +- internal/impl/nats/errors.go | 2 +- internal/impl/nats/input.go | 2 +- internal/impl/nats/input_jetstream.go | 2 +- internal/impl/nats/input_jetstream_test.go | 2 +- internal/impl/nats/input_kv.go | 2 +- internal/impl/nats/input_kv_test.go | 2 +- internal/impl/nats/input_stream.go | 2 +- internal/impl/nats/integration_jetstream_test.go | 2 +- internal/impl/nats/integration_kv_test.go | 4 ++-- internal/impl/nats/integration_nats_test.go | 2 +- internal/impl/nats/integration_req_test.go | 4 ++-- internal/impl/nats/integration_stream_test.go | 2 +- internal/impl/nats/metadata.go | 2 +- internal/impl/nats/output.go | 2 +- internal/impl/nats/output_jetstream.go | 2 +- internal/impl/nats/output_jetstream_test.go | 2 +- internal/impl/nats/output_kv.go | 2 +- internal/impl/nats/output_stream.go | 2 +- internal/impl/nats/processor_kv.go | 2 +- internal/impl/nats/processor_request_reply.go | 2 +- internal/impl/nsq/input.go | 2 +- internal/impl/nsq/integration_test.go | 2 +- internal/impl/nsq/output.go | 2 +- internal/impl/opensearch/aws/aws.go | 2 +- internal/impl/opensearch/integration_test.go | 6 +++--- internal/impl/opensearch/output.go | 2 +- internal/impl/otlp/tracer_otlp.go | 2 +- internal/impl/parquet/bloblang.go | 2 +- internal/impl/parquet/bloblang_test.go | 2 +- internal/impl/parquet/input_parquet.go | 2 +- internal/impl/parquet/input_parquet_test.go | 2 +- internal/impl/parquet/processor.go | 2 +- internal/impl/parquet/processor_decode.go | 2 +- internal/impl/parquet/processor_decode_test.go | 2 +- internal/impl/parquet/processor_encode.go | 2 +- internal/impl/parquet/processor_encode_test.go | 2 +- internal/impl/parquet/processor_test.go | 2 +- internal/impl/prometheus/metrics_prometheus.go | 2 +- internal/impl/protobuf/processor_protobuf.go | 2 +- internal/impl/protobuf/processor_protobuf_test.go | 2 +- internal/impl/pulsar/auth_field.go | 2 +- internal/impl/pulsar/input.go | 2 +- internal/impl/pulsar/input_test.go | 2 +- internal/impl/pulsar/integration_test.go | 2 +- internal/impl/pulsar/logger.go | 2 +- internal/impl/pulsar/output.go | 2 +- internal/impl/pusher/output_pusher.go | 2 +- internal/impl/redis/cache.go | 2 +- internal/impl/redis/cache_integration_test.go | 2 +- internal/impl/redis/client.go | 2 +- internal/impl/redis/input_list.go | 2 +- internal/impl/redis/input_pubsub.go | 2 +- internal/impl/redis/input_scan.go | 2 +- internal/impl/redis/input_streams.go | 2 +- internal/impl/redis/integration_test.go | 4 ++-- internal/impl/redis/output_hash.go | 2 +- internal/impl/redis/output_list.go | 2 +- internal/impl/redis/output_pubsub.go | 2 +- internal/impl/redis/output_streams.go | 2 +- internal/impl/redis/processor.go | 4 ++-- internal/impl/redis/processor_integration_test.go | 4 ++-- internal/impl/redis/rate_limit.go | 2 +- internal/impl/redis/rate_limit_integration_test.go | 2 +- internal/impl/redis/script_processor.go | 4 ++-- internal/impl/sentry/processor_capture.go | 4 ++-- internal/impl/sentry/processor_capture_test.go | 2 +- internal/impl/sftp/input.go | 4 ++-- internal/impl/sftp/integration_test.go | 4 ++-- internal/impl/sftp/output.go | 2 +- internal/impl/sftp/shared.go | 2 +- internal/impl/snowflake/output_snowflake_put.go | 2 +- internal/impl/snowflake/output_snowflake_put_test.go | 2 +- internal/impl/splunk/init.go | 2 +- internal/impl/sql/buffer_sqlite.go | 2 +- internal/impl/sql/buffer_sqlite_test.go | 4 ++-- internal/impl/sql/cache_integration_test.go | 2 +- internal/impl/sql/cache_sql.go | 2 +- internal/impl/sql/conn_fields.go | 2 +- internal/impl/sql/conn_fields_test.go | 7 ++++--- internal/impl/sql/input_sql_raw.go | 4 ++-- internal/impl/sql/input_sql_raw_test.go | 2 +- internal/impl/sql/input_sql_select.go | 4 ++-- internal/impl/sql/input_sql_select_test.go | 2 +- internal/impl/sql/integration_test.go | 9 +++++---- internal/impl/sql/output_sql_deprecated.go | 4 ++-- internal/impl/sql/output_sql_insert.go | 4 ++-- internal/impl/sql/output_sql_insert_test.go | 2 +- internal/impl/sql/output_sql_raw.go | 4 ++-- internal/impl/sql/processor_sql_deprecated.go | 4 ++-- internal/impl/sql/processor_sql_insert.go | 4 ++-- internal/impl/sql/processor_sql_raw.go | 4 ++-- internal/impl/sql/processor_sql_select.go | 4 ++-- internal/impl/statsd/metrics_statsd.go | 2 +- internal/impl/twitter/init.go | 4 ++-- internal/impl/wasm/processor_wazero.go | 2 +- internal/impl/wasm/processor_wazero_test.go | 2 +- internal/impl/xml/bloblang.go | 2 +- internal/impl/xml/bloblang_test.go | 2 +- internal/impl/xml/processor.go | 2 +- internal/impl/xml/processor_test.go | 2 +- internal/impl/zeromq/input_zmq4.go | 4 ++-- internal/impl/zeromq/integration_test.go | 2 +- internal/impl/zeromq/output_zmq4.go | 4 ++-- internal/retries/retries.go | 2 +- public/components/io/package.go | 2 +- public/components/pure/extended/package.go | 2 +- public/components/pure/package.go | 2 +- public/components/sql/package.go | 4 ++-- 289 files changed, 357 insertions(+), 355 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9a596084e0..38896d211b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,8 +20,8 @@ linters-settings: - name: superfluous-else errcheck: exclude-functions: - - (*github.com/benthosdev/benthos/v4/internal/batch.Error).Failed - - (*github.com/benthosdev/benthos/v4/public/service.BatchError).Failed + - (*github.com/redpanda-data/benthos/v4/internal/batch.Error).Failed + - (*github.com/redpanda-data/benthos/v4/public/service.BatchError).Failed govet: enable-all: true disable: diff --git a/Makefile b/Makefile index c115051156..0b817e14f9 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ all: $(APPS) install: $(APPS) @install -d $(INSTALL_DIR) - @rm -f $(INSTALL_DIR)/benthos + @rm -f $(INSTALL_DIR)/redpanda-connect @cp $(PATHINSTBIN)/* $(INSTALL_DIR)/ deps: diff --git a/cmd/redpanda-connect/main.go b/cmd/redpanda-connect/main.go index 79d9262b57..7408fb4b4b 100644 --- a/cmd/redpanda-connect/main.go +++ b/cmd/redpanda-connect/main.go @@ -4,7 +4,7 @@ import ( "context" "log/slog" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/kafka" diff --git a/internal/impl/amqp09/input.go b/internal/impl/amqp09/input.go index 2a192f8b01..af2aeeca4a 100644 --- a/internal/impl/amqp09/input.go +++ b/internal/impl/amqp09/input.go @@ -14,7 +14,7 @@ import ( amqp "github.com/rabbitmq/amqp091-go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func amqp09InputSpec() *service.ConfigSpec { diff --git a/internal/impl/amqp09/integration_test.go b/internal/impl/amqp09/integration_test.go index 961becbd49..993c8bb230 100644 --- a/internal/impl/amqp09/integration_test.go +++ b/internal/impl/amqp09/integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func doSetupAndAssertions(setQueueDeclareAutoDelete bool, t *testing.T) { diff --git a/internal/impl/amqp09/output.go b/internal/impl/amqp09/output.go index e2061defe8..ff71244843 100644 --- a/internal/impl/amqp09/output.go +++ b/internal/impl/amqp09/output.go @@ -13,7 +13,7 @@ import ( amqp "github.com/rabbitmq/amqp091-go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func amqp09OutputSpec() *service.ConfigSpec { diff --git a/internal/impl/amqp1/config.go b/internal/impl/amqp1/config.go index 6ff2d13c06..cb56efaf9b 100644 --- a/internal/impl/amqp1/config.go +++ b/internal/impl/amqp1/config.go @@ -5,7 +5,7 @@ import ( "github.com/Azure/go-amqp" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/amqp1/input.go b/internal/impl/amqp1/input.go index 482e330ec8..6588f84aa4 100644 --- a/internal/impl/amqp1/input.go +++ b/internal/impl/amqp1/input.go @@ -14,7 +14,7 @@ import ( "github.com/Azure/go-amqp" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) //go:embed input_description.adoc diff --git a/internal/impl/amqp1/integration_service_bus_test.go b/internal/impl/amqp1/integration_service_bus_test.go index 37ddb44499..e7b2f44e8c 100644 --- a/internal/impl/amqp1/integration_service_bus_test.go +++ b/internal/impl/amqp1/integration_service_bus_test.go @@ -12,8 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationAzureServiceBus(t *testing.T) { diff --git a/internal/impl/amqp1/integration_test.go b/internal/impl/amqp1/integration_test.go index 99238738a7..b8fd75016a 100644 --- a/internal/impl/amqp1/integration_test.go +++ b/internal/impl/amqp1/integration_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationAMQP1(t *testing.T) { diff --git a/internal/impl/amqp1/output.go b/internal/impl/amqp1/output.go index 7592df99bc..d56795f888 100644 --- a/internal/impl/amqp1/output.go +++ b/internal/impl/amqp1/output.go @@ -9,8 +9,8 @@ import ( "github.com/Azure/go-amqp" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func amqp1OutputSpec() *service.ConfigSpec { diff --git a/internal/impl/avro/processor.go b/internal/impl/avro/processor.go index f5f2435582..dc3218b14d 100644 --- a/internal/impl/avro/processor.go +++ b/internal/impl/avro/processor.go @@ -10,7 +10,7 @@ import ( "github.com/linkedin/goavro/v2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func avroConfigSpec() *service.ConfigSpec { diff --git a/internal/impl/avro/processor_test.go b/internal/impl/avro/processor_test.go index bd98d95309..9b20648b52 100644 --- a/internal/impl/avro/processor_test.go +++ b/internal/impl/avro/processor_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestAvroBasic(t *testing.T) { diff --git a/internal/impl/avro/scanner.go b/internal/impl/avro/scanner.go index 5462433a3b..5acce5e525 100644 --- a/internal/impl/avro/scanner.go +++ b/internal/impl/avro/scanner.go @@ -7,7 +7,7 @@ import ( "github.com/linkedin/goavro/v2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/awk/processor.go b/internal/impl/awk/processor.go index 9372e6979b..725a04ee02 100644 --- a/internal/impl/awk/processor.go +++ b/internal/impl/awk/processor.go @@ -15,7 +15,7 @@ import ( "github.com/benhoyt/goawk/interp" "github.com/benhoyt/goawk/parser" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) var varInvalidRegexp *regexp.Regexp diff --git a/internal/impl/awk/processor_test.go b/internal/impl/awk/processor_test.go index 3f0478ae6f..ca4c560ac4 100644 --- a/internal/impl/awk/processor_test.go +++ b/internal/impl/awk/processor_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func testAwk(confStr string, args ...any) (service.Processor, error) { diff --git a/internal/impl/aws/cache_dynamodb.go b/internal/impl/aws/cache_dynamodb.go index 3444929168..529bebe568 100644 --- a/internal/impl/aws/cache_dynamodb.go +++ b/internal/impl/aws/cache_dynamodb.go @@ -14,7 +14,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/cache_dynamodb_integration_test.go b/internal/impl/aws/cache_dynamodb_integration_test.go index e2474d92fc..377905d6ca 100644 --- a/internal/impl/aws/cache_dynamodb_integration_test.go +++ b/internal/impl/aws/cache_dynamodb_integration_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func createTable(ctx context.Context, t testing.TB, dynamoPort, id string) error { diff --git a/internal/impl/aws/cache_s3.go b/internal/impl/aws/cache_s3.go index 812e2dccae..3d660ebefd 100644 --- a/internal/impl/aws/cache_s3.go +++ b/internal/impl/aws/cache_s3.go @@ -12,7 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/config/config.go b/internal/impl/aws/config/config.go index f0cdf3f598..fadca90039 100644 --- a/internal/impl/aws/config/config.go +++ b/internal/impl/aws/config/config.go @@ -1,6 +1,6 @@ package config -import "github.com/benthosdev/benthos/v4/public/service" +import "github.com/redpanda-data/benthos/v4/public/service" // SessionFields defines a re-usable set of config fields for an AWS session // that is compatible with the public service APIs and avoids importing the full diff --git a/internal/impl/aws/input_kinesis.go b/internal/impl/aws/input_kinesis.go index 56e2774fdb..010aa731b5 100644 --- a/internal/impl/aws/input_kinesis.go +++ b/internal/impl/aws/input_kinesis.go @@ -15,7 +15,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/gofrs/uuid" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/input_kinesis_checkpointer.go b/internal/impl/aws/input_kinesis_checkpointer.go index 3214b23761..a9b027ba95 100644 --- a/internal/impl/aws/input_kinesis_checkpointer.go +++ b/internal/impl/aws/input_kinesis_checkpointer.go @@ -12,7 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // Common errors that might occur throughout checkpointing. diff --git a/internal/impl/aws/input_kinesis_record_batcher.go b/internal/impl/aws/input_kinesis_record_batcher.go index 4eaa22790f..16ad53985d 100644 --- a/internal/impl/aws/input_kinesis_record_batcher.go +++ b/internal/impl/aws/input_kinesis_record_batcher.go @@ -10,7 +10,7 @@ import ( "github.com/Jeffail/checkpoint" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type awsKinesisRecordBatcher struct { diff --git a/internal/impl/aws/input_s3.go b/internal/impl/aws/input_s3.go index ffab56742c..343e2856c3 100644 --- a/internal/impl/aws/input_s3.go +++ b/internal/impl/aws/input_s3.go @@ -17,8 +17,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/codec" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/input_sqs.go b/internal/impl/aws/input_sqs.go index 0fdfd6130a..446feae5f4 100644 --- a/internal/impl/aws/input_sqs.go +++ b/internal/impl/aws/input_sqs.go @@ -13,7 +13,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/input_sqs_test.go b/internal/impl/aws/input_sqs_test.go index 8f4b75815a..12d7d088ea 100644 --- a/internal/impl/aws/input_sqs_test.go +++ b/internal/impl/aws/input_sqs_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockSqsInput struct { diff --git a/internal/impl/aws/integration_kinesis_test.go b/internal/impl/aws/integration_kinesis_test.go index e009e955a1..b2a3d4c31b 100644 --- a/internal/impl/aws/integration_kinesis_test.go +++ b/internal/impl/aws/integration_kinesis_test.go @@ -12,7 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kinesis" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func createKinesisShards(ctx context.Context, t testing.TB, awsPort, id string, numShards int32) ([]string, error) { diff --git a/internal/impl/aws/integration_s3_test.go b/internal/impl/aws/integration_s3_test.go index cb49022962..101dc2947d 100644 --- a/internal/impl/aws/integration_s3_test.go +++ b/internal/impl/aws/integration_s3_test.go @@ -15,9 +15,9 @@ import ( sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" - _ "github.com/benthosdev/benthos/v4/public/components/pure" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" ) func createBucket(ctx context.Context, s3Port, bucket string) error { diff --git a/internal/impl/aws/integration_sqs_test.go b/internal/impl/aws/integration_sqs_test.go index 9a5c89d1fd..dd84f0d3dc 100644 --- a/internal/impl/aws/integration_sqs_test.go +++ b/internal/impl/aws/integration_sqs_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" _ "github.com/redpanda-data/connect/v4/public/components/pure" ) diff --git a/internal/impl/aws/integration_test.go b/internal/impl/aws/integration_test.go index 1a1968dce7..bf799953bd 100644 --- a/internal/impl/aws/integration_test.go +++ b/internal/impl/aws/integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" _ "github.com/redpanda-data/connect/v4/public/components/pure" ) diff --git a/internal/impl/aws/metrics_cloudwatch.go b/internal/impl/aws/metrics_cloudwatch.go index e005929bbc..9c973c7b77 100644 --- a/internal/impl/aws/metrics_cloudwatch.go +++ b/internal/impl/aws/metrics_cloudwatch.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/output_dynamodb.go b/internal/impl/aws/output_dynamodb.go index 88d00e5374..7d987b6c06 100644 --- a/internal/impl/aws/output_dynamodb.go +++ b/internal/impl/aws/output_dynamodb.go @@ -15,7 +15,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" "github.com/redpanda-data/connect/v4/internal/retries" diff --git a/internal/impl/aws/output_dynamodb_test.go b/internal/impl/aws/output_dynamodb_test.go index ff43776a8c..2c411bf15e 100644 --- a/internal/impl/aws/output_dynamodb_test.go +++ b/internal/impl/aws/output_dynamodb_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockDynamoDB struct { diff --git a/internal/impl/aws/output_kinesis.go b/internal/impl/aws/output_kinesis.go index 4baf97c5fe..d56f046f27 100644 --- a/internal/impl/aws/output_kinesis.go +++ b/internal/impl/aws/output_kinesis.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kinesis/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" "github.com/redpanda-data/connect/v4/internal/retries" diff --git a/internal/impl/aws/output_kinesis_firehose.go b/internal/impl/aws/output_kinesis_firehose.go index 973dec7d07..16e286f7b0 100644 --- a/internal/impl/aws/output_kinesis_firehose.go +++ b/internal/impl/aws/output_kinesis_firehose.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/firehose/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" "github.com/redpanda-data/connect/v4/internal/retries" diff --git a/internal/impl/aws/output_kinesis_firehose_test.go b/internal/impl/aws/output_kinesis_firehose_test.go index c9b1a836f2..3e74bfb703 100644 --- a/internal/impl/aws/output_kinesis_firehose_test.go +++ b/internal/impl/aws/output_kinesis_firehose_test.go @@ -14,7 +14,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockKinesisFirehose struct { diff --git a/internal/impl/aws/output_kinesis_integration_test.go b/internal/impl/aws/output_kinesis_integration_test.go index bc0cac1907..984c6de892 100644 --- a/internal/impl/aws/output_kinesis_integration_test.go +++ b/internal/impl/aws/output_kinesis_integration_test.go @@ -17,8 +17,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestKinesisIntegration(t *testing.T) { diff --git a/internal/impl/aws/output_kinesis_test.go b/internal/impl/aws/output_kinesis_test.go index f9dcfe29e1..97d82eb5e5 100644 --- a/internal/impl/aws/output_kinesis_test.go +++ b/internal/impl/aws/output_kinesis_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockKinesis struct { diff --git a/internal/impl/aws/output_s3.go b/internal/impl/aws/output_s3.go index ec2f67029c..a890416161 100644 --- a/internal/impl/aws/output_s3.go +++ b/internal/impl/aws/output_s3.go @@ -14,8 +14,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/output_sns.go b/internal/impl/aws/output_sns.go index 2a5c91e481..9709a56b2a 100644 --- a/internal/impl/aws/output_sns.go +++ b/internal/impl/aws/output_sns.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/output_sqs.go b/internal/impl/aws/output_sqs.go index ec6263130a..e1e33daf61 100644 --- a/internal/impl/aws/output_sqs.go +++ b/internal/impl/aws/output_sqs.go @@ -16,8 +16,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" "github.com/redpanda-data/connect/v4/internal/retries" diff --git a/internal/impl/aws/output_sqs_test.go b/internal/impl/aws/output_sqs_test.go index 7144fb1e4d..ee73d08fcc 100644 --- a/internal/impl/aws/output_sqs_test.go +++ b/internal/impl/aws/output_sqs_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestSQSHeaderCheck(t *testing.T) { diff --git a/internal/impl/aws/processor_dynamodb_partiql.go b/internal/impl/aws/processor_dynamodb_partiql.go index dffe0c75a0..b96dfe2a9b 100644 --- a/internal/impl/aws/processor_dynamodb_partiql.go +++ b/internal/impl/aws/processor_dynamodb_partiql.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/processor_dynamodb_partiql_test.go b/internal/impl/aws/processor_dynamodb_partiql_test.go index 77d5c2a734..9e003c38f5 100644 --- a/internal/impl/aws/processor_dynamodb_partiql_test.go +++ b/internal/impl/aws/processor_dynamodb_partiql_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockProcDynamoDB struct { diff --git a/internal/impl/aws/processor_lambda.go b/internal/impl/aws/processor_lambda.go index 5350bb44b7..b454684757 100644 --- a/internal/impl/aws/processor_lambda.go +++ b/internal/impl/aws/processor_lambda.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lambda" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/aws/processor_lambda_test.go b/internal/impl/aws/processor_lambda_test.go index 25735a79e6..83770a6033 100644 --- a/internal/impl/aws/processor_lambda_test.go +++ b/internal/impl/aws/processor_lambda_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockLambda struct { diff --git a/internal/impl/aws/session.go b/internal/impl/aws/session.go index d3d6ca2296..555e094921 100644 --- a/internal/impl/aws/session.go +++ b/internal/impl/aws/session.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func int64Field(conf *service.ParsedConfig, path ...string) (int64, error) { diff --git a/internal/impl/azure/auth.go b/internal/impl/azure/auth.go index 247806a5a0..1f27a8d4b0 100644 --- a/internal/impl/azure/auth.go +++ b/internal/impl/azure/auth.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" diff --git a/internal/impl/azure/cosmosdb/docs.go b/internal/impl/azure/cosmosdb/docs.go index da437d9727..a7f18bd606 100644 --- a/internal/impl/azure/cosmosdb/docs.go +++ b/internal/impl/azure/cosmosdb/docs.go @@ -6,8 +6,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/azure/cosmosdb/executor.go b/internal/impl/azure/cosmosdb/executor.go index 346ccb988f..890cb08edf 100644 --- a/internal/impl/azure/cosmosdb/executor.go +++ b/internal/impl/azure/cosmosdb/executor.go @@ -9,7 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/gofrs/uuid" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // Maximum number of messages which can be pushed to Azure in a TransactionalBatch diff --git a/internal/impl/azure/input_blob_storage.go b/internal/impl/azure/input_blob_storage.go index cfbb8ea378..d2cd0992d7 100644 --- a/internal/impl/azure/input_blob_storage.go +++ b/internal/impl/azure/input_blob_storage.go @@ -15,8 +15,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/Jeffail/gabs/v2" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/codec" ) const ( diff --git a/internal/impl/azure/input_cosmosdb.go b/internal/impl/azure/input_cosmosdb.go index 773fd1312d..2e492673e3 100644 --- a/internal/impl/azure/input_cosmosdb.go +++ b/internal/impl/azure/input_cosmosdb.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/mitchellh/mapstructure" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/azure/cosmosdb" ) diff --git a/internal/impl/azure/input_queue_storage.go b/internal/impl/azure/input_queue_storage.go index 219364d631..7d2c42f455 100644 --- a/internal/impl/azure/input_queue_storage.go +++ b/internal/impl/azure/input_queue_storage.go @@ -9,7 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" azq "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/azure/input_table_storage.go b/internal/impl/azure/input_table_storage.go index 233909f796..cd5d9374d4 100644 --- a/internal/impl/azure/input_table_storage.go +++ b/internal/impl/azure/input_table_storage.go @@ -7,7 +7,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/azure/integration_test.go b/internal/impl/azure/integration_test.go index a05b3c18a4..e284ec74ec 100644 --- a/internal/impl/azure/integration_test.go +++ b/internal/impl/azure/integration_test.go @@ -23,10 +23,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/bloblang" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationAzure(t *testing.T) { diff --git a/internal/impl/azure/output_blob_storage.go b/internal/impl/azure/output_blob_storage.go index 04f89d7c04..335170fc06 100644 --- a/internal/impl/azure/output_blob_storage.go +++ b/internal/impl/azure/output_blob_storage.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/azure/output_cosmosdb.go b/internal/impl/azure/output_cosmosdb.go index c53824ca8e..bee74a8689 100644 --- a/internal/impl/azure/output_cosmosdb.go +++ b/internal/impl/azure/output_cosmosdb.go @@ -7,7 +7,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/azure/cosmosdb" ) diff --git a/internal/impl/azure/output_queue_storage.go b/internal/impl/azure/output_queue_storage.go index a531b21df1..387ec705d2 100644 --- a/internal/impl/azure/output_queue_storage.go +++ b/internal/impl/azure/output_queue_storage.go @@ -9,7 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/azure/output_table_storage.go b/internal/impl/azure/output_table_storage.go index 03161018e8..1e7d040e03 100644 --- a/internal/impl/azure/output_table_storage.go +++ b/internal/impl/azure/output_table_storage.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/azure/processor_cosmosdb.go b/internal/impl/azure/processor_cosmosdb.go index e1a0d25a6e..51a290ae83 100644 --- a/internal/impl/azure/processor_cosmosdb.go +++ b/internal/impl/azure/processor_cosmosdb.go @@ -6,7 +6,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/azure/cosmosdb" ) diff --git a/internal/impl/beanstalkd/input.go b/internal/impl/beanstalkd/input.go index 0dd7efa5db..b30ff34a9f 100644 --- a/internal/impl/beanstalkd/input.go +++ b/internal/impl/beanstalkd/input.go @@ -8,7 +8,7 @@ import ( "github.com/beanstalkd/go-beanstalk" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func beanstalkdInputConfig() *service.ConfigSpec { diff --git a/internal/impl/beanstalkd/integration_test.go b/internal/impl/beanstalkd/integration_test.go index 270d69dfff..a861f16e0b 100644 --- a/internal/impl/beanstalkd/integration_test.go +++ b/internal/impl/beanstalkd/integration_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) const template string = ` diff --git a/internal/impl/beanstalkd/output.go b/internal/impl/beanstalkd/output.go index 1e747fa9a5..3e75477630 100644 --- a/internal/impl/beanstalkd/output.go +++ b/internal/impl/beanstalkd/output.go @@ -7,7 +7,7 @@ import ( "github.com/beanstalkd/go-beanstalk" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func beanstalkdOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/cassandra/input.go b/internal/impl/cassandra/input.go index 2bf1957c0b..66f87f96e4 100644 --- a/internal/impl/cassandra/input.go +++ b/internal/impl/cassandra/input.go @@ -6,7 +6,7 @@ import ( "github.com/gocql/gocql" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/cassandra/integration_test.go b/internal/impl/cassandra/integration_test.go index 22a9224d95..33265120f0 100644 --- a/internal/impl/cassandra/integration_test.go +++ b/internal/impl/cassandra/integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationCassandra(t *testing.T) { diff --git a/internal/impl/cassandra/output.go b/internal/impl/cassandra/output.go index 6353e2062f..038c6209cc 100644 --- a/internal/impl/cassandra/output.go +++ b/internal/impl/cassandra/output.go @@ -11,8 +11,8 @@ import ( "github.com/gocql/gocql" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/cassandra/shared.go b/internal/impl/cassandra/shared.go index 48a24f3df6..de076dab24 100644 --- a/internal/impl/cassandra/shared.go +++ b/internal/impl/cassandra/shared.go @@ -7,7 +7,7 @@ import ( "github.com/gocql/gocql" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/changelog/bloblang.go b/internal/impl/changelog/bloblang.go index 23277b293c..8b4dd0c91d 100644 --- a/internal/impl/changelog/bloblang.go +++ b/internal/impl/changelog/bloblang.go @@ -5,7 +5,7 @@ import ( "github.com/r3labs/diff/v3" "go.uber.org/multierr" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func init() { diff --git a/internal/impl/changelog/bloblang_test.go b/internal/impl/changelog/bloblang_test.go index 9df92b7fe7..13f5859493 100644 --- a/internal/impl/changelog/bloblang_test.go +++ b/internal/impl/changelog/bloblang_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func Test_Diff__shouldReturnDiff(t *testing.T) { diff --git a/internal/impl/cockroachdb/config_test.go b/internal/impl/cockroachdb/config_test.go index 96bc200613..d0bc87154c 100644 --- a/internal/impl/cockroachdb/config_test.go +++ b/internal/impl/cockroachdb/config_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestCRDBConfigParse(t *testing.T) { diff --git a/internal/impl/cockroachdb/exploration_test.go b/internal/impl/cockroachdb/exploration_test.go index 27c47ee5bb..54a2470c2f 100644 --- a/internal/impl/cockroachdb/exploration_test.go +++ b/internal/impl/cockroachdb/exploration_test.go @@ -15,9 +15,9 @@ import ( _ "github.com/lib/pq" - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service/integration" + _ "github.com/redpanda-data/benthos/v4/public/components/io" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationExploration(t *testing.T) { diff --git a/internal/impl/cockroachdb/input_changefeed.go b/internal/impl/cockroachdb/input_changefeed.go index 4b8b9d4ca5..eeb2b54bb6 100644 --- a/internal/impl/cockroachdb/input_changefeed.go +++ b/internal/impl/cockroachdb/input_changefeed.go @@ -16,7 +16,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" _ "github.com/lib/pq" ) diff --git a/internal/impl/cockroachdb/integration_test.go b/internal/impl/cockroachdb/integration_test.go index 644bcd4c71..fe092ec182 100644 --- a/internal/impl/cockroachdb/integration_test.go +++ b/internal/impl/cockroachdb/integration_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - _ "github.com/benthosdev/benthos/v4/public/components/io" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + _ "github.com/redpanda-data/benthos/v4/public/components/io" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationCRDB(t *testing.T) { diff --git a/internal/impl/confluent/client.go b/internal/impl/confluent/client.go index a56795d191..b49f35591a 100644 --- a/internal/impl/confluent/client.go +++ b/internal/impl/confluent/client.go @@ -12,7 +12,7 @@ import ( "net/http" "net/url" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type schemaRegistryClient struct { diff --git a/internal/impl/confluent/processor_schema_registry_decode.go b/internal/impl/confluent/processor_schema_registry_decode.go index 38dd0ede01..582b2c7ecb 100644 --- a/internal/impl/confluent/processor_schema_registry_decode.go +++ b/internal/impl/confluent/processor_schema_registry_decode.go @@ -14,7 +14,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func schemaRegistryDecoderConfig() *service.ConfigSpec { diff --git a/internal/impl/confluent/processor_schema_registry_decode_test.go b/internal/impl/confluent/processor_schema_registry_decode_test.go index a8550497e4..97d296df3b 100644 --- a/internal/impl/confluent/processor_schema_registry_decode_test.go +++ b/internal/impl/confluent/processor_schema_registry_decode_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestSchemaRegistryDecoderConfigParse(t *testing.T) { diff --git a/internal/impl/confluent/processor_schema_registry_encode.go b/internal/impl/confluent/processor_schema_registry_encode.go index 6a8f42ea54..8d9b31e18c 100644 --- a/internal/impl/confluent/processor_schema_registry_encode.go +++ b/internal/impl/confluent/processor_schema_registry_encode.go @@ -14,7 +14,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func schemaRegistryEncoderConfig() *service.ConfigSpec { diff --git a/internal/impl/confluent/processor_schema_registry_encode_test.go b/internal/impl/confluent/processor_schema_registry_encode_test.go index fb390e9e33..342055eb8a 100644 --- a/internal/impl/confluent/processor_schema_registry_encode_test.go +++ b/internal/impl/confluent/processor_schema_registry_encode_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) var noopReqSign = func(fs.FS, *http.Request) error { return nil } diff --git a/internal/impl/confluent/serde_avro.go b/internal/impl/confluent/serde_avro.go index 2042b717a9..e319ca9b22 100644 --- a/internal/impl/confluent/serde_avro.go +++ b/internal/impl/confluent/serde_avro.go @@ -7,7 +7,7 @@ import ( "github.com/linkedin/goavro/v2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func resolveAvroReferences(ctx context.Context, client *schemaRegistryClient, info SchemaInfo) (string, error) { diff --git a/internal/impl/confluent/serde_avro_test.go b/internal/impl/confluent/serde_avro_test.go index 4e6f10a02e..49bd95432c 100644 --- a/internal/impl/confluent/serde_avro_test.go +++ b/internal/impl/confluent/serde_avro_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestAvroReferences(t *testing.T) { diff --git a/internal/impl/confluent/serde_json.go b/internal/impl/confluent/serde_json.go index 64530d0fc7..4dd38d9923 100644 --- a/internal/impl/confluent/serde_json.go +++ b/internal/impl/confluent/serde_json.go @@ -6,7 +6,7 @@ import ( "github.com/xeipuuv/gojsonschema" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func resolveJSONSchema(ctx context.Context, client *schemaRegistryClient, info SchemaInfo) (*gojsonschema.Schema, error) { diff --git a/internal/impl/confluent/serde_json_test.go b/internal/impl/confluent/serde_json_test.go index 22acca9c44..4f25a36e35 100644 --- a/internal/impl/confluent/serde_json_test.go +++ b/internal/impl/confluent/serde_json_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestResolveJsonSchema(t *testing.T) { diff --git a/internal/impl/confluent/serde_protobuf.go b/internal/impl/confluent/serde_protobuf.go index 505fd25398..370176524e 100644 --- a/internal/impl/confluent/serde_protobuf.go +++ b/internal/impl/confluent/serde_protobuf.go @@ -13,7 +13,7 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/protobuf" ) diff --git a/internal/impl/confluent/serde_protobuf_test.go b/internal/impl/confluent/serde_protobuf_test.go index af54ef0dff..e25644f4a0 100644 --- a/internal/impl/confluent/serde_protobuf_test.go +++ b/internal/impl/confluent/serde_protobuf_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestProtobufEncodeMultipleMessages(t *testing.T) { diff --git a/internal/impl/couchbase/cache.go b/internal/impl/couchbase/cache.go index 316821a195..d947070f7d 100644 --- a/internal/impl/couchbase/cache.go +++ b/internal/impl/couchbase/cache.go @@ -7,7 +7,7 @@ import ( "github.com/couchbase/gocb/v2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/couchbase/client" ) diff --git a/internal/impl/couchbase/cache_test.go b/internal/impl/couchbase/cache_test.go index a3aed80e49..1baff6ab08 100644 --- a/internal/impl/couchbase/cache_test.go +++ b/internal/impl/couchbase/cache_test.go @@ -9,7 +9,7 @@ import ( "github.com/couchbase/gocb/v2" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationCouchbaseCache(t *testing.T) { diff --git a/internal/impl/couchbase/client.go b/internal/impl/couchbase/client.go index 5d5869e68d..8419a1c548 100644 --- a/internal/impl/couchbase/client.go +++ b/internal/impl/couchbase/client.go @@ -7,7 +7,7 @@ import ( "github.com/couchbase/gocb/v2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/couchbase/client" ) diff --git a/internal/impl/couchbase/client/docs.go b/internal/impl/couchbase/client/docs.go index 5eea1d14b8..173bf83325 100644 --- a/internal/impl/couchbase/client/docs.go +++ b/internal/impl/couchbase/client/docs.go @@ -1,7 +1,7 @@ package client import ( - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // NewConfigSpec constructs a new Couchbase ConfigSpec with common config fields diff --git a/internal/impl/couchbase/processor.go b/internal/impl/couchbase/processor.go index 9cd29eb13b..2b129fcac1 100644 --- a/internal/impl/couchbase/processor.go +++ b/internal/impl/couchbase/processor.go @@ -7,8 +7,8 @@ import ( "github.com/couchbase/gocb/v2" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/couchbase/client" ) diff --git a/internal/impl/couchbase/processor_test.go b/internal/impl/couchbase/processor_test.go index 59508d8d91..e2eb780b64 100644 --- a/internal/impl/couchbase/processor_test.go +++ b/internal/impl/couchbase/processor_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/redpanda-data/connect/v4/internal/impl/couchbase" ) diff --git a/internal/impl/crypto/argon2.go b/internal/impl/crypto/argon2.go index 394db3c237..5bf3c7e1bd 100644 --- a/internal/impl/crypto/argon2.go +++ b/internal/impl/crypto/argon2.go @@ -10,7 +10,7 @@ import ( "go.uber.org/multierr" "golang.org/x/crypto/argon2" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) var errInvalidArgon2Hash = errors.New("invalid argon2 hash") diff --git a/internal/impl/crypto/argon2_test.go b/internal/impl/crypto/argon2_test.go index 943db1b511..9903c88f08 100644 --- a/internal/impl/crypto/argon2_test.go +++ b/internal/impl/crypto/argon2_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestBloblangCompareArgon2(t *testing.T) { diff --git a/internal/impl/crypto/bcrypt.go b/internal/impl/crypto/bcrypt.go index 697b78e545..71f882747c 100644 --- a/internal/impl/crypto/bcrypt.go +++ b/internal/impl/crypto/bcrypt.go @@ -5,7 +5,7 @@ import ( "golang.org/x/crypto/bcrypt" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func registerCompareBCryptMethod() error { diff --git a/internal/impl/crypto/bcrypt_test.go b/internal/impl/crypto/bcrypt_test.go index b9825110a4..2d88f725f5 100644 --- a/internal/impl/crypto/bcrypt_test.go +++ b/internal/impl/crypto/bcrypt_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestBloblangCompareBCrypt(t *testing.T) { diff --git a/internal/impl/crypto/jwt_parse.go b/internal/impl/crypto/jwt_parse.go index 14ebff7def..751b0103a9 100644 --- a/internal/impl/crypto/jwt_parse.go +++ b/internal/impl/crypto/jwt_parse.go @@ -7,7 +7,7 @@ import ( "github.com/golang-jwt/jwt/v5" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) var errJWTIncorrectMethod = errors.New("incorrect signing method") diff --git a/internal/impl/crypto/jwt_parse_test.go b/internal/impl/crypto/jwt_parse_test.go index dd511d116b..9c19bf04c8 100644 --- a/internal/impl/crypto/jwt_parse_test.go +++ b/internal/impl/crypto/jwt_parse_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) const dummySecretRSA = `-----BEGIN PUBLIC KEY----- diff --git a/internal/impl/crypto/jwt_sign.go b/internal/impl/crypto/jwt_sign.go index d7aea5c7f9..8043c51151 100644 --- a/internal/impl/crypto/jwt_sign.go +++ b/internal/impl/crypto/jwt_sign.go @@ -6,7 +6,7 @@ import ( "github.com/golang-jwt/jwt/v5" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) type secretDecoderFunc func(secret string) (any, error) diff --git a/internal/impl/crypto/jwt_sign_test.go b/internal/impl/crypto/jwt_sign_test.go index ba06887458..cea7995293 100644 --- a/internal/impl/crypto/jwt_sign_test.go +++ b/internal/impl/crypto/jwt_sign_test.go @@ -7,7 +7,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestBloblangSignJwt(t *testing.T) { diff --git a/internal/impl/dgraph/cache_ristretto.go b/internal/impl/dgraph/cache_ristretto.go index 8e874f3c5c..2a766f9405 100644 --- a/internal/impl/dgraph/cache_ristretto.go +++ b/internal/impl/dgraph/cache_ristretto.go @@ -9,7 +9,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/dgraph-io/ristretto" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func ristrettoCacheConfig() *service.ConfigSpec { diff --git a/internal/impl/dgraph/cache_ristretto_test.go b/internal/impl/dgraph/cache_ristretto_test.go index 5989336fba..d48a27591e 100644 --- a/internal/impl/dgraph/cache_ristretto_test.go +++ b/internal/impl/dgraph/cache_ristretto_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestRistrettoCache(t *testing.T) { diff --git a/internal/impl/discord/input.go b/internal/impl/discord/input.go index 3f03dcec78..c00da6e64c 100644 --- a/internal/impl/discord/input.go +++ b/internal/impl/discord/input.go @@ -13,7 +13,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func inputConfig() *service.ConfigSpec { diff --git a/internal/impl/discord/output.go b/internal/impl/discord/output.go index a5a4fe19ec..c27dcc539c 100644 --- a/internal/impl/discord/output.go +++ b/internal/impl/discord/output.go @@ -7,7 +7,7 @@ import ( "github.com/bwmarrin/discordgo" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func outputConfig() *service.ConfigSpec { diff --git a/internal/impl/elasticsearch/aws/aws.go b/internal/impl/elasticsearch/aws/aws.go index 94143edc48..800b42e48c 100644 --- a/internal/impl/elasticsearch/aws/aws.go +++ b/internal/impl/elasticsearch/aws/aws.go @@ -14,7 +14,7 @@ import ( "github.com/olivere/elastic/v7" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" baws "github.com/redpanda-data/connect/v4/internal/impl/aws" "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" diff --git a/internal/impl/elasticsearch/aws/integration_test.go b/internal/impl/elasticsearch/aws/integration_test.go index 60d67afb24..2f0c3e9135 100644 --- a/internal/impl/elasticsearch/aws/integration_test.go +++ b/internal/impl/elasticsearch/aws/integration_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" diff --git a/internal/impl/elasticsearch/integration_test.go b/internal/impl/elasticsearch/integration_test.go index 7a059d9e50..ddc8e90f2f 100644 --- a/internal/impl/elasticsearch/integration_test.go +++ b/internal/impl/elasticsearch/integration_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) var elasticIndex = `{ diff --git a/internal/impl/elasticsearch/output.go b/internal/impl/elasticsearch/output.go index 90f9f79e2b..84f58e8115 100644 --- a/internal/impl/elasticsearch/output.go +++ b/internal/impl/elasticsearch/output.go @@ -12,7 +12,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/olivere/elastic/v7" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" "github.com/redpanda-data/connect/v4/internal/retries" diff --git a/internal/impl/elasticsearch/writer_integration_test.go b/internal/impl/elasticsearch/writer_integration_test.go index da2d298889..ea11dbe7de 100644 --- a/internal/impl/elasticsearch/writer_integration_test.go +++ b/internal/impl/elasticsearch/writer_integration_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/redpanda-data/connect/v4/internal/impl/elasticsearch" ) diff --git a/internal/impl/gcp/bigquery.go b/internal/impl/gcp/bigquery.go index 16b1d26723..d7ed40f7bf 100644 --- a/internal/impl/gcp/bigquery.go +++ b/internal/impl/gcp/bigquery.go @@ -8,7 +8,7 @@ import ( "github.com/Masterminds/squirrel" "go.uber.org/multierr" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type bigqueryIterator interface { diff --git a/internal/impl/gcp/bigquery_test.go b/internal/impl/gcp/bigquery_test.go index 01ad57b3f3..6a1df5d034 100644 --- a/internal/impl/gcp/bigquery_test.go +++ b/internal/impl/gcp/bigquery_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/api/iterator" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type mockBQIterator struct { diff --git a/internal/impl/gcp/cache_cloud_storage.go b/internal/impl/gcp/cache_cloud_storage.go index a7a57eeb1b..92823051d8 100644 --- a/internal/impl/gcp/cache_cloud_storage.go +++ b/internal/impl/gcp/cache_cloud_storage.go @@ -8,7 +8,7 @@ import ( "cloud.google.com/go/storage" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func gcpCloudStorageCacheConfig() *service.ConfigSpec { diff --git a/internal/impl/gcp/input_bigquery_select.go b/internal/impl/gcp/input_bigquery_select.go index bcd9c316ec..ed7d5bfe00 100644 --- a/internal/impl/gcp/input_bigquery_select.go +++ b/internal/impl/gcp/input_bigquery_select.go @@ -11,8 +11,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) type bigQuerySelectInputConfig struct { diff --git a/internal/impl/gcp/input_bigquery_select_test.go b/internal/impl/gcp/input_bigquery_select_test.go index 39bf173a90..4ace93b869 100644 --- a/internal/impl/gcp/input_bigquery_select_test.go +++ b/internal/impl/gcp/input_bigquery_select_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) var testBQInputYAML = ` diff --git a/internal/impl/gcp/input_cloud_storage.go b/internal/impl/gcp/input_cloud_storage.go index 0abde7c71d..cd6d8abd8f 100644 --- a/internal/impl/gcp/input_cloud_storage.go +++ b/internal/impl/gcp/input_cloud_storage.go @@ -11,8 +11,8 @@ import ( "cloud.google.com/go/storage" "google.golang.org/api/iterator" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/codec" ) const ( diff --git a/internal/impl/gcp/input_pubsub.go b/internal/impl/gcp/input_pubsub.go index 5d25194f79..087423e36c 100644 --- a/internal/impl/gcp/input_pubsub.go +++ b/internal/impl/gcp/input_pubsub.go @@ -9,7 +9,7 @@ import ( "cloud.google.com/go/pubsub" "google.golang.org/api/option" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/gcp/integration_pubsub_test.go b/internal/impl/gcp/integration_pubsub_test.go index c73720f3b6..7323b073be 100644 --- a/internal/impl/gcp/integration_pubsub_test.go +++ b/internal/impl/gcp/integration_pubsub_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationGCPPubSub(t *testing.T) { diff --git a/internal/impl/gcp/integration_test.go b/internal/impl/gcp/integration_test.go index 36ce209eed..50f40138d9 100644 --- a/internal/impl/gcp/integration_test.go +++ b/internal/impl/gcp/integration_test.go @@ -12,9 +12,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/api/iterator" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" - _ "github.com/benthosdev/benthos/v4/public/components/pure" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" ) func createGCPCloudStorageBucket(var1, id string) error { diff --git a/internal/impl/gcp/output_bigquery.go b/internal/impl/gcp/output_bigquery.go index 7be9bdd168..6780e90255 100644 --- a/internal/impl/gcp/output_bigquery.go +++ b/internal/impl/gcp/output_bigquery.go @@ -13,7 +13,7 @@ import ( "google.golang.org/api/googleapi" "google.golang.org/api/option" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type gcpBigQueryCSVConfig struct { diff --git a/internal/impl/gcp/output_bigquery_test.go b/internal/impl/gcp/output_bigquery_test.go index a4c01b5bc0..8c8fe9e20c 100644 --- a/internal/impl/gcp/output_bigquery_test.go +++ b/internal/impl/gcp/output_bigquery_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func gcpBigQueryConfFromYAML(t *testing.T, yamlStr string) gcpBigQueryOutputConfig { diff --git a/internal/impl/gcp/output_cloud_storage.go b/internal/impl/gcp/output_cloud_storage.go index 571ffbccc4..0c88ce1658 100644 --- a/internal/impl/gcp/output_cloud_storage.go +++ b/internal/impl/gcp/output_cloud_storage.go @@ -12,7 +12,7 @@ import ( "github.com/gofrs/uuid" "go.uber.org/multierr" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/gcp/output_pubsub.go b/internal/impl/gcp/output_pubsub.go index 0403c8e75d..0f23adf4b4 100644 --- a/internal/impl/gcp/output_pubsub.go +++ b/internal/impl/gcp/output_pubsub.go @@ -9,7 +9,7 @@ import ( "github.com/sourcegraph/conc/pool" "google.golang.org/api/option" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func newPubSubOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/gcp/output_pubsub_test.go b/internal/impl/gcp/output_pubsub_test.go index 5fac60492b..0198587aa6 100644 --- a/internal/impl/gcp/output_pubsub_test.go +++ b/internal/impl/gcp/output_pubsub_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestPubSubOutput(t *testing.T) { diff --git a/internal/impl/gcp/processor_bigquery_select.go b/internal/impl/gcp/processor_bigquery_select.go index b52f7977b4..94cbb50287 100644 --- a/internal/impl/gcp/processor_bigquery_select.go +++ b/internal/impl/gcp/processor_bigquery_select.go @@ -10,8 +10,8 @@ import ( "google.golang.org/api/iterator" "google.golang.org/api/option" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) type bigQuerySelectProcessorConfig struct { diff --git a/internal/impl/gcp/processor_bigquery_select_test.go b/internal/impl/gcp/processor_bigquery_select_test.go index ae2acc1a2a..85cc1053fd 100644 --- a/internal/impl/gcp/processor_bigquery_select_test.go +++ b/internal/impl/gcp/processor_bigquery_select_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/api/option" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) var testBQProcessorYAML = ` diff --git a/internal/impl/gcp/tracer_cloudtrace.go b/internal/impl/gcp/tracer_cloudtrace.go index baccf99931..e94c35182d 100644 --- a/internal/impl/gcp/tracer_cloudtrace.go +++ b/internal/impl/gcp/tracer_cloudtrace.go @@ -11,7 +11,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.opentelemetry.io/otel/trace" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/hdfs/input.go b/internal/impl/hdfs/input.go index 5ab40630f0..fc4b7c7960 100644 --- a/internal/impl/hdfs/input.go +++ b/internal/impl/hdfs/input.go @@ -6,7 +6,7 @@ import ( "github.com/colinmarc/hdfs" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/hdfs/integration_test.go b/internal/impl/hdfs/integration_test.go index a925cf3b9e..b4c6090346 100644 --- a/internal/impl/hdfs/integration_test.go +++ b/internal/impl/hdfs/integration_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationHDFS(t *testing.T) { diff --git a/internal/impl/hdfs/output.go b/internal/impl/hdfs/output.go index 23a6c69787..3f56d7ceb6 100644 --- a/internal/impl/hdfs/output.go +++ b/internal/impl/hdfs/output.go @@ -8,7 +8,7 @@ import ( "github.com/colinmarc/hdfs" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/influxdb/metrics_influxdb.go b/internal/impl/influxdb/metrics_influxdb.go index 37639b9600..6d5f93b06b 100644 --- a/internal/impl/influxdb/metrics_influxdb.go +++ b/internal/impl/influxdb/metrics_influxdb.go @@ -11,7 +11,7 @@ import ( client "github.com/influxdata/influxdb1-client/v2" "github.com/rcrowley/go-metrics" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/influxdb/metrics_influxdb_integration_test.go b/internal/impl/influxdb/metrics_influxdb_integration_test.go index 72724dc7d0..5a50c2c6cd 100644 --- a/internal/impl/influxdb/metrics_influxdb_integration_test.go +++ b/internal/impl/influxdb/metrics_influxdb_integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/ory/dockertest/v3" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestInfluxIntegration(t *testing.T) { diff --git a/internal/impl/jaeger/tracer_jaeger.go b/internal/impl/jaeger/tracer_jaeger.go index 3b9601ccf0..42ac26f67a 100644 --- a/internal/impl/jaeger/tracer_jaeger.go +++ b/internal/impl/jaeger/tracer_jaeger.go @@ -14,7 +14,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.opentelemetry.io/otel/trace" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/javascript/benchmark_test.go b/internal/impl/javascript/benchmark_test.go index e03202232a..3ae9238fa4 100644 --- a/internal/impl/javascript/benchmark_test.go +++ b/internal/impl/javascript/benchmark_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/impl/javascript/functions.go b/internal/impl/javascript/functions.go index e7f34f3af2..2b67874121 100644 --- a/internal/impl/javascript/functions.go +++ b/internal/impl/javascript/functions.go @@ -9,7 +9,7 @@ import ( "github.com/dop251/goja" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type jsFunction func(call goja.FunctionCall, rt *goja.Runtime, l *service.Logger) (interface{}, error) diff --git a/internal/impl/javascript/logger.go b/internal/impl/javascript/logger.go index 9b43264bd9..78484ab083 100644 --- a/internal/impl/javascript/logger.go +++ b/internal/impl/javascript/logger.go @@ -1,6 +1,6 @@ package javascript -import "github.com/benthosdev/benthos/v4/public/service" +import "github.com/redpanda-data/benthos/v4/public/service" // Logger wraps the service.Logger so that we can define the below methods. type Logger struct { diff --git a/internal/impl/javascript/processor.go b/internal/impl/javascript/processor.go index 67ad0c2312..0ea48097c1 100644 --- a/internal/impl/javascript/processor.go +++ b/internal/impl/javascript/processor.go @@ -17,7 +17,7 @@ import ( "github.com/dop251/goja_nodejs/console" "github.com/dop251/goja_nodejs/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/javascript/processor_test.go b/internal/impl/javascript/processor_test.go index 0de40190ca..16f0648c81 100644 --- a/internal/impl/javascript/processor_test.go +++ b/internal/impl/javascript/processor_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/impl/javascript/vm.go b/internal/impl/javascript/vm.go index 33500d3221..814e7048fc 100644 --- a/internal/impl/javascript/vm.go +++ b/internal/impl/javascript/vm.go @@ -7,7 +7,7 @@ import ( "github.com/dop251/goja" "github.com/dop251/goja_nodejs/console" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type vmRunner struct { diff --git a/internal/impl/jsonpath/bloblang_jsonpath.go b/internal/impl/jsonpath/bloblang_jsonpath.go index 5ed92baf56..67fcc243bd 100644 --- a/internal/impl/jsonpath/bloblang_jsonpath.go +++ b/internal/impl/jsonpath/bloblang_jsonpath.go @@ -8,7 +8,7 @@ import ( "github.com/PaesslerAG/jsonpath" "github.com/generikvault/gvalstrings" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) // jsonPathLanguage includes the full gval scripting language and the single quote extension. diff --git a/internal/impl/kafka/aws/aws.go b/internal/impl/kafka/aws/aws.go index 5d5227cd77..bdc365afce 100644 --- a/internal/impl/kafka/aws/aws.go +++ b/internal/impl/kafka/aws/aws.go @@ -3,7 +3,7 @@ package aws import ( "context" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/kafka" diff --git a/internal/impl/kafka/input_kafka_franz.go b/internal/impl/kafka/input_kafka_franz.go index d555c63533..9cfc7a8e57 100644 --- a/internal/impl/kafka/input_kafka_franz.go +++ b/internal/impl/kafka/input_kafka_franz.go @@ -16,7 +16,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func franzKafkaInputConfig() *service.ConfigSpec { diff --git a/internal/impl/kafka/input_sarama_kafka.go b/internal/impl/kafka/input_sarama_kafka.go index b997c4018e..03673f2964 100644 --- a/internal/impl/kafka/input_sarama_kafka.go +++ b/internal/impl/kafka/input_sarama_kafka.go @@ -11,7 +11,7 @@ import ( "github.com/Jeffail/checkpoint" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/kafka/input_sarama_kafka_cg.go b/internal/impl/kafka/input_sarama_kafka_cg.go index 25e5345fbe..6d4f48f2c3 100644 --- a/internal/impl/kafka/input_sarama_kafka_cg.go +++ b/internal/impl/kafka/input_sarama_kafka_cg.go @@ -7,7 +7,7 @@ import ( "github.com/IBM/sarama" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // Setup is run at the beginning of a new session, before ConsumeClaim. diff --git a/internal/impl/kafka/input_sarama_kafka_parts.go b/internal/impl/kafka/input_sarama_kafka_parts.go index 597d29cd11..c550bb3325 100644 --- a/internal/impl/kafka/input_sarama_kafka_parts.go +++ b/internal/impl/kafka/input_sarama_kafka_parts.go @@ -11,7 +11,7 @@ import ( "github.com/IBM/sarama" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type closureOffsetTracker struct { diff --git a/internal/impl/kafka/input_sarama_kafka_test.go b/internal/impl/kafka/input_sarama_kafka_test.go index 8b0ca2742e..d69d6208a1 100644 --- a/internal/impl/kafka/input_sarama_kafka_test.go +++ b/internal/impl/kafka/input_sarama_kafka_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestKafkaBadParams(t *testing.T) { diff --git a/internal/impl/kafka/integration_sarama_test.go b/internal/impl/kafka/integration_sarama_test.go index 8a6e831e98..cfe1c6f58e 100644 --- a/internal/impl/kafka/integration_sarama_test.go +++ b/internal/impl/kafka/integration_sarama_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/redpanda-data/connect/v4/internal/impl/kafka" ) diff --git a/internal/impl/kafka/integration_test.go b/internal/impl/kafka/integration_test.go index f383421e62..b908203a2a 100644 --- a/internal/impl/kafka/integration_test.go +++ b/internal/impl/kafka/integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" diff --git a/internal/impl/kafka/logger.go b/internal/impl/kafka/logger.go index 48cf57c13a..008e4c987a 100644 --- a/internal/impl/kafka/logger.go +++ b/internal/impl/kafka/logger.go @@ -3,7 +3,7 @@ package kafka import ( "github.com/twmb/franz-go/pkg/kgo" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type kgoLogger struct { diff --git a/internal/impl/kafka/output_kafka_franz.go b/internal/impl/kafka/output_kafka_franz.go index 3649a5fdae..687195e38d 100644 --- a/internal/impl/kafka/output_kafka_franz.go +++ b/internal/impl/kafka/output_kafka_franz.go @@ -13,7 +13,7 @@ import ( "github.com/twmb/franz-go/pkg/kgo" "github.com/twmb/franz-go/pkg/sasl" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func franzKafkaOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/kafka/output_kafka_franz_test.go b/internal/impl/kafka/output_kafka_franz_test.go index 1041dfe1a1..0086bde023 100644 --- a/internal/impl/kafka/output_kafka_franz_test.go +++ b/internal/impl/kafka/output_kafka_franz_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestKafkaFranzOutputBadParams(t *testing.T) { diff --git a/internal/impl/kafka/output_sarama_kafka.go b/internal/impl/kafka/output_sarama_kafka.go index 6ae80b759f..1e720d687d 100644 --- a/internal/impl/kafka/output_sarama_kafka.go +++ b/internal/impl/kafka/output_sarama_kafka.go @@ -14,8 +14,8 @@ import ( "github.com/cenkalti/backoff/v4" "golang.org/x/sync/syncmap" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/kafka/sasl.go b/internal/impl/kafka/sasl.go index c3edbe6d17..7c2bd78603 100644 --- a/internal/impl/kafka/sasl.go +++ b/internal/impl/kafka/sasl.go @@ -7,7 +7,7 @@ import ( "github.com/IBM/sarama" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" diff --git a/internal/impl/kafka/sasl_test.go b/internal/impl/kafka/sasl_test.go index 42fcb215fb..cfa7e2883a 100644 --- a/internal/impl/kafka/sasl_test.go +++ b/internal/impl/kafka/sasl_test.go @@ -9,8 +9,8 @@ import ( "github.com/redpanda-data/connect/v4/internal/impl/kafka" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestApplyPlaintext(t *testing.T) { diff --git a/internal/impl/kafka/topic_logger.go b/internal/impl/kafka/topic_logger.go index d954855118..bf2cf59fd6 100644 --- a/internal/impl/kafka/topic_logger.go +++ b/internal/impl/kafka/topic_logger.go @@ -22,7 +22,7 @@ import ( "github.com/twmb/franz-go/pkg/kgo" "github.com/twmb/franz-go/pkg/sasl" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TopicLoggerFields() []*service.ConfigField { diff --git a/internal/impl/lang/bloblang.go b/internal/impl/lang/bloblang.go index 75eddd0b92..d2a6dd4dd4 100644 --- a/internal/impl/lang/bloblang.go +++ b/internal/impl/lang/bloblang.go @@ -14,7 +14,7 @@ import ( "github.com/oklog/ulid" frand "golang.org/x/exp/rand" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func init() { diff --git a/internal/impl/lang/bloblang_test.go b/internal/impl/lang/bloblang_test.go index b43d4cfb59..5ed635a93d 100644 --- a/internal/impl/lang/bloblang_test.go +++ b/internal/impl/lang/bloblang_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestFakeFunction_Invalid(t *testing.T) { diff --git a/internal/impl/maxmind/bloblang_geoip.go b/internal/impl/maxmind/bloblang_geoip.go index e515998613..cdbbd86f84 100644 --- a/internal/impl/maxmind/bloblang_geoip.go +++ b/internal/impl/maxmind/bloblang_geoip.go @@ -8,7 +8,7 @@ import ( "github.com/oschwald/geoip2-golang" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func registerMaxmindMethodSpec(name, entity string, fn func(*geoip2.Reader, net.IP) (any, error)) { diff --git a/internal/impl/maxmind/bloblang_geoip_test.go b/internal/impl/maxmind/bloblang_geoip_test.go index 97c41e8dd7..a2b32af803 100644 --- a/internal/impl/maxmind/bloblang_geoip_test.go +++ b/internal/impl/maxmind/bloblang_geoip_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestGeoIPCity(t *testing.T) { diff --git a/internal/impl/memcached/cache.go b/internal/impl/memcached/cache.go index 0680c87786..c381651085 100644 --- a/internal/impl/memcached/cache.go +++ b/internal/impl/memcached/cache.go @@ -10,7 +10,7 @@ import ( "github.com/bradfitz/gomemcache/memcache" "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func memcachedConfig() *service.ConfigSpec { diff --git a/internal/impl/memcached/cache_integration_test.go b/internal/impl/memcached/cache_integration_test.go index 6bd72237ed..e0938dc394 100644 --- a/internal/impl/memcached/cache_integration_test.go +++ b/internal/impl/memcached/cache_integration_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationMemcachedCache(t *testing.T) { diff --git a/internal/impl/mongodb/cache.go b/internal/impl/mongodb/cache.go index a77c0bd48a..8d06f4aee8 100644 --- a/internal/impl/mongodb/cache.go +++ b/internal/impl/mongodb/cache.go @@ -9,7 +9,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const mongoDuplicateKeyErrCode = 11000 diff --git a/internal/impl/mongodb/common.go b/internal/impl/mongodb/common.go index f78e87a7c2..82d1f90fe2 100644 --- a/internal/impl/mongodb/common.go +++ b/internal/impl/mongodb/common.go @@ -12,8 +12,8 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/writeconcern" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) // JSONMarshalMode represents the way in which BSON should be marshalled to JSON. diff --git a/internal/impl/mongodb/input.go b/internal/impl/mongodb/input.go index 32a49ec454..c7c3b3cb97 100644 --- a/internal/impl/mongodb/input.go +++ b/internal/impl/mongodb/input.go @@ -9,7 +9,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // mongodb input component allowed operations. diff --git a/internal/impl/mongodb/input_test.go b/internal/impl/mongodb/input_test.go index 98297cd3f9..d88ed18803 100644 --- a/internal/impl/mongodb/input_test.go +++ b/internal/impl/mongodb/input_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestMongoInputEmptyShutdown(t *testing.T) { diff --git a/internal/impl/mongodb/integration_test.go b/internal/impl/mongodb/integration_test.go index 0b4a80ca74..e54ad378d4 100644 --- a/internal/impl/mongodb/integration_test.go +++ b/internal/impl/mongodb/integration_test.go @@ -15,7 +15,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func generateCollectionName(testID string) string { diff --git a/internal/impl/mongodb/output.go b/internal/impl/mongodb/output.go index 2383d75e80..248534edbd 100644 --- a/internal/impl/mongodb/output.go +++ b/internal/impl/mongodb/output.go @@ -9,7 +9,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/retries" ) diff --git a/internal/impl/mongodb/processor.go b/internal/impl/mongodb/processor.go index 81953e5bda..e7c93d3cd9 100644 --- a/internal/impl/mongodb/processor.go +++ b/internal/impl/mongodb/processor.go @@ -9,7 +9,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/retries" ) diff --git a/internal/impl/mongodb/processor_test.go b/internal/impl/mongodb/processor_test.go index 7bc07606fe..fd795756d7 100644 --- a/internal/impl/mongodb/processor_test.go +++ b/internal/impl/mongodb/processor_test.go @@ -14,8 +14,8 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/redpanda-data/connect/v4/internal/impl/mongodb" ) diff --git a/internal/impl/mqtt/client.go b/internal/impl/mqtt/client.go index 42c1d334a8..e9c2c76764 100644 --- a/internal/impl/mqtt/client.go +++ b/internal/impl/mqtt/client.go @@ -10,7 +10,7 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" gonanoid "github.com/matoous/go-nanoid/v2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/mqtt/input.go b/internal/impl/mqtt/input.go index 9ef4dc1b11..8d010ff600 100644 --- a/internal/impl/mqtt/input.go +++ b/internal/impl/mqtt/input.go @@ -7,7 +7,7 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/mqtt/integration_test.go b/internal/impl/mqtt/integration_test.go index 72fffa1742..fdcc4a5910 100644 --- a/internal/impl/mqtt/integration_test.go +++ b/internal/impl/mqtt/integration_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationMQTT(t *testing.T) { diff --git a/internal/impl/mqtt/output.go b/internal/impl/mqtt/output.go index 96ccddde00..853b19eab5 100644 --- a/internal/impl/mqtt/output.go +++ b/internal/impl/mqtt/output.go @@ -9,7 +9,7 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/msgpack/bloblang.go b/internal/impl/msgpack/bloblang.go index 156ae5ef61..13b58f9309 100644 --- a/internal/impl/msgpack/bloblang.go +++ b/internal/impl/msgpack/bloblang.go @@ -3,7 +3,7 @@ package msgpack import ( "github.com/vmihailenco/msgpack/v5" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func init() { diff --git a/internal/impl/msgpack/processor.go b/internal/impl/msgpack/processor.go index 24a4425eb3..d1c697c19f 100644 --- a/internal/impl/msgpack/processor.go +++ b/internal/impl/msgpack/processor.go @@ -6,7 +6,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func processorConfig() *service.ConfigSpec { diff --git a/internal/impl/msgpack/processor_test.go b/internal/impl/msgpack/processor_test.go index ed35347fbb..45fe6136f5 100644 --- a/internal/impl/msgpack/processor_test.go +++ b/internal/impl/msgpack/processor_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestMsgPackToJson(t *testing.T) { diff --git a/internal/impl/nanomsg/input.go b/internal/impl/nanomsg/input.go index 34df684c28..ff21fabcea 100644 --- a/internal/impl/nanomsg/input.go +++ b/internal/impl/nanomsg/input.go @@ -12,7 +12,7 @@ import ( "go.nanomsg.org/mangos/v3/protocol/pull" "go.nanomsg.org/mangos/v3/protocol/sub" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" // Import all transport types. _ "go.nanomsg.org/mangos/v3/transport/all" diff --git a/internal/impl/nanomsg/integration_test.go b/internal/impl/nanomsg/integration_test.go index c660dd02e3..4b86d2408c 100644 --- a/internal/impl/nanomsg/integration_test.go +++ b/internal/impl/nanomsg/integration_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationNanomsg(t *testing.T) { diff --git a/internal/impl/nanomsg/output.go b/internal/impl/nanomsg/output.go index b7f5dfed05..fdaeed9497 100644 --- a/internal/impl/nanomsg/output.go +++ b/internal/impl/nanomsg/output.go @@ -12,7 +12,7 @@ import ( "go.nanomsg.org/mangos/v3/protocol/pub" "go.nanomsg.org/mangos/v3/protocol/push" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" // Import all transport types. _ "go.nanomsg.org/mangos/v3/transport/all" diff --git a/internal/impl/nats/auth.go b/internal/impl/nats/auth.go index 58d58a8c06..4694f401d6 100644 --- a/internal/impl/nats/auth.go +++ b/internal/impl/nats/auth.go @@ -12,7 +12,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nkeys" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func authDescription() string { diff --git a/internal/impl/nats/auth_test.go b/internal/impl/nats/auth_test.go index 48670d64d9..f565ba005a 100644 --- a/internal/impl/nats/auth_test.go +++ b/internal/impl/nats/auth_test.go @@ -8,7 +8,7 @@ import ( "github.com/nats-io/nkeys" "github.com/stretchr/testify/assert" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/cache_kv.go b/internal/impl/nats/cache_kv.go index a34be9b326..4bbe73707e 100644 --- a/internal/impl/nats/cache_kv.go +++ b/internal/impl/nats/cache_kv.go @@ -10,7 +10,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func natsKVCacheConfig() *service.ConfigSpec { diff --git a/internal/impl/nats/connection.go b/internal/impl/nats/connection.go index e135a11ee1..93b03d85f6 100644 --- a/internal/impl/nats/connection.go +++ b/internal/impl/nats/connection.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // I've split the connection fields into two, which allows us to put tls and diff --git a/internal/impl/nats/docs.go b/internal/impl/nats/docs.go index 6567407777..1808a96a4a 100644 --- a/internal/impl/nats/docs.go +++ b/internal/impl/nats/docs.go @@ -1,7 +1,7 @@ package nats import ( - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/errors.go b/internal/impl/nats/errors.go index 026f1504b7..9d1e6be204 100644 --- a/internal/impl/nats/errors.go +++ b/internal/impl/nats/errors.go @@ -3,7 +3,7 @@ package nats import ( "github.com/nats-io/nats.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func errorHandlerOption(logger *service.Logger) nats.Option { diff --git a/internal/impl/nats/input.go b/internal/impl/nats/input.go index 2a52beea46..142d6bb2f6 100644 --- a/internal/impl/nats/input.go +++ b/internal/impl/nats/input.go @@ -8,7 +8,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func natsInputConfig() *service.ConfigSpec { diff --git a/internal/impl/nats/input_jetstream.go b/internal/impl/nats/input_jetstream.go index fef9a6eea3..2b47d3425e 100644 --- a/internal/impl/nats/input_jetstream.go +++ b/internal/impl/nats/input_jetstream.go @@ -12,7 +12,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func natsJetStreamInputConfig() *service.ConfigSpec { diff --git a/internal/impl/nats/input_jetstream_test.go b/internal/impl/nats/input_jetstream_test.go index f8c15feb44..12f574ed23 100644 --- a/internal/impl/nats/input_jetstream_test.go +++ b/internal/impl/nats/input_jetstream_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestInputJetStreamConfigParse(t *testing.T) { diff --git a/internal/impl/nats/input_kv.go b/internal/impl/nats/input_kv.go index bce8322c91..2e21ad1acd 100644 --- a/internal/impl/nats/input_kv.go +++ b/internal/impl/nats/input_kv.go @@ -9,7 +9,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/input_kv_test.go b/internal/impl/nats/input_kv_test.go index c15ddc614a..2ae27c7f33 100644 --- a/internal/impl/nats/input_kv_test.go +++ b/internal/impl/nats/input_kv_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestInputKVParse(t *testing.T) { diff --git a/internal/impl/nats/input_stream.go b/internal/impl/nats/input_stream.go index c5a82983ea..d85ad25032 100644 --- a/internal/impl/nats/input_stream.go +++ b/internal/impl/nats/input_stream.go @@ -10,7 +10,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/stan.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/integration_jetstream_test.go b/internal/impl/nats/integration_jetstream_test.go index 9c78cdc778..565353bad7 100644 --- a/internal/impl/nats/integration_jetstream_test.go +++ b/internal/impl/nats/integration_jetstream_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationNatsJetstream(t *testing.T) { diff --git a/internal/impl/nats/integration_kv_test.go b/internal/impl/nats/integration_kv_test.go index ad6928a547..30e8466095 100644 --- a/internal/impl/nats/integration_kv_test.go +++ b/internal/impl/nats/integration_kv_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationNatsKV(t *testing.T) { diff --git a/internal/impl/nats/integration_nats_test.go b/internal/impl/nats/integration_nats_test.go index ba2953d150..5cd17a3249 100644 --- a/internal/impl/nats/integration_nats_test.go +++ b/internal/impl/nats/integration_nats_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationNats(t *testing.T) { diff --git a/internal/impl/nats/integration_req_test.go b/internal/impl/nats/integration_req_test.go index caf8b912fb..7e4df40b76 100644 --- a/internal/impl/nats/integration_req_test.go +++ b/internal/impl/nats/integration_req_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationNatsReq(t *testing.T) { diff --git a/internal/impl/nats/integration_stream_test.go b/internal/impl/nats/integration_stream_test.go index 9ffde64410..d50fc2e6b6 100644 --- a/internal/impl/nats/integration_stream_test.go +++ b/internal/impl/nats/integration_stream_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationNatsStream(t *testing.T) { diff --git a/internal/impl/nats/metadata.go b/internal/impl/nats/metadata.go index b7d9dc9d7b..269697e739 100644 --- a/internal/impl/nats/metadata.go +++ b/internal/impl/nats/metadata.go @@ -3,7 +3,7 @@ package nats import ( "github.com/nats-io/nats.go/jetstream" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/output.go b/internal/impl/nats/output.go index 5ed38896e2..b39d56ef7f 100644 --- a/internal/impl/nats/output.go +++ b/internal/impl/nats/output.go @@ -8,7 +8,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func natsOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/nats/output_jetstream.go b/internal/impl/nats/output_jetstream.go index cedb9dbf75..172cf4ef93 100644 --- a/internal/impl/nats/output_jetstream.go +++ b/internal/impl/nats/output_jetstream.go @@ -9,7 +9,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func natsJetStreamOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/nats/output_jetstream_test.go b/internal/impl/nats/output_jetstream_test.go index 2f97b261bf..6713096c7a 100644 --- a/internal/impl/nats/output_jetstream_test.go +++ b/internal/impl/nats/output_jetstream_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestOutputJetStreamConfigParse(t *testing.T) { diff --git a/internal/impl/nats/output_kv.go b/internal/impl/nats/output_kv.go index 422eadb8c2..6d9a458955 100644 --- a/internal/impl/nats/output_kv.go +++ b/internal/impl/nats/output_kv.go @@ -9,7 +9,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/output_stream.go b/internal/impl/nats/output_stream.go index 8e7a1345cc..e7027b1171 100644 --- a/internal/impl/nats/output_stream.go +++ b/internal/impl/nats/output_stream.go @@ -11,7 +11,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/stan.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/processor_kv.go b/internal/impl/nats/processor_kv.go index 4b000a3e92..64d3fc349c 100644 --- a/internal/impl/nats/processor_kv.go +++ b/internal/impl/nats/processor_kv.go @@ -12,7 +12,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nats/processor_request_reply.go b/internal/impl/nats/processor_request_reply.go index d3d938cfba..0345570ad2 100644 --- a/internal/impl/nats/processor_request_reply.go +++ b/internal/impl/nats/processor_request_reply.go @@ -8,7 +8,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func natsRequestReplyConfig() *service.ConfigSpec { diff --git a/internal/impl/nsq/input.go b/internal/impl/nsq/input.go index ea5927042b..45589ed9f8 100644 --- a/internal/impl/nsq/input.go +++ b/internal/impl/nsq/input.go @@ -11,7 +11,7 @@ import ( "github.com/nsqio/go-nsq" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/nsq/integration_test.go b/internal/impl/nsq/integration_test.go index 8456fa120e..524df85560 100644 --- a/internal/impl/nsq/integration_test.go +++ b/internal/impl/nsq/integration_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegration(t *testing.T) { diff --git a/internal/impl/nsq/output.go b/internal/impl/nsq/output.go index dd01d1b2df..05ac6aab43 100644 --- a/internal/impl/nsq/output.go +++ b/internal/impl/nsq/output.go @@ -10,7 +10,7 @@ import ( nsq "github.com/nsqio/go-nsq" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/opensearch/aws/aws.go b/internal/impl/opensearch/aws/aws.go index 7801a8a71f..20e3b1102e 100644 --- a/internal/impl/opensearch/aws/aws.go +++ b/internal/impl/opensearch/aws/aws.go @@ -6,7 +6,7 @@ import ( "github.com/opensearch-project/opensearch-go/v3/opensearchapi" "github.com/opensearch-project/opensearch-go/v3/signer/awsv2" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" baws "github.com/redpanda-data/connect/v4/internal/impl/aws" "github.com/redpanda-data/connect/v4/internal/impl/opensearch" diff --git a/internal/impl/opensearch/integration_test.go b/internal/impl/opensearch/integration_test.go index 4b38888133..4ce51329e4 100644 --- a/internal/impl/opensearch/integration_test.go +++ b/internal/impl/opensearch/integration_test.go @@ -17,9 +17,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" "github.com/redpanda-data/connect/v4/internal/impl/opensearch" ) diff --git a/internal/impl/opensearch/output.go b/internal/impl/opensearch/output.go index bbd7353a01..9012835f3a 100644 --- a/internal/impl/opensearch/output.go +++ b/internal/impl/opensearch/output.go @@ -14,7 +14,7 @@ import ( "github.com/opensearch-project/opensearch-go/v3/opensearchapi" "github.com/opensearch-project/opensearch-go/v3/opensearchutil" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/aws/config" ) diff --git a/internal/impl/otlp/tracer_otlp.go b/internal/impl/otlp/tracer_otlp.go index 9c7aade1a1..ea38b7f681 100644 --- a/internal/impl/otlp/tracer_otlp.go +++ b/internal/impl/otlp/tracer_otlp.go @@ -15,7 +15,7 @@ import ( tracesdk "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func oltpSpec() *service.ConfigSpec { diff --git a/internal/impl/parquet/bloblang.go b/internal/impl/parquet/bloblang.go index aa9d2ecb07..16187c9fa9 100644 --- a/internal/impl/parquet/bloblang.go +++ b/internal/impl/parquet/bloblang.go @@ -5,7 +5,7 @@ import ( "errors" "io" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func init() { diff --git a/internal/impl/parquet/bloblang_test.go b/internal/impl/parquet/bloblang_test.go index a9ff931a79..61635e438e 100644 --- a/internal/impl/parquet/bloblang_test.go +++ b/internal/impl/parquet/bloblang_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestParquetParseBloblangAsStrings(t *testing.T) { diff --git a/internal/impl/parquet/input_parquet.go b/internal/impl/parquet/input_parquet.go index f3e9118474..2253670fdf 100644 --- a/internal/impl/parquet/input_parquet.go +++ b/internal/impl/parquet/input_parquet.go @@ -11,7 +11,7 @@ import ( "github.com/parquet-go/parquet-go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func parquetInputConfig() *service.ConfigSpec { diff --git a/internal/impl/parquet/input_parquet_test.go b/internal/impl/parquet/input_parquet_test.go index 1d3e3e5e3e..f274fd3685 100644 --- a/internal/impl/parquet/input_parquet_test.go +++ b/internal/impl/parquet/input_parquet_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type simpleData struct { diff --git a/internal/impl/parquet/processor.go b/internal/impl/parquet/processor.go index eab6616490..e9fefc87b5 100644 --- a/internal/impl/parquet/processor.go +++ b/internal/impl/parquet/processor.go @@ -11,7 +11,7 @@ import ( "github.com/xitongsys/parquet-go/reader" "github.com/xitongsys/parquet-go/writer" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func parquetProcessorConfig() *service.ConfigSpec { diff --git a/internal/impl/parquet/processor_decode.go b/internal/impl/parquet/processor_decode.go index ae25901ee4..cb963c7351 100644 --- a/internal/impl/parquet/processor_decode.go +++ b/internal/impl/parquet/processor_decode.go @@ -9,7 +9,7 @@ import ( "github.com/parquet-go/parquet-go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func parquetDecodeProcessorConfig() *service.ConfigSpec { diff --git a/internal/impl/parquet/processor_decode_test.go b/internal/impl/parquet/processor_decode_test.go index 2ab862ed30..d28dfcf94d 100644 --- a/internal/impl/parquet/processor_decode_test.go +++ b/internal/impl/parquet/processor_decode_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func testPMSchema() *parquet.Schema { diff --git a/internal/impl/parquet/processor_encode.go b/internal/impl/parquet/processor_encode.go index 35f6590378..71fd87150c 100644 --- a/internal/impl/parquet/processor_encode.go +++ b/internal/impl/parquet/processor_encode.go @@ -8,7 +8,7 @@ import ( "github.com/parquet-go/parquet-go" "github.com/parquet-go/parquet-go/compress" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func parquetEncodeProcessorConfig() *service.ConfigSpec { diff --git a/internal/impl/parquet/processor_encode_test.go b/internal/impl/parquet/processor_encode_test.go index 20346f85e5..c717f85f00 100644 --- a/internal/impl/parquet/processor_encode_test.go +++ b/internal/impl/parquet/processor_encode_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestParquetEncodePanic(t *testing.T) { diff --git a/internal/impl/parquet/processor_test.go b/internal/impl/parquet/processor_test.go index ed4e49bd67..688e9cd2e3 100644 --- a/internal/impl/parquet/processor_test.go +++ b/internal/impl/parquet/processor_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestParquetProcessorConfigLinting(t *testing.T) { diff --git a/internal/impl/prometheus/metrics_prometheus.go b/internal/impl/prometheus/metrics_prometheus.go index 160e62f21f..e72e9676e7 100644 --- a/internal/impl/prometheus/metrics_prometheus.go +++ b/internal/impl/prometheus/metrics_prometheus.go @@ -14,7 +14,7 @@ import ( "github.com/prometheus/client_golang/prometheus/push" "github.com/prometheus/common/model" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/protobuf/processor_protobuf.go b/internal/impl/protobuf/processor_protobuf.go index 2df8bfb69e..f4987073e2 100644 --- a/internal/impl/protobuf/processor_protobuf.go +++ b/internal/impl/protobuf/processor_protobuf.go @@ -8,7 +8,7 @@ import ( "os" "path/filepath" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" diff --git a/internal/impl/protobuf/processor_protobuf_test.go b/internal/impl/protobuf/processor_protobuf_test.go index c236e05b9f..3e20f2ce96 100644 --- a/internal/impl/protobuf/processor_protobuf_test.go +++ b/internal/impl/protobuf/processor_protobuf_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestProtobufFromJSON(t *testing.T) { diff --git a/internal/impl/pulsar/auth_field.go b/internal/impl/pulsar/auth_field.go index 17b3a60aa7..7ca86cd57d 100644 --- a/internal/impl/pulsar/auth_field.go +++ b/internal/impl/pulsar/auth_field.go @@ -3,7 +3,7 @@ package pulsar import ( "errors" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func authField() *service.ConfigField { diff --git a/internal/impl/pulsar/input.go b/internal/impl/pulsar/input.go index 0d2c2f931c..5b1344ac66 100644 --- a/internal/impl/pulsar/input.go +++ b/internal/impl/pulsar/input.go @@ -10,7 +10,7 @@ import ( "github.com/apache/pulsar-client-go/pulsar" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/pulsar/input_test.go b/internal/impl/pulsar/input_test.go index 6d001777c1..7b62eeefeb 100644 --- a/internal/impl/pulsar/input_test.go +++ b/internal/impl/pulsar/input_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestParseInputTopicXorPattern(t *testing.T) { diff --git a/internal/impl/pulsar/integration_test.go b/internal/impl/pulsar/integration_test.go index 91f598a875..4c00d60e0e 100644 --- a/internal/impl/pulsar/integration_test.go +++ b/internal/impl/pulsar/integration_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationPulsar(t *testing.T) { diff --git a/internal/impl/pulsar/logger.go b/internal/impl/pulsar/logger.go index e7a69331b6..ba3c388f0b 100644 --- a/internal/impl/pulsar/logger.go +++ b/internal/impl/pulsar/logger.go @@ -3,7 +3,7 @@ package pulsar import ( plog "github.com/apache/pulsar-client-go/pulsar/log" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // DefaultLogger returns a logger that wraps Benthos Modular logger. diff --git a/internal/impl/pulsar/output.go b/internal/impl/pulsar/output.go index c1a0002282..bac71c3f23 100644 --- a/internal/impl/pulsar/output.go +++ b/internal/impl/pulsar/output.go @@ -7,7 +7,7 @@ import ( "github.com/apache/pulsar-client-go/pulsar" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func init() { diff --git a/internal/impl/pusher/output_pusher.go b/internal/impl/pusher/output_pusher.go index 96e30d4f6e..0d24db59ab 100644 --- a/internal/impl/pusher/output_pusher.go +++ b/internal/impl/pusher/output_pusher.go @@ -5,7 +5,7 @@ import ( "github.com/pusher/pusher-http-go" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func pusherOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/redis/cache.go b/internal/impl/redis/cache.go index e952915da4..c39154801d 100644 --- a/internal/impl/redis/cache.go +++ b/internal/impl/redis/cache.go @@ -9,7 +9,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func redisCacheConfig() *service.ConfigSpec { diff --git a/internal/impl/redis/cache_integration_test.go b/internal/impl/redis/cache_integration_test.go index 2c0cc2f2db..de763af597 100644 --- a/internal/impl/redis/cache_integration_test.go +++ b/internal/impl/redis/cache_integration_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationRedisCache(t *testing.T) { diff --git a/internal/impl/redis/client.go b/internal/impl/redis/client.go index eeadf575b6..4edefa16a7 100644 --- a/internal/impl/redis/client.go +++ b/internal/impl/redis/client.go @@ -7,7 +7,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func clientFields() []*service.ConfigField { diff --git a/internal/impl/redis/input_list.go b/internal/impl/redis/input_list.go index 56e76e0328..5eb81f23a6 100644 --- a/internal/impl/redis/input_list.go +++ b/internal/impl/redis/input_list.go @@ -8,7 +8,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) type redisPopCommand string diff --git a/internal/impl/redis/input_pubsub.go b/internal/impl/redis/input_pubsub.go index 923b5de13b..faae409803 100644 --- a/internal/impl/redis/input_pubsub.go +++ b/internal/impl/redis/input_pubsub.go @@ -6,7 +6,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/redis/input_scan.go b/internal/impl/redis/input_scan.go index 2d28e2645d..c2af8291c3 100644 --- a/internal/impl/redis/input_scan.go +++ b/internal/impl/redis/input_scan.go @@ -6,7 +6,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func init() { diff --git a/internal/impl/redis/input_streams.go b/internal/impl/redis/input_streams.go index d9dcc31df8..154dd63f0b 100644 --- a/internal/impl/redis/input_streams.go +++ b/internal/impl/redis/input_streams.go @@ -11,7 +11,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/redis/integration_test.go b/internal/impl/redis/integration_test.go index 0883a63a7a..57be1c86f6 100644 --- a/internal/impl/redis/integration_test.go +++ b/internal/impl/redis/integration_test.go @@ -12,8 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - "github.com/benthosdev/benthos/v4/public/service/integration" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationRedis(t *testing.T) { diff --git a/internal/impl/redis/output_hash.go b/internal/impl/redis/output_hash.go index f471ae2bee..9c79c9507e 100644 --- a/internal/impl/redis/output_hash.go +++ b/internal/impl/redis/output_hash.go @@ -8,7 +8,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/redis/output_list.go b/internal/impl/redis/output_list.go index c1f26dde73..160884b784 100644 --- a/internal/impl/redis/output_list.go +++ b/internal/impl/redis/output_list.go @@ -7,7 +7,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/redis/output_pubsub.go b/internal/impl/redis/output_pubsub.go index 0c6efbdc45..0dec63beaf 100644 --- a/internal/impl/redis/output_pubsub.go +++ b/internal/impl/redis/output_pubsub.go @@ -7,7 +7,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/redis/output_streams.go b/internal/impl/redis/output_streams.go index 03e86f1a16..358c0df151 100644 --- a/internal/impl/redis/output_streams.go +++ b/internal/impl/redis/output_streams.go @@ -7,7 +7,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/redis/processor.go b/internal/impl/redis/processor.go index 7aa9c486a6..f5642e1301 100644 --- a/internal/impl/redis/processor.go +++ b/internal/impl/redis/processor.go @@ -10,8 +10,8 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func redisProcConfig() *service.ConfigSpec { diff --git a/internal/impl/redis/processor_integration_test.go b/internal/impl/redis/processor_integration_test.go index a4dd640ab8..aac72c7db0 100644 --- a/internal/impl/redis/processor_integration_test.go +++ b/internal/impl/redis/processor_integration_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationRedisProcessor(t *testing.T) { diff --git a/internal/impl/redis/rate_limit.go b/internal/impl/redis/rate_limit.go index f286b792d6..5968e38ec0 100644 --- a/internal/impl/redis/rate_limit.go +++ b/internal/impl/redis/rate_limit.go @@ -8,7 +8,7 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func redisRatelimitConfig() *service.ConfigSpec { diff --git a/internal/impl/redis/rate_limit_integration_test.go b/internal/impl/redis/rate_limit_integration_test.go index 269b3e1569..c6134d24ef 100644 --- a/internal/impl/redis/rate_limit_integration_test.go +++ b/internal/impl/redis/rate_limit_integration_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationRedisRateLimit(t *testing.T) { diff --git a/internal/impl/redis/script_processor.go b/internal/impl/redis/script_processor.go index 8b283f7271..e91dc5d816 100644 --- a/internal/impl/redis/script_processor.go +++ b/internal/impl/redis/script_processor.go @@ -7,8 +7,8 @@ import ( "github.com/redis/go-redis/v9" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func redisScriptProcConfig() *service.ConfigSpec { diff --git a/internal/impl/sentry/processor_capture.go b/internal/impl/sentry/processor_capture.go index 3eff5886fb..a9b81650f2 100644 --- a/internal/impl/sentry/processor_capture.go +++ b/internal/impl/sentry/processor_capture.go @@ -8,8 +8,8 @@ import ( "github.com/getsentry/sentry-go" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/sentry/processor_capture_test.go b/internal/impl/sentry/processor_capture_test.go index a86f8c043a..90f2a11500 100644 --- a/internal/impl/sentry/processor_capture_test.go +++ b/internal/impl/sentry/processor_capture_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestCaptureProcessor(t *testing.T) { diff --git a/internal/impl/sftp/input.go b/internal/impl/sftp/input.go index 7c0ee39361..3ab5d9d442 100644 --- a/internal/impl/sftp/input.go +++ b/internal/impl/sftp/input.go @@ -11,8 +11,8 @@ import ( "github.com/pkg/sftp" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/codec" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/codec" ) const ( diff --git a/internal/impl/sftp/integration_test.go b/internal/impl/sftp/integration_test.go index 485b3f2c23..cbfdd21eb9 100644 --- a/internal/impl/sftp/integration_test.go +++ b/internal/impl/sftp/integration_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" // Bring in memory cache. - _ "github.com/benthosdev/benthos/v4/public/components/pure" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" ) var ( diff --git a/internal/impl/sftp/output.go b/internal/impl/sftp/output.go index c23727e731..67228521d2 100644 --- a/internal/impl/sftp/output.go +++ b/internal/impl/sftp/output.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/sftp" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/sftp/shared.go b/internal/impl/sftp/shared.go index 4466b654af..f277964e5e 100644 --- a/internal/impl/sftp/shared.go +++ b/internal/impl/sftp/shared.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/sftp" "golang.org/x/crypto/ssh" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/snowflake/output_snowflake_put.go b/internal/impl/snowflake/output_snowflake_put.go index 3b32688ac9..fc56faa48f 100644 --- a/internal/impl/snowflake/output_snowflake_put.go +++ b/internal/impl/snowflake/output_snowflake_put.go @@ -34,7 +34,7 @@ import ( "github.com/youmark/pkcs8" "golang.org/x/crypto/ssh" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/snowflake/output_snowflake_put_test.go b/internal/impl/snowflake/output_snowflake_put_test.go index 567845c8eb..52101d41c1 100644 --- a/internal/impl/snowflake/output_snowflake_put_test.go +++ b/internal/impl/snowflake/output_snowflake_put_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/splunk/init.go b/internal/impl/splunk/init.go index db332b2811..d66f00bc93 100644 --- a/internal/impl/splunk/init.go +++ b/internal/impl/splunk/init.go @@ -3,7 +3,7 @@ package splunk import ( _ "embed" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) //go:embed template_output.yaml diff --git a/internal/impl/sql/buffer_sqlite.go b/internal/impl/sql/buffer_sqlite.go index 36494ea1d8..3259e14cab 100644 --- a/internal/impl/sql/buffer_sqlite.go +++ b/internal/impl/sql/buffer_sqlite.go @@ -14,7 +14,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/vmihailenco/msgpack/v5" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) // SQLiteBufferConfig returns a config spec for an SQLite buffer. diff --git a/internal/impl/sql/buffer_sqlite_test.go b/internal/impl/sql/buffer_sqlite_test.go index 52ffafa2c6..cd653c9756 100644 --- a/internal/impl/sql/buffer_sqlite_test.go +++ b/internal/impl/sql/buffer_sqlite_test.go @@ -14,11 +14,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/redpanda-data/connect/v4/internal/impl/sql" - _ "github.com/benthosdev/benthos/v4/public/components/pure/extended" + _ "github.com/redpanda-data/connect/v4/public/components/pure/extended" ) func msgEqualStr(t testing.TB, expected string, m *service.Message) { diff --git a/internal/impl/sql/cache_integration_test.go b/internal/impl/sql/cache_integration_test.go index f8d5267dc2..a8f9014bfc 100644 --- a/internal/impl/sql/cache_integration_test.go +++ b/internal/impl/sql/cache_integration_test.go @@ -11,7 +11,7 @@ import ( "github.com/ory/dockertest/v3" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationCache(t *testing.T) { diff --git a/internal/impl/sql/cache_sql.go b/internal/impl/sql/cache_sql.go index 5fce128532..de49b555f8 100644 --- a/internal/impl/sql/cache_sql.go +++ b/internal/impl/sql/cache_sql.go @@ -11,7 +11,7 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/sql/conn_fields.go b/internal/impl/sql/conn_fields.go index 79eccdcf01..5a86b6c6a7 100644 --- a/internal/impl/sql/conn_fields.go +++ b/internal/impl/sql/conn_fields.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) var driverField = service.NewStringEnumField("driver", "mysql", "postgres", "clickhouse", "mssql", "sqlite", "oracle", "snowflake", "trino", "gocosmos"). diff --git a/internal/impl/sql/conn_fields_test.go b/internal/impl/sql/conn_fields_test.go index fb67340a92..8f9cf9d5ae 100644 --- a/internal/impl/sql/conn_fields_test.go +++ b/internal/impl/sql/conn_fields_test.go @@ -11,10 +11,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - _ "github.com/benthosdev/benthos/v4/public/components/sql" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + + _ "github.com/redpanda-data/connect/v4/public/components/sql" ) func TestConnSettingsInitStmt(t *testing.T) { diff --git a/internal/impl/sql/input_sql_raw.go b/internal/impl/sql/input_sql_raw.go index 29edabd2be..26a2423a08 100644 --- a/internal/impl/sql/input_sql_raw.go +++ b/internal/impl/sql/input_sql_raw.go @@ -8,8 +8,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func sqlRawInputConfig() *service.ConfigSpec { diff --git a/internal/impl/sql/input_sql_raw_test.go b/internal/impl/sql/input_sql_raw_test.go index 47b6e150c9..eec49879fc 100644 --- a/internal/impl/sql/input_sql_raw_test.go +++ b/internal/impl/sql/input_sql_raw_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestSQLRawInputEmptyShutdown(t *testing.T) { diff --git a/internal/impl/sql/input_sql_select.go b/internal/impl/sql/input_sql_select.go index 40bc9058c8..229eb53178 100644 --- a/internal/impl/sql/input_sql_select.go +++ b/internal/impl/sql/input_sql_select.go @@ -10,8 +10,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func sqlSelectInputConfig() *service.ConfigSpec { diff --git a/internal/impl/sql/input_sql_select_test.go b/internal/impl/sql/input_sql_select_test.go index ef1e2c2d6a..54fb48a0c9 100644 --- a/internal/impl/sql/input_sql_select_test.go +++ b/internal/impl/sql/input_sql_select_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestSQLSelectInputEmptyShutdown(t *testing.T) { diff --git a/internal/impl/sql/integration_test.go b/internal/impl/sql/integration_test.go index bdece14fcc..bdfef49024 100644 --- a/internal/impl/sql/integration_test.go +++ b/internal/impl/sql/integration_test.go @@ -14,13 +14,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service/integration" isql "github.com/redpanda-data/connect/v4/internal/impl/sql" - _ "github.com/benthosdev/benthos/v4/public/components/pure" - _ "github.com/benthosdev/benthos/v4/public/components/sql" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" + + _ "github.com/redpanda-data/connect/v4/public/components/sql" ) type testFn func(t *testing.T, driver, dsn, table string) diff --git a/internal/impl/sql/output_sql_deprecated.go b/internal/impl/sql/output_sql_deprecated.go index 1636ef8576..641afd99d5 100644 --- a/internal/impl/sql/output_sql_deprecated.go +++ b/internal/impl/sql/output_sql_deprecated.go @@ -1,8 +1,8 @@ package sql import ( - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func sqlDeprecatedOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/sql/output_sql_insert.go b/internal/impl/sql/output_sql_insert.go index 25eccf5007..65fd9feb8b 100644 --- a/internal/impl/sql/output_sql_insert.go +++ b/internal/impl/sql/output_sql_insert.go @@ -10,8 +10,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func sqlInsertOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/sql/output_sql_insert_test.go b/internal/impl/sql/output_sql_insert_test.go index 9e1522a3e9..90a02c6782 100644 --- a/internal/impl/sql/output_sql_insert_test.go +++ b/internal/impl/sql/output_sql_insert_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestSQLInsertOutputEmptyShutdown(t *testing.T) { diff --git a/internal/impl/sql/output_sql_raw.go b/internal/impl/sql/output_sql_raw.go index bc236ad936..8273b320a5 100644 --- a/internal/impl/sql/output_sql_raw.go +++ b/internal/impl/sql/output_sql_raw.go @@ -8,8 +8,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) func sqlRawOutputConfig() *service.ConfigSpec { diff --git a/internal/impl/sql/processor_sql_deprecated.go b/internal/impl/sql/processor_sql_deprecated.go index f31d48af14..c1d3f261d8 100644 --- a/internal/impl/sql/processor_sql_deprecated.go +++ b/internal/impl/sql/processor_sql_deprecated.go @@ -1,8 +1,8 @@ package sql import ( - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) // DeprecatedProcessorConfig returns a config spec for an sql processor. diff --git a/internal/impl/sql/processor_sql_insert.go b/internal/impl/sql/processor_sql_insert.go index f53070f110..07463d4425 100644 --- a/internal/impl/sql/processor_sql_insert.go +++ b/internal/impl/sql/processor_sql_insert.go @@ -10,8 +10,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) // InsertProcessorConfig returns a config spec for an sql_insert processor. diff --git a/internal/impl/sql/processor_sql_raw.go b/internal/impl/sql/processor_sql_raw.go index c7b5edebcb..a5dff5d25a 100644 --- a/internal/impl/sql/processor_sql_raw.go +++ b/internal/impl/sql/processor_sql_raw.go @@ -8,8 +8,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) // RawProcessorConfig returns a config spec for an sql_raw processor. diff --git a/internal/impl/sql/processor_sql_select.go b/internal/impl/sql/processor_sql_select.go index 0dea57f63e..28d4c3066a 100644 --- a/internal/impl/sql/processor_sql_select.go +++ b/internal/impl/sql/processor_sql_select.go @@ -10,8 +10,8 @@ import ( "github.com/Jeffail/shutdown" - "github.com/benthosdev/benthos/v4/public/bloblang" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/service" ) // SelectProcessorConfig returns a config spec for an sql_select processor. diff --git a/internal/impl/statsd/metrics_statsd.go b/internal/impl/statsd/metrics_statsd.go index ac7fd8eefd..8f10202d0a 100644 --- a/internal/impl/statsd/metrics_statsd.go +++ b/internal/impl/statsd/metrics_statsd.go @@ -8,7 +8,7 @@ import ( statsd "github.com/smira/go-statsd" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/twitter/init.go b/internal/impl/twitter/init.go index c06d1b89ac..15d8cf6e38 100644 --- a/internal/impl/twitter/init.go +++ b/internal/impl/twitter/init.go @@ -3,11 +3,11 @@ package twitter import ( _ "embed" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" // bloblang functions are registered in init functions under this package // so ensure they are loaded first - _ "github.com/benthosdev/benthos/v4/public/components/pure" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" ) //go:embed template_search_input.yaml diff --git a/internal/impl/wasm/processor_wazero.go b/internal/impl/wasm/processor_wazero.go index a17fa99ae2..f36a5aea9e 100644 --- a/internal/impl/wasm/processor_wazero.go +++ b/internal/impl/wasm/processor_wazero.go @@ -11,7 +11,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func wazeroAllocProcessorConfig() *service.ConfigSpec { diff --git a/internal/impl/wasm/processor_wazero_test.go b/internal/impl/wasm/processor_wazero_test.go index 72f5095ce0..bd1c4fb05e 100644 --- a/internal/impl/wasm/processor_wazero_test.go +++ b/internal/impl/wasm/processor_wazero_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/impl/xml/bloblang.go b/internal/impl/xml/bloblang.go index 7250728250..beebe3fe29 100644 --- a/internal/impl/xml/bloblang.go +++ b/internal/impl/xml/bloblang.go @@ -6,7 +6,7 @@ import ( "github.com/clbanning/mxj/v2" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func init() { diff --git a/internal/impl/xml/bloblang_test.go b/internal/impl/xml/bloblang_test.go index 691a97c8b8..1cb775cf05 100644 --- a/internal/impl/xml/bloblang_test.go +++ b/internal/impl/xml/bloblang_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/bloblang" + "github.com/redpanda-data/benthos/v4/public/bloblang" ) func TestParseXML(t *testing.T) { diff --git a/internal/impl/xml/processor.go b/internal/impl/xml/processor.go index b3e07f21df..25cb8cdde2 100644 --- a/internal/impl/xml/processor.go +++ b/internal/impl/xml/processor.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/internal/impl/xml/processor_test.go b/internal/impl/xml/processor_test.go index 518d007f5c..930ac6ee47 100644 --- a/internal/impl/xml/processor_test.go +++ b/internal/impl/xml/processor_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func TestXMLCases(t *testing.T) { diff --git a/internal/impl/zeromq/input_zmq4.go b/internal/impl/zeromq/input_zmq4.go index 3393816bec..a184fdd04b 100644 --- a/internal/impl/zeromq/input_zmq4.go +++ b/internal/impl/zeromq/input_zmq4.go @@ -11,7 +11,7 @@ import ( "github.com/pebbe/zmq4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func zmqInputConfig() *service.ConfigSpec { @@ -24,7 +24,7 @@ By default Benthos does not build with components that require linking to extern ` + "```bash" + ` # With go -go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest +go install -tags "x_benthos_extra" github.com/redpanda-data/benthos/v4/cmd/benthos@latest # Using make make TAGS=x_benthos_extra diff --git a/internal/impl/zeromq/integration_test.go b/internal/impl/zeromq/integration_test.go index 40efdd1920..79eac7fd6d 100644 --- a/internal/impl/zeromq/integration_test.go +++ b/internal/impl/zeromq/integration_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/benthosdev/benthos/v4/public/service/integration" + "github.com/redpanda-data/benthos/v4/public/service/integration" ) func TestIntegrationZMQ(t *testing.T) { diff --git a/internal/impl/zeromq/output_zmq4.go b/internal/impl/zeromq/output_zmq4.go index 213b898eff..398434dc6d 100644 --- a/internal/impl/zeromq/output_zmq4.go +++ b/internal/impl/zeromq/output_zmq4.go @@ -11,7 +11,7 @@ import ( "github.com/pebbe/zmq4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) func zmqOutputConfig() *service.ConfigSpec { @@ -24,7 +24,7 @@ By default Benthos does not build with components that require linking to extern ` + "```bash" + ` # With go -go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest +go install -tags "x_benthos_extra" github.com/redpanda-data/benthos/v4/cmd/benthos@latest # Using make make TAGS=x_benthos_extra diff --git a/internal/retries/retries.go b/internal/retries/retries.go index 0a32c2618d..de9ef6fe74 100644 --- a/internal/retries/retries.go +++ b/internal/retries/retries.go @@ -5,7 +5,7 @@ import ( "github.com/cenkalti/backoff/v4" - "github.com/benthosdev/benthos/v4/public/service" + "github.com/redpanda-data/benthos/v4/public/service" ) const ( diff --git a/public/components/io/package.go b/public/components/io/package.go index 11d0887867..99610136fa 100644 --- a/public/components/io/package.go +++ b/public/components/io/package.go @@ -10,5 +10,5 @@ package io import ( // Import only io packages. - _ "github.com/benthosdev/benthos/v4/public/components/io" + _ "github.com/redpanda-data/benthos/v4/public/components/io" ) diff --git a/public/components/pure/extended/package.go b/public/components/pure/extended/package.go index 6328d992d3..4163daeb5a 100644 --- a/public/components/pure/extended/package.go +++ b/public/components/pure/extended/package.go @@ -10,7 +10,7 @@ package extended import ( // Import pure but larger packages. - _ "github.com/benthosdev/benthos/v4/public/components/pure/extended" + _ "github.com/redpanda-data/benthos/v4/public/components/pure/extended" _ "github.com/redpanda-data/connect/v4/internal/impl/awk" _ "github.com/redpanda-data/connect/v4/internal/impl/jsonpath" diff --git a/public/components/pure/package.go b/public/components/pure/package.go index 8aada7e0be..5aa1eb3350 100644 --- a/public/components/pure/package.go +++ b/public/components/pure/package.go @@ -10,5 +10,5 @@ package pure import ( // Import only pure packages. - _ "github.com/benthosdev/benthos/v4/public/components/pure" + _ "github.com/redpanda-data/benthos/v4/public/components/pure" ) diff --git a/public/components/sql/package.go b/public/components/sql/package.go index b346e4140a..038ebf2065 100644 --- a/public/components/sql/package.go +++ b/public/components/sql/package.go @@ -1,12 +1,12 @@ // Package sql brings in the sql components and _all_ officially supported // drivers. In order to hand-pick which drivers are included import -// github.com/benthosdev/benthos/v4/public/components/sql/base instead along +// github.com/redpanda-data/benthos/v4/public/components/sql/base instead along // with the specific drivers you want. package sql import ( // Bring in the base plugin definitions. - _ "github.com/benthosdev/benthos/v4/public/components/sql/base" + _ "github.com/redpanda-data/connect/v4/public/components/sql/base" // Import all (supported) sql drivers. _ "github.com/ClickHouse/clickhouse-go/v2" From e6ab281afd818b7c34ebe863268ee6b0920a7794 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Wed, 29 May 2024 15:17:39 +0100 Subject: [PATCH 15/17] Add strip_html --- cmd/redpanda-connect/main.go | 6 ++- internal/impl/html/bloblang.go | 55 ++++++++++++++++++++++ internal/impl/html/bloblang_test.go | 36 ++++++++++++++ public/components/pure/extended/package.go | 1 + 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 internal/impl/html/bloblang.go create mode 100644 internal/impl/html/bloblang_test.go diff --git a/cmd/redpanda-connect/main.go b/cmd/redpanda-connect/main.go index 7408fb4b4b..5b8013602f 100644 --- a/cmd/redpanda-connect/main.go +++ b/cmd/redpanda-connect/main.go @@ -12,8 +12,9 @@ import ( ) var ( - Version string - DateBuilt string + Version string + DateBuilt string + BinaryName string = "redpanda-connect" ) func redpandaTopLevelConfigField() *service.ConfigField { @@ -26,6 +27,7 @@ func main() { service.RunCLI( context.Background(), service.CLIOptSetVersion(Version, DateBuilt), + service.CLIOptSetBinaryName(BinaryName), service.CLIOptSetProductName("Redpanda Connect"), service.CLIOptSetDocumentationURL("https://docs.redpanda.com/redpanda-connect"), service.CLIOptSetMainSchemaFrom(func() *service.ConfigSchema { diff --git a/internal/impl/html/bloblang.go b/internal/impl/html/bloblang.go new file mode 100644 index 0000000000..7d10a7d4b2 --- /dev/null +++ b/internal/impl/html/bloblang.go @@ -0,0 +1,55 @@ +package html + +import ( + "fmt" + + "github.com/microcosm-cc/bluemonday" + "github.com/redpanda-data/benthos/v4/public/bloblang" +) + +func init() { + stripHTMLSpec := bloblang.NewPluginSpec(). + Category("String Manipulation"). + Description(`Attempts to remove all HTML tags from a target string.`). + Example("", `root.stripped = this.value.strip_html()`, + [2]string{ + `{"value":"

the plain old text

"}`, + `{"stripped":"the plain old text"}`, + }). + Example("It's also possible to provide an explicit list of element types to preserve in the output.", + `root.stripped = this.value.strip_html(["article"])`, + [2]string{ + `{"value":"

the plain old text

"}`, + `{"stripped":"
the plain old text
"}`, + }). + Param(bloblang.NewAnyParam("preserve").Description("An optional array of element types to preserve in the output.").Optional()) + + if err := bloblang.RegisterMethodV2( + "strip_html", stripHTMLSpec, + func(args *bloblang.ParsedParams) (bloblang.Method, error) { + p := bluemonday.NewPolicy() + + var tags []any + if rawArgs := args.AsSlice(); len(rawArgs) > 0 { + tags, _ = rawArgs[0].([]any) + } + + if len(tags) > 0 { + tagStrs := make([]string, len(tags)) + for i, ele := range tags { + var ok bool + if tagStrs[i], ok = ele.(string); !ok { + return nil, fmt.Errorf("invalid arg at index %v: expected string, got %T", i, ele) + } + } + p = p.AllowElements(tagStrs...) + } + + return bloblang.StringMethod(func(s string) (any, error) { + return p.Sanitize(s), nil + }), nil + }, + ); err != nil { + panic(err) + } +} diff --git a/internal/impl/html/bloblang_test.go b/internal/impl/html/bloblang_test.go new file mode 100644 index 0000000000..2e929d0de5 --- /dev/null +++ b/internal/impl/html/bloblang_test.go @@ -0,0 +1,36 @@ +package html + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/redpanda-data/benthos/v4/public/bloblang" +) + +func TestStripHTMLNoArgs(t *testing.T) { + e, err := bloblang.Parse(`root = this.strip_html()`) + require.NoError(t, err) + + res, err := e.Query(`
meow
`) + require.NoError(t, err) + + assert.Equal(t, "meow", res) +} + +func TestStripHTMLWithArgs(t *testing.T) { + e, err := bloblang.Parse(`root = this.strip_html(["strong","h1"])`) + require.NoError(t, err) + + res, err := e.Query(`
+

meow

+

hello world this is some text. +

`) + require.NoError(t, err) + + assert.Equal(t, ` +

meow

+ hello world this is some text. +`, res) +} diff --git a/public/components/pure/extended/package.go b/public/components/pure/extended/package.go index 4163daeb5a..0418318746 100644 --- a/public/components/pure/extended/package.go +++ b/public/components/pure/extended/package.go @@ -13,6 +13,7 @@ import ( _ "github.com/redpanda-data/benthos/v4/public/components/pure/extended" _ "github.com/redpanda-data/connect/v4/internal/impl/awk" + _ "github.com/redpanda-data/connect/v4/internal/impl/html" _ "github.com/redpanda-data/connect/v4/internal/impl/jsonpath" _ "github.com/redpanda-data/connect/v4/internal/impl/lang" _ "github.com/redpanda-data/connect/v4/internal/impl/msgpack" From d438131d5b2d7eae43fb978b4048b62398b2aaee Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Wed, 29 May 2024 17:55:38 +0100 Subject: [PATCH 16/17] Update README --- CHANGELOG.md | 7 ++- README.md | 120 ++++++++++++++++++++++----------------------------- go.mod | 16 +++---- go.sum | 62 +++++++++++++++++++++++++- icon.png | Bin 49154 -> 0 bytes 5 files changed, 123 insertions(+), 82 deletions(-) delete mode 100644 icon.png diff --git a/CHANGELOG.md b/CHANGELOG.md index cbda110a0d..fa995d43d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,11 @@ Changelog All notable changes to this project will be documented in this file. -## 4.28.0 - TBD +## 4.28.0 - 2024-05-30 -### Added +### Changed -- Go API: Variadic options added to the public `service.RunCLI` function for customising CLI behaviour. -- Go API: New schema APIs added with linting, generation and marshalling capabilities. +- The repository has been moved to `redpanda-data/connect` and no longer contains the core Benthos engine, which is now broken out into `redpanda-data/benthos`. ## 4.27.0 - 2024-04-23 diff --git a/README.md b/README.md index 3810af3409..32fdfe9788 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ -![Benthos](icon.png "Benthos") +Redpanda Connect +================ -[![godoc for benthosdev/benthos][godoc-badge]][godoc-url] +[![godoc for redpanda-data/connect][godoc-badge]][godoc-url] [![Build Status][actions-badge]][actions-url] -[![Discord invite][discord-badge]][discord-url] -[![Docs site][website-badge]][website-url] -Benthos is a high performance and resilient stream processor, able to connect various [sources][inputs] and [sinks][outputs] in a range of brokering patterns and perform [hydration, enrichments, transformations and filters][processors] on payloads. +Redpanda Connect is a high performance and resilient stream processor, able to connect various [sources][inputs] and [sinks][outputs] in a range of brokering patterns and perform [hydration, enrichments, transformations and filters][processors] on payloads. -It comes with a [powerful mapping language][bloblang-about], is easy to deploy and monitor, and ready to drop into your pipeline either as a static binary, docker image, or [serverless function][serverless], making it cloud native as heck. +It comes with a [powerful mapping language][bloblang-about], is easy to deploy and monitor, and ready to drop into your pipeline either as a static binary or docker image, making it cloud native as heck. -Benthos is declarative, with stream pipelines defined in as few as a single config file, allowing you to specify connectors and a list of processing stages: +Redpanda Connect is declarative, with stream pipelines defined in as few as a single config file, allowing you to specify connectors and a list of processing stages: ```yaml input: @@ -33,46 +32,39 @@ output: ### Delivery Guarantees -Delivery guarantees [can be a dodgy subject](https://youtu.be/QmpBOCvY8mY). Benthos processes and acknowledges messages using an in-process transaction model with no need for any disk persisted state, so when connecting to at-least-once sources and sinks it's able to guarantee at-least-once delivery even in the event of crashes, disk corruption, or other unexpected server faults. +Delivery guarantees [can be a dodgy subject](https://youtu.be/QmpBOCvY8mY). Redpanda Connect processes and acknowledges messages using an in-process transaction model with no need for any disk persisted state, so when connecting to at-least-once sources and sinks it's able to guarantee at-least-once delivery even in the event of crashes, disk corruption, or other unexpected server faults. -This behaviour is the default and free of caveats, which also makes deploying and scaling Benthos much simpler. +This behaviour is the default and free of caveats, which also makes deploying and scaling Redpanda Connect much simpler. ## Supported Sources & Sinks AWS (DynamoDB, Kinesis, S3, SQS, SNS), Azure (Blob storage, Queue storage, Table storage), GCP (Pub/Sub, Cloud storage, Big query), Kafka, NATS (JetStream, Streaming), NSQ, MQTT, AMQP 0.91 (RabbitMQ), AMQP 1, Redis (streams, list, pubsub, hashes), Cassandra, Elasticsearch, HDFS, HTTP (server and client, including websockets), MongoDB, SQL (MySQL, PostgreSQL, Clickhouse, MSSQL), and [you know what just click here to see them all, they don't fit in a README][about-categories]. -Connectors are being added constantly, if something you want is missing then [open an issue](https://github.com/benthosdev/benthos/issues/new). - ## Documentation -If you want to dive fully into Benthos then don't waste your time in this dump, check out the [documentation site][general-docs]. - -For guidance on how to configure more advanced stream processing concepts such as stream joins, enrichment workflows, etc, check out the [cookbooks section.][cookbooks] +If you want to dive fully into Redpanda Connect then don't waste your time in this dump, check out the [documentation site][general-docs]. For guidance on building your own custom plugins in Go check out [the public APIs.][godoc-url] -## Visual Interface - -Do you like looking at stuff? Get angry and smash things when you're forced to read? If you're looking for a visual interface for Benthos check out [Benthos Studio][benthos-studio], it's a config builder, linter, and deployment management solution all baked into a single application. - ## Install -Grab a binary for your OS from [here.][releases] Or use this script: +Install on Linux: ```shell -curl -Lsf https://www.benthos.dev/sh/install | bash +curl -LO https://github.com/redpanda-data/redpanda/releases/latest/download/rpk-linux-amd64.zip +unzip rpk-linux-amd64.zip -d ~/.local/bin/ ``` -Or pull the docker image: +Or use Homebrew: ```shell -docker pull ghcr.io/benthosdev/benthos +brew install redpanda-data/tap/redpanda ``` -Benthos can also be installed via Homebrew: +Or pull the docker image: ```shell -brew install benthos +docker pull ghcr.io/redpanda-data/connect ``` For more information check out the [getting started guide][getting-started]. @@ -80,56 +72,56 @@ For more information check out the [getting started guide][getting-started]. ## Run ```shell -benthos -c ./config.yaml +rpk connect -c ./config.yaml ``` Or, with docker: ```shell # Using a config file -docker run --rm -v /path/to/your/config.yaml:/benthos.yaml ghcr.io/benthosdev/benthos +docker run --rm -v /path/to/your/config.yaml:/connect.yaml ghcr.io/redpanda-data/connect # Using a series of -s flags -docker run --rm -p 4195:4195 ghcr.io/benthosdev/benthos \ +docker run --rm -p 4195:4195 ghcr.io/redpanda-data/connect \ -s "input.type=http_server" \ -s "output.type=kafka" \ -s "output.kafka.addresses=kafka-server:9092" \ - -s "output.kafka.topic=benthos_topic" + -s "output.kafka.topic=redpanda_topic" ``` ## Monitoring ### Health Checks -Benthos serves two HTTP endpoints for health checks: +Redpanda Connect serves two HTTP endpoints for health checks: - `/ping` can be used as a liveness probe as it always returns a 200. - `/ready` can be used as a readiness probe as it serves a 200 only when both the input and output are connected, otherwise a 503 is returned. ### Metrics -Benthos [exposes lots of metrics][metrics] either to Statsd, Prometheus, a JSON HTTP endpoint, [and more][metrics]. +Redpanda Connect [exposes lots of metrics][metrics] either to Statsd, Prometheus, a JSON HTTP endpoint, [and more][metrics]. ### Tracing -Benthos also [emits open telemetry tracing events][tracers], which can be used to visualise the processors within a pipeline. +Redpanda Connect also [emits open telemetry tracing events][tracers], which can be used to visualise the processors within a pipeline. ## Configuration -Benthos provides lots of tools for making configuration discovery, debugging and organisation easy. You can [read about them here][config-doc]. +Redpanda Connect provides lots of tools for making configuration discovery, debugging and organisation easy. You can [read about them here][config-doc]. ## Build Build with Go (any [currently supported version](https://go.dev/dl/)): ```shell -git clone git@github.com:benthosdev/benthos -cd benthos +git clone git@github.com:redpanda-data/connect +cd connect make ``` ## Lint -Benthos uses [golangci-lint][golangci-lint] for linting, which you can install with: +Redpanda Connect uses [golangci-lint][golangci-lint] for linting, which you can install with: ```shell curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin @@ -139,15 +131,15 @@ And then run it with `make lint`. ## Plugins -It's pretty easy to write your own custom plugins for Benthos in Go, for information check out [the API docs][godoc-url], and for inspiration there's an [example repo][plugin-repo] demonstrating a variety of plugin implementations. +It's pretty easy to write your own custom plugins for Redpanda Connect in Go, for information check out [the API docs][godoc-url], and for inspiration there's an [example repo][plugin-repo] demonstrating a variety of plugin implementations. ## Extra Plugins -By default Benthos does not build with components that require linking to external libraries, such as the `zmq4` input and outputs. If you wish to build Benthos locally with these dependencies then set the build tag `x_benthos_extra`: +By default Redpanda Connect does not build with components that require linking to external libraries, such as the `zmq4` input and outputs. If you wish to build Redpanda Connect locally with these dependencies then set the build tag `x_benthos_extra`: ```shell # With go -go install -tags "x_benthos_extra" github.com/benthosdev/benthos/v4/cmd/benthos@latest +go install -tags "x_benthos_extra" github.com/redpanda-data/connect/v4/cmd/redpanda-connect@latest # Using make make TAGS=x_benthos_extra @@ -157,7 +149,7 @@ Note that this tag may change or be broken out into granular tags for individual ## Docker Builds -There's a multi-stage `Dockerfile` for creating a Benthos docker image which results in a minimal image from scratch. You can build it with: +There's a multi-stage `Dockerfile` for creating a Redpanda Connect docker image which results in a minimal image from scratch. You can build it with: ```shell make docker @@ -170,42 +162,34 @@ docker run --rm \ -v /path/to/your/benthos.yaml:/config.yaml \ -v /tmp/data:/data \ -p 4195:4195 \ - benthos -c /config.yaml + redpanda-connect -c /config.yaml ``` ## Contributing -Contributions are welcome, please [read the guidelines](CONTRIBUTING.md), come and chat (links are on the [community page][community]), and watch your back. - -[inputs]: https://www.benthos.dev/docs/components/inputs/about -[about-categories]: https://www.benthos.dev/docs/about#components -[processors]: https://www.benthos.dev/docs/components/processors/about -[outputs]: https://www.benthos.dev/docs/components/outputs/about -[metrics]: https://www.benthos.dev/docs/components/metrics/about -[tracers]: https://www.benthos.dev/docs/components/tracers/about -[config-interp]: https://www.benthos.dev/docs/configuration/interpolation -[streams-api]: https://www.benthos.dev/docs/guides/streams_mode/streams_api -[streams-mode]: https://www.benthos.dev/docs/guides/streams_mode/about -[general-docs]: https://www.benthos.dev/docs/about -[bloblang-about]: https://www.benthos.dev/docs/guides/bloblang/about -[config-doc]: https://www.benthos.dev/docs/configuration/about -[serverless]: https://www.benthos.dev/docs/guides/serverless/about -[cookbooks]: https://www.benthos.dev/cookbooks -[releases]: https://github.com/benthosdev/benthos/releases +Contributions are welcome, please [read the guidelines](CONTRIBUTING.md). + +[inputs]: https://docs.redpanda.com/redpanda-connect/components/inputs/about +[about-categories]: https://docs.redpanda.com/redpanda-connect/about#components +[processors]: https://docs.redpanda.com/redpanda-connect/components/processors/about +[outputs]: https://docs.redpanda.com/redpanda-connect/components/outputs/about +[metrics]: https://docs.redpanda.com/redpanda-connect/components/metrics/about +[tracers]: https://docs.redpanda.com/redpanda-connect/components/tracers/about +[config-interp]: https://docs.redpanda.com/redpanda-connect/configuration/interpolation +[streams-api]: https://docs.redpanda.com/redpanda-connect/guides/streams_mode/streams_api +[streams-mode]: https://docs.redpanda.com/redpanda-connect/guides/streams_mode/about +[general-docs]: https://docs.redpanda.com/redpanda-connect/about +[bloblang-about]: https://docs.redpanda.com/redpanda-connect/guides/bloblang/about +[config-doc]: https://docs.redpanda.com/redpanda-connect/configuration/about +[releases]: https://github.com/redpanda-data/connect/releases [plugin-repo]: https://github.com/benthosdev/benthos-plugin-example -[getting-started]: https://www.benthos.dev/docs/guides/getting_started +[getting-started]: https://docs.redpanda.com/redpanda-connect/guides/getting_started [benthos-studio]: https://studio.benthos.dev -[godoc-badge]: https://pkg.go.dev/badge/github.com/benthosdev/benthos/v4/public -[godoc-url]: https://pkg.go.dev/github.com/benthosdev/benthos/v4/public -[actions-badge]: https://github.com/benthosdev/benthos/actions/workflows/test.yml/badge.svg -[actions-url]: https://github.com/benthosdev/benthos/actions/workflows/test.yml -[discord-badge]: https://img.shields.io/discord/746368194196799589 -[discord-url]: https://discord.com/invite/6VaWjzP -[website-badge]: https://img.shields.io/badge/Docs-Learn%20more-ffc7c7 -[website-url]: https://www.benthos.dev - -[community]: https://www.benthos.dev/community +[godoc-badge]: https://pkg.go.dev/badge/github.com/redpanda-data/benthos/v4/public +[godoc-url]: https://pkg.go.dev/github.com/redpanda-data/benthos/v4/public +[actions-badge]: https://github.com/redpanda-data/connect/actions/workflows/test.yml/badge.svg +[actions-url]: https://github.com/redpanda-data/connect/actions/workflows/test.yml [golangci-lint]: https://golangci-lint.run/ [jaeger]: https://www.jaegertracing.io/ diff --git a/go.mod b/go.mod index d21df7ca1d..304b13607f 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 github.com/beanstalkd/go-beanstalk v0.2.0 github.com/benhoyt/goawk v1.25.0 - github.com/benthosdev/benthos/v4 v4.27.1-0.20240528154316-e55f48c9d190 github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/snowflake v0.3.0 @@ -67,6 +66,7 @@ require ( github.com/lib/pq v1.10.9 github.com/linkedin/goavro/v2 v2.12.0 github.com/matoous/go-nanoid/v2 v2.0.0 + github.com/microcosm-cc/bluemonday v1.0.25 github.com/microsoft/gocosmos v1.1.1 github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats.go v1.32.0 @@ -89,6 +89,7 @@ require ( github.com/rabbitmq/amqp091-go v1.9.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/redis/go-redis/v9 v9.4.0 + github.com/redpanda-data/benthos/v4 v4.28.0 github.com/sijms/go-ora/v2 v2.8.7 github.com/smira/go-statsd v1.3.3 github.com/snowflakedb/gosnowflake v1.7.2 @@ -152,6 +153,7 @@ require ( github.com/apache/arrow/go/v14 v14.0.2 // indirect github.com/apache/thrift v0.18.1 // indirect github.com/ardielle/ardielle-go v1.5.2 // indirect + github.com/armon/go-metrics v0.3.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 // indirect github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.12.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect @@ -201,6 +203,7 @@ require ( github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect + github.com/frankban/quicktest v1.14.6 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-faster/city v1.0.1 // indirect @@ -233,13 +236,12 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-hclog v1.1.0 // indirect - github.com/hashicorp/go-msgpack v1.1.5 // indirect + github.com/hashicorp/go-immutable-radix v1.3.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/raft v1.3.9 // indirect github.com/influxdata/go-syslog/v3 v3.0.0 // indirect github.com/itchyny/gojq v0.12.14 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect @@ -270,13 +272,12 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/microcosm-cc/bluemonday v1.0.25 // indirect - github.com/minio/highwayhash v1.0.2 // indirect github.com/moby/term v0.5.0 // indirect github.com/montanaflynn/stats v0.7.0 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/mtibben/percent v0.2.1 // indirect - github.com/nats-io/jwt/v2 v2.5.0 // indirect + github.com/nats-io/nats-server/v2 v2.9.23 // indirect + github.com/nats-io/nats-streaming-server v0.24.6 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -314,7 +315,6 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.etcd.io/bbolt v1.3.6 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect diff --git a/go.sum b/go.sum index 21368b9b4a..71eff8ff87 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,7 @@ github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9z github.com/ClickHouse/clickhouse-go/v2 v2.21.1 h1:x8wZEMOHDh4K8kLQBtGMeIIguejiaj8/bUiF2VzG6n4= github.com/ClickHouse/clickhouse-go/v2 v2.21.1/go.mod h1:hTWNkV9mkQwiQ/df0rbN17VXF05UTResY4krnjbzVZA= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.21.0 h1:OEgjQy1rH4Fbn5IpuI9d0uhLl+j6DkDvh9Q2Ucd6GK8= @@ -139,6 +140,10 @@ github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -241,9 +246,8 @@ github.com/beanstalkd/go-beanstalk v0.2.0/go.mod h1:/G8YTyChOtpOArwLTQPY1CHB+i21 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benhoyt/goawk v1.25.0 h1:DW4DCn2IrVp6FUar2W404G1YyQDXseWAVDwb11PUL+I= github.com/benhoyt/goawk v1.25.0/go.mod h1:FjIAicXvrv3wbqAhSTo5bn4mIM5y1iy3lcnIynlJvoI= -github.com/benthosdev/benthos/v4 v4.27.1-0.20240528154316-e55f48c9d190 h1:IJn9xZWz0MZ/icyb1rJLPoIN1wamlhNU5Y+LbDPRAEw= -github.com/benthosdev/benthos/v4 v4.27.1-0.20240528154316-e55f48c9d190/go.mod h1:KbKrzlHGhf67eIdUqk4pjT5yaD8nTPb7vczP5/twTOc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= @@ -439,8 +443,12 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -452,6 +460,7 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -462,6 +471,7 @@ github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJr github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -525,8 +535,10 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= @@ -695,8 +707,11 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -706,6 +721,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= @@ -716,6 +732,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -738,6 +755,7 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= @@ -779,6 +797,10 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= @@ -787,12 +809,18 @@ github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9 github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/nats-server/v2 v2.8.2/go.mod h1:vIdpKz3OG+DCg4q/xVPdXHoztEyKDWRtykQ4N7hd7C4= github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU= github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0= github.com/nats-io/nats-streaming-server v0.24.6 h1:iIZXuPSznnYkiy0P3L0AP9zEN9Etp+tITbbX1KKeq4Q= github.com/nats-io/nats-streaming-server v0.24.6/go.mod h1:tdKXltY3XLeBJ21sHiZiaPl+j8sK3vcCKBWVyxeQs10= +github.com/nats-io/nats.go v1.13.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nats.go v1.14.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nats.go v1.15.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= github.com/nats-io/nats.go v1.32.0 h1:Bx9BZS+aXYlxW08k8Gd3yR2s73pV5XSoAQUyp1Kwvp0= github.com/nats-io/nats.go v1.32.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= @@ -801,6 +829,7 @@ github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nats-io/stan.go v0.10.2/go.mod h1:vo2ax8K2IxaR3JtEMLZRFKIdoK/3o1/PKueapB7ezX0= github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw= github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k= github.com/ncw/swift v1.0.52/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= @@ -857,6 +886,8 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -864,17 +895,28 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= @@ -891,6 +933,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redpanda-data/benthos/v4 v4.28.0 h1:63achhq9Yztlm+gJLH7VqYr8NtzRI4FfkajENwgQtvU= +github.com/redpanda-data/benthos/v4 v4.28.0/go.mod h1:veuREp5S8MJ21MXofdfMPVm5qOwQGmymh9c13jax284= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rickb777/date v1.20.5 h1:Ybjz7J7ga9ui4VJizQpil0l330r6wkn6CicaoattIxQ= @@ -905,6 +949,7 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk= github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -927,6 +972,7 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sijms/go-ora/v2 v2.8.7 h1:lkbCuXqd5/wn8niyJs/qvfTcSAfi8wBbzc5LYz41g5g= github.com/sijms/go-ora/v2 v2.8.7/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -1086,6 +1132,7 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1103,6 +1150,7 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= @@ -1161,6 +1209,7 @@ golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1169,6 +1218,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1210,6 +1260,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1217,6 +1268,7 @@ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1259,7 +1311,9 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1302,6 +1356,7 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1442,6 +1497,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1469,8 +1525,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/icon.png b/icon.png deleted file mode 100644 index 3ec688d7a9a14ed316402d7a1f79abc8e9e1efa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49154 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�9!lsxlWJSLvBMkFCN%%W4ZpCI)r{`c zl37@2p^u%3RTgSczqn~nqg@n@Y-FXF?pn4k0&|1SU-he_y{&9QtI~UUh-1CkhD&Gm zCi)avT=q#hWNL8ziL1tBg`d-I-h6(f2Qhe;%!PVc(tT{Ro7NOqrj! z3?Ce>4+s&`{Hu5iDWMgrR+^fXKmAA?F8kjVEOfc0&Zo`W95n65@2gIu+~JiK{raeJ z;L{Y!`v!XyT7J9wIiZ*;Pc^X&TZFYdtznoK!8@-|xsBFHxOD`EUAe++?gv zeSo3RNNDNr=RJ#Ze<+dVnCmFifvXk!?A3J2;|j%Y4NJ|&ANs9&zFpHPU*6?%PH}E? zra6b#eZ=O!AFIfh{CblhxWLQf4e;*rO4)lTkL3n^g$IPkgli>UNuTn_Me~0iRI{VMmbfe8HiapiE=-8F&Vb3&j~HoLsmmYlA8V5 zk=9|>$0t#&n}>OR{#ktwh699e7I&tnTH%X5$QMtX@U8b?CUf$5KX^sFG~VZ;hW2(F zhk~rxl%Kcv34>a@-73aAw_67}1X(3YI@{Y&r%@iC8QgwmbNNxDvd{gPf)xEd#yUB> H_MiPfzVt!+ From 62de33d0680bf6e6ba26f4f1008757c99a629470 Mon Sep 17 00:00:00 2001 From: Ashley Jeffs Date: Thu, 30 May 2024 11:16:17 +0100 Subject: [PATCH 17/17] Add run subcommand --- .goreleaser.yml | 1 + cmd/redpanda-connect/main.go | 17 +++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- resources/scripts/sign_for_darwin.sh | 9 ++++++++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index fa1d94136f..8d6ad236f8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -27,6 +27,7 @@ builds: -s -w -X main.Version={{.Version}} -X main.DateBuilt={{.Date}} + -X main.BinaryName=redpanda-connect # - id: connect-lambda # main: cmd/serverless/connect-lambda/main.go # binary: redpanda-connect-lambda diff --git a/cmd/redpanda-connect/main.go b/cmd/redpanda-connect/main.go index 5b8013602f..2561252cc4 100644 --- a/cmd/redpanda-connect/main.go +++ b/cmd/redpanda-connect/main.go @@ -29,7 +29,24 @@ func main() { service.CLIOptSetVersion(Version, DateBuilt), service.CLIOptSetBinaryName(BinaryName), service.CLIOptSetProductName("Redpanda Connect"), + service.CLIOptSetDefaultConfigPaths( + "redpanda-connect.yaml", + "/redpanda-connect.yaml", + "/etc/redpanda-connect/config.yaml", + "/etc/redpanda-connect.yaml", + + "connect.yaml", + "/connect.yaml", + "/etc/connect/config.yaml", + "/etc/connect.yaml", + + // Keep these for now, for backwards compatibility + "/benthos.yaml", + "/etc/benthos/config.yaml", + "/etc/benthos.yaml", + ), service.CLIOptSetDocumentationURL("https://docs.redpanda.com/redpanda-connect"), + service.CLIOptSetShowRunCommand(true), service.CLIOptSetMainSchemaFrom(func() *service.ConfigSchema { return service.NewEnvironment().FullConfigSchema(Version, DateBuilt). Field(redpandaTopLevelConfigField()) diff --git a/go.mod b/go.mod index 304b13607f..d7e2eb4eae 100644 --- a/go.mod +++ b/go.mod @@ -89,7 +89,7 @@ require ( github.com/rabbitmq/amqp091-go v1.9.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/redis/go-redis/v9 v9.4.0 - github.com/redpanda-data/benthos/v4 v4.28.0 + github.com/redpanda-data/benthos/v4 v4.28.1 github.com/sijms/go-ora/v2 v2.8.7 github.com/smira/go-statsd v1.3.3 github.com/snowflakedb/gosnowflake v1.7.2 diff --git a/go.sum b/go.sum index 71eff8ff87..53a9f9ec7e 100644 --- a/go.sum +++ b/go.sum @@ -933,8 +933,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/redpanda-data/benthos/v4 v4.28.0 h1:63achhq9Yztlm+gJLH7VqYr8NtzRI4FfkajENwgQtvU= -github.com/redpanda-data/benthos/v4 v4.28.0/go.mod h1:veuREp5S8MJ21MXofdfMPVm5qOwQGmymh9c13jax284= +github.com/redpanda-data/benthos/v4 v4.28.1 h1:+pXURW62NVbLHMLQXpS8qEUtxCsptpKneh+7chzv7Fw= +github.com/redpanda-data/benthos/v4 v4.28.1/go.mod h1:veuREp5S8MJ21MXofdfMPVm5qOwQGmymh9c13jax284= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rickb777/date v1.20.5 h1:Ybjz7J7ga9ui4VJizQpil0l330r6wkn6CicaoattIxQ= diff --git a/resources/scripts/sign_for_darwin.sh b/resources/scripts/sign_for_darwin.sh index 9e236c151c..9a68cfdce8 100755 --- a/resources/scripts/sign_for_darwin.sh +++ b/resources/scripts/sign_for_darwin.sh @@ -6,9 +6,16 @@ _OS=$1 _PATH_TO_SIGN=$2 _IS_SNAPSHOT=$3 +check_cmd() { + command -v "$1" > /dev/null 2>&1 +} if [ "$_OS" = "darwin" ]; then - quill sign-and-notarize "$_PATH_TO_SIGN" --dry-run="$_IS_SNAPSHOT" --ad-hoc="$_IS_SNAPSHOT" -vv + if check_cmd "quill"; then + quill sign-and-notarize "$_PATH_TO_SIGN" --dry-run="$_IS_SNAPSHOT" --ad-hoc="$_IS_SNAPSHOT" -vv + else + echo "Aborted, missing quill" + fi else echo "No need to sign binaries for ${_OS}" fi